feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
// 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/>.
|
||||
|
||||
//! Primitives for tracking collations-related data.
|
||||
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
|
||||
use futures::{future::BoxFuture, stream::FuturesUnordered};
|
||||
|
||||
use pezkuwi_node_network_protocol::{
|
||||
request_response::{incoming::OutgoingResponse, v2 as protocol_v2, IncomingRequest},
|
||||
PeerId,
|
||||
};
|
||||
use pezkuwi_node_primitives::PoV;
|
||||
use pezkuwi_primitives::{
|
||||
CandidateHash, CandidateReceiptV2 as CandidateReceipt, Hash, HeadData, Id as ParaId,
|
||||
};
|
||||
|
||||
/// The status of a collation as seen from the collator.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum CollationStatus {
|
||||
/// The collation was created, but we did not advertise it to any validator.
|
||||
Created,
|
||||
/// The collation was advertised to at least one validator.
|
||||
Advertised,
|
||||
/// The collation was requested by at least one validator.
|
||||
Requested,
|
||||
}
|
||||
|
||||
impl CollationStatus {
|
||||
/// Advance to the [`Self::Advertised`] status.
|
||||
///
|
||||
/// This ensures that `self` isn't already [`Self::Requested`].
|
||||
pub fn advance_to_advertised(&mut self) {
|
||||
if !matches!(self, Self::Requested) {
|
||||
*self = Self::Advertised;
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance to the [`Self::Requested`] status.
|
||||
pub fn advance_to_requested(&mut self) {
|
||||
*self = Self::Requested;
|
||||
}
|
||||
|
||||
/// Return label for metrics.
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
CollationStatus::Created => "created",
|
||||
CollationStatus::Advertised => "advertised",
|
||||
CollationStatus::Requested => "requested",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A collation built by the collator.
|
||||
pub struct Collation {
|
||||
/// Candidate receipt.
|
||||
pub receipt: CandidateReceipt,
|
||||
/// Proof to verify the state transition of the teyrchain.
|
||||
pub pov: PoV,
|
||||
/// Parent head-data
|
||||
pub parent_head_data: HeadData,
|
||||
/// Collation status.
|
||||
pub status: CollationStatus,
|
||||
}
|
||||
|
||||
/// Stores the state for waiting collation fetches per relay parent.
|
||||
#[derive(Default)]
|
||||
pub struct WaitingCollationFetches {
|
||||
/// A flag indicating that we have an ongoing request.
|
||||
/// This limits the number of collations being sent at any moment
|
||||
/// of time to 1 for each relay parent.
|
||||
///
|
||||
/// If set to `true`, any new request will be queued.
|
||||
pub collation_fetch_active: bool,
|
||||
/// The collation fetches waiting to be fulfilled.
|
||||
pub req_queue: VecDeque<VersionedCollationRequest>,
|
||||
/// All peers that are waiting or actively uploading.
|
||||
///
|
||||
/// We will not accept multiple requests from the same peer, otherwise our DoS protection of
|
||||
/// moving on to the next peer after `MAX_UNSHARED_UPLOAD_TIME` would be pointless.
|
||||
pub waiting_peers: HashSet<(PeerId, CandidateHash)>,
|
||||
}
|
||||
|
||||
/// Backwards-compatible wrapper for incoming collations requests.
|
||||
pub enum VersionedCollationRequest {
|
||||
V2(IncomingRequest<protocol_v2::CollationFetchingRequest>),
|
||||
}
|
||||
|
||||
impl From<IncomingRequest<protocol_v2::CollationFetchingRequest>> for VersionedCollationRequest {
|
||||
fn from(req: IncomingRequest<protocol_v2::CollationFetchingRequest>) -> Self {
|
||||
Self::V2(req)
|
||||
}
|
||||
}
|
||||
|
||||
impl VersionedCollationRequest {
|
||||
/// Returns teyrchain id from the request payload.
|
||||
pub fn para_id(&self) -> ParaId {
|
||||
match self {
|
||||
VersionedCollationRequest::V2(req) => req.payload.para_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns candidate hash from the request payload.
|
||||
pub fn candidate_hash(&self) -> CandidateHash {
|
||||
match self {
|
||||
VersionedCollationRequest::V2(req) => req.payload.candidate_hash,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns relay parent from the request payload.
|
||||
pub fn relay_parent(&self) -> Hash {
|
||||
match self {
|
||||
VersionedCollationRequest::V2(req) => req.payload.relay_parent,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns id of the peer the request was received from.
|
||||
pub fn peer_id(&self) -> PeerId {
|
||||
match self {
|
||||
VersionedCollationRequest::V2(req) => req.peer,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the response back to requester.
|
||||
pub fn send_outgoing_response(
|
||||
self,
|
||||
response: OutgoingResponse<protocol_v2::CollationFetchingResponse>,
|
||||
) -> Result<(), ()> {
|
||||
match self {
|
||||
VersionedCollationRequest::V2(req) => req.send_outgoing_response(response),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of the finished background send-collation task.
|
||||
///
|
||||
/// Note that if the timeout was hit the request doesn't get
|
||||
/// aborted, it only indicates that we should start processing
|
||||
/// the next one from the queue.
|
||||
pub struct CollationSendResult {
|
||||
/// Candidate's relay parent.
|
||||
pub relay_parent: Hash,
|
||||
/// Candidate hash.
|
||||
pub candidate_hash: CandidateHash,
|
||||
/// Peer id.
|
||||
pub peer_id: PeerId,
|
||||
/// Whether the max unshared timeout was hit.
|
||||
pub timed_out: bool,
|
||||
}
|
||||
|
||||
pub type ActiveCollationFetches = FuturesUnordered<BoxFuture<'static, CollationSendResult>>;
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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_network_protocol::request_response::incoming;
|
||||
use pezkuwi_node_primitives::UncheckedSignedFullStatement;
|
||||
use pezkuwi_node_subsystem::{errors::SubsystemError, RuntimeApiError};
|
||||
use pezkuwi_node_subsystem_util::{backing_implicit_view, runtime};
|
||||
|
||||
use crate::LOG_TARGET;
|
||||
|
||||
/// General result.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
use fatality::Nested;
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[fatality::fatality(splitable)]
|
||||
pub enum Error {
|
||||
#[fatal]
|
||||
#[error("Receiving message from overseer failed")]
|
||||
SubsystemReceive(#[from] SubsystemError),
|
||||
|
||||
#[fatal(forward)]
|
||||
#[error("Retrieving next incoming request failed")]
|
||||
IncomingRequest(#[from] incoming::Error),
|
||||
|
||||
#[fatal(forward)]
|
||||
#[error("Error while accessing runtime information")]
|
||||
Runtime(#[from] runtime::Error),
|
||||
|
||||
#[error("Error while accessing Runtime API")]
|
||||
RuntimeApi(#[from] RuntimeApiError),
|
||||
|
||||
#[error(transparent)]
|
||||
ImplicitViewFetchError(backing_implicit_view::FetchError),
|
||||
|
||||
#[error("CollationSeconded contained statement with invalid signature")]
|
||||
InvalidStatementSignature(UncheckedSignedFullStatement),
|
||||
}
|
||||
|
||||
/// 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()? {
|
||||
Ok(()) => Ok(()),
|
||||
Err(jfyi) => {
|
||||
gum::warn!(target: LOG_TARGET, error = ?jfyi, ctx);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,608 @@
|
||||
// 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::{Duration, Instant},
|
||||
};
|
||||
|
||||
use pezkuwi_node_subsystem::prometheus::prometheus::HistogramTimer;
|
||||
use pezkuwi_node_subsystem_util::metrics::{self, prometheus};
|
||||
use pezkuwi_primitives::{BlockNumber, CandidateReceiptV2 as CandidateReceipt, Hash};
|
||||
use sp_core::H256;
|
||||
|
||||
use super::collation::CollationStatus;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Metrics(Option<MetricsInner>);
|
||||
|
||||
impl Metrics {
|
||||
/// Record the time a collation took to be backed.
|
||||
pub fn on_collation_backed(&self, latency: f64) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
metrics.collation_backing_latency.observe(latency);
|
||||
}
|
||||
}
|
||||
|
||||
/// Record the time a collation took to be included.
|
||||
pub fn on_collation_included(&self, latency: f64) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
metrics.collation_inclusion_latency.observe(latency);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_advertisement_made(&self) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
metrics.advertisements_made.inc();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_collation_sent_requested(&self) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
metrics.collations_send_requested.inc();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_collation_sent(&self) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
metrics.collations_sent.inc();
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide a timer for `process_msg` which observes on drop.
|
||||
pub fn time_process_msg(&self) -> Option<prometheus::prometheus::HistogramTimer> {
|
||||
self.0.as_ref().map(|metrics| metrics.process_msg.start_timer())
|
||||
}
|
||||
|
||||
/// Provide a timer for `distribute_collation` which observes on drop.
|
||||
pub fn time_collation_distribution(
|
||||
&self,
|
||||
label: &'static str,
|
||||
) -> Option<prometheus::prometheus::HistogramTimer> {
|
||||
self.0.as_ref().map(|metrics| {
|
||||
metrics.collation_distribution_time.with_label_values(&[label]).start_timer()
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a timer to measure how much time collations spend before being fetched.
|
||||
pub fn time_collation_fetch_latency(&self) -> Option<prometheus::prometheus::HistogramTimer> {
|
||||
self.0.as_ref().map(|metrics| metrics.collation_fetch_latency.start_timer())
|
||||
}
|
||||
|
||||
/// Create a timer to measure how much time it takes for fetched collations to be backed.
|
||||
pub fn time_collation_backing_latency(&self) -> Option<prometheus::prometheus::HistogramTimer> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.map(|metrics| metrics.collation_backing_latency_time.start_timer())
|
||||
}
|
||||
|
||||
/// Record the time a collation took before expiring.
|
||||
/// Collations can expire in the following states: "advertised, fetched or backed"
|
||||
pub fn on_collation_expired(&self, latency: f64, state: &'static str) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
metrics.collation_expired_total.with_label_values(&[state]).observe(latency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MetricsInner {
|
||||
advertisements_made: prometheus::Counter<prometheus::U64>,
|
||||
collations_sent: prometheus::Counter<prometheus::U64>,
|
||||
collations_send_requested: prometheus::Counter<prometheus::U64>,
|
||||
process_msg: prometheus::Histogram,
|
||||
collation_distribution_time: prometheus::HistogramVec,
|
||||
collation_fetch_latency: prometheus::Histogram,
|
||||
collation_backing_latency_time: prometheus::Histogram,
|
||||
collation_backing_latency: prometheus::Histogram,
|
||||
collation_inclusion_latency: prometheus::Histogram,
|
||||
collation_expired_total: prometheus::HistogramVec,
|
||||
}
|
||||
|
||||
impl metrics::Metrics for Metrics {
|
||||
fn try_register(
|
||||
registry: &prometheus::Registry,
|
||||
) -> std::result::Result<Self, prometheus::PrometheusError> {
|
||||
let metrics = MetricsInner {
|
||||
advertisements_made: prometheus::register(
|
||||
prometheus::Counter::new(
|
||||
"pezkuwi_teyrchain_collation_advertisements_made_total",
|
||||
"A number of collation advertisements sent to validators.",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
collations_send_requested: prometheus::register(
|
||||
prometheus::Counter::new(
|
||||
"pezkuwi_teyrchain_collations_sent_requested_total",
|
||||
"A number of collations requested to be sent to validators.",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
collations_sent: prometheus::register(
|
||||
prometheus::Counter::new(
|
||||
"pezkuwi_teyrchain_collations_sent_total",
|
||||
"A number of collations sent to validators.",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
process_msg: prometheus::register(
|
||||
prometheus::Histogram::with_opts(
|
||||
prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_collator_protocol_collator_process_msg",
|
||||
"Time spent within `collator_protocol_collator::process_msg`",
|
||||
)
|
||||
.buckets(vec![
|
||||
0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.15, 0.25, 0.35, 0.5, 0.75,
|
||||
1.0,
|
||||
]),
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
collation_distribution_time: prometheus::register(
|
||||
prometheus::HistogramVec::new(
|
||||
prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_collator_protocol_collator_distribution_time",
|
||||
"Time spent within `collator_protocol_collator::distribute_collation`",
|
||||
)
|
||||
.buckets(vec![
|
||||
0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.15, 0.25, 0.35, 0.5, 0.75,
|
||||
1.0,
|
||||
]),
|
||||
&["state"],
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
collation_fetch_latency: prometheus::register(
|
||||
prometheus::Histogram::with_opts(
|
||||
prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_collation_fetch_latency",
|
||||
"How much time collations spend waiting to be fetched",
|
||||
)
|
||||
.buckets(vec![
|
||||
0.001, 0.01, 0.025, 0.05, 0.1, 0.15, 0.25, 0.35, 0.5, 0.75, 1.0, 2.0, 5.0,
|
||||
]),
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
collation_backing_latency_time: prometheus::register(
|
||||
prometheus::Histogram::with_opts(
|
||||
prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_collation_backing_latency_time",
|
||||
"How much time it takes for a fetched collation to be backed",
|
||||
)
|
||||
.buckets(vec![
|
||||
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 12.0, 15.0, 18.0, 24.0, 30.0,
|
||||
]),
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
collation_backing_latency: prometheus::register(
|
||||
prometheus::Histogram::with_opts(
|
||||
prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_collation_backing_latency",
|
||||
"How many blocks away from the relay parent are collations backed",
|
||||
)
|
||||
.buckets(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]),
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
collation_inclusion_latency: prometheus::register(
|
||||
prometheus::Histogram::with_opts(
|
||||
prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_collation_inclusion_latency",
|
||||
"How many blocks it takes for a backed collation to be included",
|
||||
)
|
||||
.buckets(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]),
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
collation_expired_total: prometheus::register(
|
||||
prometheus::HistogramVec::new(
|
||||
prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_collation_expired",
|
||||
"How many collations expired (not backed or not included)",
|
||||
)
|
||||
.buckets(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]),
|
||||
&["state"],
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
};
|
||||
|
||||
Ok(Metrics(Some(metrics)))
|
||||
}
|
||||
}
|
||||
|
||||
// Equal to claim queue length.
|
||||
pub(crate) const MAX_BACKING_DELAY: BlockNumber = 3;
|
||||
// Paras availability period. In practice, candidates time out in exceptional situations.
|
||||
pub(crate) const MAX_AVAILABILITY_DELAY: BlockNumber = 10;
|
||||
|
||||
/// Collations are kept in the tracker, until they are included or expired
|
||||
#[derive(Default)]
|
||||
pub(crate) struct CollationTracker {
|
||||
/// All un-expired collation entries
|
||||
entries: HashMap<Hash, CollationStats>,
|
||||
}
|
||||
|
||||
impl CollationTracker {
|
||||
/// Mark a tracked collation as backed.
|
||||
///
|
||||
/// Block built on top of N is earliest backed at N + 1.
|
||||
pub fn collation_backed(
|
||||
&mut self,
|
||||
block_number: BlockNumber,
|
||||
leaf: H256,
|
||||
receipt: CandidateReceipt,
|
||||
) {
|
||||
let head = receipt.descriptor.para_head();
|
||||
let Some(entry) = self.entries.get_mut(&head) else {
|
||||
gum::debug!(
|
||||
target: crate::LOG_TARGET_STATS,
|
||||
?head,
|
||||
"Backed collation not found in tracker",
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
if entry.backed().is_some() {
|
||||
gum::debug!(
|
||||
target: crate::LOG_TARGET_STATS,
|
||||
?head,
|
||||
"Collation already backed in a fork, skipping",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
entry.set_backed_at(block_number);
|
||||
if let Some(latency) = entry.backed() {
|
||||
// Observe the backing latency since the collation was fetched.
|
||||
let maybe_latency =
|
||||
entry.backed_latency_metric.take().map(|metric| metric.stop_and_record());
|
||||
gum::debug!(
|
||||
target: crate::LOG_TARGET_STATS,
|
||||
latency_blocks = ?latency,
|
||||
latency_time = ?maybe_latency,
|
||||
relay_block = ?leaf,
|
||||
relay_parent = ?entry.relay_parent,
|
||||
para_id = ?receipt.descriptor.para_id(),
|
||||
?head,
|
||||
"A fetched collation was backed on relay chain",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a previously backed collation as included.
|
||||
///
|
||||
/// Block built on top of N is earliest included at N + 2.
|
||||
pub fn collation_included(
|
||||
&mut self,
|
||||
block_number: BlockNumber,
|
||||
leaf: H256,
|
||||
receipt: CandidateReceipt,
|
||||
) {
|
||||
let head = receipt.descriptor.para_head();
|
||||
let para_id = receipt.descriptor.para_id();
|
||||
let Some(entry) = self.entries.get_mut(&head) else {
|
||||
gum::debug!(
|
||||
target: crate::LOG_TARGET_STATS,
|
||||
?para_id,
|
||||
?head,
|
||||
"Included collation not found in tracker",
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let pov_hash = entry.pov_hash();
|
||||
let candidate_hash = entry.candidate_hash();
|
||||
|
||||
if entry.included().is_some() {
|
||||
gum::debug!(
|
||||
target: crate::LOG_TARGET_STATS,
|
||||
?para_id,
|
||||
?head,
|
||||
?candidate_hash,
|
||||
?pov_hash,
|
||||
"Collation already included in a fork, skipping",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
entry.set_included_at(block_number);
|
||||
if let Some(latency) = entry.included() {
|
||||
gum::debug!(
|
||||
target: crate::LOG_TARGET_STATS,
|
||||
?latency,
|
||||
relay_block = ?leaf,
|
||||
relay_parent = ?entry.relay_parent,
|
||||
?para_id,
|
||||
?head,
|
||||
?candidate_hash,
|
||||
?pov_hash,
|
||||
"Collation included on relay chain",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all the collations that have expired at `block_number`.
|
||||
pub fn drain_expired(&mut self, block_number: BlockNumber) -> Vec<CollationStats> {
|
||||
let expired = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|(head, entry)| entry.is_tracking_expired(block_number).then_some(*head))
|
||||
.collect::<Vec<_>>();
|
||||
expired
|
||||
.iter()
|
||||
.filter_map(|head| self.entries.remove(head))
|
||||
.map(|mut entry| {
|
||||
entry.set_expired_at(block_number);
|
||||
entry
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
/// Drain and return all collations that are possibly finalized at `block_number`.
|
||||
///
|
||||
/// We only track the inclusion block number, not the inclusion block hash.
|
||||
/// There is a small chance that a collation was included in a fork that is not finalized.
|
||||
pub fn drain_finalized(&mut self, block_number: BlockNumber) -> Vec<CollationStats> {
|
||||
let finalized = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|(head, entry)| entry.is_possibly_finalized(block_number).then_some(*head))
|
||||
.collect::<Vec<_>>();
|
||||
finalized
|
||||
.iter()
|
||||
.filter_map(|head| self.entries.remove(head))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
/// Track a collation for a given period of time (TTL). TTL depends
|
||||
/// on the collation state.
|
||||
/// Collation is evicted after it expires.
|
||||
pub fn track(&mut self, mut stats: CollationStats) {
|
||||
// Disable the fetch timer, to prevent bogus observe on drop.
|
||||
if let Some(fetch_latency_metric) = stats.fetch_latency_metric.take() {
|
||||
fetch_latency_metric.stop_and_discard();
|
||||
}
|
||||
|
||||
if let Some(entry) = self
|
||||
.entries
|
||||
.values()
|
||||
.find(|entry| entry.relay_parent_number == stats.relay_parent_number)
|
||||
{
|
||||
gum::debug!(
|
||||
target: crate::LOG_TARGET_STATS,
|
||||
?stats.relay_parent_number,
|
||||
?stats.relay_parent,
|
||||
entry_relay_parent = ?entry.relay_parent,
|
||||
"Collation built on a fork",
|
||||
);
|
||||
}
|
||||
|
||||
self.entries.insert(stats.head, stats);
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about how collations live their lives.
|
||||
pub(crate) struct CollationStats {
|
||||
/// The pre-backing collation status information
|
||||
pre_backing_status: CollationStatus,
|
||||
/// The block header hash.
|
||||
head: Hash,
|
||||
/// The relay parent on top of which collation was built
|
||||
relay_parent_number: BlockNumber,
|
||||
/// The relay parent hash.
|
||||
relay_parent: Hash,
|
||||
/// The expiration block number if expired.
|
||||
expired_at: Option<BlockNumber>,
|
||||
/// The backed block number.
|
||||
backed_at: Option<BlockNumber>,
|
||||
/// The included block number if backed.
|
||||
included_at: Option<BlockNumber>,
|
||||
/// The collation fetch time.
|
||||
fetched_at: Option<Instant>,
|
||||
/// Advertisement time
|
||||
advertised_at: Instant,
|
||||
/// The collation fetch latency (seconds).
|
||||
fetch_latency_metric: Option<HistogramTimer>,
|
||||
/// The collation backing latency (seconds). Duration since collation fetched
|
||||
/// until the import of a relay chain block where collation is backed.
|
||||
backed_latency_metric: Option<HistogramTimer>,
|
||||
/// The Collation candidate hash
|
||||
candidate_hash: Hash,
|
||||
/// The Collation PoV hash
|
||||
pov_hash: Hash,
|
||||
}
|
||||
|
||||
impl CollationStats {
|
||||
/// Create new empty instance.
|
||||
pub fn new(
|
||||
head: Hash,
|
||||
relay_parent_number: BlockNumber,
|
||||
relay_parent: Hash,
|
||||
metrics: &Metrics,
|
||||
candidate_hash: Hash,
|
||||
pov_hash: Hash,
|
||||
) -> Self {
|
||||
Self {
|
||||
pre_backing_status: CollationStatus::Created,
|
||||
head,
|
||||
relay_parent_number,
|
||||
relay_parent,
|
||||
advertised_at: std::time::Instant::now(),
|
||||
backed_at: None,
|
||||
expired_at: None,
|
||||
fetched_at: None,
|
||||
included_at: None,
|
||||
fetch_latency_metric: metrics.time_collation_fetch_latency(),
|
||||
backed_latency_metric: None,
|
||||
candidate_hash,
|
||||
pov_hash,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the hash and number of the relay parent.
|
||||
pub fn relay_parent(&self) -> (Hash, BlockNumber) {
|
||||
(self.relay_parent, self.relay_parent_number)
|
||||
}
|
||||
|
||||
/// Returns the age at which the collation expired.
|
||||
pub fn expired(&self) -> Option<BlockNumber> {
|
||||
let expired_at = self.expired_at?;
|
||||
Some(expired_at.saturating_sub(self.relay_parent_number))
|
||||
}
|
||||
|
||||
/// Returns the age of the collation at the moment of backing.
|
||||
pub fn backed(&self) -> Option<BlockNumber> {
|
||||
let backed_at = self.backed_at?;
|
||||
Some(backed_at.saturating_sub(self.relay_parent_number))
|
||||
}
|
||||
|
||||
/// Returns the age of the collation at the moment of inclusion.
|
||||
pub fn included(&self) -> Option<BlockNumber> {
|
||||
let included_at = self.included_at?;
|
||||
let backed_at = self.backed_at?;
|
||||
Some(included_at.saturating_sub(backed_at))
|
||||
}
|
||||
|
||||
/// Returns time the collation waited to be fetched.
|
||||
pub fn fetch_latency(&self) -> Option<Duration> {
|
||||
let fetched_at = self.fetched_at?;
|
||||
Some(fetched_at - self.advertised_at)
|
||||
}
|
||||
|
||||
/// Get teyrchain block header hash.
|
||||
pub fn head(&self) -> H256 {
|
||||
self.head
|
||||
}
|
||||
|
||||
/// Get candidate hash.
|
||||
pub fn candidate_hash(&self) -> H256 {
|
||||
self.candidate_hash
|
||||
}
|
||||
|
||||
/// Get candidate PoV hash.
|
||||
pub fn pov_hash(&self) -> H256 {
|
||||
self.pov_hash
|
||||
}
|
||||
|
||||
/// Set the timestamp at which collation is fetched.
|
||||
pub fn set_fetched_at(&mut self, fetched_at: Instant) {
|
||||
self.fetched_at = Some(fetched_at);
|
||||
}
|
||||
|
||||
/// Set the timestamp at which collation is backed.
|
||||
pub fn set_backed_at(&mut self, backed_at: BlockNumber) {
|
||||
self.backed_at = Some(backed_at);
|
||||
}
|
||||
|
||||
/// Set the timestamp at which collation is included.
|
||||
pub fn set_included_at(&mut self, included_at: BlockNumber) {
|
||||
self.included_at = Some(included_at);
|
||||
}
|
||||
|
||||
/// Set the timestamp at which collation is expired.
|
||||
pub fn set_expired_at(&mut self, expired_at: BlockNumber) {
|
||||
self.expired_at = Some(expired_at);
|
||||
}
|
||||
|
||||
/// Sets the pre-backing status of the collation.
|
||||
pub fn set_pre_backing_status(&mut self, status: CollationStatus) {
|
||||
self.pre_backing_status = status;
|
||||
}
|
||||
|
||||
/// Returns the pre-backing status of the collation.
|
||||
pub fn pre_backing_status(&self) -> &CollationStatus {
|
||||
&self.pre_backing_status
|
||||
}
|
||||
|
||||
/// Take the fetch latency metric timer.
|
||||
pub fn take_fetch_latency_metric(&mut self) -> Option<HistogramTimer> {
|
||||
self.fetch_latency_metric.take()
|
||||
}
|
||||
|
||||
/// Set the backing latency metric timer.
|
||||
pub fn set_backed_latency_metric(&mut self, timer: Option<HistogramTimer>) {
|
||||
self.backed_latency_metric = timer;
|
||||
}
|
||||
|
||||
/// Returns the time to live for the collation.
|
||||
pub fn tracking_ttl(&self) -> BlockNumber {
|
||||
if self.fetch_latency().is_none() {
|
||||
0 // Collation was never fetched, expires ASAP
|
||||
} else if self.backed().is_none() {
|
||||
MAX_BACKING_DELAY
|
||||
} else if self.included().is_none() {
|
||||
self.backed().expect("backed, checked above") + MAX_AVAILABILITY_DELAY
|
||||
} else {
|
||||
0 // If block included no reason to track it.
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the state of the collation at the moment of expiry.
|
||||
pub fn expiry_state(&self) -> &'static str {
|
||||
if self.fetch_latency().is_none() {
|
||||
// If collation was not fetched, we rely on the status provided
|
||||
// by the collator protocol.
|
||||
self.pre_backing_status().label()
|
||||
} else if self.backed().is_none() {
|
||||
"fetched"
|
||||
} else if self.included().is_none() {
|
||||
"backed"
|
||||
} else {
|
||||
"none"
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the collation is expired.
|
||||
pub fn is_tracking_expired(&self, current_block: BlockNumber) -> bool {
|
||||
// Don't expire included collations
|
||||
if self.included().is_some() {
|
||||
return false;
|
||||
}
|
||||
let expiry_block = self.relay_parent_number + self.tracking_ttl();
|
||||
expiry_block <= current_block
|
||||
}
|
||||
|
||||
/// Check if this collation is possibly finalized based on block number.
|
||||
///
|
||||
/// Returns `true` if the collation was included at or before `last_finalized`.
|
||||
///
|
||||
/// We only track the inclusion block number, not the inclusion block hash.
|
||||
/// There is a small chance that a collation was included in a fork that is not finalized.
|
||||
pub fn is_possibly_finalized(&self, last_finalized: BlockNumber) -> bool {
|
||||
self.included_at
|
||||
.map(|included_at| included_at <= last_finalized)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CollationStats {
|
||||
fn drop(&mut self) {
|
||||
if let Some(fetch_latency_metric) = self.fetch_latency_metric.take() {
|
||||
// This metric is only observed when collation was sent fully to the validator.
|
||||
//
|
||||
// If `fetch_latency_metric` is Some it means that the metrics was observed.
|
||||
// We don't want to observe it again and report a higher value at a later point in time.
|
||||
fetch_latency_metric.stop_and_discard();
|
||||
}
|
||||
// If timer still exists, drop it. It is measured in `collation_backed`.
|
||||
if let Some(backed_latency_metric) = self.backed_latency_metric.take() {
|
||||
backed_latency_metric.stop_and_discard();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+883
@@ -0,0 +1,883 @@
|
||||
// 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/>.
|
||||
|
||||
//! Tests for the collator side with enabled prospective teyrchains.
|
||||
|
||||
use super::*;
|
||||
|
||||
use pezkuwi_node_subsystem::messages::ChainApiMessage;
|
||||
use pezkuwi_primitives::Header;
|
||||
use rstest::rstest;
|
||||
|
||||
fn get_parent_hash(hash: Hash) -> Hash {
|
||||
Hash::from_low_u64_be(hash.to_low_u64_be() + 1)
|
||||
}
|
||||
|
||||
/// Handle a view update.
|
||||
pub(super) async fn update_view(
|
||||
expected_connected: Option<Vec<AuthorityDiscoveryId>>,
|
||||
test_state: &TestState,
|
||||
virtual_overseer: &mut VirtualOverseer,
|
||||
new_view: Vec<(Hash, u32)>, // Hash and block number.
|
||||
activated: u8, // How many new heads does this update contain?
|
||||
) {
|
||||
let new_view: HashMap<Hash, u32> = HashMap::from_iter(new_view);
|
||||
|
||||
let our_view = OurView::new(new_view.keys().map(|hash| *hash), 0);
|
||||
|
||||
overseer_send(
|
||||
virtual_overseer,
|
||||
CollatorProtocolMessage::NetworkBridgeUpdate(NetworkBridgeEvent::OurViewChange(our_view)),
|
||||
)
|
||||
.await;
|
||||
|
||||
for _ in 0..activated {
|
||||
assert_matches!(
|
||||
overseer_recv(virtual_overseer).await,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
_,
|
||||
RuntimeApiRequest::SessionIndexForChild(tx),
|
||||
)) => {
|
||||
tx.send(Ok(test_state.current_session_index())).unwrap();
|
||||
}
|
||||
);
|
||||
|
||||
// obtain the claim queue schedule.
|
||||
let (leaf_hash, leaf_number) = assert_matches!(
|
||||
overseer_recv(virtual_overseer).await,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
parent,
|
||||
RuntimeApiRequest::ClaimQueue(tx),
|
||||
)) => {
|
||||
tx.send(Ok(test_state.claim_queue.clone())).unwrap();
|
||||
(parent, new_view.get(&parent).copied().expect("Unknown parent requested"))
|
||||
}
|
||||
);
|
||||
|
||||
let min_number = leaf_number.saturating_sub(SCHEDULING_LOOKAHEAD as u32 - 1);
|
||||
|
||||
let ancestry_len = leaf_number + 1 - min_number;
|
||||
let ancestry_hashes = std::iter::successors(Some(leaf_hash), |h| Some(get_parent_hash(*h)))
|
||||
.take(ancestry_len as usize);
|
||||
let ancestry_numbers = (min_number..=leaf_number).rev();
|
||||
let mut ancestry_iter = ancestry_hashes.clone().zip(ancestry_numbers).peekable();
|
||||
if let Some((hash, number)) = ancestry_iter.next() {
|
||||
assert_matches!(
|
||||
overseer_recv_with_timeout(virtual_overseer, Duration::from_millis(50)).await.unwrap(),
|
||||
AllMessages::ChainApi(ChainApiMessage::BlockHeader(.., tx)) => {
|
||||
let header = Header {
|
||||
parent_hash: get_parent_hash(hash),
|
||||
number,
|
||||
state_root: Hash::zero(),
|
||||
extrinsics_root: Hash::zero(),
|
||||
digest: Default::default(),
|
||||
};
|
||||
|
||||
tx.send(Ok(Some(header))).unwrap();
|
||||
}
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
overseer_recv_with_timeout(virtual_overseer, Duration::from_millis(50)).await.unwrap(),
|
||||
AllMessages::RuntimeApi(
|
||||
RuntimeApiMessage::Request(
|
||||
..,
|
||||
RuntimeApiRequest::SessionIndexForChild(
|
||||
tx
|
||||
)
|
||||
)
|
||||
) => {
|
||||
tx.send(Ok(1)).unwrap();
|
||||
}
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
overseer_recv_with_timeout(virtual_overseer, Duration::from_millis(50)).await.unwrap(),
|
||||
AllMessages::RuntimeApi(
|
||||
RuntimeApiMessage::Request(
|
||||
..,
|
||||
RuntimeApiRequest::SchedulingLookahead(
|
||||
session_index,
|
||||
tx
|
||||
)
|
||||
)
|
||||
) => {
|
||||
assert_eq!(session_index, 1);
|
||||
tx.send(Ok(SCHEDULING_LOOKAHEAD as u32)).unwrap();
|
||||
}
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
overseer_recv_with_timeout(virtual_overseer, Duration::from_millis(50)).await.unwrap(),
|
||||
AllMessages::ChainApi(
|
||||
ChainApiMessage::Ancestors {
|
||||
k,
|
||||
response_channel: tx,
|
||||
..
|
||||
}
|
||||
) => {
|
||||
assert_eq!(k, SCHEDULING_LOOKAHEAD - 1);
|
||||
let hashes: Vec<_> = ancestry_hashes.clone().skip(1).into_iter().collect();
|
||||
assert_eq!(k, hashes.len());
|
||||
tx.send(Ok(hashes)).unwrap();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for _ in ancestry_iter.clone() {
|
||||
assert_matches!(
|
||||
overseer_recv_with_timeout(virtual_overseer, Duration::from_millis(50)).await.unwrap(),
|
||||
AllMessages::RuntimeApi(
|
||||
RuntimeApiMessage::Request(
|
||||
..,
|
||||
RuntimeApiRequest::SessionIndexForChild(
|
||||
tx
|
||||
)
|
||||
)
|
||||
) => {
|
||||
tx.send(Ok(1)).unwrap();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let mut iter_clone = ancestry_iter.clone();
|
||||
while let Some((hash, number)) = iter_clone.next() {
|
||||
// May be `None` for the last element.
|
||||
let parent_hash =
|
||||
iter_clone.peek().map(|(h, _)| *h).unwrap_or_else(|| get_parent_hash(hash));
|
||||
|
||||
let Some(msg) =
|
||||
overseer_peek_with_timeout(virtual_overseer, Duration::from_millis(50)).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !matches!(
|
||||
&msg,
|
||||
AllMessages::ChainApi(ChainApiMessage::BlockHeader(_hash, ..))
|
||||
if *_hash == hash
|
||||
) {
|
||||
// Ancestry has already been cached for this leaf.
|
||||
break;
|
||||
}
|
||||
|
||||
assert_matches!(
|
||||
overseer_recv_with_timeout(virtual_overseer, Duration::from_millis(50)).await.unwrap(),
|
||||
AllMessages::ChainApi(ChainApiMessage::BlockHeader(.., tx)) => {
|
||||
let header = Header {
|
||||
parent_hash,
|
||||
number,
|
||||
state_root: Hash::zero(),
|
||||
extrinsics_root: Hash::zero(),
|
||||
digest: Default::default(),
|
||||
};
|
||||
|
||||
tx.send(Ok(Some(header))).unwrap();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for (_core, _paras) in test_state
|
||||
.claim_queue
|
||||
.iter()
|
||||
.filter(|(_, paras)| paras.contains(&test_state.para_id))
|
||||
{
|
||||
expect_determine_validator_group(virtual_overseer, &test_state).await;
|
||||
}
|
||||
|
||||
for _ in ancestry_iter {
|
||||
while let Some(msg) =
|
||||
overseer_peek_with_timeout(virtual_overseer, Duration::from_millis(50)).await
|
||||
{
|
||||
if !matches!(
|
||||
&msg,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
_,
|
||||
RuntimeApiRequest::ClaimQueue(_),
|
||||
))
|
||||
) && !matches!(
|
||||
&msg,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
_,
|
||||
RuntimeApiRequest::CandidateEvents(_),
|
||||
))
|
||||
) && !matches!(
|
||||
&msg,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
_,
|
||||
RuntimeApiRequest::SessionIndexForChild(_),
|
||||
))
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
&msg,
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
_,
|
||||
RuntimeApiRequest::SessionIndexForChild(_),
|
||||
))
|
||||
) {
|
||||
for (_core, _paras) in test_state
|
||||
.claim_queue
|
||||
.iter()
|
||||
.filter(|(_, paras)| paras.contains(&test_state.para_id))
|
||||
{
|
||||
expect_determine_validator_group(virtual_overseer, &test_state).await;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
match overseer_recv_with_timeout(virtual_overseer, Duration::from_millis(50))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
_,
|
||||
RuntimeApiRequest::ClaimQueue(tx),
|
||||
)) => {
|
||||
tx.send(Ok(test_state.claim_queue.clone())).unwrap();
|
||||
},
|
||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||
..,
|
||||
RuntimeApiRequest::CandidateEvents(tx),
|
||||
)) => {
|
||||
tx.send(Ok(vec![])).unwrap();
|
||||
},
|
||||
_ => {
|
||||
unimplemented!()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(expected_connected) = expected_connected {
|
||||
check_connected_to_validators(virtual_overseer, expected_connected).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that the next received message is a `Declare` message.
|
||||
pub(super) async fn expect_declare_msg(
|
||||
virtual_overseer: &mut VirtualOverseer,
|
||||
test_state: &TestState,
|
||||
peer: &PeerId,
|
||||
) {
|
||||
assert_matches!(
|
||||
overseer_recv(virtual_overseer).await,
|
||||
AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendCollationMessage(
|
||||
to,
|
||||
CollationProtocols::V2(protocol_v2::CollationProtocol::CollatorProtocol(
|
||||
wire_message,
|
||||
)),
|
||||
)) => {
|
||||
assert_eq!(to[0], *peer);
|
||||
assert_matches!(
|
||||
wire_message,
|
||||
protocol_v2::CollatorProtocolMessage::Declare(
|
||||
collator_id,
|
||||
para_id,
|
||||
signature,
|
||||
) => {
|
||||
assert!(signature.verify(
|
||||
&*protocol_v2::declare_signature_payload(&test_state.local_peer_id),
|
||||
&collator_id),
|
||||
);
|
||||
assert_eq!(collator_id, test_state.collator_pair.public());
|
||||
assert_eq!(para_id, test_state.para_id);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that a collator distributes a collation from the allowed ancestry
|
||||
/// to correct validators group.
|
||||
/// Run once with validators sending their view first and then the collator setting their own
|
||||
/// view first.
|
||||
#[rstest]
|
||||
#[case(true)]
|
||||
#[case(false)]
|
||||
fn distribute_collation_from_implicit_view(#[case] validator_sends_view_first: bool) {
|
||||
let head_a = Hash::from_low_u64_be(126);
|
||||
let head_a_num: u32 = 66;
|
||||
|
||||
// Grandparent of head `a`.
|
||||
let head_b = Hash::from_low_u64_be(128);
|
||||
let head_b_num: u32 = 64;
|
||||
|
||||
// Grandparent of head `b`.
|
||||
let head_c = Hash::from_low_u64_be(130);
|
||||
let head_c_num = 62;
|
||||
|
||||
let group_rotation_info = GroupRotationInfo {
|
||||
session_start_block: head_c_num - 2,
|
||||
group_rotation_frequency: 3,
|
||||
now: head_c_num,
|
||||
};
|
||||
|
||||
let mut test_state = TestState::default();
|
||||
test_state.group_rotation_info = group_rotation_info;
|
||||
|
||||
let local_peer_id = test_state.local_peer_id;
|
||||
let collator_pair = test_state.collator_pair.clone();
|
||||
|
||||
test_harness(
|
||||
local_peer_id,
|
||||
collator_pair,
|
||||
ReputationAggregator::new(|_| true),
|
||||
|mut test_harness| async move {
|
||||
let virtual_overseer = &mut test_harness.virtual_overseer;
|
||||
|
||||
overseer_send(virtual_overseer, CollatorProtocolMessage::ConnectToBackingGroups).await;
|
||||
|
||||
// Set collating para id.
|
||||
overseer_send(virtual_overseer, CollatorProtocolMessage::CollateOn(test_state.para_id))
|
||||
.await;
|
||||
|
||||
if validator_sends_view_first {
|
||||
// Activate leaf `c` to accept at least the collation.
|
||||
update_view(
|
||||
Some(test_state.current_group_validator_authority_ids()),
|
||||
&test_state,
|
||||
virtual_overseer,
|
||||
vec![(head_c, head_c_num)],
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
// Activated leaf is `b`, but the collation will be based on `c`.
|
||||
update_view(
|
||||
Some(test_state.current_group_validator_authority_ids()),
|
||||
&test_state,
|
||||
virtual_overseer,
|
||||
vec![(head_b, head_b_num)],
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let validator_peer_ids = test_state.current_group_validator_peer_ids();
|
||||
for (val, peer) in test_state
|
||||
.current_group_validator_authority_ids()
|
||||
.into_iter()
|
||||
.zip(validator_peer_ids.clone())
|
||||
{
|
||||
connect_peer(virtual_overseer, peer, CollationVersion::V2, Some(val.clone())).await;
|
||||
}
|
||||
|
||||
// Collator declared itself to each peer.
|
||||
for peer_id in &validator_peer_ids {
|
||||
expect_declare_msg(virtual_overseer, &test_state, peer_id).await;
|
||||
}
|
||||
|
||||
let pov = PoV { block_data: BlockData(vec![1, 2, 3]) };
|
||||
let parent_head_data_hash = Hash::repeat_byte(0xAA);
|
||||
let candidate = TestCandidateBuilder {
|
||||
para_id: test_state.para_id,
|
||||
relay_parent: head_c,
|
||||
pov_hash: pov.hash(),
|
||||
..Default::default()
|
||||
}
|
||||
.build();
|
||||
|
||||
let DistributeCollation { candidate, pov_block: _ } =
|
||||
distribute_collation_with_receipt(
|
||||
virtual_overseer,
|
||||
test_state.current_group_validator_authority_ids(),
|
||||
candidate,
|
||||
pov,
|
||||
parent_head_data_hash,
|
||||
)
|
||||
.await;
|
||||
|
||||
let candidate_hash = candidate.hash();
|
||||
|
||||
// Update peer views.
|
||||
for peer_id in &validator_peer_ids {
|
||||
send_peer_view_change(virtual_overseer, peer_id, vec![head_b]).await;
|
||||
|
||||
if !validator_sends_view_first {
|
||||
expect_advertise_collation_msg(
|
||||
virtual_overseer,
|
||||
&[*peer_id],
|
||||
head_c,
|
||||
vec![candidate_hash],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
if validator_sends_view_first {
|
||||
// Activated leaf is `b`, but the collation will be based on `c`.
|
||||
update_view(None, &test_state, virtual_overseer, vec![(head_b, head_b_num)], 1)
|
||||
.await;
|
||||
|
||||
for _ in &validator_peer_ids {
|
||||
expect_advertise_collation_msg(
|
||||
virtual_overseer,
|
||||
&validator_peer_ids,
|
||||
head_c,
|
||||
vec![candidate_hash],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
check_connected_to_validators(
|
||||
virtual_overseer,
|
||||
test_state.current_group_validator_authority_ids(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Head `c` goes out of view.
|
||||
// Build a different candidate for this relay parent and attempt to distribute it.
|
||||
update_view(
|
||||
Some(test_state.current_group_validator_authority_ids()),
|
||||
&test_state,
|
||||
virtual_overseer,
|
||||
vec![(head_a, head_a_num)],
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
||||
let pov = PoV { block_data: BlockData(vec![4, 5, 6]) };
|
||||
let parent_head_data_hash = Hash::repeat_byte(0xBB);
|
||||
let candidate = TestCandidateBuilder {
|
||||
para_id: test_state.para_id,
|
||||
relay_parent: head_c,
|
||||
pov_hash: pov.hash(),
|
||||
..Default::default()
|
||||
}
|
||||
.build();
|
||||
overseer_send(
|
||||
virtual_overseer,
|
||||
CollatorProtocolMessage::DistributeCollation {
|
||||
candidate_receipt: candidate.clone(),
|
||||
parent_head_data_hash,
|
||||
pov: pov.clone(),
|
||||
parent_head_data: HeadData(vec![1, 2, 3]),
|
||||
result_sender: None,
|
||||
core_index: CoreIndex(0),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
check_connected_to_validators(
|
||||
virtual_overseer,
|
||||
test_state.current_group_validator_authority_ids(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Parent out of view, nothing happens.
|
||||
assert!(overseer_recv_with_timeout(virtual_overseer, Duration::from_millis(100))
|
||||
.await
|
||||
.is_none());
|
||||
|
||||
test_harness
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests that collator respects the per relay parent limit of collations, which is equal to the
|
||||
/// number of assignments they have in the claim queue for that core.
|
||||
#[test]
|
||||
fn distribute_collation_up_to_limit() {
|
||||
let mut test_state = TestState::default();
|
||||
// Claim queue has 4 assignments for our paraid on core 0, 1 assignment for another paraid on
|
||||
// core 1. Let's replace one of our assignments on core 0.
|
||||
|
||||
*test_state.claim_queue.get_mut(&CoreIndex(0)).unwrap().get_mut(1).unwrap() = ParaId::from(3);
|
||||
let expected_assignments = SCHEDULING_LOOKAHEAD - 1;
|
||||
|
||||
let local_peer_id = test_state.local_peer_id;
|
||||
let collator_pair = test_state.collator_pair.clone();
|
||||
|
||||
test_harness(
|
||||
local_peer_id,
|
||||
collator_pair,
|
||||
ReputationAggregator::new(|_| true),
|
||||
|mut test_harness| async move {
|
||||
let virtual_overseer = &mut test_harness.virtual_overseer;
|
||||
|
||||
let head_a = Hash::from_low_u64_be(128);
|
||||
let head_a_num: u32 = 64;
|
||||
|
||||
// Grandparent of head `a`.
|
||||
let head_b = Hash::from_low_u64_be(130);
|
||||
|
||||
overseer_send(virtual_overseer, CollatorProtocolMessage::ConnectToBackingGroups).await;
|
||||
|
||||
// Set collating para id.
|
||||
overseer_send(virtual_overseer, CollatorProtocolMessage::CollateOn(test_state.para_id))
|
||||
.await;
|
||||
// Activated leaf is `a`, but the collation will be based on `b`.
|
||||
update_view(
|
||||
Some(test_state.current_group_validator_authority_ids()),
|
||||
&test_state,
|
||||
virtual_overseer,
|
||||
vec![(head_a, head_a_num)],
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
||||
for i in 0..expected_assignments {
|
||||
let pov = PoV { block_data: BlockData(vec![i as u8]) };
|
||||
let parent_head_data_hash = Hash::repeat_byte(0xAA);
|
||||
let candidate = TestCandidateBuilder {
|
||||
para_id: test_state.para_id,
|
||||
relay_parent: head_b,
|
||||
pov_hash: pov.hash(),
|
||||
core_index: CoreIndex(0),
|
||||
..Default::default()
|
||||
}
|
||||
.build();
|
||||
distribute_collation_with_receipt(
|
||||
virtual_overseer,
|
||||
test_state.current_group_validator_authority_ids(),
|
||||
candidate,
|
||||
pov,
|
||||
parent_head_data_hash,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let pov = PoV { block_data: BlockData(vec![10, 12, 6]) };
|
||||
let parent_head_data_hash = Hash::repeat_byte(0xBB);
|
||||
let candidate = TestCandidateBuilder {
|
||||
para_id: test_state.para_id,
|
||||
relay_parent: head_b,
|
||||
pov_hash: pov.hash(),
|
||||
core_index: CoreIndex(0),
|
||||
..Default::default()
|
||||
}
|
||||
.build();
|
||||
overseer_send(
|
||||
virtual_overseer,
|
||||
CollatorProtocolMessage::DistributeCollation {
|
||||
candidate_receipt: candidate.clone(),
|
||||
parent_head_data_hash,
|
||||
pov: pov.clone(),
|
||||
parent_head_data: HeadData(vec![1, 2, 3]),
|
||||
result_sender: None,
|
||||
core_index: CoreIndex(0),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
check_connected_to_validators(
|
||||
virtual_overseer,
|
||||
test_state.current_group_validator_authority_ids(),
|
||||
)
|
||||
.await;
|
||||
// Limit has been reached.
|
||||
assert!(overseer_recv_with_timeout(virtual_overseer, Duration::from_millis(100))
|
||||
.await
|
||||
.is_none());
|
||||
|
||||
// Let's also try on core 1, where we don't have any assignments.
|
||||
|
||||
let pov = PoV { block_data: BlockData(vec![10, 12, 6]) };
|
||||
let parent_head_data_hash = Hash::repeat_byte(0xBB);
|
||||
let candidate = TestCandidateBuilder {
|
||||
para_id: test_state.para_id,
|
||||
relay_parent: head_b,
|
||||
pov_hash: pov.hash(),
|
||||
core_index: CoreIndex(1),
|
||||
..Default::default()
|
||||
}
|
||||
.build();
|
||||
overseer_send(
|
||||
virtual_overseer,
|
||||
CollatorProtocolMessage::DistributeCollation {
|
||||
candidate_receipt: candidate.clone(),
|
||||
parent_head_data_hash,
|
||||
pov: pov.clone(),
|
||||
parent_head_data: HeadData(vec![1, 2, 3]),
|
||||
result_sender: None,
|
||||
core_index: CoreIndex(1),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
check_connected_to_validators(
|
||||
virtual_overseer,
|
||||
test_state.current_group_validator_authority_ids(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(overseer_recv_with_timeout(virtual_overseer, Duration::from_millis(100))
|
||||
.await
|
||||
.is_none());
|
||||
|
||||
test_harness
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Tests that collator send the parent head data in
|
||||
/// case the para is assigned to multiple cores (elastic scaling).
|
||||
#[test]
|
||||
fn send_parent_head_data_for_elastic_scaling() {
|
||||
let test_state = TestState::with_elastic_scaling();
|
||||
|
||||
let local_peer_id = test_state.local_peer_id;
|
||||
let collator_pair = test_state.collator_pair.clone();
|
||||
|
||||
test_harness(
|
||||
local_peer_id,
|
||||
collator_pair,
|
||||
ReputationAggregator::new(|_| true),
|
||||
|test_harness| async move {
|
||||
let mut virtual_overseer = test_harness.virtual_overseer;
|
||||
let mut req_v2_cfg = test_harness.req_v2_cfg;
|
||||
|
||||
let head_b = Hash::from_low_u64_be(129);
|
||||
let head_b_num: u32 = 63;
|
||||
|
||||
overseer_send(&mut virtual_overseer, CollatorProtocolMessage::ConnectToBackingGroups)
|
||||
.await;
|
||||
|
||||
// Set collating para id.
|
||||
overseer_send(
|
||||
&mut virtual_overseer,
|
||||
CollatorProtocolMessage::CollateOn(test_state.para_id),
|
||||
)
|
||||
.await;
|
||||
let expected_connected = [CoreIndex(0), CoreIndex(2), CoreIndex(3)]
|
||||
.into_iter()
|
||||
.map(|core| test_state.validator_authority_ids_for_core(core))
|
||||
.fold(HashSet::new(), |mut acc, res| {
|
||||
acc.extend(res.into_iter());
|
||||
acc
|
||||
})
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
update_view(
|
||||
Some(expected_connected.clone()),
|
||||
&test_state,
|
||||
&mut virtual_overseer,
|
||||
vec![(head_b, head_b_num)],
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
||||
let pov_data = PoV { block_data: BlockData(vec![1 as u8]) };
|
||||
let candidate = TestCandidateBuilder {
|
||||
para_id: test_state.para_id,
|
||||
relay_parent: head_b,
|
||||
pov_hash: pov_data.hash(),
|
||||
..Default::default()
|
||||
}
|
||||
.build();
|
||||
|
||||
let phd = HeadData(vec![1, 2, 3]);
|
||||
let phdh = phd.hash();
|
||||
|
||||
distribute_collation_with_receipt(
|
||||
&mut virtual_overseer,
|
||||
expected_connected,
|
||||
candidate.clone(),
|
||||
pov_data.clone(),
|
||||
phdh,
|
||||
)
|
||||
.await;
|
||||
|
||||
let peer = test_state.validator_peer_id[0];
|
||||
let validator_id = test_state.current_group_validator_authority_ids()[0].clone();
|
||||
connect_peer(
|
||||
&mut virtual_overseer,
|
||||
peer,
|
||||
CollationVersion::V2,
|
||||
Some(validator_id.clone()),
|
||||
)
|
||||
.await;
|
||||
expect_declare_msg(&mut virtual_overseer, &test_state, &peer).await;
|
||||
|
||||
send_peer_view_change(&mut virtual_overseer, &peer, vec![head_b]).await;
|
||||
let hashes: Vec<_> = vec![candidate.hash()];
|
||||
expect_advertise_collation_msg(&mut virtual_overseer, &[peer], head_b, hashes).await;
|
||||
|
||||
let (pending_response, rx) = oneshot::channel();
|
||||
req_v2_cfg
|
||||
.inbound_queue
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.send(RawIncomingRequest {
|
||||
peer,
|
||||
payload: CollationFetchingRequest {
|
||||
relay_parent: head_b,
|
||||
para_id: test_state.para_id,
|
||||
candidate_hash: candidate.hash(),
|
||||
}
|
||||
.encode(),
|
||||
pending_response,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_matches!(
|
||||
rx.await,
|
||||
Ok(full_response) => {
|
||||
let response: CollationFetchingResponse =
|
||||
CollationFetchingResponse::decode(
|
||||
&mut full_response.result
|
||||
.expect("We should have a proper answer").as_ref()
|
||||
).expect("Decoding should work");
|
||||
assert_matches!(
|
||||
response,
|
||||
CollationFetchingResponse::CollationWithParentHeadData {
|
||||
receipt, pov, parent_head_data
|
||||
} => {
|
||||
assert_eq!(receipt, candidate);
|
||||
assert_eq!(pov, pov_data);
|
||||
assert_eq!(parent_head_data, phd);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TestHarness { virtual_overseer, req_v2_cfg }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Tests that collator correctly handles peer V2 requests.
|
||||
#[test]
|
||||
fn advertise_and_send_collation_by_hash() {
|
||||
let test_state = TestState::default();
|
||||
|
||||
let local_peer_id = test_state.local_peer_id;
|
||||
let collator_pair = test_state.collator_pair.clone();
|
||||
|
||||
test_harness(
|
||||
local_peer_id,
|
||||
collator_pair,
|
||||
ReputationAggregator::new(|_| true),
|
||||
|test_harness| async move {
|
||||
let mut virtual_overseer = test_harness.virtual_overseer;
|
||||
let mut req_v2_cfg = test_harness.req_v2_cfg;
|
||||
|
||||
let head_a = Hash::from_low_u64_be(128);
|
||||
let head_a_num: u32 = 64;
|
||||
|
||||
// Parent of head `a`.
|
||||
let head_b = Hash::from_low_u64_be(129);
|
||||
let head_b_num: u32 = 63;
|
||||
|
||||
overseer_send(&mut virtual_overseer, CollatorProtocolMessage::ConnectToBackingGroups)
|
||||
.await;
|
||||
|
||||
// Set collating para id.
|
||||
overseer_send(
|
||||
&mut virtual_overseer,
|
||||
CollatorProtocolMessage::CollateOn(test_state.para_id),
|
||||
)
|
||||
.await;
|
||||
update_view(
|
||||
Some(test_state.current_group_validator_authority_ids()),
|
||||
&test_state,
|
||||
&mut virtual_overseer,
|
||||
vec![(head_b, head_b_num)],
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
update_view(
|
||||
Some(test_state.current_group_validator_authority_ids()),
|
||||
&test_state,
|
||||
&mut virtual_overseer,
|
||||
vec![(head_a, head_a_num)],
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
||||
let candidates: Vec<_> = (0..2)
|
||||
.map(|i| {
|
||||
let pov = PoV { block_data: BlockData(vec![i as u8]) };
|
||||
let candidate = TestCandidateBuilder {
|
||||
para_id: test_state.para_id,
|
||||
relay_parent: head_b,
|
||||
pov_hash: pov.hash(),
|
||||
..Default::default()
|
||||
}
|
||||
.build();
|
||||
(candidate, pov)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (candidate, pov) in &candidates {
|
||||
distribute_collation_with_receipt(
|
||||
&mut virtual_overseer,
|
||||
test_state.current_group_validator_authority_ids(),
|
||||
candidate.clone(),
|
||||
pov.clone(),
|
||||
Hash::zero(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let peer = test_state.validator_peer_id[0];
|
||||
let validator_id = test_state.current_group_validator_authority_ids()[0].clone();
|
||||
connect_peer(
|
||||
&mut virtual_overseer,
|
||||
peer,
|
||||
CollationVersion::V2,
|
||||
Some(validator_id.clone()),
|
||||
)
|
||||
.await;
|
||||
expect_declare_msg(&mut virtual_overseer, &test_state, &peer).await;
|
||||
|
||||
// Head `b` is not a leaf, but both advertisements are still relevant.
|
||||
send_peer_view_change(&mut virtual_overseer, &peer, vec![head_b]).await;
|
||||
let hashes: Vec<_> = candidates.iter().map(|(candidate, _)| candidate.hash()).collect();
|
||||
expect_advertise_collation_msg(&mut virtual_overseer, &[peer], head_b, hashes).await;
|
||||
|
||||
for (candidate, pov_block) in candidates {
|
||||
let (pending_response, rx) = oneshot::channel();
|
||||
req_v2_cfg
|
||||
.inbound_queue
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.send(RawIncomingRequest {
|
||||
peer,
|
||||
payload: CollationFetchingRequest {
|
||||
relay_parent: head_b,
|
||||
para_id: test_state.para_id,
|
||||
candidate_hash: candidate.hash(),
|
||||
}
|
||||
.encode(),
|
||||
pending_response,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_matches!(
|
||||
rx.await,
|
||||
Ok(full_response) => {
|
||||
// Response is the same for v2.
|
||||
let (receipt, pov) = decode_collation_response(
|
||||
full_response.result
|
||||
.expect("We should have a proper answer").as_ref()
|
||||
);
|
||||
assert_eq!(receipt, candidate);
|
||||
assert_eq!(pov, pov_block);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
TestHarness { virtual_overseer, req_v2_cfg }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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/>.
|
||||
|
||||
//! The Collator Protocol allows collators and validators talk to each other.
|
||||
//! This subsystem implements both sides of the collator protocol.
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(unused_crate_dependencies)]
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use futures::{
|
||||
stream::{FusedStream, StreamExt},
|
||||
FutureExt, TryFutureExt,
|
||||
};
|
||||
|
||||
use pezkuwi_node_subsystem_util::reputation::ReputationAggregator;
|
||||
use sp_keystore::KeystorePtr;
|
||||
|
||||
use pezkuwi_node_network_protocol::{
|
||||
request_response::{v2 as protocol_v2, IncomingRequestReceiver},
|
||||
PeerId, UnifiedReputationChange as Rep,
|
||||
};
|
||||
use pezkuwi_primitives::CollatorPair;
|
||||
|
||||
use pezkuwi_node_subsystem::{errors::SubsystemError, overseer, DummySubsystem, SpawnedSubsystem};
|
||||
|
||||
mod collator_side;
|
||||
mod validator_side;
|
||||
#[cfg(feature = "experimental-collator-protocol")]
|
||||
mod validator_side_experimental;
|
||||
|
||||
const LOG_TARGET: &'static str = "teyrchain::collator-protocol";
|
||||
const LOG_TARGET_STATS: &'static str = "teyrchain::collator-protocol::stats";
|
||||
|
||||
/// A collator eviction policy - how fast to evict collators which are inactive.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct CollatorEvictionPolicy {
|
||||
/// How fast to evict collators who are inactive.
|
||||
pub inactive_collator: Duration,
|
||||
/// How fast to evict peers which don't declare their para.
|
||||
pub undeclared: Duration,
|
||||
}
|
||||
|
||||
impl Default for CollatorEvictionPolicy {
|
||||
fn default() -> Self {
|
||||
CollatorEvictionPolicy {
|
||||
inactive_collator: Duration::from_secs(24),
|
||||
undeclared: Duration::from_secs(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What side of the collator protocol is being engaged
|
||||
pub enum ProtocolSide {
|
||||
/// Validators operate on the relay chain.
|
||||
Validator {
|
||||
/// The keystore holding validator keys.
|
||||
keystore: KeystorePtr,
|
||||
/// An eviction policy for inactive peers or validators.
|
||||
eviction_policy: CollatorEvictionPolicy,
|
||||
/// Prometheus metrics for validators.
|
||||
metrics: validator_side::Metrics,
|
||||
/// List of invulnerable collators which is handled with a priority.
|
||||
invulnerables: HashSet<PeerId>,
|
||||
/// Override for `HOLD_OFF_DURATION` constant .
|
||||
collator_protocol_hold_off: Option<Duration>,
|
||||
},
|
||||
/// Experimental variant of the validator side. Do not use in production.
|
||||
#[cfg(feature = "experimental-collator-protocol")]
|
||||
ValidatorExperimental {
|
||||
/// The keystore holding validator keys.
|
||||
keystore: KeystorePtr,
|
||||
/// Prometheus metrics for validators.
|
||||
metrics: validator_side_experimental::Metrics,
|
||||
},
|
||||
/// Collators operate on a teyrchain.
|
||||
Collator {
|
||||
/// Local peer id.
|
||||
peer_id: PeerId,
|
||||
/// Teyrchain collator pair.
|
||||
collator_pair: CollatorPair,
|
||||
/// Receiver for v2 collation fetching requests.
|
||||
request_receiver_v2: IncomingRequestReceiver<protocol_v2::CollationFetchingRequest>,
|
||||
/// Metrics.
|
||||
metrics: collator_side::Metrics,
|
||||
},
|
||||
/// No protocol side, just disable it.
|
||||
None,
|
||||
}
|
||||
|
||||
/// The collator protocol subsystem.
|
||||
pub struct CollatorProtocolSubsystem {
|
||||
protocol_side: ProtocolSide,
|
||||
}
|
||||
|
||||
#[overseer::contextbounds(CollatorProtocol, prefix = self::overseer)]
|
||||
impl CollatorProtocolSubsystem {
|
||||
/// Start the collator protocol.
|
||||
/// If `id` is `Some` this is a collator side of the protocol.
|
||||
/// If `id` is `None` this is a validator side of the protocol.
|
||||
/// Caller must provide a registry for prometheus metrics.
|
||||
pub fn new(protocol_side: ProtocolSide) -> Self {
|
||||
Self { protocol_side }
|
||||
}
|
||||
}
|
||||
|
||||
#[overseer::subsystem(CollatorProtocol, error=SubsystemError, prefix=self::overseer)]
|
||||
impl<Context> CollatorProtocolSubsystem {
|
||||
fn start(self, ctx: Context) -> SpawnedSubsystem {
|
||||
let future = match self.protocol_side {
|
||||
ProtocolSide::Validator {
|
||||
keystore,
|
||||
eviction_policy,
|
||||
metrics,
|
||||
invulnerables,
|
||||
collator_protocol_hold_off,
|
||||
} => {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?invulnerables,
|
||||
?collator_protocol_hold_off,
|
||||
"AH collator protocol params",
|
||||
);
|
||||
validator_side::run(
|
||||
ctx,
|
||||
keystore,
|
||||
eviction_policy,
|
||||
metrics,
|
||||
invulnerables,
|
||||
collator_protocol_hold_off,
|
||||
)
|
||||
.map_err(|e| SubsystemError::with_origin("collator-protocol", e))
|
||||
.boxed()
|
||||
},
|
||||
#[cfg(feature = "experimental-collator-protocol")]
|
||||
ProtocolSide::ValidatorExperimental { keystore, metrics } =>
|
||||
validator_side_experimental::run(ctx, keystore, metrics)
|
||||
.map_err(|e| SubsystemError::with_origin("collator-protocol", e))
|
||||
.boxed(),
|
||||
ProtocolSide::Collator { peer_id, collator_pair, request_receiver_v2, metrics } =>
|
||||
collator_side::run(ctx, peer_id, collator_pair, request_receiver_v2, metrics)
|
||||
.map_err(|e| SubsystemError::with_origin("collator-protocol", e))
|
||||
.boxed(),
|
||||
ProtocolSide::None => return DummySubsystem.start(ctx),
|
||||
};
|
||||
|
||||
SpawnedSubsystem { name: "collator-protocol-subsystem", future }
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the reputation of a peer based on its behavior.
|
||||
async fn modify_reputation(
|
||||
reputation: &mut ReputationAggregator,
|
||||
sender: &mut impl overseer::CollatorProtocolSenderTrait,
|
||||
peer: PeerId,
|
||||
rep: Rep,
|
||||
) {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
rep = ?rep,
|
||||
peer_id = %peer,
|
||||
"reputation change for peer",
|
||||
);
|
||||
|
||||
reputation.modify(sender, peer, rep).await;
|
||||
}
|
||||
|
||||
/// Wait until tick and return the timestamp for the following one.
|
||||
async fn wait_until_next_tick(last_poll: Instant, period: Duration) -> Instant {
|
||||
let now = Instant::now();
|
||||
let next_poll = last_poll + period;
|
||||
|
||||
if next_poll > now {
|
||||
futures_timer::Delay::new(next_poll - now).await
|
||||
}
|
||||
|
||||
Instant::now()
|
||||
}
|
||||
|
||||
/// Returns an infinite stream that yields with an interval of `period`.
|
||||
fn tick_stream(period: Duration) -> impl FusedStream<Item = ()> {
|
||||
futures::stream::unfold(Instant::now(), move |next_check| async move {
|
||||
Some(((), wait_until_next_tick(next_check, period).await))
|
||||
})
|
||||
.fuse()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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/>.
|
||||
|
||||
//! Primitives for tracking collations-related data.
|
||||
//!
|
||||
//! Usually a path of collations is as follows:
|
||||
//! 1. First, collation must be advertised by collator.
|
||||
//! 2. The validator inspects the claim queue and decides if the collation should be fetched
|
||||
//! based on the entries there. A teyrchain can't have more fetched collations than the
|
||||
//! entries in the claim queue at a specific relay parent. When calculating this limit the
|
||||
//! validator counts all advertisements within its view not just at the relay parent.
|
||||
//! 3. If the advertisement was accepted, it's queued for fetch (per relay parent).
|
||||
//! 4. Once it's requested, the collation is said to be pending fetch
|
||||
//! (`CollationStatus::Fetching`).
|
||||
//! 5. Pending fetch collation becomes pending validation
|
||||
//! (`CollationStatus::WaitingOnValidation`) once received, we send it to backing for
|
||||
//! validation.
|
||||
//! 6. If it turns to be invalid or async backing allows seconding another candidate, carry on
|
||||
//! with the next advertisement, otherwise we're done with this relay parent.
|
||||
//!
|
||||
//! ┌───────────────────────────────────┐
|
||||
//! └─▶Waiting ─▶ Fetching ─▶ WaitingOnValidation
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::Poll,
|
||||
};
|
||||
|
||||
use futures::{future::BoxFuture, FutureExt};
|
||||
use pezkuwi_node_network_protocol::{
|
||||
peer_set::CollationVersion,
|
||||
request_response::{outgoing::RequestError, v1 as request_v1, OutgoingResult},
|
||||
PeerId,
|
||||
};
|
||||
use pezkuwi_node_primitives::PoV;
|
||||
use pezkuwi_node_subsystem_util::metrics::prometheus::prometheus::HistogramTimer;
|
||||
use pezkuwi_primitives::{
|
||||
CandidateHash, CandidateReceiptV2 as CandidateReceipt, CollatorId, Hash, HeadData,
|
||||
Id as ParaId, PersistedValidationData,
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use super::error::SecondingError;
|
||||
use crate::LOG_TARGET;
|
||||
|
||||
/// Candidate supplied with a para head it's built on top of.
|
||||
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
|
||||
pub struct ProspectiveCandidate {
|
||||
/// Candidate hash.
|
||||
pub candidate_hash: CandidateHash,
|
||||
/// Parent head-data hash as supplied in advertisement.
|
||||
pub parent_head_data_hash: Hash,
|
||||
}
|
||||
|
||||
impl ProspectiveCandidate {
|
||||
pub fn candidate_hash(&self) -> CandidateHash {
|
||||
self.candidate_hash
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifier of a fetched collation.
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||
pub struct FetchedCollation {
|
||||
/// Candidate's relay parent.
|
||||
pub relay_parent: Hash,
|
||||
/// Teyrchain id.
|
||||
pub para_id: ParaId,
|
||||
/// Candidate hash.
|
||||
pub candidate_hash: CandidateHash,
|
||||
}
|
||||
|
||||
impl From<&CandidateReceipt<Hash>> for FetchedCollation {
|
||||
fn from(receipt: &CandidateReceipt<Hash>) -> Self {
|
||||
let descriptor = receipt.descriptor();
|
||||
Self {
|
||||
relay_parent: descriptor.relay_parent(),
|
||||
para_id: descriptor.para_id(),
|
||||
candidate_hash: receipt.hash(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifier of a collation being requested.
|
||||
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
|
||||
pub struct PendingCollation {
|
||||
/// Candidate's relay parent.
|
||||
pub relay_parent: Hash,
|
||||
/// Teyrchain id.
|
||||
pub para_id: ParaId,
|
||||
/// Peer that advertised this collation.
|
||||
pub peer_id: PeerId,
|
||||
/// Optional candidate hash and parent head-data hash if were
|
||||
/// supplied in advertisement.
|
||||
pub prospective_candidate: Option<ProspectiveCandidate>,
|
||||
/// Hash of the candidate's commitments.
|
||||
pub commitments_hash: Option<Hash>,
|
||||
}
|
||||
|
||||
impl PendingCollation {
|
||||
pub fn new(
|
||||
relay_parent: Hash,
|
||||
para_id: ParaId,
|
||||
peer_id: &PeerId,
|
||||
prospective_candidate: Option<ProspectiveCandidate>,
|
||||
) -> Self {
|
||||
Self {
|
||||
relay_parent,
|
||||
para_id,
|
||||
peer_id: *peer_id,
|
||||
prospective_candidate,
|
||||
commitments_hash: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An identifier for a fetched collation that was blocked from being seconded because we don't have
|
||||
/// access to the parent's HeadData. Can be retried once the candidate outputting this head data is
|
||||
/// seconded.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct BlockedCollationId {
|
||||
/// Para id.
|
||||
pub para_id: ParaId,
|
||||
/// Hash of the parent head data.
|
||||
pub parent_head_data_hash: Hash,
|
||||
}
|
||||
|
||||
/// Performs a sanity check between advertised and fetched collations.
|
||||
pub fn fetched_collation_sanity_check(
|
||||
advertised: &PendingCollation,
|
||||
fetched: &CandidateReceipt,
|
||||
persisted_validation_data: &PersistedValidationData,
|
||||
maybe_parent_head_and_hash: Option<(HeadData, Hash)>,
|
||||
) -> Result<(), SecondingError> {
|
||||
if persisted_validation_data.hash() != fetched.descriptor().persisted_validation_data_hash() {
|
||||
return Err(SecondingError::PersistedValidationDataMismatch);
|
||||
}
|
||||
|
||||
if advertised
|
||||
.prospective_candidate
|
||||
.map_or(false, |pc| pc.candidate_hash() != fetched.hash())
|
||||
{
|
||||
return Err(SecondingError::CandidateHashMismatch);
|
||||
}
|
||||
|
||||
if advertised.relay_parent != fetched.descriptor.relay_parent() {
|
||||
return Err(SecondingError::RelayParentMismatch);
|
||||
}
|
||||
|
||||
if maybe_parent_head_and_hash.map_or(false, |(head, hash)| head.hash() != hash) {
|
||||
return Err(SecondingError::ParentHeadDataMismatch);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Identifier for a requested collation and the respective collator that advertised it.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CollationEvent {
|
||||
/// Collator id.
|
||||
pub collator_id: CollatorId,
|
||||
/// The network protocol version the collator is using.
|
||||
pub collator_protocol_version: CollationVersion,
|
||||
/// The requested collation data.
|
||||
pub pending_collation: PendingCollation,
|
||||
}
|
||||
|
||||
/// Fetched collation data.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PendingCollationFetch {
|
||||
/// Collation identifier.
|
||||
pub collation_event: CollationEvent,
|
||||
/// Candidate receipt.
|
||||
pub candidate_receipt: CandidateReceipt,
|
||||
/// Proof of validity.
|
||||
pub pov: PoV,
|
||||
/// Optional teyrchain parent head data.
|
||||
/// Only needed for elastic scaling.
|
||||
pub maybe_parent_head_data: Option<HeadData>,
|
||||
}
|
||||
|
||||
/// The status of the collations in [`CollationsPerRelayParent`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CollationStatus {
|
||||
/// We are waiting for a collation to be advertised to us.
|
||||
Waiting,
|
||||
/// We are currently fetching a collation for the specified `ParaId`.
|
||||
Fetching(ParaId),
|
||||
/// We are waiting that a collation is being validated.
|
||||
WaitingOnValidation,
|
||||
}
|
||||
|
||||
impl Default for CollationStatus {
|
||||
fn default() -> Self {
|
||||
Self::Waiting
|
||||
}
|
||||
}
|
||||
|
||||
impl CollationStatus {
|
||||
/// Downgrades to `Waiting`
|
||||
pub fn back_to_waiting(&mut self) {
|
||||
*self = Self::Waiting
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of claims in the claim queue and seconded candidates count for a specific `ParaId`.
|
||||
#[derive(Default, Debug)]
|
||||
struct CandidatesStatePerPara {
|
||||
/// How many collations have been seconded.
|
||||
pub seconded_per_para: usize,
|
||||
// Claims in the claim queue for the `ParaId`.
|
||||
pub claims_per_para: usize,
|
||||
}
|
||||
|
||||
/// Information about collations per relay parent.
|
||||
pub struct Collations {
|
||||
/// What is the current status in regards to a collation for this relay parent?
|
||||
pub status: CollationStatus,
|
||||
/// Collator we're fetching from, optionally which candidate was requested.
|
||||
///
|
||||
/// This is the currently last started fetch, which did not exceed `MAX_UNSHARED_DOWNLOAD_TIME`
|
||||
/// yet.
|
||||
pub fetching_from: Option<(CollatorId, Option<CandidateHash>)>,
|
||||
/// Collation that were advertised to us, but we did not yet request or fetch. Grouped by
|
||||
/// `ParaId`.
|
||||
waiting_queue: BTreeMap<ParaId, VecDeque<(PendingCollation, CollatorId)>>,
|
||||
/// Number of seconded candidates and claims in the claim queue per `ParaId`.
|
||||
candidates_state: BTreeMap<ParaId, CandidatesStatePerPara>,
|
||||
}
|
||||
|
||||
impl Collations {
|
||||
pub(super) fn new(group_assignments: &Vec<ParaId>) -> Self {
|
||||
let mut candidates_state = BTreeMap::<ParaId, CandidatesStatePerPara>::new();
|
||||
|
||||
for para_id in group_assignments {
|
||||
candidates_state.entry(*para_id).or_default().claims_per_para += 1;
|
||||
}
|
||||
|
||||
Self {
|
||||
status: Default::default(),
|
||||
fetching_from: None,
|
||||
waiting_queue: Default::default(),
|
||||
candidates_state,
|
||||
}
|
||||
}
|
||||
|
||||
/// Note a seconded collation for a given para.
|
||||
pub(super) fn note_seconded(&mut self, para_id: ParaId) {
|
||||
self.candidates_state.entry(para_id).or_default().seconded_per_para += 1;
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?para_id,
|
||||
new_count=self.candidates_state.entry(para_id).or_default().seconded_per_para,
|
||||
"Note seconded."
|
||||
);
|
||||
self.status.back_to_waiting();
|
||||
}
|
||||
|
||||
/// Adds a new collation to the waiting queue for the relay parent. This function doesn't
|
||||
/// perform any limits check. The caller should assure that the collation limit is respected.
|
||||
pub(super) fn add_to_waiting_queue(&mut self, collation: (PendingCollation, CollatorId)) {
|
||||
self.waiting_queue.entry(collation.0.para_id).or_default().push_back(collation);
|
||||
}
|
||||
|
||||
/// Picks a collation to fetch from the waiting queue.
|
||||
/// When fetching collations we need to ensure that each teyrchain has got a fair core time
|
||||
/// share depending on its assignments in the claim queue. This means that the number of
|
||||
/// collations seconded per teyrchain should ideally be equal to the number of claims for the
|
||||
/// particular teyrchain in the claim queue.
|
||||
///
|
||||
/// To achieve this each seconded collation is mapped to an entry from the claim queue. The next
|
||||
/// fetch is the first unfulfilled entry from the claim queue for which there is an
|
||||
/// advertisement.
|
||||
///
|
||||
/// `unfulfilled_claim_queue_entries` represents all claim queue entries which are still not
|
||||
/// fulfilled.
|
||||
pub(super) fn pick_a_collation_to_fetch(
|
||||
&mut self,
|
||||
unfulfilled_claim_queue_entries: Vec<ParaId>,
|
||||
) -> Option<(PendingCollation, CollatorId)> {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
waiting_queue=?self.waiting_queue,
|
||||
candidates_state=?self.candidates_state,
|
||||
?unfulfilled_claim_queue_entries,
|
||||
"Pick a collation to fetch."
|
||||
);
|
||||
|
||||
for assignment in unfulfilled_claim_queue_entries {
|
||||
// if there is an unfulfilled assignment - return it
|
||||
if let Some(collation) = self
|
||||
.waiting_queue
|
||||
.get_mut(&assignment)
|
||||
.and_then(|collations| collations.pop_front())
|
||||
{
|
||||
return Some(collation);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn seconded_for_para(&self, para_id: &ParaId) -> usize {
|
||||
self.candidates_state
|
||||
.get(¶_id)
|
||||
.map(|state| state.seconded_per_para)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(super) fn queued_for_para(&self, para_id: &ParaId) -> usize {
|
||||
self.waiting_queue.get(para_id).map(|queue| queue.len()).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
// Any error that can occur when awaiting a collation fetch response.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(super) enum CollationFetchError {
|
||||
#[error("Future was cancelled.")]
|
||||
Cancelled,
|
||||
#[error("{0}")]
|
||||
Request(#[from] RequestError),
|
||||
}
|
||||
|
||||
/// Future that concludes when the collator has responded to our collation fetch request
|
||||
/// or the request was cancelled by the validator.
|
||||
pub(super) struct CollationFetchRequest {
|
||||
/// Info about the requested collation.
|
||||
pub pending_collation: PendingCollation,
|
||||
/// Collator id.
|
||||
pub collator_id: CollatorId,
|
||||
/// The network protocol version the collator is using.
|
||||
pub collator_protocol_version: CollationVersion,
|
||||
/// Responses from collator.
|
||||
pub from_collator: BoxFuture<'static, OutgoingResult<request_v1::CollationFetchingResponse>>,
|
||||
/// Handle used for checking if this request was cancelled.
|
||||
pub cancellation_token: CancellationToken,
|
||||
/// A metric histogram for the lifetime of the request
|
||||
pub _lifetime_timer: Option<HistogramTimer>,
|
||||
}
|
||||
|
||||
impl Future for CollationFetchRequest {
|
||||
type Output = (
|
||||
CollationEvent,
|
||||
std::result::Result<request_v1::CollationFetchingResponse, CollationFetchError>,
|
||||
);
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
||||
// First check if this fetch request was cancelled.
|
||||
let cancelled = match std::pin::pin!(self.cancellation_token.cancelled()).poll(cx) {
|
||||
Poll::Ready(()) => true,
|
||||
Poll::Pending => false,
|
||||
};
|
||||
|
||||
if cancelled {
|
||||
return Poll::Ready((
|
||||
CollationEvent {
|
||||
collator_protocol_version: self.collator_protocol_version,
|
||||
collator_id: self.collator_id.clone(),
|
||||
pending_collation: self.pending_collation,
|
||||
},
|
||||
Err(CollationFetchError::Cancelled),
|
||||
));
|
||||
}
|
||||
|
||||
let res = self.from_collator.poll_unpin(cx).map(|res| {
|
||||
(
|
||||
CollationEvent {
|
||||
collator_protocol_version: self.collator_protocol_version,
|
||||
collator_id: self.collator_id.clone(),
|
||||
pending_collation: self.pending_collation,
|
||||
},
|
||||
res.map_err(CollationFetchError::Request),
|
||||
)
|
||||
});
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// 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 fatality::thiserror::Error;
|
||||
use futures::channel::oneshot;
|
||||
|
||||
use pezkuwi_node_subsystem::RuntimeApiError;
|
||||
use pezkuwi_node_subsystem_util::backing_implicit_view;
|
||||
use pezkuwi_primitives::CandidateDescriptorVersion;
|
||||
|
||||
/// General result.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// General subsystem error.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
ImplicitViewFetchError(backing_implicit_view::FetchError),
|
||||
|
||||
#[error("Response receiver for active validators request cancelled")]
|
||||
CancelledActiveValidators(oneshot::Canceled),
|
||||
|
||||
#[error("Response receiver for validator groups request cancelled")]
|
||||
CancelledValidatorGroups(oneshot::Canceled),
|
||||
|
||||
#[error("Response receiver for session index request cancelled")]
|
||||
CancelledSessionIndex(oneshot::Canceled),
|
||||
|
||||
#[error("Response receiver for claim queue request cancelled")]
|
||||
CancelledClaimQueue(oneshot::Canceled),
|
||||
|
||||
#[error("Response receiver for node features request cancelled")]
|
||||
CancelledNodeFeatures(oneshot::Canceled),
|
||||
|
||||
#[error("No state for the relay parent")]
|
||||
RelayParentStateNotFound,
|
||||
|
||||
#[error("Error while accessing Runtime API")]
|
||||
RuntimeApi(#[from] RuntimeApiError),
|
||||
}
|
||||
|
||||
/// An error occurred when attempting to start seconding a candidate.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SecondingError {
|
||||
#[error("Error while accessing Runtime API")]
|
||||
RuntimeApi(#[from] RuntimeApiError),
|
||||
|
||||
#[error("Response receiver for persisted validation data request cancelled")]
|
||||
CancelledRuntimePersistedValidationData(oneshot::Canceled),
|
||||
|
||||
#[error("Response receiver for prospective validation data request cancelled")]
|
||||
CancelledProspectiveValidationData(oneshot::Canceled),
|
||||
|
||||
#[error("Persisted validation data is not available")]
|
||||
PersistedValidationDataNotFound,
|
||||
|
||||
#[error("Persisted validation data hash doesn't match one in the candidate receipt.")]
|
||||
PersistedValidationDataMismatch,
|
||||
|
||||
#[error("Candidate hash doesn't match the advertisement")]
|
||||
CandidateHashMismatch,
|
||||
|
||||
#[error("Relay parent hash doesn't match the advertisement")]
|
||||
RelayParentMismatch,
|
||||
|
||||
#[error("Received duplicate collation from the peer")]
|
||||
Duplicate,
|
||||
|
||||
#[error("The provided parent head data does not match the hash")]
|
||||
ParentHeadDataMismatch,
|
||||
|
||||
#[error("Core index {0} present in descriptor is different than the assigned core {1}")]
|
||||
InvalidCoreIndex(u32, u32),
|
||||
|
||||
#[error("Session index {0} present in descriptor is different than the expected one {1}")]
|
||||
InvalidSessionIndex(u32, u32),
|
||||
|
||||
#[error("Invalid candidate receipt version {0:?}")]
|
||||
InvalidReceiptVersion(CandidateDescriptorVersion),
|
||||
}
|
||||
|
||||
impl SecondingError {
|
||||
/// Returns true if an error indicates that a peer is malicious.
|
||||
pub fn is_malicious(&self) -> bool {
|
||||
use SecondingError::*;
|
||||
matches!(
|
||||
self,
|
||||
PersistedValidationDataMismatch |
|
||||
CandidateHashMismatch |
|
||||
RelayParentMismatch |
|
||||
ParentHeadDataMismatch |
|
||||
InvalidCoreIndex(_, _) |
|
||||
InvalidSessionIndex(_, _) |
|
||||
InvalidReceiptVersion(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Failed to request a collation due to an error.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FetchError {
|
||||
#[error("Collation was not previously advertised")]
|
||||
NotAdvertised,
|
||||
|
||||
#[error("Peer is unknown")]
|
||||
UnknownPeer,
|
||||
|
||||
#[error("Collation was already requested")]
|
||||
AlreadyRequested,
|
||||
|
||||
#[error("Relay parent went out of view")]
|
||||
RelayParentOutOfView,
|
||||
|
||||
#[error("Peer's protocol doesn't match the advertisement")]
|
||||
ProtocolMismatch,
|
||||
}
|
||||
|
||||
/// Represents a `RelayParentHoldOffState` error
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HoldOffError {
|
||||
#[error("`on_hold_off_complete` called in `NotStarted`")]
|
||||
InvalidStateNotStarted,
|
||||
#[error("`on_hold_off_complete` called in `Done`")]
|
||||
InvalidStateDone,
|
||||
#[error("`on_hold_off_complete` called in the right state but there are no advertisements in the queue")]
|
||||
QueueEmpty,
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// 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::{self, prometheus};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Metrics(Option<MetricsInner>);
|
||||
|
||||
impl Metrics {
|
||||
pub fn on_request(&self, succeeded: std::result::Result<(), ()>) {
|
||||
if let Some(metrics) = &self.0 {
|
||||
match succeeded {
|
||||
Ok(()) => metrics.collation_requests.with_label_values(&["succeeded"]).inc(),
|
||||
Err(()) => metrics.collation_requests.with_label_values(&["failed"]).inc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide a timer for `process_msg` which observes on drop.
|
||||
pub fn time_process_msg(&self) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
|
||||
self.0.as_ref().map(|metrics| metrics.process_msg.start_timer())
|
||||
}
|
||||
|
||||
/// Provide a timer for `handle_collation_request_result` which observes on drop.
|
||||
pub fn time_handle_collation_request_result(
|
||||
&self,
|
||||
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.map(|metrics| metrics.handle_collation_request_result.start_timer())
|
||||
}
|
||||
|
||||
/// Note the current number of collator peers.
|
||||
pub fn note_collator_peer_count(&self, collator_peers: usize) {
|
||||
self.0
|
||||
.as_ref()
|
||||
.map(|metrics| metrics.collator_peer_count.set(collator_peers as u64));
|
||||
}
|
||||
|
||||
/// Provide a timer for `CollationFetchRequest` structure which observes on drop.
|
||||
pub fn time_collation_request_duration(
|
||||
&self,
|
||||
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
|
||||
self.0.as_ref().map(|metrics| metrics.collation_request_duration.start_timer())
|
||||
}
|
||||
|
||||
/// Provide a timer for `request_unblocked_collations` which observes on drop.
|
||||
pub fn time_request_unblocked_collations(
|
||||
&self,
|
||||
) -> Option<metrics::prometheus::prometheus::HistogramTimer> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.map(|metrics| metrics.request_unblocked_collations.start_timer())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MetricsInner {
|
||||
collation_requests: prometheus::CounterVec<prometheus::U64>,
|
||||
process_msg: prometheus::Histogram,
|
||||
handle_collation_request_result: prometheus::Histogram,
|
||||
collator_peer_count: prometheus::Gauge<prometheus::U64>,
|
||||
collation_request_duration: prometheus::Histogram,
|
||||
request_unblocked_collations: prometheus::Histogram,
|
||||
}
|
||||
|
||||
impl metrics::Metrics for Metrics {
|
||||
fn try_register(
|
||||
registry: &prometheus::Registry,
|
||||
) -> std::result::Result<Self, prometheus::PrometheusError> {
|
||||
let metrics = MetricsInner {
|
||||
collation_requests: prometheus::register(
|
||||
prometheus::CounterVec::new(
|
||||
prometheus::Opts::new(
|
||||
"pezkuwi_teyrchain_collation_requests_total",
|
||||
"Number of collations requested from Collators.",
|
||||
),
|
||||
&["success"],
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
process_msg: prometheus::register(
|
||||
prometheus::Histogram::with_opts(
|
||||
prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_collator_protocol_validator_process_msg",
|
||||
"Time spent within `collator_protocol_validator::process_msg`",
|
||||
)
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
handle_collation_request_result: prometheus::register(
|
||||
prometheus::Histogram::with_opts(
|
||||
prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_collator_protocol_validator_handle_collation_request_result",
|
||||
"Time spent within `collator_protocol_validator::handle_collation_request_result`",
|
||||
)
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
collator_peer_count: prometheus::register(
|
||||
prometheus::Gauge::new(
|
||||
"pezkuwi_teyrchain_collator_peer_count",
|
||||
"Amount of collator peers connected",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
collation_request_duration: prometheus::register(
|
||||
prometheus::Histogram::with_opts(
|
||||
prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_collator_protocol_validator_collation_request_duration",
|
||||
"Lifetime of the `CollationFetchRequest` structure",
|
||||
).buckets(vec![0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.75, 0.9, 1.0, 1.2, 1.5, 1.75]),
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
request_unblocked_collations: prometheus::register(
|
||||
prometheus::Histogram::with_opts(
|
||||
prometheus::HistogramOpts::new(
|
||||
"pezkuwi_teyrchain_collator_protocol_validator_request_unblocked_collations",
|
||||
"Time spent within `collator_protocol_validator::request_unblocked_collations`",
|
||||
)
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
};
|
||||
|
||||
Ok(Metrics(Some(metrics)))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3080
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,159 @@
|
||||
// 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::num::NonZeroU16;
|
||||
|
||||
use pezkuwi_node_network_protocol::peer_set::CollationVersion;
|
||||
use pezkuwi_primitives::Id as ParaId;
|
||||
|
||||
/// Maximum reputation score.
|
||||
pub const MAX_SCORE: u16 = 5000;
|
||||
|
||||
/// Limit for the total number connected peers.
|
||||
pub const CONNECTED_PEERS_LIMIT: NonZeroU16 = NonZeroU16::new(300).expect("300 is greater than 0");
|
||||
|
||||
/// Limit for the total number of connected peers for a paraid.
|
||||
/// Must be smaller than `CONNECTED_PEERS_LIMIT`.
|
||||
pub const CONNECTED_PEERS_PARA_LIMIT: NonZeroU16 = const {
|
||||
assert!(CONNECTED_PEERS_LIMIT.get() >= 100);
|
||||
NonZeroU16::new(100).expect("100 is greater than 0")
|
||||
};
|
||||
|
||||
/// Maximum number of relay parents to process for reputation bumps on startup and between finality
|
||||
/// notifications.
|
||||
pub const MAX_STARTUP_ANCESTRY_LOOKBACK: u32 = 20;
|
||||
|
||||
/// Reputation bump for getting a valid candidate included.
|
||||
pub const VALID_INCLUDED_CANDIDATE_BUMP: u16 = 50;
|
||||
|
||||
/// Reputation slash for peer inactivity (for each included candidate of the para that was not
|
||||
/// authored by the peer)
|
||||
pub const INACTIVITY_DECAY: u16 = 1;
|
||||
|
||||
/// Maximum number of stored peer scores for a paraid. Should be greater than
|
||||
/// `CONNECTED_PEERS_PARA_LIMIT`.
|
||||
pub const MAX_STORED_SCORES_PER_PARA: u8 = 150;
|
||||
/// Reputation score type.
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy, Default)]
|
||||
pub struct Score(u16);
|
||||
|
||||
impl Score {
|
||||
/// Create a new instance. Fail if over the `MAX_SCORE`.
|
||||
pub const fn new(val: u16) -> Option<Self> {
|
||||
if val > MAX_SCORE {
|
||||
None
|
||||
} else {
|
||||
Some(Self(val))
|
||||
}
|
||||
}
|
||||
|
||||
/// Add `val` to the inner value, saturating at `MAX_SCORE`.
|
||||
pub fn saturating_add(&mut self, val: u16) {
|
||||
if (self.0 + val) <= MAX_SCORE {
|
||||
self.0 += val;
|
||||
} else {
|
||||
self.0 = MAX_SCORE;
|
||||
}
|
||||
}
|
||||
|
||||
/// Subtract `val` from the inner value, saturating at 0.
|
||||
pub fn saturating_sub(&mut self, val: u16) {
|
||||
self.0 = self.0.saturating_sub(val);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Score> for u16 {
|
||||
fn from(value: Score) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a connected peer.
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub struct PeerInfo {
|
||||
/// Protocol version.
|
||||
pub version: CollationVersion,
|
||||
/// State of the peer.
|
||||
pub state: PeerState,
|
||||
}
|
||||
|
||||
/// State of a connected peer
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum PeerState {
|
||||
/// Connected.
|
||||
Connected,
|
||||
/// Peer has declared.
|
||||
Collating(ParaId),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Test that the `Score` functions are working correctly.
|
||||
#[test]
|
||||
fn score_functions() {
|
||||
assert!(MAX_SCORE > 50);
|
||||
|
||||
// Test that the constructor returns None for values that exceed the limit.
|
||||
for score in (0..MAX_SCORE).step_by(10) {
|
||||
assert_eq!(u16::from(Score::new(score).unwrap()), score);
|
||||
}
|
||||
assert_eq!(u16::from(Score::new(MAX_SCORE).unwrap()), MAX_SCORE);
|
||||
for score in ((MAX_SCORE + 1)..(MAX_SCORE + 50)).step_by(5) {
|
||||
assert_eq!(Score::new(score), None);
|
||||
}
|
||||
|
||||
// Test saturating arithmetic functions.
|
||||
let score = Score::new(50).unwrap();
|
||||
|
||||
// Test addition with value that does not go over the limit.
|
||||
for other_score in (0..(MAX_SCORE - 50)).step_by(10) {
|
||||
let expected_value = u16::from(score) + other_score;
|
||||
|
||||
let mut score = score;
|
||||
score.saturating_add(other_score);
|
||||
|
||||
assert_eq!(expected_value, u16::from(score));
|
||||
}
|
||||
|
||||
// Test overflowing addition.
|
||||
for other_score in ((MAX_SCORE - 50)..MAX_SCORE).step_by(10) {
|
||||
let mut score = score;
|
||||
score.saturating_add(other_score);
|
||||
|
||||
assert_eq!(MAX_SCORE, u16::from(score));
|
||||
}
|
||||
|
||||
// Test subtraction with value that does not go under zero.
|
||||
for other_score in (0..50).step_by(10) {
|
||||
let expected_value = u16::from(score) - other_score;
|
||||
|
||||
let mut score = score;
|
||||
score.saturating_sub(other_score);
|
||||
|
||||
assert_eq!(expected_value, u16::from(score));
|
||||
}
|
||||
|
||||
// Test underflowing subtraction.
|
||||
for other_score in (50..100).step_by(10) {
|
||||
let mut score = score;
|
||||
score.saturating_sub(other_score);
|
||||
|
||||
assert_eq!(0, u16::from(score));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// 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 crate::LOG_TARGET;
|
||||
use fatality::Nested;
|
||||
use pezkuwi_node_subsystem::{ChainApiError, SubsystemError};
|
||||
use pezkuwi_node_subsystem_util::runtime;
|
||||
use pezkuwi_primitives::Hash;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub type FatalResult<T> = std::result::Result<T, FatalError>;
|
||||
|
||||
#[fatality::fatality(splitable)]
|
||||
pub enum Error {
|
||||
#[fatal]
|
||||
#[error("Oneshot for receiving ancestors from chain API got cancelled")]
|
||||
CanceledAncestors,
|
||||
#[fatal]
|
||||
#[error("Oneshot for receiving finalized block number from chain API got cancelled")]
|
||||
CanceledFinalizedBlockNumber,
|
||||
#[fatal]
|
||||
#[error("Oneshot for receiving finalized block hash from chain API got cancelled")]
|
||||
CanceledFinalizedBlockHash,
|
||||
#[error("Finalized block hash for {0} not found")]
|
||||
FinalizedBlockNotFound(u32),
|
||||
#[error(transparent)]
|
||||
ChainApi(#[from] ChainApiError),
|
||||
#[fatal(forward)]
|
||||
#[error("Error while accessing runtime information {0}")]
|
||||
Runtime(#[from] runtime::Error),
|
||||
#[fatal]
|
||||
#[error("Receiving message from overseer failed: {0}")]
|
||||
SubsystemReceive(#[source] SubsystemError),
|
||||
}
|
||||
|
||||
/// 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<()>) -> FatalResult<()> {
|
||||
match result.into_nested()? {
|
||||
Ok(()) => Ok(()),
|
||||
Err(jfyi) => {
|
||||
jfyi.log();
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl JfyiError {
|
||||
/// Log a `JfyiError`.
|
||||
pub fn log(self) {
|
||||
gum::warn!(target: LOG_TARGET, error = ?self);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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::{self, prometheus};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Metrics;
|
||||
|
||||
impl metrics::Metrics for Metrics {
|
||||
fn try_register(
|
||||
_registry: &prometheus::Registry,
|
||||
) -> std::result::Result<Self, prometheus::PrometheusError> {
|
||||
Ok(Metrics)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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/>.
|
||||
|
||||
#![allow(unused)]
|
||||
|
||||
// See reasoning in Cargo.toml why this temporary useless import is needed.
|
||||
use tokio as _;
|
||||
|
||||
mod common;
|
||||
mod error;
|
||||
mod metrics;
|
||||
mod peer_manager;
|
||||
mod state;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use common::MAX_STORED_SCORES_PER_PARA;
|
||||
use error::{log_error, FatalError, FatalResult, Result};
|
||||
use fatality::Split;
|
||||
use peer_manager::{Db, PeerManager};
|
||||
use pezkuwi_node_subsystem::{
|
||||
overseer, ActivatedLeaf, CollatorProtocolSenderTrait, FromOrchestra, OverseerSignal,
|
||||
};
|
||||
use pezkuwi_node_subsystem_util::{
|
||||
find_validator_group, request_claim_queue, request_validator_groups, request_validators,
|
||||
runtime::recv_runtime, signing_key_and_index,
|
||||
};
|
||||
use pezkuwi_primitives::{Hash, Id as ParaId};
|
||||
use sp_keystore::KeystorePtr;
|
||||
use state::State;
|
||||
|
||||
pub use metrics::Metrics;
|
||||
|
||||
use crate::LOG_TARGET;
|
||||
|
||||
/// The main run loop.
|
||||
#[overseer::contextbounds(CollatorProtocol, prefix = self::overseer)]
|
||||
pub(crate) async fn run<Context>(
|
||||
mut ctx: Context,
|
||||
keystore: KeystorePtr,
|
||||
metrics: Metrics,
|
||||
) -> FatalResult<()> {
|
||||
if let Some(_state) = initialize(&mut ctx, keystore, metrics).await? {
|
||||
// run_inner(state);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[overseer::contextbounds(CollatorProtocol, prefix = self::overseer)]
|
||||
async fn initialize<Context>(
|
||||
ctx: &mut Context,
|
||||
keystore: KeystorePtr,
|
||||
metrics: Metrics,
|
||||
) -> FatalResult<Option<State<Db>>> {
|
||||
loop {
|
||||
let first_leaf = match wait_for_first_leaf(ctx).await? {
|
||||
Some(activated_leaf) => activated_leaf,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let scheduled_paras = match scheduled_paras(ctx.sender(), first_leaf.hash, &keystore).await
|
||||
{
|
||||
Ok(paras) => paras,
|
||||
Err(err) => {
|
||||
log_error(Err(err))?;
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
let backend = Db::new(MAX_STORED_SCORES_PER_PARA).await;
|
||||
|
||||
match PeerManager::startup(backend, ctx.sender(), scheduled_paras.into_iter().collect())
|
||||
.await
|
||||
{
|
||||
Ok(peer_manager) => return Ok(Some(State::new(peer_manager, keystore, metrics))),
|
||||
Err(err) => {
|
||||
log_error(Err(err))?;
|
||||
continue;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for `ActiveLeavesUpdate`, returns `None` if `Conclude` signal came first.
|
||||
#[overseer::contextbounds(CollatorProtocol, prefix = self::overseer)]
|
||||
async fn wait_for_first_leaf<Context>(ctx: &mut Context) -> FatalResult<Option<ActivatedLeaf>> {
|
||||
loop {
|
||||
match ctx.recv().await.map_err(FatalError::SubsystemReceive)? {
|
||||
FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(None),
|
||||
FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update)) => {
|
||||
if let Some(activated) = update.activated {
|
||||
return Ok(Some(activated));
|
||||
}
|
||||
},
|
||||
FromOrchestra::Signal(OverseerSignal::BlockFinalized(_, _)) => {},
|
||||
FromOrchestra::Communication { msg } => {
|
||||
// TODO: we should actually disconnect peers connected on collation protocol while
|
||||
// we're still bootstrapping. OR buffer these messages until we've bootstrapped.
|
||||
gum::warn!(
|
||||
target: LOG_TARGET,
|
||||
?msg,
|
||||
"Received msg before first active leaves update. This is not expected - message will be dropped."
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn scheduled_paras<Sender: CollatorProtocolSenderTrait>(
|
||||
sender: &mut Sender,
|
||||
hash: Hash,
|
||||
keystore: &KeystorePtr,
|
||||
) -> Result<VecDeque<ParaId>> {
|
||||
let validators = recv_runtime(request_validators(hash, sender).await).await?;
|
||||
|
||||
let (groups, rotation_info) =
|
||||
recv_runtime(request_validator_groups(hash, sender).await).await?;
|
||||
|
||||
let core_now = if let Some(group) = signing_key_and_index(&validators, keystore)
|
||||
.and_then(|(_, index)| find_validator_group(&groups, index))
|
||||
{
|
||||
rotation_info.core_for_group(group, groups.len())
|
||||
} else {
|
||||
gum::trace!(target: LOG_TARGET, ?hash, "Not a validator");
|
||||
return Ok(VecDeque::new());
|
||||
};
|
||||
|
||||
let mut claim_queue = recv_runtime(request_claim_queue(hash, sender).await).await?;
|
||||
Ok(claim_queue.remove(&core_now).unwrap_or_else(|| VecDeque::new()))
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
// 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 crate::validator_side_experimental::{common::Score, peer_manager::ReputationUpdate};
|
||||
use async_trait::async_trait;
|
||||
use pezkuwi_node_network_protocol::PeerId;
|
||||
use pezkuwi_primitives::{BlockNumber, Id as ParaId};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
/// Trait describing the interface of the reputation database.
|
||||
#[async_trait]
|
||||
pub trait Backend {
|
||||
/// Return the latest finalized block for which the backend processed bumps.
|
||||
async fn processed_finalized_block_number(&self) -> Option<BlockNumber>;
|
||||
/// Get the peer's stored reputation for this paraid, if any.
|
||||
async fn query(&self, peer_id: &PeerId, para_id: &ParaId) -> Option<Score>;
|
||||
/// Slash the peer's reputation for this paraid, with the given value.
|
||||
async fn slash(&mut self, peer_id: &PeerId, para_id: &ParaId, value: Score);
|
||||
/// Prune all data for paraids that are no longer in this registered set.
|
||||
async fn prune_paras(&mut self, registered_paras: BTreeSet<ParaId>);
|
||||
/// Process the reputation bumps, returning all the reputation changes that were done in
|
||||
/// consequence. This is needed because a reputation bump for a para also means a reputation
|
||||
/// decay for the other collators of that para (if the `decay_value` param is present) and
|
||||
/// because if the number of stored reputations go over the `stored_limit_per_para`, we'll 100%
|
||||
/// slash the least recently bumped peers. `leaf_number` needs to be at least equal to the
|
||||
/// `processed_finalized_block_number`
|
||||
async fn process_bumps(
|
||||
&mut self,
|
||||
leaf_number: BlockNumber,
|
||||
bumps: BTreeMap<ParaId, HashMap<PeerId, Score>>,
|
||||
decay_value: Option<Score>,
|
||||
) -> Vec<ReputationUpdate>;
|
||||
}
|
||||
+1280
File diff suppressed because it is too large
Load Diff
+765
@@ -0,0 +1,765 @@
|
||||
// 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 crate::validator_side_experimental::{
|
||||
common::Score,
|
||||
peer_manager::{backend::Backend, ReputationUpdate, ReputationUpdateKind},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use pezkuwi_node_network_protocol::PeerId;
|
||||
use pezkuwi_primitives::{BlockNumber, Hash, Id as ParaId};
|
||||
use std::{
|
||||
collections::{btree_map, hash_map, BTreeMap, BTreeSet, HashMap},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
/// This is an in-memory temporary implementation for the DB, to be used only for prototyping and
|
||||
/// testing purposes.
|
||||
pub struct Db {
|
||||
db: BTreeMap<ParaId, HashMap<PeerId, ScoreEntry>>,
|
||||
last_finalized: Option<BlockNumber>,
|
||||
stored_limit_per_para: u8,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
/// Create a new instance of the in-memory DB.
|
||||
///
|
||||
/// `stored_limit_per_para` is the maximum number of reputations that can be stored per para.
|
||||
pub async fn new(stored_limit_per_para: u8) -> Self {
|
||||
Self { db: BTreeMap::new(), last_finalized: None, stored_limit_per_para }
|
||||
}
|
||||
}
|
||||
|
||||
type Timestamp = u128;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ScoreEntry {
|
||||
score: Score,
|
||||
last_bumped: Timestamp,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Backend for Db {
|
||||
async fn processed_finalized_block_number(&self) -> Option<BlockNumber> {
|
||||
self.last_finalized
|
||||
}
|
||||
|
||||
async fn query(&self, peer_id: &PeerId, para_id: &ParaId) -> Option<Score> {
|
||||
self.db.get(para_id).and_then(|per_para| per_para.get(peer_id).map(|e| e.score))
|
||||
}
|
||||
|
||||
async fn slash(&mut self, peer_id: &PeerId, para_id: &ParaId, value: Score) {
|
||||
if let btree_map::Entry::Occupied(mut per_para_entry) = self.db.entry(*para_id) {
|
||||
if let hash_map::Entry::Occupied(mut e) = per_para_entry.get_mut().entry(*peer_id) {
|
||||
let score = e.get_mut().score;
|
||||
// Remove the entry if it goes to zero.
|
||||
if score <= value {
|
||||
e.remove();
|
||||
} else {
|
||||
e.get_mut().score.saturating_sub(value.into());
|
||||
}
|
||||
}
|
||||
|
||||
// If the per_para length went to 0, remove it completely
|
||||
if per_para_entry.get().is_empty() {
|
||||
per_para_entry.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn prune_paras(&mut self, registered_paras: BTreeSet<ParaId>) {
|
||||
self.db.retain(|para, _| registered_paras.contains(¶));
|
||||
}
|
||||
|
||||
async fn process_bumps(
|
||||
&mut self,
|
||||
leaf_number: BlockNumber,
|
||||
bumps: BTreeMap<ParaId, HashMap<PeerId, Score>>,
|
||||
decay_value: Option<Score>,
|
||||
) -> Vec<ReputationUpdate> {
|
||||
if self.last_finalized.unwrap_or(0) >= leaf_number {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
self.last_finalized = Some(leaf_number);
|
||||
self.bump_reputations(bumps, decay_value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Db {
|
||||
fn bump_reputations(
|
||||
&mut self,
|
||||
bumps: BTreeMap<ParaId, HashMap<PeerId, Score>>,
|
||||
maybe_decay_value: Option<Score>,
|
||||
) -> Vec<ReputationUpdate> {
|
||||
let mut reported_updates = vec![];
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
|
||||
|
||||
for (para, bumps_per_para) in bumps {
|
||||
reported_updates.reserve(bumps_per_para.len());
|
||||
|
||||
for (peer_id, bump) in bumps_per_para.iter() {
|
||||
if u16::from(*bump) == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.db
|
||||
.entry(para)
|
||||
.or_default()
|
||||
.entry(*peer_id)
|
||||
.and_modify(|e| {
|
||||
e.score.saturating_add(u16::from(*bump));
|
||||
e.last_bumped = now;
|
||||
})
|
||||
.or_insert(ScoreEntry { score: *bump, last_bumped: now });
|
||||
|
||||
reported_updates.push(ReputationUpdate {
|
||||
peer_id: *peer_id,
|
||||
para_id: para,
|
||||
value: *bump,
|
||||
kind: ReputationUpdateKind::Bump,
|
||||
});
|
||||
}
|
||||
|
||||
if let btree_map::Entry::Occupied(mut per_para_entry) = self.db.entry(para) {
|
||||
if let Some(decay_value) = maybe_decay_value {
|
||||
let peers_to_slash = per_para_entry
|
||||
.get()
|
||||
.keys()
|
||||
.filter(|peer_id| !bumps_per_para.contains_key(peer_id))
|
||||
.copied()
|
||||
.collect::<Vec<PeerId>>();
|
||||
|
||||
for peer_id in peers_to_slash {
|
||||
if let hash_map::Entry::Occupied(mut e) =
|
||||
per_para_entry.get_mut().entry(peer_id)
|
||||
{
|
||||
// Remove the entry if it goes to zero.
|
||||
if e.get_mut().score <= decay_value {
|
||||
let score = e.remove().score;
|
||||
reported_updates.push(ReputationUpdate {
|
||||
peer_id,
|
||||
para_id: para,
|
||||
value: score,
|
||||
kind: ReputationUpdateKind::Slash,
|
||||
});
|
||||
} else {
|
||||
e.get_mut().score.saturating_sub(decay_value.into());
|
||||
reported_updates.push(ReputationUpdate {
|
||||
peer_id,
|
||||
para_id: para,
|
||||
value: decay_value,
|
||||
kind: ReputationUpdateKind::Slash,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let per_para_limit = self.stored_limit_per_para as usize;
|
||||
if per_para_entry.get().is_empty() {
|
||||
// If the per_para length went to 0, remove it completely
|
||||
per_para_entry.remove();
|
||||
} else if per_para_entry.get().len() > per_para_limit {
|
||||
// We have exceeded the maximum capacity, in which case we need to prune
|
||||
// the least recently bumped values
|
||||
let diff = per_para_entry.get().len() - per_para_limit;
|
||||
Self::prune_for_para(¶, &mut per_para_entry, diff, &mut reported_updates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reported_updates
|
||||
}
|
||||
|
||||
fn prune_for_para(
|
||||
para_id: &ParaId,
|
||||
per_para: &mut btree_map::OccupiedEntry<ParaId, HashMap<PeerId, ScoreEntry>>,
|
||||
diff: usize,
|
||||
reported_updates: &mut Vec<ReputationUpdate>,
|
||||
) {
|
||||
for _ in 0..diff {
|
||||
let (peer_id_to_remove, score) = per_para
|
||||
.get()
|
||||
.iter()
|
||||
.min_by_key(|(_peer, entry)| entry.last_bumped)
|
||||
.map(|(peer, entry)| (*peer, entry.score))
|
||||
.expect("We know there are enough reps over the limit");
|
||||
|
||||
per_para.get_mut().remove(&peer_id_to_remove);
|
||||
|
||||
reported_updates.push(ReputationUpdate {
|
||||
peer_id: peer_id_to_remove,
|
||||
para_id: *para_id,
|
||||
value: score,
|
||||
kind: ReputationUpdateKind::Slash,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn len(&self) -> usize {
|
||||
self.db.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
// Test different types of reputation updates and their effects.
|
||||
async fn test_reputation_updates() {
|
||||
let mut db = Db::new(10).await;
|
||||
assert_eq!(db.processed_finalized_block_number().await, None);
|
||||
assert_eq!(db.len(), 0);
|
||||
|
||||
// Test empty update with no decay.
|
||||
assert!(db.process_bumps(10, Default::default(), None).await.is_empty());
|
||||
assert_eq!(db.processed_finalized_block_number().await, Some(10));
|
||||
assert_eq!(db.len(), 0);
|
||||
|
||||
// Test a query on a non-existant entry.
|
||||
assert_eq!(db.query(&PeerId::random(), &ParaId::from(1000)).await, None);
|
||||
|
||||
// Test empty update with decay.
|
||||
assert!(db
|
||||
.process_bumps(11, Default::default(), Some(Score::new(1).unwrap()))
|
||||
.await
|
||||
.is_empty());
|
||||
assert_eq!(db.processed_finalized_block_number().await, Some(11));
|
||||
assert_eq!(db.len(), 0);
|
||||
|
||||
// Test empty update with a leaf number smaller than the latest one.
|
||||
assert!(db
|
||||
.process_bumps(5, Default::default(), Some(Score::new(1).unwrap()))
|
||||
.await
|
||||
.is_empty());
|
||||
assert_eq!(db.processed_finalized_block_number().await, Some(11));
|
||||
assert_eq!(db.len(), 0);
|
||||
|
||||
// Test an update with zeroed score.
|
||||
assert!(db
|
||||
.process_bumps(
|
||||
12,
|
||||
[(
|
||||
ParaId::from(100),
|
||||
[(PeerId::random(), Score::new(0).unwrap())].into_iter().collect()
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
Some(Score::new(1).unwrap())
|
||||
)
|
||||
.await
|
||||
.is_empty());
|
||||
assert_eq!(db.processed_finalized_block_number().await, Some(12));
|
||||
assert_eq!(db.len(), 0);
|
||||
|
||||
// Reuse the same 12 block height, it should not be taken into consideration.
|
||||
let first_peer_id = PeerId::random();
|
||||
let first_para_id = ParaId::from(100);
|
||||
assert!(db
|
||||
.process_bumps(
|
||||
12,
|
||||
[(first_para_id, [(first_peer_id, Score::new(10).unwrap())].into_iter().collect())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
Some(Score::new(1).unwrap())
|
||||
)
|
||||
.await
|
||||
.is_empty());
|
||||
assert_eq!(db.processed_finalized_block_number().await, Some(12));
|
||||
assert_eq!(db.len(), 0);
|
||||
assert_eq!(db.query(&first_peer_id, &first_para_id).await, None);
|
||||
|
||||
// Test a non-zero update on an empty DB.
|
||||
assert_eq!(
|
||||
db.process_bumps(
|
||||
13,
|
||||
[(first_para_id, [(first_peer_id, Score::new(10).unwrap())].into_iter().collect())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
Some(Score::new(1).unwrap())
|
||||
)
|
||||
.await,
|
||||
vec![ReputationUpdate {
|
||||
peer_id: first_peer_id,
|
||||
para_id: first_para_id,
|
||||
kind: ReputationUpdateKind::Bump,
|
||||
value: Score::new(10).unwrap()
|
||||
}]
|
||||
);
|
||||
assert_eq!(db.processed_finalized_block_number().await, Some(13));
|
||||
assert_eq!(db.len(), 1);
|
||||
assert_eq!(
|
||||
db.query(&first_peer_id, &first_para_id).await.unwrap(),
|
||||
Score::new(10).unwrap()
|
||||
);
|
||||
// Query a non-existant peer_id for this para.
|
||||
assert_eq!(db.query(&PeerId::random(), &first_para_id).await, None);
|
||||
// Query this peer's rep for a different para.
|
||||
assert_eq!(db.query(&first_peer_id, &ParaId::from(200)).await, None);
|
||||
|
||||
// Test a subsequent update with a lower block height. Will be ignored.
|
||||
assert!(db
|
||||
.process_bumps(
|
||||
10,
|
||||
[(first_para_id, [(first_peer_id, Score::new(10).unwrap())].into_iter().collect())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
Some(Score::new(1).unwrap())
|
||||
)
|
||||
.await
|
||||
.is_empty());
|
||||
assert_eq!(db.processed_finalized_block_number().await, Some(13));
|
||||
assert_eq!(db.len(), 1);
|
||||
assert_eq!(
|
||||
db.query(&first_peer_id, &first_para_id).await.unwrap(),
|
||||
Score::new(10).unwrap()
|
||||
);
|
||||
|
||||
let second_para_id = ParaId::from(200);
|
||||
let second_peer_id = PeerId::random();
|
||||
// Test a subsequent update with no decay.
|
||||
assert_eq!(
|
||||
db.process_bumps(
|
||||
14,
|
||||
[
|
||||
(
|
||||
first_para_id,
|
||||
[(second_peer_id, Score::new(10).unwrap())].into_iter().collect()
|
||||
),
|
||||
(
|
||||
second_para_id,
|
||||
[(first_peer_id, Score::new(5).unwrap())].into_iter().collect()
|
||||
)
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
None
|
||||
)
|
||||
.await,
|
||||
vec![
|
||||
ReputationUpdate {
|
||||
peer_id: second_peer_id,
|
||||
para_id: first_para_id,
|
||||
kind: ReputationUpdateKind::Bump,
|
||||
value: Score::new(10).unwrap()
|
||||
},
|
||||
ReputationUpdate {
|
||||
peer_id: first_peer_id,
|
||||
para_id: second_para_id,
|
||||
kind: ReputationUpdateKind::Bump,
|
||||
value: Score::new(5).unwrap()
|
||||
}
|
||||
]
|
||||
);
|
||||
assert_eq!(db.len(), 2);
|
||||
assert_eq!(db.processed_finalized_block_number().await, Some(14));
|
||||
assert_eq!(
|
||||
db.query(&first_peer_id, &first_para_id).await.unwrap(),
|
||||
Score::new(10).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
db.query(&second_peer_id, &first_para_id).await.unwrap(),
|
||||
Score::new(10).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
db.query(&first_peer_id, &second_para_id).await.unwrap(),
|
||||
Score::new(5).unwrap()
|
||||
);
|
||||
|
||||
// Empty update with decay has no effect.
|
||||
assert!(db
|
||||
.process_bumps(15, Default::default(), Some(Score::new(1).unwrap()))
|
||||
.await
|
||||
.is_empty());
|
||||
assert_eq!(db.processed_finalized_block_number().await, Some(15));
|
||||
assert_eq!(db.len(), 2);
|
||||
assert_eq!(
|
||||
db.query(&first_peer_id, &first_para_id).await.unwrap(),
|
||||
Score::new(10).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
db.query(&second_peer_id, &first_para_id).await.unwrap(),
|
||||
Score::new(10).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
db.query(&first_peer_id, &second_para_id).await.unwrap(),
|
||||
Score::new(5).unwrap()
|
||||
);
|
||||
|
||||
// Test a subsequent update with decay.
|
||||
assert_eq!(
|
||||
db.process_bumps(
|
||||
16,
|
||||
[
|
||||
(
|
||||
first_para_id,
|
||||
[(first_peer_id, Score::new(10).unwrap())].into_iter().collect()
|
||||
),
|
||||
(
|
||||
second_para_id,
|
||||
[(second_peer_id, Score::new(10).unwrap())].into_iter().collect()
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
Some(Score::new(1).unwrap())
|
||||
)
|
||||
.await,
|
||||
vec![
|
||||
ReputationUpdate {
|
||||
peer_id: first_peer_id,
|
||||
para_id: first_para_id,
|
||||
kind: ReputationUpdateKind::Bump,
|
||||
value: Score::new(10).unwrap()
|
||||
},
|
||||
ReputationUpdate {
|
||||
peer_id: second_peer_id,
|
||||
para_id: first_para_id,
|
||||
kind: ReputationUpdateKind::Slash,
|
||||
value: Score::new(1).unwrap()
|
||||
},
|
||||
ReputationUpdate {
|
||||
peer_id: second_peer_id,
|
||||
para_id: second_para_id,
|
||||
kind: ReputationUpdateKind::Bump,
|
||||
value: Score::new(10).unwrap()
|
||||
},
|
||||
ReputationUpdate {
|
||||
peer_id: first_peer_id,
|
||||
para_id: second_para_id,
|
||||
kind: ReputationUpdateKind::Slash,
|
||||
value: Score::new(1).unwrap()
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(db.processed_finalized_block_number().await, Some(16));
|
||||
assert_eq!(db.len(), 2);
|
||||
assert_eq!(
|
||||
db.query(&first_peer_id, &first_para_id).await.unwrap(),
|
||||
Score::new(20).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
db.query(&second_peer_id, &first_para_id).await.unwrap(),
|
||||
Score::new(9).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
db.query(&first_peer_id, &second_para_id).await.unwrap(),
|
||||
Score::new(4).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
db.query(&second_peer_id, &second_para_id).await.unwrap(),
|
||||
Score::new(10).unwrap()
|
||||
);
|
||||
|
||||
// Test a decay that makes the reputation go to 0 (The peer's entry will be removed)
|
||||
assert_eq!(
|
||||
db.process_bumps(
|
||||
17,
|
||||
[(
|
||||
second_para_id,
|
||||
[(second_peer_id, Score::new(10).unwrap())].into_iter().collect()
|
||||
),]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
Some(Score::new(5).unwrap())
|
||||
)
|
||||
.await,
|
||||
vec![
|
||||
ReputationUpdate {
|
||||
peer_id: second_peer_id,
|
||||
para_id: second_para_id,
|
||||
kind: ReputationUpdateKind::Bump,
|
||||
value: Score::new(10).unwrap()
|
||||
},
|
||||
ReputationUpdate {
|
||||
peer_id: first_peer_id,
|
||||
para_id: second_para_id,
|
||||
kind: ReputationUpdateKind::Slash,
|
||||
value: Score::new(4).unwrap()
|
||||
}
|
||||
]
|
||||
);
|
||||
assert_eq!(db.processed_finalized_block_number().await, Some(17));
|
||||
assert_eq!(db.len(), 2);
|
||||
assert_eq!(
|
||||
db.query(&first_peer_id, &first_para_id).await.unwrap(),
|
||||
Score::new(20).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
db.query(&second_peer_id, &first_para_id).await.unwrap(),
|
||||
Score::new(9).unwrap()
|
||||
);
|
||||
assert_eq!(db.query(&first_peer_id, &second_para_id).await, None);
|
||||
assert_eq!(
|
||||
db.query(&second_peer_id, &second_para_id).await.unwrap(),
|
||||
Score::new(20).unwrap()
|
||||
);
|
||||
|
||||
// Test an update which ends up pruning least recently used entries. The per-para limit is
|
||||
// 10.
|
||||
let mut db = Db::new(10).await;
|
||||
let peer_ids = (0..10).map(|_| PeerId::random()).collect::<Vec<_>>();
|
||||
|
||||
// Add an equal reputation for all peers.
|
||||
assert_eq!(
|
||||
db.process_bumps(
|
||||
1,
|
||||
[(
|
||||
first_para_id,
|
||||
peer_ids.iter().map(|peer_id| (*peer_id, Score::new(10).unwrap())).collect()
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.len(),
|
||||
10
|
||||
);
|
||||
assert_eq!(db.len(), 1);
|
||||
|
||||
for peer_id in peer_ids.iter() {
|
||||
assert_eq!(db.query(peer_id, &first_para_id).await.unwrap(), Score::new(10).unwrap());
|
||||
}
|
||||
|
||||
// Now sleep for one second and then bump the reputations of all peers except for the one
|
||||
// with 4th index. We need to sleep so that the update time of the 4th peer is older than
|
||||
// the rest.
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
assert_eq!(
|
||||
db.process_bumps(
|
||||
2,
|
||||
[(
|
||||
first_para_id,
|
||||
peer_ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(
|
||||
|(i, peer_id)| (i != 4).then_some((*peer_id, Score::new(10).unwrap()))
|
||||
)
|
||||
.collect()
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
Some(Score::new(5).unwrap()),
|
||||
)
|
||||
.await
|
||||
.len(),
|
||||
10
|
||||
);
|
||||
|
||||
for (i, peer_id) in peer_ids.iter().enumerate() {
|
||||
if i == 4 {
|
||||
assert_eq!(
|
||||
db.query(peer_id, &first_para_id).await.unwrap(),
|
||||
Score::new(5).unwrap()
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
db.query(peer_id, &first_para_id).await.unwrap(),
|
||||
Score::new(20).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Now add a 11th peer. It should evict the 4th peer.
|
||||
let new_peer = PeerId::random();
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
assert_eq!(
|
||||
db.process_bumps(
|
||||
3,
|
||||
[(first_para_id, [(new_peer, Score::new(10).unwrap())].into_iter().collect())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
Some(Score::new(5).unwrap()),
|
||||
)
|
||||
.await
|
||||
.len(),
|
||||
11
|
||||
);
|
||||
for (i, peer_id) in peer_ids.iter().enumerate() {
|
||||
if i == 4 {
|
||||
assert_eq!(db.query(peer_id, &first_para_id).await, None);
|
||||
} else {
|
||||
assert_eq!(
|
||||
db.query(peer_id, &first_para_id).await.unwrap(),
|
||||
Score::new(15).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(db.query(&new_peer, &first_para_id).await.unwrap(), Score::new(10).unwrap());
|
||||
|
||||
// Now try adding yet another peer. The decay would naturally evict the new peer so no need
|
||||
// to evict the least recently bumped.
|
||||
let yet_another_peer = PeerId::random();
|
||||
assert_eq!(
|
||||
db.process_bumps(
|
||||
4,
|
||||
[(
|
||||
first_para_id,
|
||||
[(yet_another_peer, Score::new(10).unwrap())].into_iter().collect()
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
Some(Score::new(10).unwrap()),
|
||||
)
|
||||
.await
|
||||
.len(),
|
||||
11
|
||||
);
|
||||
for (i, peer_id) in peer_ids.iter().enumerate() {
|
||||
if i == 4 {
|
||||
assert_eq!(db.query(peer_id, &first_para_id).await, None);
|
||||
} else {
|
||||
assert_eq!(
|
||||
db.query(peer_id, &first_para_id).await.unwrap(),
|
||||
Score::new(5).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(db.query(&new_peer, &first_para_id).await, None);
|
||||
assert_eq!(
|
||||
db.query(&yet_another_peer, &first_para_id).await,
|
||||
Some(Score::new(10).unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
// Test reputation slashes.
|
||||
async fn test_slash() {
|
||||
let mut db = Db::new(10).await;
|
||||
|
||||
// Test slash on empty DB
|
||||
let peer_id = PeerId::random();
|
||||
db.slash(&peer_id, &ParaId::from(100), Score::new(50).unwrap()).await;
|
||||
assert_eq!(db.query(&peer_id, &ParaId::from(100)).await, None);
|
||||
|
||||
// Test slash on non-existent para
|
||||
let another_peer_id = PeerId::random();
|
||||
assert_eq!(
|
||||
db.process_bumps(
|
||||
1,
|
||||
[
|
||||
(ParaId::from(100), [(peer_id, Score::new(10).unwrap())].into_iter().collect()),
|
||||
(
|
||||
ParaId::from(200),
|
||||
[(another_peer_id, Score::new(12).unwrap())].into_iter().collect()
|
||||
),
|
||||
(ParaId::from(300), [(peer_id, Score::new(15).unwrap())].into_iter().collect())
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
Some(Score::new(10).unwrap()),
|
||||
)
|
||||
.await
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
assert_eq!(db.query(&peer_id, &ParaId::from(100)).await.unwrap(), Score::new(10).unwrap());
|
||||
assert_eq!(
|
||||
db.query(&another_peer_id, &ParaId::from(200)).await.unwrap(),
|
||||
Score::new(12).unwrap()
|
||||
);
|
||||
assert_eq!(db.query(&peer_id, &ParaId::from(300)).await.unwrap(), Score::new(15).unwrap());
|
||||
|
||||
db.slash(&peer_id, &ParaId::from(200), Score::new(4).unwrap()).await;
|
||||
assert_eq!(db.query(&peer_id, &ParaId::from(100)).await.unwrap(), Score::new(10).unwrap());
|
||||
assert_eq!(
|
||||
db.query(&another_peer_id, &ParaId::from(200)).await.unwrap(),
|
||||
Score::new(12).unwrap()
|
||||
);
|
||||
assert_eq!(db.query(&peer_id, &ParaId::from(300)).await.unwrap(), Score::new(15).unwrap());
|
||||
|
||||
// Test regular slash
|
||||
db.slash(&peer_id, &ParaId::from(100), Score::new(4).unwrap()).await;
|
||||
assert_eq!(db.query(&peer_id, &ParaId::from(100)).await.unwrap(), Score::new(6).unwrap());
|
||||
|
||||
// Test slash which removes the entry altogether
|
||||
db.slash(&peer_id, &ParaId::from(100), Score::new(8).unwrap()).await;
|
||||
assert_eq!(db.query(&peer_id, &ParaId::from(100)).await, None);
|
||||
assert_eq!(db.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
// Test para pruning.
|
||||
async fn test_prune_paras() {
|
||||
let mut db = Db::new(10).await;
|
||||
|
||||
db.prune_paras(BTreeSet::new()).await;
|
||||
assert_eq!(db.len(), 0);
|
||||
|
||||
db.prune_paras([ParaId::from(100), ParaId::from(200)].into_iter().collect())
|
||||
.await;
|
||||
assert_eq!(db.len(), 0);
|
||||
|
||||
let peer_id = PeerId::random();
|
||||
let another_peer_id = PeerId::random();
|
||||
|
||||
assert_eq!(
|
||||
db.process_bumps(
|
||||
1,
|
||||
[
|
||||
(ParaId::from(100), [(peer_id, Score::new(10).unwrap())].into_iter().collect()),
|
||||
(
|
||||
ParaId::from(200),
|
||||
[(another_peer_id, Score::new(12).unwrap())].into_iter().collect()
|
||||
),
|
||||
(ParaId::from(300), [(peer_id, Score::new(15).unwrap())].into_iter().collect())
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
Some(Score::new(10).unwrap()),
|
||||
)
|
||||
.await
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
assert_eq!(db.len(), 3);
|
||||
|
||||
// Registered paras include the existing ones. Does nothing
|
||||
db.prune_paras(
|
||||
[ParaId::from(100), ParaId::from(200), ParaId::from(300), ParaId::from(400)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(db.len(), 3);
|
||||
|
||||
assert_eq!(db.query(&peer_id, &ParaId::from(100)).await.unwrap(), Score::new(10).unwrap());
|
||||
assert_eq!(
|
||||
db.query(&another_peer_id, &ParaId::from(200)).await.unwrap(),
|
||||
Score::new(12).unwrap()
|
||||
);
|
||||
assert_eq!(db.query(&peer_id, &ParaId::from(300)).await.unwrap(), Score::new(15).unwrap());
|
||||
|
||||
// Prunes multiple paras.
|
||||
db.prune_paras([ParaId::from(300)].into_iter().collect()).await;
|
||||
assert_eq!(db.len(), 1);
|
||||
assert_eq!(db.query(&peer_id, &ParaId::from(100)).await, None);
|
||||
assert_eq!(db.query(&another_peer_id, &ParaId::from(200)).await, None);
|
||||
assert_eq!(db.query(&peer_id, &ParaId::from(300)).await.unwrap(), Score::new(15).unwrap());
|
||||
|
||||
// Prunes all paras.
|
||||
db.prune_paras(BTreeSet::new()).await;
|
||||
assert_eq!(db.len(), 0);
|
||||
assert_eq!(db.query(&peer_id, &ParaId::from(300)).await, None);
|
||||
}
|
||||
}
|
||||
+518
@@ -0,0 +1,518 @@
|
||||
// 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/>.
|
||||
mod backend;
|
||||
mod connected;
|
||||
mod db;
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
|
||||
use crate::{
|
||||
validator_side_experimental::{
|
||||
common::{
|
||||
PeerInfo, PeerState, Score, CONNECTED_PEERS_LIMIT, CONNECTED_PEERS_PARA_LIMIT,
|
||||
INACTIVITY_DECAY, MAX_STARTUP_ANCESTRY_LOOKBACK, MAX_STORED_SCORES_PER_PARA,
|
||||
VALID_INCLUDED_CANDIDATE_BUMP,
|
||||
},
|
||||
error::{Error, Result},
|
||||
},
|
||||
LOG_TARGET,
|
||||
};
|
||||
pub use backend::Backend;
|
||||
use connected::ConnectedPeers;
|
||||
pub use db::Db;
|
||||
use pezkuwi_node_network_protocol::{
|
||||
peer_set::{CollationVersion, PeerSet},
|
||||
PeerId,
|
||||
};
|
||||
use pezkuwi_node_subsystem::{
|
||||
messages::{ChainApiMessage, NetworkBridgeTxMessage},
|
||||
ActivatedLeaf, CollatorProtocolSenderTrait,
|
||||
};
|
||||
use pezkuwi_node_subsystem_util::{
|
||||
request_candidate_events, request_candidates_pending_availability, runtime::recv_runtime,
|
||||
};
|
||||
use pezkuwi_primitives::{
|
||||
BlockNumber, CandidateDescriptorVersion, CandidateEvent, CandidateHash, Hash, Id as ParaId,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct ReputationUpdate {
|
||||
pub peer_id: PeerId,
|
||||
pub para_id: ParaId,
|
||||
pub value: Score,
|
||||
pub kind: ReputationUpdateKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum ReputationUpdateKind {
|
||||
Bump,
|
||||
Slash,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum TryAcceptOutcome {
|
||||
Added,
|
||||
// This can hold more than one `PeerId` because before receiving the `Declare` message,
|
||||
// one peer can hold connection slots for multiple paraids.
|
||||
// The set can also be empty if this peer replaced some other peer's slot but that other peer
|
||||
// maintained a connection slot for another para (therefore not disconnected).
|
||||
// The number of peers in the set is bound to the number of scheduled paras.
|
||||
Replaced(HashSet<PeerId>),
|
||||
Rejected,
|
||||
}
|
||||
|
||||
impl TryAcceptOutcome {
|
||||
fn combine(self, other: Self) -> Self {
|
||||
use TryAcceptOutcome::*;
|
||||
match (self, other) {
|
||||
(Added, Added) => Added,
|
||||
(Rejected, Rejected) => Rejected,
|
||||
(Added, Rejected) | (Rejected, Added) => Added,
|
||||
(Replaced(mut replaced_a), Replaced(replaced_b)) => {
|
||||
replaced_a.extend(replaced_b);
|
||||
Replaced(replaced_a)
|
||||
},
|
||||
(_, Replaced(replaced)) | (Replaced(replaced), _) => Replaced(replaced),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum DeclarationOutcome {
|
||||
Rejected,
|
||||
Switched(ParaId),
|
||||
Accepted,
|
||||
}
|
||||
|
||||
pub struct PeerManager<B> {
|
||||
db: B,
|
||||
connected: ConnectedPeers,
|
||||
}
|
||||
|
||||
impl<B: Backend> PeerManager<B> {
|
||||
/// Initialize the peer manager (called on subsystem startup, after the node finished syncing to
|
||||
/// the tip of the chain).
|
||||
pub async fn startup<Sender: CollatorProtocolSenderTrait>(
|
||||
backend: B,
|
||||
sender: &mut Sender,
|
||||
scheduled_paras: BTreeSet<ParaId>,
|
||||
) -> Result<Self> {
|
||||
let mut instance = Self {
|
||||
db: backend,
|
||||
connected: ConnectedPeers::new(
|
||||
scheduled_paras,
|
||||
CONNECTED_PEERS_LIMIT,
|
||||
CONNECTED_PEERS_PARA_LIMIT,
|
||||
),
|
||||
};
|
||||
|
||||
let (latest_finalized_block_number, latest_finalized_block_hash) =
|
||||
get_latest_finalized_block(sender).await?;
|
||||
|
||||
let processed_finalized_block_number =
|
||||
instance.db.processed_finalized_block_number().await.unwrap_or_default();
|
||||
|
||||
let bumps = extract_reputation_bumps_on_new_finalized_block(
|
||||
sender,
|
||||
processed_finalized_block_number,
|
||||
(latest_finalized_block_number, latest_finalized_block_hash),
|
||||
)
|
||||
.await?;
|
||||
|
||||
instance.db.process_bumps(latest_finalized_block_number, bumps, None).await;
|
||||
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
/// Handle a new block finality notification, by updating peer reputations.
|
||||
pub async fn update_reputations_on_new_finalized_block<Sender: CollatorProtocolSenderTrait>(
|
||||
&mut self,
|
||||
sender: &mut Sender,
|
||||
(finalized_block_hash, finalized_block_number): (Hash, BlockNumber),
|
||||
) -> Result<()> {
|
||||
let processed_finalized_block_number =
|
||||
self.db.processed_finalized_block_number().await.unwrap_or_default();
|
||||
|
||||
let bumps = extract_reputation_bumps_on_new_finalized_block(
|
||||
sender,
|
||||
processed_finalized_block_number,
|
||||
(finalized_block_number, finalized_block_hash),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let updates = self
|
||||
.db
|
||||
.process_bumps(
|
||||
finalized_block_number,
|
||||
bumps,
|
||||
Some(Score::new(INACTIVITY_DECAY).expect("INACTIVITY_DECAY is a valid score")),
|
||||
)
|
||||
.await;
|
||||
for update in updates {
|
||||
self.connected.update_reputation(update);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process the registered paras and cleanup all data pertaining to any unregistered paras, if
|
||||
/// any. Should be called every N finalized block notifications, since it's expected that para
|
||||
/// deregistrations are rare.
|
||||
pub async fn registered_paras_update(&mut self, registered_paras: BTreeSet<ParaId>) {
|
||||
// Tell the DB to cleanup paras that are no longer registered. No need to clean up the
|
||||
// connected peers state, since it will get automatically cleaned up as the claim queue
|
||||
// gets rid of these stale assignments.
|
||||
self.db.prune_paras(registered_paras).await;
|
||||
}
|
||||
|
||||
/// Process a potential change of the scheduled paras.
|
||||
pub async fn scheduled_paras_update<Sender: CollatorProtocolSenderTrait>(
|
||||
&mut self,
|
||||
sender: &mut Sender,
|
||||
scheduled_paras: BTreeSet<ParaId>,
|
||||
) {
|
||||
let mut prev_scheduled_paras: BTreeSet<_> =
|
||||
self.connected.scheduled_paras().copied().collect();
|
||||
|
||||
if prev_scheduled_paras == scheduled_paras {
|
||||
// Nothing to do if the scheduled paras didn't change.
|
||||
return;
|
||||
}
|
||||
|
||||
// Recreate the connected peers based on the new schedule and try populating it again based
|
||||
// on their reputations. Disconnect any peers that couldn't be kept
|
||||
let mut new_instance =
|
||||
ConnectedPeers::new(scheduled_paras, CONNECTED_PEERS_LIMIT, CONNECTED_PEERS_PARA_LIMIT);
|
||||
|
||||
std::mem::swap(&mut new_instance, &mut self.connected);
|
||||
let prev_instance = new_instance;
|
||||
let (prev_peers, cached_scores) = prev_instance.consume();
|
||||
|
||||
// Build a closure that can be used to first query the in-memory past reputations of the
|
||||
// peers before reaching for the DB.
|
||||
|
||||
// Borrow these for use in the closure.
|
||||
let cached_scores = &cached_scores;
|
||||
let db = &self.db;
|
||||
let reputation_query_fn = |peer_id: PeerId, para_id: ParaId| async move {
|
||||
if let Some(cached_score) =
|
||||
cached_scores.get(¶_id).and_then(|per_para| per_para.get_score(&peer_id))
|
||||
{
|
||||
cached_score
|
||||
} else {
|
||||
db.query(&peer_id, ¶_id).await.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
// See which of the old peers we should keep.
|
||||
let mut peers_to_disconnect = HashSet::new();
|
||||
for (peer_id, peer_info) in prev_peers {
|
||||
let outcome = self.connected.try_accept(reputation_query_fn, peer_id, peer_info).await;
|
||||
|
||||
match outcome {
|
||||
TryAcceptOutcome::Rejected => {
|
||||
peers_to_disconnect.insert(peer_id);
|
||||
},
|
||||
TryAcceptOutcome::Replaced(replaced_peer_ids) => {
|
||||
peers_to_disconnect.extend(replaced_peer_ids);
|
||||
},
|
||||
TryAcceptOutcome::Added => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect peers that couldn't be kept.
|
||||
self.disconnect_peers(sender, peers_to_disconnect).await;
|
||||
}
|
||||
|
||||
/// Process a declaration message of a peer.
|
||||
pub async fn declared<Sender: CollatorProtocolSenderTrait>(
|
||||
&mut self,
|
||||
sender: &mut Sender,
|
||||
peer_id: PeerId,
|
||||
para_id: ParaId,
|
||||
) {
|
||||
let Some(peer_info) = self.connected.peer_info(&peer_id).cloned() else { return };
|
||||
let outcome = self.connected.declared(peer_id, para_id);
|
||||
|
||||
match outcome {
|
||||
DeclarationOutcome::Accepted => {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
?para_id,
|
||||
?peer_id,
|
||||
"Peer declared",
|
||||
);
|
||||
},
|
||||
DeclarationOutcome::Switched(old_para_id) => {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
?para_id,
|
||||
?old_para_id,
|
||||
?peer_id,
|
||||
"Peer switched collating paraid. Trying to accept it on the new one.",
|
||||
);
|
||||
|
||||
self.try_accept_connection(sender, peer_id, peer_info).await;
|
||||
},
|
||||
DeclarationOutcome::Rejected => {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
?para_id,
|
||||
?peer_id,
|
||||
"Peer declared but rejected. Going to disconnect.",
|
||||
);
|
||||
|
||||
self.disconnect_peers(sender, [peer_id].into_iter().collect()).await;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Slash a peer's reputation for this paraid.
|
||||
pub async fn slash_reputation(&mut self, peer_id: &PeerId, para_id: &ParaId, value: Score) {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
?peer_id,
|
||||
?para_id,
|
||||
?value,
|
||||
"Slashing peer's reputation",
|
||||
);
|
||||
|
||||
self.db.slash(peer_id, para_id, value).await;
|
||||
self.connected.update_reputation(ReputationUpdate {
|
||||
peer_id: *peer_id,
|
||||
para_id: *para_id,
|
||||
value,
|
||||
kind: ReputationUpdateKind::Slash,
|
||||
});
|
||||
}
|
||||
|
||||
/// Process a peer disconnected event coming from the network.
|
||||
pub fn disconnected(&mut self, peer_id: &PeerId) {
|
||||
self.connected.remove(peer_id);
|
||||
}
|
||||
|
||||
/// A connection was made, triage it. Return whether or not is was kept.
|
||||
pub async fn try_accept_connection<Sender: CollatorProtocolSenderTrait>(
|
||||
&mut self,
|
||||
sender: &mut Sender,
|
||||
peer_id: PeerId,
|
||||
peer_info: PeerInfo,
|
||||
) -> bool {
|
||||
let db = &self.db;
|
||||
let reputation_query_fn = |peer_id: PeerId, para_id: ParaId| async move {
|
||||
// Go straight to the DB. We only store in-memory the reputations of connected peers.
|
||||
db.query(&peer_id, ¶_id).await.unwrap_or_default()
|
||||
};
|
||||
|
||||
let outcome = self.connected.try_accept(reputation_query_fn, peer_id, peer_info).await;
|
||||
|
||||
match outcome {
|
||||
TryAcceptOutcome::Added => true,
|
||||
TryAcceptOutcome::Replaced(other_peers) => {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
"Peer {:?} replaced the connection slots of other peers: {:?}",
|
||||
peer_id,
|
||||
&other_peers
|
||||
);
|
||||
self.disconnect_peers(sender, other_peers).await;
|
||||
true
|
||||
},
|
||||
TryAcceptOutcome::Rejected => {
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
?peer_id,
|
||||
"Peer connection was rejected",
|
||||
);
|
||||
self.disconnect_peers(sender, [peer_id].into_iter().collect()).await;
|
||||
false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the score of the connected peer. We assume the peer is declared for this paraid.
|
||||
pub fn connected_peer_score(&self, peer_id: &PeerId, para_id: &ParaId) -> Option<Score> {
|
||||
self.connected.peer_score(peer_id, para_id)
|
||||
}
|
||||
|
||||
async fn disconnect_peers<Sender: CollatorProtocolSenderTrait>(
|
||||
&self,
|
||||
sender: &mut Sender,
|
||||
peers: HashSet<PeerId>,
|
||||
) {
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?peers,
|
||||
"Disconnecting peers",
|
||||
);
|
||||
|
||||
sender
|
||||
.send_message(NetworkBridgeTxMessage::DisconnectPeers(
|
||||
peers.into_iter().collect(),
|
||||
PeerSet::Collation,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_ancestors<Sender: CollatorProtocolSenderTrait>(
|
||||
sender: &mut Sender,
|
||||
k: usize,
|
||||
hash: Hash,
|
||||
) -> Result<Vec<Hash>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
sender
|
||||
.send_message(ChainApiMessage::Ancestors { hash, k, response_channel: tx })
|
||||
.await;
|
||||
|
||||
Ok(rx.await.map_err(|_| Error::CanceledAncestors)??)
|
||||
}
|
||||
|
||||
async fn get_latest_finalized_block<Sender: CollatorProtocolSenderTrait>(
|
||||
sender: &mut Sender,
|
||||
) -> Result<(BlockNumber, Hash)> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
sender.send_message(ChainApiMessage::FinalizedBlockNumber(tx)).await;
|
||||
|
||||
let block_number = rx.await.map_err(|_| Error::CanceledFinalizedBlockNumber)??;
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
sender.send_message(ChainApiMessage::FinalizedBlockHash(block_number, tx)).await;
|
||||
|
||||
let block_hash = rx
|
||||
.await
|
||||
.map_err(|_| Error::CanceledFinalizedBlockHash)??
|
||||
.ok_or_else(|| Error::FinalizedBlockNotFound(block_number))?;
|
||||
|
||||
Ok((block_number, block_hash))
|
||||
}
|
||||
|
||||
async fn extract_reputation_bumps_on_new_finalized_block<Sender: CollatorProtocolSenderTrait>(
|
||||
sender: &mut Sender,
|
||||
processed_finalized_block_number: BlockNumber,
|
||||
(latest_finalized_block_number, latest_finalized_block_hash): (BlockNumber, Hash),
|
||||
) -> Result<BTreeMap<ParaId, HashMap<PeerId, Score>>> {
|
||||
if latest_finalized_block_number < processed_finalized_block_number {
|
||||
// Shouldn't be possible, but in this case there is no other initialisation needed.
|
||||
gum::warn!(
|
||||
target: LOG_TARGET,
|
||||
latest_finalized_block_number,
|
||||
?latest_finalized_block_hash,
|
||||
"Peer manager stored finalized block number {} is higher than the latest finalized block.",
|
||||
processed_finalized_block_number,
|
||||
);
|
||||
return Ok(BTreeMap::new());
|
||||
}
|
||||
|
||||
let ancestry_len = std::cmp::min(
|
||||
latest_finalized_block_number.saturating_sub(processed_finalized_block_number),
|
||||
MAX_STARTUP_ANCESTRY_LOOKBACK,
|
||||
);
|
||||
|
||||
if ancestry_len == 0 {
|
||||
return Ok(BTreeMap::new());
|
||||
}
|
||||
|
||||
let mut ancestors =
|
||||
get_ancestors(sender, ancestry_len as usize, latest_finalized_block_hash).await?;
|
||||
ancestors.push(latest_finalized_block_hash);
|
||||
ancestors.reverse();
|
||||
|
||||
gum::trace!(
|
||||
target: LOG_TARGET,
|
||||
?latest_finalized_block_hash,
|
||||
processed_finalized_block_number,
|
||||
"Processing reputation bumps for finalized relay parent {} and its {} ancestors",
|
||||
latest_finalized_block_number,
|
||||
ancestry_len
|
||||
);
|
||||
|
||||
let mut v2_candidates_per_rp: HashMap<Hash, BTreeMap<ParaId, HashSet<CandidateHash>>> =
|
||||
HashMap::with_capacity(ancestors.len());
|
||||
|
||||
for i in 1..ancestors.len() {
|
||||
let rp = ancestors[i];
|
||||
let parent_rp = ancestors[i - 1];
|
||||
let candidate_events = recv_runtime(request_candidate_events(rp, sender).await).await?;
|
||||
|
||||
for event in candidate_events {
|
||||
if let CandidateEvent::CandidateIncluded(receipt, _, _, _) = event {
|
||||
// Only v2 receipts can contain UMP signals.
|
||||
if receipt.descriptor.version() == CandidateDescriptorVersion::V2 {
|
||||
v2_candidates_per_rp
|
||||
.entry(parent_rp)
|
||||
.or_default()
|
||||
.entry(receipt.descriptor.para_id())
|
||||
.or_default()
|
||||
.insert(receipt.hash());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This could be removed if we implemented https://github.com/pezkuwichain/pezkuwi-sdk/issues/152.
|
||||
let mut updates: BTreeMap<ParaId, HashMap<PeerId, Score>> = BTreeMap::new();
|
||||
for (rp, per_para) in v2_candidates_per_rp {
|
||||
for (para_id, included_candidates) in per_para {
|
||||
let candidates_pending_availability =
|
||||
recv_runtime(request_candidates_pending_availability(rp, para_id, sender).await)
|
||||
.await?;
|
||||
|
||||
for candidate in candidates_pending_availability {
|
||||
let candidate_hash = candidate.hash();
|
||||
if included_candidates.contains(&candidate_hash) {
|
||||
match candidate.commitments.ump_signals() {
|
||||
Ok(ump_signals) => {
|
||||
if let Some(approved_peer) = ump_signals.approved_peer() {
|
||||
match PeerId::from_bytes(approved_peer) {
|
||||
Ok(peer_id) => updates
|
||||
.entry(para_id)
|
||||
.or_default()
|
||||
.entry(peer_id)
|
||||
.or_default()
|
||||
.saturating_add(VALID_INCLUDED_CANDIDATE_BUMP),
|
||||
Err(err) => {
|
||||
// Collator sent an invalid peerid. It's only harming
|
||||
// itself.
|
||||
gum::debug!(
|
||||
target: LOG_TARGET,
|
||||
?candidate_hash,
|
||||
"UMP signal contains invalid ApprovedPeer id: {}",
|
||||
err
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
// This should never happen, as the ump signals are checked during
|
||||
// on-chain backing.
|
||||
gum::warn!(
|
||||
target: LOG_TARGET,
|
||||
?candidate_hash,
|
||||
"Failed to parse UMP signals for included candidate: {}",
|
||||
err
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(updates)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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 crate::validator_side_experimental::{peer_manager::Backend, Metrics, PeerManager};
|
||||
use sp_keystore::KeystorePtr;
|
||||
|
||||
/// All state relevant for the validator side of the protocol lives here.
|
||||
pub struct State<B> {
|
||||
peer_manager: PeerManager<B>,
|
||||
keystore: KeystorePtr,
|
||||
metrics: Metrics,
|
||||
}
|
||||
|
||||
impl<B: Backend> State<B> {
|
||||
/// Instantiate a new subsystem `State`.
|
||||
pub fn new(peer_manager: PeerManager<B>, keystore: KeystorePtr, metrics: Metrics) -> Self {
|
||||
Self { peer_manager, keystore, metrics }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user