feat: initialize Kurdistan SDK - independent fork of Polkadot SDK

This commit is contained in:
2025-12-13 15:44:15 +03:00
commit e4778b4576
6838 changed files with 1847450 additions and 0 deletions
@@ -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
@@ -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(&para_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
@@ -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()))
}
@@ -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>;
}
@@ -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(&para));
}
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(&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);
}
}
@@ -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(&para_id).and_then(|per_para| per_para.get_score(&peer_id))
{
cached_score
} else {
db.query(&peer_id, &para_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, &para_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 }
}
}