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
+45
View File
@@ -0,0 +1,45 @@
[package]
name = "pezkuwi-node-network-protocol"
version = "7.0.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
description = "Primitives types for the Node-side"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-channel = { workspace = true }
async-trait = { workspace = true }
bitvec = { workspace = true, default-features = true }
codec = { features = ["derive"], workspace = true }
derive_more = { workspace = true, default-features = true }
fatality = { workspace = true }
futures = { workspace = true }
gum = { workspace = true, default-features = true }
hex = { workspace = true, default-features = true }
pezkuwi-node-primitives = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
rand = { workspace = true, default-features = true }
sc-authority-discovery = { workspace = true, default-features = true }
sc-network = { workspace = true, default-features = true }
sc-network-types = { workspace = true, default-features = true }
sp-runtime = { workspace = true, default-features = true }
strum = { features = ["derive"], workspace = true, default-features = true }
thiserror = { workspace = true }
[dev-dependencies]
rand_chacha = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"gum/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"sc-authority-discovery/runtime-benchmarks",
"sc-network/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
@@ -0,0 +1,61 @@
// 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/>.
//! Authority discovery service interfacing.
use std::{collections::HashSet, fmt::Debug};
use async_trait::async_trait;
use sc_authority_discovery::Service as AuthorityDiscoveryService;
use pezkuwi_primitives::AuthorityDiscoveryId;
use sc_network::Multiaddr;
use sc_network_types::PeerId;
/// An abstraction over the authority discovery service.
///
/// Needed for mocking in tests mostly.
#[async_trait]
pub trait AuthorityDiscovery: Send + Debug + 'static {
/// Get the addresses for the given [`AuthorityDiscoveryId`] from the local address cache.
async fn get_addresses_by_authority_id(
&mut self,
authority: AuthorityDiscoveryId,
) -> Option<HashSet<Multiaddr>>;
/// Get the [`AuthorityDiscoveryId`] for the given [`PeerId`] from the local address cache.
async fn get_authority_ids_by_peer_id(
&mut self,
peer_id: PeerId,
) -> Option<HashSet<AuthorityDiscoveryId>>;
}
#[async_trait]
impl AuthorityDiscovery for AuthorityDiscoveryService {
async fn get_addresses_by_authority_id(
&mut self,
authority: AuthorityDiscoveryId,
) -> Option<HashSet<Multiaddr>> {
AuthorityDiscoveryService::get_addresses_by_authority_id(self, authority).await
}
async fn get_authority_ids_by_peer_id(
&mut self,
peer_id: PeerId,
) -> Option<HashSet<AuthorityDiscoveryId>> {
AuthorityDiscoveryService::get_authority_ids_by_peer_id(self, peer_id).await
}
}
@@ -0,0 +1,738 @@
// 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/>.
//! Grid topology support implementation
//! The basic operation of the 2D grid topology is that:
//! * A validator producing a message sends it to its row-neighbors and its column-neighbors
//! * A validator receiving a message originating from one of its row-neighbors sends it to its
//! column-neighbors
//! * A validator receiving a message originating from one of its column-neighbors sends it to its
//! row-neighbors
//!
//! This grid approach defines 2 unique paths for every validator to reach every other validator in
//! at most 2 hops.
//!
//! However, we also supplement this with some degree of random propagation:
//! every validator, upon seeing a message for the first time, propagates it to 8 random peers.
//! This inserts some redundancy in case the grid topology isn't working or is being attacked -
//! an adversary doesn't know which peers a validator will send to.
//! This is combined with the property that the adversary doesn't know which validators will elect
//! to check a block.
use crate::PeerId;
use pezkuwi_primitives::{AuthorityDiscoveryId, SessionIndex, ValidatorIndex};
use rand::{CryptoRng, Rng};
use std::{
collections::{hash_map, HashMap, HashSet},
fmt::Debug,
};
const LOG_TARGET: &str = "teyrchain::grid-topology";
/// The sample rate for randomly propagating messages. This
/// reduces the left tail of the binomial distribution but also
/// introduces a bias towards peers who we sample before others
/// (i.e. those who get a block before others).
pub const DEFAULT_RANDOM_SAMPLE_RATE: usize = crate::MIN_GOSSIP_PEERS;
/// The number of peers to randomly propagate messages to.
pub const DEFAULT_RANDOM_CIRCULATION: usize = 4;
/// Information about a peer in the gossip topology for a session.
#[derive(Debug, Clone, PartialEq)]
pub struct TopologyPeerInfo {
/// The validator's known peer IDs.
pub peer_ids: Vec<PeerId>,
/// The index of the validator in the discovery keys of the corresponding
/// `SessionInfo`. This can extend _beyond_ the set of active teyrchain validators.
pub validator_index: ValidatorIndex,
/// The authority discovery public key of the validator in the corresponding
/// `SessionInfo`.
pub discovery_id: AuthorityDiscoveryId,
}
/// Topology representation for a session.
#[derive(Default, Clone, Debug, PartialEq)]
pub struct SessionGridTopology {
/// An array mapping validator indices to their indices in the
/// shuffling itself. This has the same size as the number of validators
/// in the session.
shuffled_indices: Vec<usize>,
/// The canonical shuffling of validators for the session.
canonical_shuffling: Vec<TopologyPeerInfo>,
/// The list of peer-ids in an efficient way to search.
peer_ids: HashSet<PeerId>,
}
impl SessionGridTopology {
/// Create a new session grid topology.
pub fn new(shuffled_indices: Vec<usize>, canonical_shuffling: Vec<TopologyPeerInfo>) -> Self {
let mut peer_ids = HashSet::new();
for peer_info in canonical_shuffling.iter() {
for peer_id in peer_info.peer_ids.iter() {
peer_ids.insert(*peer_id);
}
}
SessionGridTopology { shuffled_indices, canonical_shuffling, peer_ids }
}
/// Updates the known peer ids for the passed authorities ids.
pub fn update_authority_ids(
&mut self,
peer_id: PeerId,
ids: &HashSet<AuthorityDiscoveryId>,
) -> bool {
let mut updated = false;
if !self.peer_ids.contains(&peer_id) {
for peer in self
.canonical_shuffling
.iter_mut()
.filter(|peer| ids.contains(&peer.discovery_id))
{
peer.peer_ids.push(peer_id);
self.peer_ids.insert(peer_id);
updated = true;
}
}
updated
}
/// Produces the outgoing routing logic for a particular peer.
///
/// Returns `None` if the validator index is out of bounds.
pub fn compute_grid_neighbors_for(&self, v: ValidatorIndex) -> Option<GridNeighbors> {
if self.shuffled_indices.len() != self.canonical_shuffling.len() {
return None;
}
let shuffled_val_index = *self.shuffled_indices.get(v.0 as usize)?;
let neighbors = matrix_neighbors(shuffled_val_index, self.shuffled_indices.len())?;
let mut grid_subset = GridNeighbors::empty();
for r_n in neighbors.row_neighbors {
let n = &self.canonical_shuffling[r_n];
grid_subset.validator_indices_x.insert(n.validator_index);
for p in &n.peer_ids {
grid_subset.peers_x.insert(*p);
}
}
for c_n in neighbors.column_neighbors {
let n = &self.canonical_shuffling[c_n];
grid_subset.validator_indices_y.insert(n.validator_index);
for p in &n.peer_ids {
grid_subset.peers_y.insert(*p);
}
}
Some(grid_subset)
}
/// Tells if a given peer id is validator in a session
pub fn is_validator(&self, peer: &PeerId) -> bool {
self.peer_ids.contains(peer)
}
}
struct MatrixNeighbors<R, C> {
row_neighbors: R,
column_neighbors: C,
}
/// Compute the row and column neighbors of `val_index` in a matrix
fn matrix_neighbors(
val_index: usize,
len: usize,
) -> Option<MatrixNeighbors<impl Iterator<Item = usize>, impl Iterator<Item = usize>>> {
if val_index >= len {
return None;
}
// e.g. for size 11 the matrix would be
//
// 0 1 2
// 3 4 5
// 6 7 8
// 9 10
//
// and for index 10, the neighbors would be 1, 4, 7, 9
let sqrt = (len as f64).sqrt() as usize;
let our_row = val_index / sqrt;
let our_column = val_index % sqrt;
let row_neighbors = our_row * sqrt..std::cmp::min(our_row * sqrt + sqrt, len);
let column_neighbors = (our_column..len).step_by(sqrt);
Some(MatrixNeighbors {
row_neighbors: row_neighbors.filter(move |i| *i != val_index),
column_neighbors: column_neighbors.filter(move |i| *i != val_index),
})
}
/// Information about the grid neighbors for a particular node in the topology.
#[derive(Debug, Clone, PartialEq)]
pub struct GridNeighbors {
/// Represent peers in the X axis
pub peers_x: HashSet<PeerId>,
/// Represent validators in the X axis
pub validator_indices_x: HashSet<ValidatorIndex>,
/// Represent peers in the Y axis
pub peers_y: HashSet<PeerId>,
/// Represent validators in the Y axis
pub validator_indices_y: HashSet<ValidatorIndex>,
}
impl GridNeighbors {
/// Utility function for creating an empty set of grid neighbors.
/// Useful for testing.
pub fn empty() -> Self {
GridNeighbors {
peers_x: HashSet::new(),
validator_indices_x: HashSet::new(),
peers_y: HashSet::new(),
validator_indices_y: HashSet::new(),
}
}
/// Given the originator of a message as a validator index, indicates the part of the topology
/// we're meant to send the message to.
pub fn required_routing_by_index(
&self,
originator: ValidatorIndex,
local: bool,
) -> RequiredRouting {
if local {
return RequiredRouting::GridXY;
}
let grid_x = self.validator_indices_x.contains(&originator);
let grid_y = self.validator_indices_y.contains(&originator);
match (grid_x, grid_y) {
(false, false) => RequiredRouting::None,
(true, false) => RequiredRouting::GridY, // messages from X go to Y
(false, true) => RequiredRouting::GridX, // messages from Y go to X
(true, true) => RequiredRouting::GridXY, /* if the grid works as expected, this
* shouldn't happen. */
}
}
/// Given the originator of a message as a peer index, indicates the part of the topology
/// we're meant to send the message to.
pub fn required_routing_by_peer_id(&self, originator: PeerId, local: bool) -> RequiredRouting {
if local {
return RequiredRouting::GridXY;
}
let grid_x = self.peers_x.contains(&originator);
let grid_y = self.peers_y.contains(&originator);
match (grid_x, grid_y) {
(false, false) => RequiredRouting::None,
(true, false) => RequiredRouting::GridY, // messages from X go to Y
(false, true) => RequiredRouting::GridX, // messages from Y go to X
(true, true) => {
gum::debug!(
target: LOG_TARGET,
?originator,
"Grid topology is unexpected, play it safe and send to X AND Y"
);
RequiredRouting::GridXY
}, /* if the grid works as expected, this
* shouldn't happen. */
}
}
/// Get a filter function based on this topology and the required routing
/// which returns `true` for peers that are within the required routing set
/// and false otherwise.
pub fn route_to_peer(&self, required_routing: RequiredRouting, peer: &PeerId) -> bool {
match required_routing {
RequiredRouting::All => true,
RequiredRouting::GridX => self.peers_x.contains(peer),
RequiredRouting::GridY => self.peers_y.contains(peer),
RequiredRouting::GridXY => self.peers_x.contains(peer) || self.peers_y.contains(peer),
RequiredRouting::None | RequiredRouting::PendingTopology => false,
}
}
/// Returns the difference between this and the `other` topology as a vector of peers
pub fn peers_diff(&self, other: &Self) -> Vec<PeerId> {
self.peers_x
.iter()
.chain(self.peers_y.iter())
.filter(|peer_id| !(other.peers_x.contains(peer_id) || other.peers_y.contains(peer_id)))
.cloned()
.collect::<Vec<_>>()
}
/// A convenience method that returns total number of peers in the topology
pub fn len(&self) -> usize {
self.peers_x.len().saturating_add(self.peers_y.len())
}
}
/// An entry tracking a session grid topology and some cached local neighbors.
#[derive(Debug)]
pub struct SessionGridTopologyEntry {
topology: SessionGridTopology,
local_neighbors: GridNeighbors,
local_index: Option<ValidatorIndex>,
}
impl SessionGridTopologyEntry {
/// Access the local grid neighbors.
pub fn local_grid_neighbors(&self) -> &GridNeighbors {
&self.local_neighbors
}
/// Access the local grid neighbors mutably.
pub fn local_grid_neighbors_mut(&mut self) -> &mut GridNeighbors {
&mut self.local_neighbors
}
/// Access the underlying topology.
pub fn get(&self) -> &SessionGridTopology {
&self.topology
}
/// Tells if a given peer id is validator in a session
pub fn is_validator(&self, peer: &PeerId) -> bool {
self.topology.is_validator(peer)
}
/// Returns the list of peers to route based on the required routing.
pub fn peers_to_route(&self, required_routing: RequiredRouting) -> Vec<PeerId> {
match required_routing {
RequiredRouting::All => self.topology.peer_ids.iter().copied().collect(),
RequiredRouting::GridX => self.local_neighbors.peers_x.iter().copied().collect(),
RequiredRouting::GridY => self.local_neighbors.peers_y.iter().copied().collect(),
RequiredRouting::GridXY => self
.local_neighbors
.peers_x
.iter()
.chain(self.local_neighbors.peers_y.iter())
.copied()
.collect(),
RequiredRouting::None | RequiredRouting::PendingTopology => Vec::new(),
}
}
/// Updates the known peer ids for the passed authorities ids.
pub fn update_authority_ids(
&mut self,
peer_id: PeerId,
ids: &HashSet<AuthorityDiscoveryId>,
) -> bool {
let peer_id_updated = self.topology.update_authority_ids(peer_id, ids);
// If we added a new peer id we need to recompute the grid neighbors, so that
// neighbors_x and neighbors_y reflect the right peer ids.
if peer_id_updated {
if let Some(local_index) = self.local_index.as_ref() {
if let Some(new_grid) = self.topology.compute_grid_neighbors_for(*local_index) {
self.local_neighbors = new_grid;
}
}
}
peer_id_updated
}
}
/// A set of topologies indexed by session
#[derive(Default)]
pub struct SessionGridTopologies {
inner: HashMap<SessionIndex, (Option<SessionGridTopologyEntry>, usize)>,
}
impl SessionGridTopologies {
/// Returns a topology for the specific session index
pub fn get_topology(&self, session: SessionIndex) -> Option<&SessionGridTopologyEntry> {
self.inner.get(&session).and_then(|val| val.0.as_ref())
}
/// Updates the known peer ids for the passed authorities ids.
pub fn update_authority_ids(
&mut self,
peer_id: PeerId,
ids: &HashSet<AuthorityDiscoveryId>,
) -> bool {
self.inner
.iter_mut()
.map(|(_, topology)| {
topology.0.as_mut().map(|topology| topology.update_authority_ids(peer_id, ids))
})
.any(|updated| updated.unwrap_or_default())
}
/// Increase references counter for a specific topology
pub fn inc_session_refs(&mut self, session: SessionIndex) {
self.inner.entry(session).or_insert((None, 0)).1 += 1;
}
/// Decrease references counter for a specific topology
pub fn dec_session_refs(&mut self, session: SessionIndex) {
if let hash_map::Entry::Occupied(mut occupied) = self.inner.entry(session) {
occupied.get_mut().1 = occupied.get().1.saturating_sub(1);
if occupied.get().1 == 0 {
let _ = occupied.remove();
}
}
}
/// Insert a new topology, no-op if already present.
pub fn insert_topology(
&mut self,
session: SessionIndex,
topology: SessionGridTopology,
local_index: Option<ValidatorIndex>,
) {
let entry = self.inner.entry(session).or_insert((None, 0));
if entry.0.is_none() {
let local_neighbors = local_index
.and_then(|l| topology.compute_grid_neighbors_for(l))
.unwrap_or_else(GridNeighbors::empty);
entry.0 = Some(SessionGridTopologyEntry { topology, local_neighbors, local_index });
}
}
}
/// A simple storage for a topology and the corresponding session index
#[derive(Debug)]
struct GridTopologySessionBound {
entry: SessionGridTopologyEntry,
session_index: SessionIndex,
}
/// A storage for the current and maybe previous topology
#[derive(Debug)]
pub struct SessionBoundGridTopologyStorage {
current_topology: GridTopologySessionBound,
prev_topology: Option<GridTopologySessionBound>,
}
impl Default for SessionBoundGridTopologyStorage {
fn default() -> Self {
// having this struct be `Default` is objectively stupid
// but used in a few places
SessionBoundGridTopologyStorage {
current_topology: GridTopologySessionBound {
// session 0 is valid so we should use the upper bound
// as the default instead of the lower bound.
session_index: SessionIndex::max_value(),
entry: SessionGridTopologyEntry {
topology: SessionGridTopology {
shuffled_indices: Vec::new(),
canonical_shuffling: Vec::new(),
peer_ids: Default::default(),
},
local_neighbors: GridNeighbors::empty(),
local_index: None,
},
},
prev_topology: None,
}
}
}
impl SessionBoundGridTopologyStorage {
/// Return a grid topology based on the session index:
/// If we need a previous session and it is registered in the storage, then return that session.
/// Otherwise, return a current session to have some grid topology in any case
pub fn get_topology_or_fallback(&self, idx: SessionIndex) -> &SessionGridTopologyEntry {
self.get_topology(idx).unwrap_or(&self.current_topology.entry)
}
/// Return the grid topology for the specific session index, if no such a session is stored
/// returns `None`.
pub fn get_topology(&self, idx: SessionIndex) -> Option<&SessionGridTopologyEntry> {
if let Some(prev_topology) = &self.prev_topology {
if idx == prev_topology.session_index {
return Some(&prev_topology.entry);
}
}
if self.current_topology.session_index == idx {
return Some(&self.current_topology.entry);
}
None
}
/// Update the current topology preserving the previous one
pub fn update_topology(
&mut self,
session_index: SessionIndex,
topology: SessionGridTopology,
local_index: Option<ValidatorIndex>,
) {
let local_neighbors = local_index
.and_then(|l| topology.compute_grid_neighbors_for(l))
.unwrap_or_else(GridNeighbors::empty);
let old_current = std::mem::replace(
&mut self.current_topology,
GridTopologySessionBound {
entry: SessionGridTopologyEntry { topology, local_neighbors, local_index },
session_index,
},
);
self.prev_topology.replace(old_current);
}
/// Returns a current grid topology
pub fn get_current_topology(&self) -> &SessionGridTopologyEntry {
&self.current_topology.entry
}
/// Returns the current session index.
pub fn get_current_session_index(&self) -> SessionIndex {
self.current_topology.session_index
}
/// Access the current grid topology mutably. Dangerous and intended
/// to be used in tests.
pub fn get_current_topology_mut(&mut self) -> &mut SessionGridTopologyEntry {
&mut self.current_topology.entry
}
}
/// A representation of routing based on sample
#[derive(Debug, Clone, Copy)]
pub struct RandomRouting {
/// The number of peers to target.
target: usize,
/// The number of peers this has been sent to.
sent: usize,
/// Sampling rate
sample_rate: usize,
}
impl Default for RandomRouting {
fn default() -> Self {
RandomRouting {
target: DEFAULT_RANDOM_CIRCULATION,
sent: 0_usize,
sample_rate: DEFAULT_RANDOM_SAMPLE_RATE,
}
}
}
impl RandomRouting {
/// Perform random sampling for a specific peer
/// Returns `true` for a lucky peer
pub fn sample(&self, n_peers_total: usize, rng: &mut (impl CryptoRng + Rng)) -> bool {
if n_peers_total == 0 || self.sent >= self.target {
false
} else if self.sample_rate > n_peers_total {
true
} else {
rng.gen_ratio(self.sample_rate as _, n_peers_total as _)
}
}
/// Increase number of messages being sent
pub fn inc_sent(&mut self) {
self.sent += 1
}
/// Returns `true` if we already took all the necessary samples.
pub fn is_complete(&self) -> bool {
self.sent >= self.target
}
}
/// Routing mode
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RequiredRouting {
/// We don't know yet, because we're waiting for topology info
/// (race condition between learning about the first blocks in a new session
/// and getting the topology for that session)
PendingTopology,
/// Propagate to all peers of any kind.
All,
/// Propagate to all peers sharing either the X or Y dimension of the grid.
GridXY,
/// Propagate to all peers sharing the X dimension of the grid.
GridX,
/// Propagate to all peers sharing the Y dimension of the grid.
GridY,
/// No required propagation.
None,
}
impl RequiredRouting {
/// Whether the required routing set is definitely empty.
pub fn is_empty(self) -> bool {
match self {
RequiredRouting::PendingTopology | RequiredRouting::None => true,
_ => false,
}
}
/// Combine two required routing sets into one that would cover both routing modes.
pub fn combine(self, other: Self) -> Self {
match (self, other) {
(RequiredRouting::All, _) | (_, RequiredRouting::All) => RequiredRouting::All,
(RequiredRouting::GridXY, _) | (_, RequiredRouting::GridXY) => RequiredRouting::GridXY,
(RequiredRouting::GridX, RequiredRouting::GridY) |
(RequiredRouting::GridY, RequiredRouting::GridX) => RequiredRouting::GridXY,
(RequiredRouting::GridX, RequiredRouting::GridX) => RequiredRouting::GridX,
(RequiredRouting::GridY, RequiredRouting::GridY) => RequiredRouting::GridY,
(RequiredRouting::None, RequiredRouting::PendingTopology) |
(RequiredRouting::PendingTopology, RequiredRouting::None) => RequiredRouting::PendingTopology,
(RequiredRouting::None, _) | (RequiredRouting::PendingTopology, _) => other,
(_, RequiredRouting::None) | (_, RequiredRouting::PendingTopology) => self,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use rand_chacha::ChaCha12Rng;
fn dummy_rng() -> ChaCha12Rng {
rand_chacha::ChaCha12Rng::seed_from_u64(12345)
}
#[test]
fn test_required_routing_combine() {
assert_eq!(RequiredRouting::All.combine(RequiredRouting::None), RequiredRouting::All);
assert_eq!(RequiredRouting::All.combine(RequiredRouting::GridXY), RequiredRouting::All);
assert_eq!(RequiredRouting::GridXY.combine(RequiredRouting::All), RequiredRouting::All);
assert_eq!(RequiredRouting::None.combine(RequiredRouting::All), RequiredRouting::All);
assert_eq!(RequiredRouting::None.combine(RequiredRouting::None), RequiredRouting::None);
assert_eq!(
RequiredRouting::PendingTopology.combine(RequiredRouting::GridX),
RequiredRouting::GridX
);
assert_eq!(
RequiredRouting::GridX.combine(RequiredRouting::PendingTopology),
RequiredRouting::GridX
);
assert_eq!(RequiredRouting::GridX.combine(RequiredRouting::GridY), RequiredRouting::GridXY);
assert_eq!(RequiredRouting::GridY.combine(RequiredRouting::GridX), RequiredRouting::GridXY);
assert_eq!(
RequiredRouting::GridXY.combine(RequiredRouting::GridXY),
RequiredRouting::GridXY
);
assert_eq!(RequiredRouting::GridX.combine(RequiredRouting::GridX), RequiredRouting::GridX);
assert_eq!(RequiredRouting::GridY.combine(RequiredRouting::GridY), RequiredRouting::GridY);
assert_eq!(RequiredRouting::None.combine(RequiredRouting::GridY), RequiredRouting::GridY);
assert_eq!(RequiredRouting::None.combine(RequiredRouting::GridX), RequiredRouting::GridX);
assert_eq!(RequiredRouting::None.combine(RequiredRouting::GridXY), RequiredRouting::GridXY);
assert_eq!(RequiredRouting::GridY.combine(RequiredRouting::None), RequiredRouting::GridY);
assert_eq!(RequiredRouting::GridX.combine(RequiredRouting::None), RequiredRouting::GridX);
assert_eq!(RequiredRouting::GridXY.combine(RequiredRouting::None), RequiredRouting::GridXY);
assert_eq!(
RequiredRouting::PendingTopology.combine(RequiredRouting::None),
RequiredRouting::PendingTopology
);
assert_eq!(
RequiredRouting::None.combine(RequiredRouting::PendingTopology),
RequiredRouting::PendingTopology
);
}
#[test]
fn test_random_routing_sample() {
// This test is fragile as it relies on a specific ChaCha12Rng
// sequence that might be implementation defined even for a static seed
let mut rng = dummy_rng();
let mut random_routing = RandomRouting { target: 4, sent: 0, sample_rate: 8 };
assert_eq!(random_routing.sample(16, &mut rng), true);
random_routing.inc_sent();
assert_eq!(random_routing.sample(16, &mut rng), false);
assert_eq!(random_routing.sample(16, &mut rng), false);
assert_eq!(random_routing.sample(16, &mut rng), true);
random_routing.inc_sent();
assert_eq!(random_routing.sample(16, &mut rng), true);
random_routing.inc_sent();
assert_eq!(random_routing.sample(16, &mut rng), false);
assert_eq!(random_routing.sample(16, &mut rng), false);
assert_eq!(random_routing.sample(16, &mut rng), false);
assert_eq!(random_routing.sample(16, &mut rng), true);
random_routing.inc_sent();
for _ in 0..16 {
assert_eq!(random_routing.sample(16, &mut rng), false);
}
}
fn run_random_routing(
random_routing: &mut RandomRouting,
rng: &mut (impl CryptoRng + Rng),
npeers: usize,
iters: usize,
) -> usize {
let mut ret = 0_usize;
for _ in 0..iters {
if random_routing.sample(npeers, rng) {
random_routing.inc_sent();
ret += 1;
}
}
ret
}
#[test]
fn test_random_routing_distribution() {
let mut rng = dummy_rng();
let mut random_routing = RandomRouting { target: 4, sent: 0, sample_rate: 8 };
assert_eq!(run_random_routing(&mut random_routing, &mut rng, 100, 10000), 4);
let mut random_routing = RandomRouting { target: 8, sent: 0, sample_rate: 100 };
assert_eq!(run_random_routing(&mut random_routing, &mut rng, 100, 10000), 8);
let mut random_routing = RandomRouting { target: 0, sent: 0, sample_rate: 100 };
assert_eq!(run_random_routing(&mut random_routing, &mut rng, 100, 10000), 0);
let mut random_routing = RandomRouting { target: 10, sent: 0, sample_rate: 10 };
assert_eq!(run_random_routing(&mut random_routing, &mut rng, 10, 100), 10);
}
#[test]
fn test_matrix_neighbors() {
for (our_index, len, expected_row, expected_column) in vec![
(0usize, 1usize, vec![], vec![]),
(1, 2, vec![], vec![0usize]),
(0, 9, vec![1, 2], vec![3, 6]),
(9, 10, vec![], vec![0, 3, 6]),
(10, 11, vec![9], vec![1, 4, 7]),
(7, 11, vec![6, 8], vec![1, 4, 10]),
]
.into_iter()
{
let matrix = matrix_neighbors(our_index, len).unwrap();
let mut row_result: Vec<_> = matrix.row_neighbors.collect();
let mut column_result: Vec<_> = matrix.column_neighbors.collect();
row_result.sort();
column_result.sort();
assert_eq!(row_result, expected_row);
assert_eq!(column_result, expected_column);
}
}
}
+773
View File
@@ -0,0 +1,773 @@
// 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/>.
//! Network protocol types for teyrchains.
#![deny(unused_crate_dependencies)]
#![warn(missing_docs)]
use codec::{Decode, Encode};
use pezkuwi_primitives::{BlockNumber, Hash};
use std::fmt;
#[doc(hidden)]
pub use sc_network::IfDisconnected;
pub use sc_network_types::PeerId;
#[doc(hidden)]
pub use std::sync::Arc;
mod reputation;
pub use self::reputation::{ReputationChange, UnifiedReputationChange};
/// Peer-sets and protocols used for teyrchains.
pub mod peer_set;
/// Request/response protocols used in Pezkuwi.
pub mod request_response;
/// Accessing authority discovery service
pub mod authority_discovery;
/// Grid topology support module
pub mod grid_topology;
/// The minimum amount of peers to send gossip messages to.
pub const MIN_GOSSIP_PEERS: usize = 25;
/// An error indicating that this the over-arching message type had the wrong variant
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WrongVariant;
impl fmt::Display for WrongVariant {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "Wrong message variant")
}
}
impl std::error::Error for WrongVariant {}
/// The advertised role of a node.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ObservedRole {
/// A light node.
Light,
/// A full node.
Full,
/// A node claiming to be an authority (unauthenticated)
Authority,
}
impl From<sc_network::ObservedRole> for ObservedRole {
fn from(role: sc_network::ObservedRole) -> ObservedRole {
match role {
sc_network::ObservedRole::Light => ObservedRole::Light,
sc_network::ObservedRole::Authority => ObservedRole::Authority,
sc_network::ObservedRole::Full => ObservedRole::Full,
}
}
}
impl Into<sc_network::ObservedRole> for ObservedRole {
fn into(self) -> sc_network::ObservedRole {
match self {
ObservedRole::Light => sc_network::ObservedRole::Light,
ObservedRole::Full => sc_network::ObservedRole::Full,
ObservedRole::Authority => sc_network::ObservedRole::Authority,
}
}
}
/// Specialized wrapper around [`View`].
#[derive(Debug, Clone, Default)]
pub struct OurView {
view: View,
}
impl OurView {
/// Creates a new instance.
pub fn new(heads: impl IntoIterator<Item = Hash>, finalized_number: BlockNumber) -> Self {
let view = View::new(heads, finalized_number);
Self { view }
}
}
impl PartialEq for OurView {
fn eq(&self, other: &Self) -> bool {
self.view == other.view
}
}
impl std::ops::Deref for OurView {
type Target = View;
fn deref(&self) -> &View {
&self.view
}
}
/// Construct a new [`OurView`] with the given chain heads, finalized number 0
///
/// NOTE: Use for tests only.
///
/// # Example
///
/// ```
/// # use pezkuwi_node_network_protocol::our_view;
/// # use pezkuwi_primitives::Hash;
/// let our_view = our_view![Hash::repeat_byte(1), Hash::repeat_byte(2)];
/// ```
#[macro_export]
macro_rules! our_view {
( $( $hash:expr ),* $(,)? ) => {
$crate::OurView::new(
vec![ $( $hash.clone() ),* ].into_iter().map(|h| h),
0,
)
};
}
/// A succinct representation of a peer's view. This consists of a bounded amount of chain heads
/// and the highest known finalized block number.
///
/// Up to `N` (5?) chain heads.
#[derive(Default, Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub struct View {
/// A bounded amount of chain heads.
/// Invariant: Sorted.
heads: Vec<Hash>,
/// The highest known finalized block number.
pub finalized_number: BlockNumber,
}
/// Construct a new view with the given chain heads and finalized number 0.
///
/// NOTE: Use for tests only.
///
/// # Example
///
/// ```
/// # use pezkuwi_node_network_protocol::view;
/// # use pezkuwi_primitives::Hash;
/// let view = view![Hash::repeat_byte(1), Hash::repeat_byte(2)];
/// ```
#[macro_export]
macro_rules! view {
( $( $hash:expr ),* $(,)? ) => {
$crate::View::new(vec![ $( $hash.clone() ),* ], 0)
};
}
impl View {
/// Construct a new view based on heads and a finalized block number.
pub fn new(heads: impl IntoIterator<Item = Hash>, finalized_number: BlockNumber) -> Self {
let mut heads = heads.into_iter().collect::<Vec<Hash>>();
heads.sort();
Self { heads, finalized_number }
}
/// Start with no heads, but only a finalized block number.
pub fn with_finalized(finalized_number: BlockNumber) -> Self {
Self { heads: Vec::new(), finalized_number }
}
/// Obtain the number of heads that are in view.
pub fn len(&self) -> usize {
self.heads.len()
}
/// Check if the number of heads contained, is null.
pub fn is_empty(&self) -> bool {
self.heads.is_empty()
}
/// Obtain an iterator over all heads.
pub fn iter(&self) -> impl Iterator<Item = &Hash> {
self.heads.iter()
}
/// Obtain an iterator over all heads.
pub fn into_iter(self) -> impl Iterator<Item = Hash> {
self.heads.into_iter()
}
/// Replace `self` with `new`.
///
/// Returns an iterator that will yield all elements of `new` that were not part of `self`.
pub fn replace_difference(&mut self, new: View) -> impl Iterator<Item = &Hash> {
let old = std::mem::replace(self, new);
self.heads.iter().filter(move |h| !old.contains(h))
}
/// Returns an iterator of the hashes present in `Self` but not in `other`.
pub fn difference<'a>(&'a self, other: &'a View) -> impl Iterator<Item = &'a Hash> + 'a {
self.heads.iter().filter(move |h| !other.contains(h))
}
/// An iterator containing hashes present in both `Self` and in `other`.
pub fn intersection<'a>(&'a self, other: &'a View) -> impl Iterator<Item = &'a Hash> + 'a {
self.heads.iter().filter(move |h| other.contains(h))
}
/// Whether the view contains a given hash.
pub fn contains(&self, hash: &Hash) -> bool {
self.heads.contains(hash)
}
/// Check if two views have the same heads.
///
/// Equivalent to the `PartialEq` function,
/// but ignores the `finalized_number` field.
pub fn check_heads_eq(&self, other: &Self) -> bool {
self.heads == other.heads
}
}
/// A protocol-versioned type for validation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationProtocols<V3> {
/// V3 type.
V3(V3),
}
/// A protocol-versioned type for collation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CollationProtocols<V1, V2> {
/// V1 type.
V1(V1),
/// V2 type.
V2(V2),
}
impl<V3: Clone> ValidationProtocols<&'_ V3> {
/// Convert to a fully-owned version of the message.
pub fn clone_inner(&self) -> ValidationProtocols<V3> {
match *self {
ValidationProtocols::V3(inner) => ValidationProtocols::V3(inner.clone()),
}
}
}
impl<V1: Clone, V2: Clone> CollationProtocols<&'_ V1, &'_ V2> {
/// Convert to a fully-owned version of the message.
pub fn clone_inner(&self) -> CollationProtocols<V1, V2> {
match *self {
CollationProtocols::V1(inner) => CollationProtocols::V1(inner.clone()),
CollationProtocols::V2(inner) => CollationProtocols::V2(inner.clone()),
}
}
}
/// All supported versions of the validation protocol message.
pub type VersionedValidationProtocol = ValidationProtocols<v3::ValidationProtocol>;
impl From<v3::ValidationProtocol> for VersionedValidationProtocol {
fn from(v3: v3::ValidationProtocol) -> Self {
VersionedValidationProtocol::V3(v3)
}
}
/// All supported versions of the collation protocol message.
pub type VersionedCollationProtocol =
CollationProtocols<v1::CollationProtocol, v2::CollationProtocol>;
impl From<v1::CollationProtocol> for VersionedCollationProtocol {
fn from(v1: v1::CollationProtocol) -> Self {
VersionedCollationProtocol::V1(v1)
}
}
impl From<v2::CollationProtocol> for VersionedCollationProtocol {
fn from(v2: v2::CollationProtocol) -> Self {
VersionedCollationProtocol::V2(v2)
}
}
macro_rules! impl_versioned_validation_full_protocol_from {
($from:ty, $out:ty, $variant:ident) => {
impl From<$from> for $out {
fn from(versioned_from: $from) -> $out {
match versioned_from {
ValidationProtocols::V3(x) => ValidationProtocols::V3(x.into()),
}
}
}
};
}
macro_rules! impl_versioned_collation_full_protocol_from {
($from:ty, $out:ty, $variant:ident) => {
impl From<$from> for $out {
fn from(versioned_from: $from) -> $out {
match versioned_from {
CollationProtocols::V1(x) => CollationProtocols::V1(x.into()),
CollationProtocols::V2(x) => CollationProtocols::V2(x.into()),
}
}
}
};
}
/// Implement `TryFrom` for one versioned validation enum variant into the inner type.
/// `$m_ty::$variant(inner) -> Ok(inner)`
macro_rules! impl_versioned_validation_try_from {
(
$from:ty,
$out:ty,
$v3_pat:pat => $v3_out:expr
) => {
impl TryFrom<$from> for $out {
type Error = crate::WrongVariant;
fn try_from(x: $from) -> Result<$out, Self::Error> {
#[allow(unreachable_patterns)] // when there is only one variant
match x {
ValidationProtocols::V3($v3_pat) => Ok(ValidationProtocols::V3($v3_out)),
_ => Err(crate::WrongVariant),
}
}
}
impl<'a> TryFrom<&'a $from> for $out {
type Error = crate::WrongVariant;
fn try_from(x: &'a $from) -> Result<$out, Self::Error> {
#[allow(unreachable_patterns)] // when there is only one variant
match x {
ValidationProtocols::V3($v3_pat) =>
Ok(ValidationProtocols::V3($v3_out.clone())),
_ => Err(crate::WrongVariant),
}
}
}
};
}
/// Implement `TryFrom` for one versioned collation enum variant into the inner type.
/// `$m_ty::$variant(inner) -> Ok(inner)`
macro_rules! impl_versioned_collation_try_from {
(
$from:ty,
$out:ty,
$v1_pat:pat => $v1_out:expr,
$v2_pat:pat => $v2_out:expr
) => {
impl TryFrom<$from> for $out {
type Error = crate::WrongVariant;
fn try_from(x: $from) -> Result<$out, Self::Error> {
#[allow(unreachable_patterns)] // when there is only one variant
match x {
CollationProtocols::V1($v1_pat) => Ok(CollationProtocols::V1($v1_out)),
CollationProtocols::V2($v2_pat) => Ok(CollationProtocols::V2($v2_out)),
_ => Err(crate::WrongVariant),
}
}
}
impl<'a> TryFrom<&'a $from> for $out {
type Error = crate::WrongVariant;
fn try_from(x: &'a $from) -> Result<$out, Self::Error> {
#[allow(unreachable_patterns)] // when there is only one variant
match x {
CollationProtocols::V1($v1_pat) => Ok(CollationProtocols::V1($v1_out.clone())),
CollationProtocols::V2($v2_pat) => Ok(CollationProtocols::V2($v2_out.clone())),
_ => Err(crate::WrongVariant),
}
}
}
};
}
/// Version-annotated messages used by the bitfield distribution subsystem.
pub type BitfieldDistributionMessage = ValidationProtocols<v3::BitfieldDistributionMessage>;
impl_versioned_validation_full_protocol_from!(
BitfieldDistributionMessage,
VersionedValidationProtocol,
BitfieldDistribution
);
impl_versioned_validation_try_from!(
VersionedValidationProtocol,
BitfieldDistributionMessage,
v3::ValidationProtocol::BitfieldDistribution(x) => x
);
/// Version-annotated messages used by the statement distribution subsystem.
pub type StatementDistributionMessage = ValidationProtocols<v3::StatementDistributionMessage>;
impl_versioned_validation_full_protocol_from!(
StatementDistributionMessage,
VersionedValidationProtocol,
StatementDistribution
);
impl_versioned_validation_try_from!(
VersionedValidationProtocol,
StatementDistributionMessage,
v3::ValidationProtocol::StatementDistribution(x) => x
);
/// Version-annotated messages used by the approval distribution subsystem.
pub type ApprovalDistributionMessage = ValidationProtocols<v3::ApprovalDistributionMessage>;
impl_versioned_validation_full_protocol_from!(
ApprovalDistributionMessage,
VersionedValidationProtocol,
ApprovalDistribution
);
impl_versioned_validation_try_from!(
VersionedValidationProtocol,
ApprovalDistributionMessage,
v3::ValidationProtocol::ApprovalDistribution(x) => x
);
/// Version-annotated messages used by the gossip-support subsystem (this is void).
pub type GossipSupportNetworkMessage = ValidationProtocols<v3::GossipSupportNetworkMessage>;
// This is a void enum placeholder, so never gets sent over the wire.
impl TryFrom<VersionedValidationProtocol> for GossipSupportNetworkMessage {
type Error = WrongVariant;
fn try_from(_: VersionedValidationProtocol) -> Result<Self, Self::Error> {
Err(WrongVariant)
}
}
impl<'a> TryFrom<&'a VersionedValidationProtocol> for GossipSupportNetworkMessage {
type Error = WrongVariant;
fn try_from(_: &'a VersionedValidationProtocol) -> Result<Self, Self::Error> {
Err(WrongVariant)
}
}
/// Version-annotated messages used by the collator protocol subsystem.
pub type CollatorProtocolMessage =
CollationProtocols<v1::CollatorProtocolMessage, v2::CollatorProtocolMessage>;
impl_versioned_collation_full_protocol_from!(
CollatorProtocolMessage,
VersionedCollationProtocol,
CollatorProtocol
);
impl_versioned_collation_try_from!(
VersionedCollationProtocol,
CollatorProtocolMessage,
v1::CollationProtocol::CollatorProtocol(x) => x,
v2::CollationProtocol::CollatorProtocol(x) => x
);
/// v1 notification protocol types.
pub mod v1 {
use codec::{Decode, Encode};
use pezkuwi_primitives::{CollatorId, CollatorSignature, Hash, Id as ParaId};
use pezkuwi_node_primitives::UncheckedSignedFullStatement;
/// Network messages used by the collator protocol subsystem
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub enum CollatorProtocolMessage {
/// Declare the intent to advertise collations under a collator ID, attaching a
/// signature of the `PeerId` of the node using the given collator ID key.
#[codec(index = 0)]
Declare(CollatorId, ParaId, CollatorSignature),
/// Advertise a collation to a validator. Can only be sent once the peer has
/// declared that they are a collator with given ID.
#[codec(index = 1)]
AdvertiseCollation(Hash),
/// A collation sent to a validator was seconded.
#[codec(index = 4)]
CollationSeconded(Hash, UncheckedSignedFullStatement),
}
/// All network messages on the collation peer-set.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq, derive_more::From)]
pub enum CollationProtocol {
/// Collator protocol messages
#[codec(index = 0)]
#[from]
CollatorProtocol(CollatorProtocolMessage),
}
/// Get the payload that should be signed and included in a `Declare` message.
///
/// The payload is the local peer id of the node, which serves to prove that it
/// controls the collator key it is declaring an intention to collate under.
pub fn declare_signature_payload(peer_id: &sc_network_types::PeerId) -> Vec<u8> {
let mut payload = peer_id.to_bytes();
payload.extend_from_slice(b"COLL");
payload
}
}
/// v2 network protocol types.
pub mod v2 {
use codec::{Decode, Encode};
use pezkuwi_primitives::{CandidateHash, CollatorId, CollatorSignature, Hash, Id as ParaId};
use pezkuwi_node_primitives::UncheckedSignedFullStatement;
/// This parts of the protocol did not change from v1, so just alias them in v2.
pub use super::v1::declare_signature_payload;
/// Network messages used by the collator protocol subsystem
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub enum CollatorProtocolMessage {
/// Declare the intent to advertise collations under a collator ID, attaching a
/// signature of the `PeerId` of the node using the given collator ID key.
#[codec(index = 0)]
Declare(CollatorId, ParaId, CollatorSignature),
/// Advertise a collation to a validator. Can only be sent once the peer has
/// declared that they are a collator with given ID.
#[codec(index = 1)]
AdvertiseCollation {
/// Hash of the relay parent advertised collation is based on.
relay_parent: Hash,
/// Candidate hash.
candidate_hash: CandidateHash,
/// Teyrchain head data hash before candidate execution.
parent_head_data_hash: Hash,
},
/// A collation sent to a validator was seconded.
#[codec(index = 4)]
CollationSeconded(Hash, UncheckedSignedFullStatement),
}
/// All network messages on the collation peer-set.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq, derive_more::From)]
pub enum CollationProtocol {
/// Collator protocol messages
#[codec(index = 0)]
#[from]
CollatorProtocol(CollatorProtocolMessage),
}
}
/// v3 network protocol types.
/// Purpose is for changing ApprovalDistributionMessage to
/// include more than one assignment and approval in a message.
pub mod v3 {
use bitvec::{order::Lsb0, slice::BitSlice, vec::BitVec};
use codec::{Decode, Encode};
use pezkuwi_primitives::{
CandidateHash, GroupIndex, Hash, Id as ParaId, UncheckedSignedAvailabilityBitfield,
UncheckedSignedStatement,
};
use pezkuwi_node_primitives::approval::v2::{
CandidateBitfield, IndirectAssignmentCertV2, IndirectSignedApprovalVoteV2,
};
/// This parts of the protocol did not change from v2, so just alias them in v3.
pub use super::v2::declare_signature_payload;
/// Network messages used by the bitfield distribution subsystem.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub enum BitfieldDistributionMessage {
/// A signed availability bitfield for a given relay-parent hash.
#[codec(index = 0)]
Bitfield(Hash, UncheckedSignedAvailabilityBitfield),
}
/// Bitfields indicating the statements that are known or undesired
/// about a candidate.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub struct StatementFilter {
/// Seconded statements. '1' is known or undesired.
pub seconded_in_group: BitVec<u8, Lsb0>,
/// Valid statements. '1' is known or undesired.
pub validated_in_group: BitVec<u8, Lsb0>,
}
impl StatementFilter {
/// Create a new blank filter with the given group size.
pub fn blank(group_size: usize) -> Self {
StatementFilter {
seconded_in_group: BitVec::repeat(false, group_size),
validated_in_group: BitVec::repeat(false, group_size),
}
}
/// Create a new full filter with the given group size.
pub fn full(group_size: usize) -> Self {
StatementFilter {
seconded_in_group: BitVec::repeat(true, group_size),
validated_in_group: BitVec::repeat(true, group_size),
}
}
/// Whether the filter has a specific expected length, consistent across both
/// bitfields.
pub fn has_len(&self, len: usize) -> bool {
self.seconded_in_group.len() == len && self.validated_in_group.len() == len
}
/// Determine the number of backing validators in the statement filter.
pub fn backing_validators(&self) -> usize {
self.seconded_in_group
.iter()
.by_vals()
.zip(self.validated_in_group.iter().by_vals())
.filter(|&(s, v)| s || v) // no double-counting
.count()
}
/// Whether the statement filter has at least one seconded statement.
pub fn has_seconded(&self) -> bool {
self.seconded_in_group.iter().by_vals().any(|x| x)
}
/// Mask out `Seconded` statements in `self` according to the provided
/// bitvec. Bits appearing in `mask` will not appear in `self` afterwards.
pub fn mask_seconded(&mut self, mask: &BitSlice<u8, Lsb0>) {
for (mut x, mask) in self
.seconded_in_group
.iter_mut()
.zip(mask.iter().by_vals().chain(std::iter::repeat(false)))
{
// (x, mask) => x
// (true, true) => false
// (true, false) => true
// (false, true) => false
// (false, false) => false
*x = *x && !mask;
}
}
/// Mask out `Valid` statements in `self` according to the provided
/// bitvec. Bits appearing in `mask` will not appear in `self` afterwards.
pub fn mask_valid(&mut self, mask: &BitSlice<u8, Lsb0>) {
for (mut x, mask) in self
.validated_in_group
.iter_mut()
.zip(mask.iter().by_vals().chain(std::iter::repeat(false)))
{
// (x, mask) => x
// (true, true) => false
// (true, false) => true
// (false, true) => false
// (false, false) => false
*x = *x && !mask;
}
}
}
/// A manifest of a known backed candidate, along with a description
/// of the statements backing it.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub struct BackedCandidateManifest {
/// The relay-parent of the candidate.
pub relay_parent: Hash,
/// The hash of the candidate.
pub candidate_hash: CandidateHash,
/// The group index backing the candidate at the relay-parent.
pub group_index: GroupIndex,
/// The para ID of the candidate. It is illegal for this to
/// be a para ID which is not assigned to the group indicated
/// in this manifest.
pub para_id: ParaId,
/// The head-data corresponding to the candidate.
pub parent_head_data_hash: Hash,
/// A statement filter which indicates which validators in the
/// para's group at the relay-parent have validated this candidate
/// and issued statements about it, to the advertiser's knowledge.
///
/// This MUST have exactly the minimum amount of bytes
/// necessary to represent the number of validators in the assigned
/// backing group as-of the relay-parent.
pub statement_knowledge: StatementFilter,
}
/// An acknowledgement of a backed candidate being known.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub struct BackedCandidateAcknowledgement {
/// The hash of the candidate.
pub candidate_hash: CandidateHash,
/// A statement filter which indicates which validators in the
/// para's group at the relay-parent have validated this candidate
/// and issued statements about it, to the advertiser's knowledge.
///
/// This MUST have exactly the minimum amount of bytes
/// necessary to represent the number of validators in the assigned
/// backing group as-of the relay-parent.
pub statement_knowledge: StatementFilter,
}
/// Network messages used by the statement distribution subsystem.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub enum StatementDistributionMessage {
/// A notification of a signed statement in compact form, for a given relay parent.
#[codec(index = 0)]
Statement(Hash, UncheckedSignedStatement),
/// A notification of a backed candidate being known by the
/// sending node, for the purpose of being requested by the receiving node
/// if needed.
#[codec(index = 1)]
BackedCandidateManifest(BackedCandidateManifest),
/// A notification of a backed candidate being known by the sending node,
/// for the purpose of informing a receiving node which already has the candidate.
#[codec(index = 2)]
BackedCandidateKnown(BackedCandidateAcknowledgement),
}
/// Network messages used by the approval distribution subsystem.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub enum ApprovalDistributionMessage {
/// Assignments for candidates in recent, unfinalized blocks.
/// We use a bitfield to reference claimed candidates, where the bit index is equal to
/// candidate index.
///
/// Actually checking the assignment may yield a different result.
///
/// TODO at next protocol upgrade opportunity:
/// - remove redundancy `candidate_index` vs `core_index`
/// - `<https://github.com/pezkuwichain/pezkuwi-sdk/issues/106>`
#[codec(index = 0)]
Assignments(Vec<(IndirectAssignmentCertV2, CandidateBitfield)>),
/// Approvals for candidates in some recent, unfinalized block.
#[codec(index = 1)]
Approvals(Vec<IndirectSignedApprovalVoteV2>),
}
/// Dummy network message type, so we will receive connect/disconnect events.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GossipSupportNetworkMessage {}
/// All network messages on the validation peer-set.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq, derive_more::From)]
pub enum ValidationProtocol {
/// Bitfield distribution messages
#[codec(index = 1)]
#[from]
BitfieldDistribution(BitfieldDistributionMessage),
/// Statement distribution messages
#[codec(index = 3)]
#[from]
StatementDistribution(StatementDistributionMessage),
/// Approval distribution messages
#[codec(index = 4)]
#[from]
ApprovalDistribution(ApprovalDistributionMessage),
}
}
/// Returns the subset of `peers` with the specified `version`.
pub fn filter_by_peer_version(
peers: &[(PeerId, peer_set::ProtocolVersion)],
version: peer_set::ProtocolVersion,
) -> Vec<PeerId> {
peers.iter().filter(|(_, v)| v == &version).map(|(p, _)| *p).collect::<Vec<_>>()
}
@@ -0,0 +1,616 @@
// 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/>.
//! All peersets and protocols used for teyrchains.
use derive_more::Display;
use pezkuwi_primitives::Hash;
use sc_network::{
config::SetConfig, peer_store::PeerStoreProvider, service::NotificationMetrics,
types::ProtocolName, NetworkBackend, NotificationService,
};
use sp_runtime::traits::Block;
use std::{
collections::{hash_map::Entry, HashMap},
ops::{Index, IndexMut},
sync::Arc,
};
use strum::{EnumIter, IntoEnumIterator};
/// The legacy collation protocol name. Only supported on version = 1.
const LEGACY_COLLATION_PROTOCOL_V1: &str = "/pezkuwi/collation/1";
/// The legacy protocol version. Is always 1 for collation.
const LEGACY_COLLATION_PROTOCOL_VERSION_V1: u32 = 1;
/// Max notification size is currently constant.
pub const MAX_NOTIFICATION_SIZE: u64 = 100 * 1024;
/// Maximum allowed incoming connection streams for validator nodes on the collation protocol.
pub const MAX_AUTHORITY_INCOMING_STREAMS: u32 = 100;
/// The peer-sets and thus the protocols which are used for the network.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum PeerSet {
/// The validation peer-set is responsible for all messages related to candidate validation and
/// communication among validators.
Validation,
/// The collation peer-set is used for validator<>collator communication.
Collation,
}
/// Whether a node is an authority or not.
///
/// Peer set configuration gets adjusted accordingly.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum IsAuthority {
/// Node is authority.
Yes,
/// Node is not an authority.
No,
}
impl PeerSet {
/// Get `sc_network` peer set configurations for each peerset on the default version.
///
/// Those should be used in the network configuration to register the protocols with the
/// network service.
pub fn get_info<B: Block, N: NetworkBackend<B, <B as Block>::Hash>>(
self,
is_authority: IsAuthority,
peerset_protocol_names: &PeerSetProtocolNames,
metrics: NotificationMetrics,
peer_store_handle: Arc<dyn PeerStoreProvider>,
) -> (N::NotificationProtocolConfig, (PeerSet, Box<dyn NotificationService>)) {
// Networking layer relies on `get_main_name()` being the main name of the protocol
// for peersets and connection management.
let protocol = peerset_protocol_names.get_main_name(self);
let fallback_names = PeerSetProtocolNames::get_fallback_names(
self,
&peerset_protocol_names.genesis_hash,
peerset_protocol_names.fork_id.as_deref(),
);
let max_notification_size = self.get_max_notification_size(is_authority);
match self {
PeerSet::Validation => {
let (config, notification_service) = N::notification_config(
protocol,
fallback_names,
max_notification_size,
None,
SetConfig {
// we allow full nodes to connect to validators for gossip
// to ensure any `MIN_GOSSIP_PEERS` always include reserved peers
// we limit the amount of non-reserved slots to be less
// than `MIN_GOSSIP_PEERS` in total
in_peers: super::MIN_GOSSIP_PEERS as u32 / 2 - 1,
out_peers: super::MIN_GOSSIP_PEERS as u32 / 2 - 1,
reserved_nodes: Vec::new(),
non_reserved_mode: sc_network::config::NonReservedPeerMode::Accept,
},
metrics,
peer_store_handle,
);
(config, (PeerSet::Validation, notification_service))
},
PeerSet::Collation => {
let (config, notification_service) = N::notification_config(
protocol,
fallback_names,
max_notification_size,
None,
SetConfig {
// Non-authority nodes don't need to accept incoming connections on this
// peer set:
in_peers: if is_authority == IsAuthority::Yes {
MAX_AUTHORITY_INCOMING_STREAMS
} else {
0
},
out_peers: 0,
reserved_nodes: Vec::new(),
non_reserved_mode: if is_authority == IsAuthority::Yes {
sc_network::config::NonReservedPeerMode::Accept
} else {
sc_network::config::NonReservedPeerMode::Deny
},
},
metrics,
peer_store_handle,
);
(config, (PeerSet::Collation, notification_service))
},
}
}
/// Get the main protocol version for this peer set.
///
/// Networking layer relies on `get_main_version()` being the version
/// of the main protocol name reported by [`PeerSetProtocolNames::get_main_name()`].
pub fn get_main_version(self) -> ProtocolVersion {
match self {
PeerSet::Validation => ValidationVersion::V3.into(),
PeerSet::Collation => CollationVersion::V2.into(),
}
}
/// Get the max notification size for this peer set.
pub fn get_max_notification_size(self, _: IsAuthority) -> u64 {
MAX_NOTIFICATION_SIZE
}
/// Get the peer set label for metrics reporting.
pub fn get_label(self) -> &'static str {
match self {
PeerSet::Validation => "validation",
PeerSet::Collation => "collation",
}
}
/// Get the protocol label for metrics reporting.
pub fn get_protocol_label(self, version: ProtocolVersion) -> Option<&'static str> {
// Unfortunately, labels must be static strings, so we must manually cover them
// for all protocol versions here.
match self {
PeerSet::Validation =>
if version == ValidationVersion::V3.into() {
Some("validation/3")
} else {
None
},
PeerSet::Collation =>
if version == CollationVersion::V1.into() {
Some("collation/1")
} else if version == CollationVersion::V2.into() {
Some("collation/2")
} else {
None
},
}
}
}
/// A small and nifty collection that allows to store data pertaining to each peer set.
#[derive(Debug, Default)]
pub struct PerPeerSet<T> {
validation: T,
collation: T,
}
impl<T> Index<PeerSet> for PerPeerSet<T> {
type Output = T;
fn index(&self, index: PeerSet) -> &T {
match index {
PeerSet::Validation => &self.validation,
PeerSet::Collation => &self.collation,
}
}
}
impl<T> IndexMut<PeerSet> for PerPeerSet<T> {
fn index_mut(&mut self, index: PeerSet) -> &mut T {
match index {
PeerSet::Validation => &mut self.validation,
PeerSet::Collation => &mut self.collation,
}
}
}
/// Get `NonDefaultSetConfig`s for all available peer sets, at their default versions.
///
/// Should be used during network configuration (added to `NetworkConfiguration::extra_sets`)
/// or shortly after startup to register the protocols with the network service.
pub fn peer_sets_info<B: Block, N: NetworkBackend<B, <B as Block>::Hash>>(
is_authority: IsAuthority,
peerset_protocol_names: &PeerSetProtocolNames,
metrics: NotificationMetrics,
peer_store_handle: Arc<dyn PeerStoreProvider>,
) -> Vec<(N::NotificationProtocolConfig, (PeerSet, Box<dyn NotificationService>))> {
PeerSet::iter()
.map(|s| {
s.get_info::<B, N>(
is_authority,
&peerset_protocol_names,
metrics.clone(),
Arc::clone(&peer_store_handle),
)
})
.collect()
}
/// A generic version of the protocol. This struct must not be created directly.
#[derive(Debug, Clone, Copy, Display, PartialEq, Eq, Hash)]
pub struct ProtocolVersion(u32);
impl From<ProtocolVersion> for u32 {
fn from(version: ProtocolVersion) -> u32 {
version.0
}
}
/// Supported validation protocol versions. Only versions defined here must be used in the codebase.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum ValidationVersion {
/// The third version.
V3 = 3,
}
/// Supported collation protocol versions. Only versions defined here must be used in the codebase.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum CollationVersion {
/// The first version.
V1 = 1,
/// The second version.
V2 = 2,
}
/// Marker indicating the version is unknown.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnknownVersion;
impl TryFrom<ProtocolVersion> for ValidationVersion {
type Error = UnknownVersion;
fn try_from(p: ProtocolVersion) -> Result<Self, UnknownVersion> {
for v in Self::iter() {
if v as u32 == p.0 {
return Ok(v);
}
}
Err(UnknownVersion)
}
}
impl TryFrom<ProtocolVersion> for CollationVersion {
type Error = UnknownVersion;
fn try_from(p: ProtocolVersion) -> Result<Self, UnknownVersion> {
for v in Self::iter() {
if v as u32 == p.0 {
return Ok(v);
}
}
Err(UnknownVersion)
}
}
impl From<ValidationVersion> for ProtocolVersion {
fn from(version: ValidationVersion) -> ProtocolVersion {
ProtocolVersion(version as u32)
}
}
impl From<CollationVersion> for ProtocolVersion {
fn from(version: CollationVersion) -> ProtocolVersion {
ProtocolVersion(version as u32)
}
}
/// On the wire protocol name to [`PeerSet`] mapping.
#[derive(Debug, Clone)]
pub struct PeerSetProtocolNames {
protocols: HashMap<ProtocolName, (PeerSet, ProtocolVersion)>,
names: HashMap<(PeerSet, ProtocolVersion), ProtocolName>,
genesis_hash: Hash,
fork_id: Option<String>,
}
impl PeerSetProtocolNames {
/// Construct [`PeerSetProtocolNames`] using `genesis_hash` and `fork_id`.
pub fn new(genesis_hash: Hash, fork_id: Option<&str>) -> Self {
let mut protocols = HashMap::new();
let mut names = HashMap::new();
for protocol in PeerSet::iter() {
match protocol {
PeerSet::Validation =>
for version in ValidationVersion::iter() {
Self::register_main_protocol(
&mut protocols,
&mut names,
protocol,
version.into(),
&genesis_hash,
fork_id,
);
},
PeerSet::Collation => {
for version in CollationVersion::iter() {
Self::register_main_protocol(
&mut protocols,
&mut names,
protocol,
version.into(),
&genesis_hash,
fork_id,
);
}
Self::register_legacy_collation_protocol(&mut protocols, protocol);
},
}
}
Self { protocols, names, genesis_hash, fork_id: fork_id.map(|fork_id| fork_id.into()) }
}
/// Helper function to register main protocol.
fn register_main_protocol(
protocols: &mut HashMap<ProtocolName, (PeerSet, ProtocolVersion)>,
names: &mut HashMap<(PeerSet, ProtocolVersion), ProtocolName>,
protocol: PeerSet,
version: ProtocolVersion,
genesis_hash: &Hash,
fork_id: Option<&str>,
) {
let protocol_name = Self::generate_name(genesis_hash, fork_id, protocol, version);
names.insert((protocol, version), protocol_name.clone());
Self::insert_protocol_or_panic(protocols, protocol_name, protocol, version);
}
/// Helper function to register legacy collation protocol.
fn register_legacy_collation_protocol(
protocols: &mut HashMap<ProtocolName, (PeerSet, ProtocolVersion)>,
protocol: PeerSet,
) {
Self::insert_protocol_or_panic(
protocols,
LEGACY_COLLATION_PROTOCOL_V1.into(),
protocol,
ProtocolVersion(LEGACY_COLLATION_PROTOCOL_VERSION_V1),
)
}
/// Helper function to make sure no protocols have the same name.
fn insert_protocol_or_panic(
protocols: &mut HashMap<ProtocolName, (PeerSet, ProtocolVersion)>,
name: ProtocolName,
protocol: PeerSet,
version: ProtocolVersion,
) {
match protocols.entry(name) {
Entry::Vacant(entry) => {
entry.insert((protocol, version));
},
Entry::Occupied(entry) => {
panic!(
"Protocol {:?} (version {}) has the same on-the-wire name as protocol {:?} (version {}): `{}`.",
protocol,
version,
entry.get().0,
entry.get().1,
entry.key(),
);
},
}
}
/// Lookup the protocol using its on the wire name.
pub fn try_get_protocol(&self, name: &ProtocolName) -> Option<(PeerSet, ProtocolVersion)> {
self.protocols.get(name).map(ToOwned::to_owned)
}
/// Get the main protocol name. It's used by the networking for keeping track
/// of peersets and connections.
pub fn get_main_name(&self, protocol: PeerSet) -> ProtocolName {
self.get_name(protocol, protocol.get_main_version())
}
/// Get the protocol name for specific version.
pub fn get_name(&self, protocol: PeerSet, version: ProtocolVersion) -> ProtocolName {
self.names
.get(&(protocol, version))
.expect("Protocols & versions are specified via enums defined above, and they are all registered in `new()`; qed")
.clone()
}
/// The protocol name of this protocol based on `genesis_hash` and `fork_id`.
fn generate_name(
genesis_hash: &Hash,
fork_id: Option<&str>,
protocol: PeerSet,
version: ProtocolVersion,
) -> ProtocolName {
let prefix = if let Some(fork_id) = fork_id {
format!("/{}/{}", hex::encode(genesis_hash), fork_id)
} else {
format!("/{}", hex::encode(genesis_hash))
};
let short_name = match protocol {
PeerSet::Validation => "validation",
PeerSet::Collation => "collation",
};
format!("{}/{}/{}", prefix, short_name, version).into()
}
/// Get the protocol fallback names. Currently, it only holds
/// the legacy name for the collation protocol version 1.
fn get_fallback_names(
protocol: PeerSet,
_genesis_hash: &Hash,
_fork_id: Option<&str>,
) -> Vec<ProtocolName> {
let mut fallbacks = vec![];
match protocol {
PeerSet::Validation => {
// The validation protocol no longer supports protocol versions 1 and 2,
// and only version 3 is used. Therefore, fallback protocols remain empty.
},
PeerSet::Collation => {
fallbacks.push(LEGACY_COLLATION_PROTOCOL_V1.into());
},
};
fallbacks
}
}
#[cfg(test)]
mod tests {
use super::{
CollationVersion, Hash, PeerSet, PeerSetProtocolNames, ProtocolVersion, ValidationVersion,
};
use strum::IntoEnumIterator;
struct TestVersion(u32);
impl From<TestVersion> for ProtocolVersion {
fn from(version: TestVersion) -> ProtocolVersion {
ProtocolVersion(version.0)
}
}
#[test]
fn protocol_names_are_correctly_generated() {
let genesis_hash = Hash::from([
122, 200, 116, 29, 232, 183, 20, 109, 138, 86, 23, 253, 70, 41, 20, 85, 127, 230, 60,
38, 90, 127, 28, 16, 231, 218, 227, 40, 88, 238, 187, 128,
]);
let name = PeerSetProtocolNames::generate_name(
&genesis_hash,
None,
PeerSet::Validation,
TestVersion(3).into(),
);
let expected =
"/7ac8741de8b7146d8a5617fd462914557fe63c265a7f1c10e7dae32858eebb80/validation/3";
assert_eq!(name, expected.into());
let name = PeerSetProtocolNames::generate_name(
&genesis_hash,
None,
PeerSet::Collation,
TestVersion(5).into(),
);
let expected =
"/7ac8741de8b7146d8a5617fd462914557fe63c265a7f1c10e7dae32858eebb80/collation/5";
assert_eq!(name, expected.into());
let fork_id = Some("test-fork");
let name = PeerSetProtocolNames::generate_name(
&genesis_hash,
fork_id,
PeerSet::Validation,
TestVersion(7).into(),
);
let expected =
"/7ac8741de8b7146d8a5617fd462914557fe63c265a7f1c10e7dae32858eebb80/test-fork/validation/7";
assert_eq!(name, expected.into());
let name = PeerSetProtocolNames::generate_name(
&genesis_hash,
fork_id,
PeerSet::Collation,
TestVersion(11).into(),
);
let expected =
"/7ac8741de8b7146d8a5617fd462914557fe63c265a7f1c10e7dae32858eebb80/test-fork/collation/11";
assert_eq!(name, expected.into());
}
#[test]
fn all_protocol_names_are_known() {
let genesis_hash = Hash::from([
122, 200, 116, 29, 232, 183, 20, 109, 138, 86, 23, 253, 70, 41, 20, 85, 127, 230, 60,
38, 90, 127, 28, 16, 231, 218, 227, 40, 88, 238, 187, 128,
]);
let protocol_names = PeerSetProtocolNames::new(genesis_hash, None);
let validation_main =
"/7ac8741de8b7146d8a5617fd462914557fe63c265a7f1c10e7dae32858eebb80/validation/3";
assert_eq!(
protocol_names.try_get_protocol(&validation_main.into()),
Some((PeerSet::Validation, TestVersion(3).into())),
);
let validation_legacy = "/pezkuwi/validation/1";
assert!(protocol_names.try_get_protocol(&validation_legacy.into()).is_none());
let collation_main =
"/7ac8741de8b7146d8a5617fd462914557fe63c265a7f1c10e7dae32858eebb80/collation/1";
assert_eq!(
protocol_names.try_get_protocol(&collation_main.into()),
Some((PeerSet::Collation, TestVersion(1).into())),
);
let collation_legacy = "/pezkuwi/collation/1";
assert_eq!(
protocol_names.try_get_protocol(&collation_legacy.into()),
Some((PeerSet::Collation, TestVersion(1).into())),
);
}
#[test]
fn all_protocol_versions_are_registered() {
let genesis_hash = Hash::from([
122, 200, 116, 29, 232, 183, 20, 109, 138, 86, 23, 253, 70, 41, 20, 85, 127, 230, 60,
38, 90, 127, 28, 16, 231, 218, 227, 40, 88, 238, 187, 128,
]);
let protocol_names = PeerSetProtocolNames::new(genesis_hash, None);
for protocol in PeerSet::iter() {
match protocol {
PeerSet::Validation =>
for version in ValidationVersion::iter() {
assert_eq!(
protocol_names.get_name(protocol, version.into()),
PeerSetProtocolNames::generate_name(
&genesis_hash,
None,
protocol,
version.into(),
),
);
},
PeerSet::Collation =>
for version in CollationVersion::iter() {
assert_eq!(
protocol_names.get_name(protocol, version.into()),
PeerSetProtocolNames::generate_name(
&genesis_hash,
None,
protocol,
version.into(),
),
);
},
}
}
}
#[test]
fn all_protocol_versions_have_labels() {
for protocol in PeerSet::iter() {
match protocol {
PeerSet::Validation =>
for version in ValidationVersion::iter() {
protocol
.get_protocol_label(version.into())
.expect("All validation protocol versions must have a label.");
},
PeerSet::Collation =>
for version in CollationVersion::iter() {
protocol
.get_protocol_label(version.into())
.expect("All collation protocol versions must have a label.");
},
}
}
}
}
@@ -0,0 +1,90 @@
// 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/>.
pub use sc_network::ReputationChange;
/// Unified annoyance cost and good behavior benefits.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum UnifiedReputationChange {
CostMajor(&'static str),
CostMinor(&'static str),
CostMajorRepeated(&'static str),
CostMinorRepeated(&'static str),
Malicious(&'static str),
BenefitMinorFirst(&'static str),
BenefitMinor(&'static str),
BenefitMajorFirst(&'static str),
BenefitMajor(&'static str),
}
impl UnifiedReputationChange {
/// Obtain the cost or benefit associated with
/// the enum variant.
///
/// Order of magnitude rationale:
///
/// * the peerset will not connect to a peer whose reputation is below a fixed value
/// * `max(2% *$rep, 1)` is the delta of convergence towards a reputation of 0
///
/// The whole range of an `i32` should be used, so order of magnitude of
/// something malicious should be `1<<20` (give or take).
pub const fn cost_or_benefit(&self) -> i32 {
match self {
Self::CostMinor(_) => -100_000,
Self::CostMajor(_) => -300_000,
Self::CostMinorRepeated(_) => -200_000,
Self::CostMajorRepeated(_) => -600_000,
Self::Malicious(_) => i32::MIN,
Self::BenefitMajorFirst(_) => 300_000,
Self::BenefitMajor(_) => 200_000,
Self::BenefitMinorFirst(_) => 15_000,
Self::BenefitMinor(_) => 10_000,
}
}
/// Extract the static description.
pub const fn description(&self) -> &'static str {
match self {
Self::CostMinor(description) => description,
Self::CostMajor(description) => description,
Self::CostMinorRepeated(description) => description,
Self::CostMajorRepeated(description) => description,
Self::Malicious(description) => description,
Self::BenefitMajorFirst(description) => description,
Self::BenefitMajor(description) => description,
Self::BenefitMinorFirst(description) => description,
Self::BenefitMinor(description) => description,
}
}
/// Whether the reputation change is for good behavior.
pub const fn is_benefit(&self) -> bool {
match self {
Self::BenefitMajorFirst(_) |
Self::BenefitMajor(_) |
Self::BenefitMinorFirst(_) |
Self::BenefitMinor(_) => true,
_ => false,
}
}
}
impl From<UnifiedReputationChange> for ReputationChange {
fn from(value: UnifiedReputationChange) -> Self {
ReputationChange::new(value.cost_or_benefit(), value.description())
}
}
@@ -0,0 +1,41 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Pezkuwi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Error handling related code and Error/Result definitions.
use sc_network_types::PeerId;
use codec::Error as DecodingError;
#[allow(missing_docs)]
#[fatality::fatality(splitable)]
pub enum Error {
// Incoming request stream exhausted. Should only happen on shutdown.
#[fatal]
#[error("Incoming request channel got closed.")]
RequestChannelExhausted,
/// Decoding failed, we were able to change the peer's reputation accordingly.
#[error("Decoding request failed for peer {0}.")]
DecodingError(PeerId, #[source] DecodingError),
/// Decoding failed, but sending reputation change failed.
#[error("Decoding request failed for peer {0}, and changing reputation failed.")]
DecodingErrorNoReputationChange(PeerId, #[source] DecodingError),
}
/// General result based on above `Error`.
pub type Result<T> = std::result::Result<T, Error>;
@@ -0,0 +1,232 @@
// 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::marker::PhantomData;
use futures::{channel::oneshot, StreamExt};
use codec::{Decode, Encode};
use sc_network::{config as netconfig, NetworkBackend};
use sc_network_types::PeerId;
use sp_runtime::traits::Block;
use super::{IsRequest, ReqProtocolNames};
use crate::UnifiedReputationChange;
mod error;
pub use error::{Error, FatalError, JfyiError, Result};
/// A request coming in, including a sender for sending responses.
///
/// Typed `IncomingRequest`s, see `IncomingRequest::get_config_receiver` and substrate
/// `NetworkConfiguration` for more information.
#[derive(Debug)]
pub struct IncomingRequest<Req> {
/// `PeerId` of sending peer.
pub peer: PeerId,
/// The sent request.
pub payload: Req,
/// Sender for sending response back.
pub pending_response: OutgoingResponseSender<Req>,
}
impl<Req> IncomingRequest<Req>
where
Req: IsRequest + Decode + Encode,
Req::Response: Encode,
{
/// Create configuration for `NetworkConfiguration::request_response_protocols` and a
/// corresponding typed receiver.
///
/// This Register that config with substrate networking and receive incoming requests via the
/// returned `IncomingRequestReceiver`.
pub fn get_config_receiver<B: Block, N: NetworkBackend<B, <B as Block>::Hash>>(
req_protocol_names: &ReqProtocolNames,
) -> (IncomingRequestReceiver<Req>, N::RequestResponseProtocolConfig) {
let (raw, cfg) = Req::PROTOCOL.get_config::<B, N>(req_protocol_names);
(IncomingRequestReceiver { raw, phantom: PhantomData {} }, cfg)
}
/// Create new `IncomingRequest`.
pub fn new(
peer: PeerId,
payload: Req,
pending_response: oneshot::Sender<netconfig::OutgoingResponse>,
) -> Self {
Self {
peer,
payload,
pending_response: OutgoingResponseSender { pending_response, phantom: PhantomData {} },
}
}
/// Try building from raw substrate request.
///
/// This function will fail if the request cannot be decoded and will apply passed in
/// reputation changes in that case.
///
/// Params:
/// - The raw request to decode
/// - Reputation changes to apply for the peer in case decoding fails.
fn try_from_raw(
raw: sc_network::config::IncomingRequest,
reputation_changes: Vec<UnifiedReputationChange>,
) -> std::result::Result<Self, JfyiError> {
let sc_network::config::IncomingRequest { payload, peer, pending_response } = raw;
let payload = match Req::decode(&mut payload.as_ref()) {
Ok(payload) => payload,
Err(err) => {
let reputation_changes = reputation_changes.into_iter().map(|r| r.into()).collect();
let response = sc_network::config::OutgoingResponse {
result: Err(()),
reputation_changes,
sent_feedback: None,
};
if let Err(_) = pending_response.send(response) {
return Err(JfyiError::DecodingErrorNoReputationChange(peer, err));
}
return Err(JfyiError::DecodingError(peer, err));
},
};
Ok(Self::new(peer, payload, pending_response))
}
/// Convert into raw untyped substrate `IncomingRequest`.
///
/// This is mostly useful for testing.
pub fn into_raw(self) -> sc_network::config::IncomingRequest {
sc_network::config::IncomingRequest {
peer: self.peer,
payload: self.payload.encode(),
pending_response: self.pending_response.pending_response,
}
}
/// Send the response back.
///
/// Calls [`OutgoingResponseSender::send_response`].
pub fn send_response(self, resp: Req::Response) -> std::result::Result<(), Req::Response> {
self.pending_response.send_response(resp)
}
/// Send response with additional options.
///
/// Calls [`OutgoingResponseSender::send_outgoing_response`].
pub fn send_outgoing_response(
self,
resp: OutgoingResponse<<Req as IsRequest>::Response>,
) -> std::result::Result<(), ()> {
self.pending_response.send_outgoing_response(resp)
}
}
/// Sender for sending back responses on an `IncomingRequest`.
#[derive(Debug)]
pub struct OutgoingResponseSender<Req> {
pending_response: oneshot::Sender<netconfig::OutgoingResponse>,
phantom: PhantomData<Req>,
}
impl<Req> OutgoingResponseSender<Req>
where
Req: IsRequest + Decode,
Req::Response: Encode,
{
/// Send the response back.
///
/// On success we return `Ok(())`, on error we return the not sent `Response`.
///
/// `netconfig::OutgoingResponse` exposes a way of modifying the peer's reputation. If needed we
/// can change this function to expose this feature as well.
pub fn send_response(self, resp: Req::Response) -> std::result::Result<(), Req::Response> {
self.pending_response
.send(netconfig::OutgoingResponse {
result: Ok(resp.encode()),
reputation_changes: Vec::new(),
sent_feedback: None,
})
.map_err(|_| resp)
}
/// Send response with additional options.
///
/// This variant allows for waiting for the response to be sent out, allows for changing peer's
/// reputation and allows for not sending a response at all (for only changing the peer's
/// reputation).
pub fn send_outgoing_response(
self,
resp: OutgoingResponse<<Req as IsRequest>::Response>,
) -> std::result::Result<(), ()> {
let OutgoingResponse { result, reputation_changes, sent_feedback } = resp;
let response = netconfig::OutgoingResponse {
result: result.map(|v| v.encode()),
reputation_changes: reputation_changes.into_iter().map(|c| c.into()).collect(),
sent_feedback,
};
self.pending_response.send(response).map_err(|_| ())
}
}
/// Typed variant of [`netconfig::OutgoingResponse`].
///
/// Responses to `IncomingRequest`s.
pub struct OutgoingResponse<Response> {
/// The payload of the response.
///
/// `Err(())` if none is available e.g. due to an error while handling the request.
pub result: std::result::Result<Response, ()>,
/// Reputation changes accrued while handling the request. To be applied to the reputation of
/// the peer sending the request.
pub reputation_changes: Vec<UnifiedReputationChange>,
/// If provided, the `oneshot::Sender` will be notified when the request has been sent to the
/// peer.
pub sent_feedback: Option<oneshot::Sender<()>>,
}
/// Receiver for incoming requests.
///
/// Takes care of decoding and handling of invalid encoded requests.
pub struct IncomingRequestReceiver<Req> {
raw: async_channel::Receiver<netconfig::IncomingRequest>,
phantom: PhantomData<Req>,
}
impl<Req> IncomingRequestReceiver<Req>
where
Req: IsRequest + Decode + Encode,
Req::Response: Encode,
{
/// Try to receive the next incoming request.
///
/// Any received request will be decoded, on decoding errors the provided reputation changes
/// will be applied and an error will be reported.
pub async fn recv<F>(&mut self, reputation_changes: F) -> Result<IncomingRequest<Req>>
where
F: FnOnce() -> Vec<UnifiedReputationChange>,
{
let req = match self.raw.next().await {
None => return Err(FatalError::RequestChannelExhausted.into()),
Some(raw) => IncomingRequest::<Req>::try_from_raw(raw, reputation_changes())?,
};
Ok(req)
}
}
@@ -0,0 +1,377 @@
// 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/>.
//! Overview over request/responses as used in `Pezkuwi`.
//!
//! `enum Protocol` .... List of all supported protocols.
//!
//! `enum Requests` .... List of all supported requests, each entry matches one in protocols, but
//! has the actual request as payload.
//!
//! `struct IncomingRequest` .... wrapper for incoming requests, containing a sender for sending
//! responses.
//!
//! `struct OutgoingRequest` .... wrapper for outgoing requests, containing a sender used by the
//! networking code for delivering responses/delivery errors.
//!
//! `trait IsRequest` .... A trait describing a particular request. It is used for gathering meta
//! data, like what is the corresponding response type.
//!
//! ## Versioning
//!
//! Versioning for request-response protocols can be done in multiple ways.
//!
//! If you're just changing the protocol name but the binary payloads are the same, just add a new
//! `fallback_name` to the protocol config.
//!
//! One way in which versioning has historically been achieved for req-response protocols is to
//! bundle the new req-resp version with an upgrade of a notifications protocol. The subsystem would
//! then know which request version to use based on stored data about the peer's notifications
//! protocol version.
//!
//! When bumping a notifications protocol version is not needed/desirable, you may add a new
//! req-resp protocol and set the old request as a fallback (see
//! `OutgoingRequest::new_with_fallback`). A request with the new version will be attempted and if
//! the protocol is refused by the peer, the fallback protocol request will be used.
//! Information about the actually used protocol will be returned alongside the raw response, so
//! that you know how to decode it.
use std::{collections::HashMap, time::Duration, u64};
use pezkuwi_primitives::MAX_CODE_SIZE;
use sc_network::{NetworkBackend, MAX_RESPONSE_SIZE};
use sp_runtime::traits::Block;
use strum::{EnumIter, IntoEnumIterator};
pub use sc_network::{config as network, config::RequestResponseConfig, ProtocolName};
/// Everything related to handling of incoming requests.
pub mod incoming;
/// Everything related to handling of outgoing requests.
pub mod outgoing;
pub use incoming::{IncomingRequest, IncomingRequestReceiver};
pub use outgoing::{OutgoingRequest, OutgoingResult, Recipient, Requests, ResponseSender};
///// Multiplexer for incoming requests.
// pub mod multiplexer;
/// Actual versioned requests and responses that are sent over the wire.
pub mod v1;
/// Actual versioned requests and responses that are sent over the wire.
pub mod v2;
/// A protocol per subsystem seems to make the most sense, this way we don't need any dispatching
/// within protocols.
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, EnumIter)]
pub enum Protocol {
/// Protocol for chunk fetching, used by availability distribution and availability recovery.
ChunkFetchingV1,
/// Protocol for fetching collations from collators.
CollationFetchingV1,
/// Protocol for fetching collations from collators when async backing is enabled.
CollationFetchingV2,
/// Protocol for fetching seconded PoVs from validators of the same group.
PoVFetchingV1,
/// Protocol for fetching available data.
AvailableDataFetchingV1,
/// Sending of dispute statements with application level confirmations.
DisputeSendingV1,
/// Protocol for requesting candidates with attestations in statement distribution
/// when async backing is enabled.
AttestedCandidateV2,
/// Protocol for chunk fetching version 2, used by availability distribution and availability
/// recovery.
ChunkFetchingV2,
}
/// Minimum bandwidth we expect for validators - 500Mbit/s is the recommendation, so approximately
/// 50MB per second:
const MIN_BANDWIDTH_BYTES: u64 = 50 * 1024 * 1024;
/// Default request timeout in seconds.
///
/// When decreasing this value, take into account that the very first request might need to open a
/// connection, which can be slow. If this causes problems, we should ensure connectivity via peer
/// sets.
#[allow(dead_code)]
const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(3);
/// Request timeout where we can assume the connection is already open (e.g. we have peers in a
/// peer set as well).
const DEFAULT_REQUEST_TIMEOUT_CONNECTED: Duration = Duration::from_secs(1);
/// Timeout for requesting availability chunks.
pub const CHUNK_REQUEST_TIMEOUT: Duration = DEFAULT_REQUEST_TIMEOUT_CONNECTED;
/// This timeout is based on the following parameters, assuming we use asynchronous backing with no
/// time budget within a relay block:
/// - 500 Mbit/s networking speed
/// - 10 MB PoV
/// - 10 parallel executions
const POV_REQUEST_TIMEOUT_CONNECTED: Duration = Duration::from_millis(2000);
/// We want attested candidate requests to time out relatively fast,
/// because slow requests will bottleneck the backing system. Ideally, we'd have
/// an adaptive timeout based on the candidate size, because there will be a lot of variance
/// in candidate sizes: candidates with no code and no messages vs candidates with code
/// and messages.
///
/// We supply leniency because there are often large candidates and asynchronous
/// backing allows them to be included over a longer window of time. Exponential back-off
/// up to a maximum of 10 seconds would be ideal, but isn't supported by the
/// infrastructure here yet: see https://github.com/paritytech/polkadot/issues/6009
const ATTESTED_CANDIDATE_TIMEOUT: Duration = Duration::from_millis(2500);
/// We don't want a slow peer to slow down all the others, at the same time we want to get out the
/// data quickly in full to at least some peers (as this will reduce load on us as they then can
/// start serving the data). So this value is a tradeoff. 5 seems to be sensible. So we would need
/// to have 5 slow nodes connected, to delay transfer for others by `ATTESTED_CANDIDATE_TIMEOUT`.
pub const MAX_PARALLEL_ATTESTED_CANDIDATE_REQUESTS: u32 = 5;
/// Response size limit for responses of POV like data.
///
/// Same as what we use in substrate networking.
const POV_RESPONSE_SIZE: u64 = MAX_RESPONSE_SIZE;
/// Maximum response sizes for `AttestedCandidateV2`.
///
/// This is `MAX_CODE_SIZE` plus some additional space for protocol overhead and
/// additional backing statements.
const ATTESTED_CANDIDATE_RESPONSE_SIZE: u64 = MAX_CODE_SIZE as u64 + 100_000;
/// We can have relative large timeouts here, there is no value of hitting a
/// timeout as we want to get statements through to each node in any case.
pub const DISPUTE_REQUEST_TIMEOUT: Duration = Duration::from_secs(12);
impl Protocol {
/// Get a configuration for a given Request response protocol.
///
/// Returns a `ProtocolConfig` for this protocol.
/// Use this if you plan only to send requests for this protocol.
pub fn get_outbound_only_config<B: Block, N: NetworkBackend<B, <B as Block>::Hash>>(
self,
req_protocol_names: &ReqProtocolNames,
) -> N::RequestResponseProtocolConfig {
self.create_config::<B, N>(req_protocol_names, None)
}
/// Get a configuration for a given Request response protocol.
///
/// Returns a receiver for messages received on this protocol and the requested
/// `ProtocolConfig`.
pub fn get_config<B: Block, N: NetworkBackend<B, <B as Block>::Hash>>(
self,
req_protocol_names: &ReqProtocolNames,
) -> (async_channel::Receiver<network::IncomingRequest>, N::RequestResponseProtocolConfig) {
let (tx, rx) = async_channel::bounded(self.get_channel_size());
let cfg = self.create_config::<B, N>(req_protocol_names, Some(tx));
(rx, cfg)
}
fn create_config<B: Block, N: NetworkBackend<B, <B as Block>::Hash>>(
self,
req_protocol_names: &ReqProtocolNames,
tx: Option<async_channel::Sender<network::IncomingRequest>>,
) -> N::RequestResponseProtocolConfig {
let name = req_protocol_names.get_name(self);
let legacy_names = self.get_legacy_name().into_iter().map(Into::into).collect();
match self {
Protocol::ChunkFetchingV1 | Protocol::ChunkFetchingV2 => N::request_response_config(
name,
legacy_names,
1_000,
POV_RESPONSE_SIZE,
// We are connected to all validators:
CHUNK_REQUEST_TIMEOUT,
tx,
),
Protocol::CollationFetchingV1 | Protocol::CollationFetchingV2 => {
N::request_response_config(
name,
legacy_names,
1_000,
POV_RESPONSE_SIZE,
// Taken from initial implementation in collator protocol:
POV_REQUEST_TIMEOUT_CONNECTED,
tx,
)
},
Protocol::PoVFetchingV1 => N::request_response_config(
name,
legacy_names,
1_000,
POV_RESPONSE_SIZE,
POV_REQUEST_TIMEOUT_CONNECTED,
tx,
),
Protocol::AvailableDataFetchingV1 => N::request_response_config(
name,
legacy_names,
1_000,
// Available data size is dominated by the PoV size.
POV_RESPONSE_SIZE,
POV_REQUEST_TIMEOUT_CONNECTED,
tx,
),
Protocol::DisputeSendingV1 => N::request_response_config(
name,
legacy_names,
1_000,
// Responses are just confirmation, in essence not even a bit. So 100 seems
// plenty.
100,
DISPUTE_REQUEST_TIMEOUT,
tx,
),
Protocol::AttestedCandidateV2 => N::request_response_config(
name,
legacy_names,
1_000,
ATTESTED_CANDIDATE_RESPONSE_SIZE,
ATTESTED_CANDIDATE_TIMEOUT,
tx,
),
}
}
// Channel sizes for the supported protocols.
fn get_channel_size(self) -> usize {
match self {
// Hundreds of validators will start requesting their chunks once they see a candidate
// awaiting availability on chain. Given that they will see that block at different
// times (due to network delays), 100 seems big enough to accommodate for "bursts",
// assuming we can service requests relatively quickly, which would need to be measured
// as well.
Protocol::ChunkFetchingV1 | Protocol::ChunkFetchingV2 => 100,
// 10 seems reasonable, considering group sizes of max 10 validators.
Protocol::CollationFetchingV1 | Protocol::CollationFetchingV2 => 10,
// 10 seems reasonable, considering group sizes of max 10 validators.
Protocol::PoVFetchingV1 => 10,
// Validators are constantly self-selecting to request available data which may lead
// to constant load and occasional burstiness.
Protocol::AvailableDataFetchingV1 => 100,
// Incoming requests can get bursty, we should also be able to handle them fast on
// average, so something in the ballpark of 100 should be fine. Nodes will retry on
// failure, so having a good value here is mostly about performance tuning.
Protocol::DisputeSendingV1 => 100,
Protocol::AttestedCandidateV2 => {
// We assume we can utilize up to 70% of the available bandwidth for statements.
// This is just a guess/estimate, with the following considerations: If we are
// faster than that, queue size will stay low anyway, even if not - requesters will
// get an immediate error, but if we are slower, requesters will run in a timeout -
// wasting precious time.
let available_bandwidth = 7 * MIN_BANDWIDTH_BYTES / 10;
let size = u64::saturating_sub(
ATTESTED_CANDIDATE_TIMEOUT.as_millis() as u64 * available_bandwidth /
(1000 * MAX_CODE_SIZE as u64),
MAX_PARALLEL_ATTESTED_CANDIDATE_REQUESTS as u64,
);
debug_assert!(
size > 0,
"We should have a channel size greater zero, otherwise we won't accept any requests."
);
size as usize
},
}
}
/// Legacy protocol name associated with each peer set, if any.
/// The request will be tried on this legacy protocol name if the remote refuses to speak the
/// protocol.
const fn get_legacy_name(self) -> Option<&'static str> {
match self {
Protocol::ChunkFetchingV1 => Some("/pezkuwi/req_chunk/1"),
Protocol::CollationFetchingV1 => Some("/pezkuwi/req_collation/1"),
Protocol::PoVFetchingV1 => Some("/pezkuwi/req_pov/1"),
Protocol::AvailableDataFetchingV1 => Some("/pezkuwi/req_available_data/1"),
Protocol::DisputeSendingV1 => Some("/pezkuwi/send_dispute/1"),
// Introduced after legacy names became legacy.
Protocol::AttestedCandidateV2 => None,
Protocol::CollationFetchingV2 => None,
Protocol::ChunkFetchingV2 => None,
}
}
}
/// Common properties of any `Request`.
pub trait IsRequest {
/// Each request has a corresponding `Response`.
type Response;
/// What protocol this `Request` implements.
const PROTOCOL: Protocol;
}
/// Type for getting on the wire [`Protocol`] names using genesis hash & fork id.
#[derive(Clone)]
pub struct ReqProtocolNames {
names: HashMap<Protocol, ProtocolName>,
}
impl ReqProtocolNames {
/// Construct [`ReqProtocolNames`] from `genesis_hash` and `fork_id`.
pub fn new<Hash: AsRef<[u8]>>(genesis_hash: Hash, fork_id: Option<&str>) -> Self {
let mut names = HashMap::new();
for protocol in Protocol::iter() {
names.insert(protocol, Self::generate_name(protocol, &genesis_hash, fork_id));
}
Self { names }
}
/// Get on the wire [`Protocol`] name.
pub fn get_name(&self, protocol: Protocol) -> ProtocolName {
self.names
.get(&protocol)
.expect("All `Protocol` enum variants are added above via `strum`; qed")
.clone()
}
/// Protocol name of this protocol based on `genesis_hash` and `fork_id`.
fn generate_name<Hash: AsRef<[u8]>>(
protocol: Protocol,
genesis_hash: &Hash,
fork_id: Option<&str>,
) -> ProtocolName {
let prefix = if let Some(fork_id) = fork_id {
format!("/{}/{}", hex::encode(genesis_hash), fork_id)
} else {
format!("/{}", hex::encode(genesis_hash))
};
let short_name = match protocol {
// V1:
Protocol::ChunkFetchingV1 => "/req_chunk/1",
Protocol::CollationFetchingV1 => "/req_collation/1",
Protocol::PoVFetchingV1 => "/req_pov/1",
Protocol::AvailableDataFetchingV1 => "/req_available_data/1",
Protocol::DisputeSendingV1 => "/send_dispute/1",
// V2:
Protocol::CollationFetchingV2 => "/req_collation/2",
Protocol::AttestedCandidateV2 => "/req_attested_candidate/2",
Protocol::ChunkFetchingV2 => "/req_chunk/2",
};
format!("{}{}", prefix, short_name).into()
}
}
@@ -0,0 +1,205 @@
// 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 futures::{channel::oneshot, prelude::Future, FutureExt};
use codec::{Decode, Encode, Error as DecodingError};
use network::ProtocolName;
use sc_network as network;
use sc_network_types::PeerId;
use pezkuwi_primitives::AuthorityDiscoveryId;
use super::{v1, v2, IsRequest, Protocol};
/// All requests that can be sent to the network bridge via `NetworkBridgeTxMessage::SendRequest`.
#[derive(Debug)]
pub enum Requests {
/// Request an availability chunk from a node.
ChunkFetching(OutgoingRequest<v2::ChunkFetchingRequest, v1::ChunkFetchingRequest>),
/// Fetch a collation from a collator which previously announced it.
CollationFetchingV1(OutgoingRequest<v1::CollationFetchingRequest>),
/// Fetch a PoV from a validator which previously sent out a seconded statement.
PoVFetchingV1(OutgoingRequest<v1::PoVFetchingRequest>),
/// Request full available data from a node.
AvailableDataFetchingV1(OutgoingRequest<v1::AvailableDataFetchingRequest>),
/// Requests for notifying about an ongoing dispute.
DisputeSendingV1(OutgoingRequest<v1::DisputeRequest>),
/// Request a candidate and attestations.
AttestedCandidateV2(OutgoingRequest<v2::AttestedCandidateRequest>),
/// Fetch a collation from a collator which previously announced it.
/// Compared to V1 it requires specifying which candidate is requested by its hash.
CollationFetchingV2(OutgoingRequest<v2::CollationFetchingRequest>),
}
impl Requests {
/// Encode the request.
///
/// The corresponding protocol is returned as well, as we are now leaving typed territory.
///
/// Note: `Requests` is just an enum collecting all supported requests supported by network
/// bridge, it is never sent over the wire. This function just encodes the individual requests
/// contained in the `enum`.
pub fn encode_request(self) -> (Protocol, OutgoingRequest<Vec<u8>>) {
match self {
Self::ChunkFetching(r) => r.encode_request(),
Self::CollationFetchingV1(r) => r.encode_request(),
Self::CollationFetchingV2(r) => r.encode_request(),
Self::PoVFetchingV1(r) => r.encode_request(),
Self::AvailableDataFetchingV1(r) => r.encode_request(),
Self::DisputeSendingV1(r) => r.encode_request(),
Self::AttestedCandidateV2(r) => r.encode_request(),
}
}
}
/// Used by the network to send us a response to a request.
pub type ResponseSender = oneshot::Sender<Result<(Vec<u8>, ProtocolName), network::RequestFailure>>;
/// Any error that can occur when sending a request.
#[derive(Debug, thiserror::Error)]
pub enum RequestError {
/// Response could not be decoded.
#[error("Response could not be decoded: {0}")]
InvalidResponse(#[from] DecodingError),
/// Some error in substrate/libp2p happened.
#[error("{0}")]
NetworkError(#[from] network::RequestFailure),
/// Response got canceled by networking.
#[error("Response channel got canceled")]
Canceled(#[from] oneshot::Canceled),
}
impl RequestError {
/// Whether the error represents some kind of timeout condition.
pub fn is_timed_out(&self) -> bool {
match self {
Self::Canceled(_) |
Self::NetworkError(network::RequestFailure::Obsolete) |
Self::NetworkError(network::RequestFailure::Network(
network::OutboundFailure::Timeout,
)) => true,
_ => false,
}
}
}
/// A request to be sent to the network bridge, including a sender for sending responses/failures.
///
/// The network implementation will make use of that sender for informing the requesting subsystem
/// about responses/errors.
///
/// When using `Recipient::Peer`, keep in mind that no address (as in IP address and port) might
/// be known for that specific peer. You are encouraged to use `Peer` for peers that you are
/// expected to be already connected to.
/// When using `Recipient::Authority`, the addresses can be found thanks to the authority
/// discovery system.
#[derive(Debug)]
pub struct OutgoingRequest<Req, FallbackReq = Req> {
/// Intended recipient of this request.
pub peer: Recipient,
/// The actual request to send over the wire.
pub payload: Req,
/// Optional fallback request and protocol.
pub fallback_request: Option<(FallbackReq, Protocol)>,
/// Sender which is used by networking to get us back a response.
pub pending_response: ResponseSender,
}
/// Potential recipients of an outgoing request.
#[derive(Debug, Eq, Hash, PartialEq, Clone)]
pub enum Recipient {
/// Recipient is a regular peer and we know its peer id.
Peer(PeerId),
/// Recipient is a validator, we address it via this `AuthorityDiscoveryId`.
Authority(AuthorityDiscoveryId),
}
/// Responses received for an `OutgoingRequest`.
pub type OutgoingResult<Res> = Result<Res, RequestError>;
impl<Req, FallbackReq> OutgoingRequest<Req, FallbackReq>
where
Req: IsRequest + Encode,
Req::Response: Decode,
FallbackReq: IsRequest + Encode,
FallbackReq::Response: Decode,
{
/// Create a new `OutgoingRequest`.
///
/// It will contain a sender that is used by the networking for sending back responses. The
/// connected receiver is returned as the second element in the returned tuple.
pub fn new(
peer: Recipient,
payload: Req,
) -> (Self, impl Future<Output = OutgoingResult<Req::Response>>) {
let (tx, rx) = oneshot::channel();
let r = Self { peer, payload, pending_response: tx, fallback_request: None };
(r, receive_response::<Req>(rx.map(|r| r.map(|r| r.map(|(resp, _)| resp)))))
}
/// Create a new `OutgoingRequest` with a fallback in case the remote does not support this
/// protocol. Useful when adding a new version of a req-response protocol, to achieve
/// compatibility with the older version.
///
/// Returns a raw `Vec<u8>` response over the channel. Use the associated `ProtocolName` to know
/// which request was the successful one and appropriately decode the response.
pub fn new_with_fallback(
peer: Recipient,
payload: Req,
fallback_request: FallbackReq,
) -> (Self, impl Future<Output = OutgoingResult<(Vec<u8>, ProtocolName)>>) {
let (tx, rx) = oneshot::channel();
let r = Self {
peer,
payload,
pending_response: tx,
fallback_request: Some((fallback_request, FallbackReq::PROTOCOL)),
};
(r, async { Ok(rx.await??) })
}
/// Encode a request into a `Vec<u8>`.
///
/// As this throws away type information, we also return the `Protocol` this encoded request
/// adheres to.
pub fn encode_request(self) -> (Protocol, OutgoingRequest<Vec<u8>>) {
let OutgoingRequest { peer, payload, pending_response, fallback_request } = self;
let encoded = OutgoingRequest {
peer,
payload: payload.encode(),
fallback_request: fallback_request.map(|(r, p)| (r.encode(), p)),
pending_response,
};
(Req::PROTOCOL, encoded)
}
}
/// Future for actually receiving a typed response for an `OutgoingRequest`.
async fn receive_response<Req>(
rec: impl Future<Output = Result<Result<Vec<u8>, network::RequestFailure>, oneshot::Canceled>>,
) -> OutgoingResult<Req::Response>
where
Req: IsRequest,
Req::Response: Decode,
{
let raw = rec.await??;
Ok(Decode::decode(&mut raw.as_ref())?)
}
@@ -0,0 +1,214 @@
// 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/>.
//! Requests and responses as sent over the wire for the individual protocols.
use codec::{Decode, Encode};
use pezkuwi_node_primitives::{
AvailableData, DisputeMessage, ErasureChunk, PoV, Proof, UncheckedDisputeMessage,
};
use pezkuwi_primitives::{
CandidateHash, CandidateReceiptV2 as CandidateReceipt, Hash, HeadData, Id as ParaId,
ValidatorIndex,
};
use super::{IsRequest, Protocol};
/// Request an availability chunk.
#[derive(Debug, Copy, Clone, Encode, Decode)]
pub struct ChunkFetchingRequest {
/// Hash of candidate we want a chunk for.
pub candidate_hash: CandidateHash,
/// The validator index we are requesting from. This must be identical to the index of the
/// chunk we'll receive. For v2, this may not be the case.
pub index: ValidatorIndex,
}
/// Receive a requested erasure chunk.
#[derive(Debug, Clone, Encode, Decode)]
pub enum ChunkFetchingResponse {
/// The requested chunk data.
#[codec(index = 0)]
Chunk(ChunkResponse),
/// Node was not in possession of the requested chunk.
#[codec(index = 1)]
NoSuchChunk,
}
impl From<Option<ChunkResponse>> for ChunkFetchingResponse {
fn from(x: Option<ChunkResponse>) -> Self {
match x {
Some(c) => ChunkFetchingResponse::Chunk(c),
None => ChunkFetchingResponse::NoSuchChunk,
}
}
}
impl From<ChunkFetchingResponse> for Option<ChunkResponse> {
fn from(x: ChunkFetchingResponse) -> Self {
match x {
ChunkFetchingResponse::Chunk(c) => Some(c),
ChunkFetchingResponse::NoSuchChunk => None,
}
}
}
/// Skimmed down variant of `ErasureChunk`.
///
/// Instead of transmitting a full `ErasureChunk` we transmit `ChunkResponse` in
/// `ChunkFetchingResponse`, which omits the chunk's index. The index is already known by
/// the requester and by not transmitting it, we ensure the requester is going to use his index
/// value for validating the response, thus making sure he got what he requested.
#[derive(Debug, Clone, Encode, Decode)]
pub struct ChunkResponse {
/// The erasure-encoded chunk of data belonging to the candidate block.
pub chunk: Vec<u8>,
/// Proof for this chunk's branch in the Merkle tree.
pub proof: Proof,
}
impl From<ErasureChunk> for ChunkResponse {
fn from(ErasureChunk { chunk, index: _, proof }: ErasureChunk) -> Self {
ChunkResponse { chunk, proof }
}
}
impl ChunkResponse {
/// Re-build an `ErasureChunk` from response and request.
pub fn recombine_into_chunk(self, req: &ChunkFetchingRequest) -> ErasureChunk {
ErasureChunk { chunk: self.chunk, proof: self.proof, index: req.index.into() }
}
}
impl IsRequest for ChunkFetchingRequest {
type Response = ChunkFetchingResponse;
const PROTOCOL: Protocol = Protocol::ChunkFetchingV1;
}
/// Request the advertised collation at that relay-parent.
#[derive(Debug, Clone, Encode, Decode)]
pub struct CollationFetchingRequest {
/// Relay parent we want a collation for.
pub relay_parent: Hash,
/// The `ParaId` of the collation.
pub para_id: ParaId,
}
/// Responses as sent by collators.
#[derive(Debug, Clone, Encode, Decode)]
pub enum CollationFetchingResponse {
/// Deliver requested collation.
#[codec(index = 0)]
Collation(CandidateReceipt, PoV),
/// Deliver requested collation along with parent head data.
#[codec(index = 1)]
CollationWithParentHeadData {
/// The receipt of the candidate.
receipt: CandidateReceipt,
/// Candidate's proof of validity.
pov: PoV,
/// The head data of the candidate's parent.
/// This is needed for elastic scaling to work.
parent_head_data: HeadData,
},
}
impl IsRequest for CollationFetchingRequest {
type Response = CollationFetchingResponse;
const PROTOCOL: Protocol = Protocol::CollationFetchingV1;
}
/// Request the advertised collation at that relay-parent.
#[derive(Debug, Clone, Encode, Decode)]
pub struct PoVFetchingRequest {
/// Candidate we want a PoV for.
pub candidate_hash: CandidateHash,
}
/// Responses to `PoVFetchingRequest`.
#[derive(Debug, Clone, Encode, Decode)]
pub enum PoVFetchingResponse {
/// Deliver requested PoV.
#[codec(index = 0)]
PoV(PoV),
/// PoV was not found in store.
#[codec(index = 1)]
NoSuchPoV,
}
impl IsRequest for PoVFetchingRequest {
type Response = PoVFetchingResponse;
const PROTOCOL: Protocol = Protocol::PoVFetchingV1;
}
/// Request the entire available data for a candidate.
#[derive(Debug, Clone, Encode, Decode)]
pub struct AvailableDataFetchingRequest {
/// The candidate hash to get the available data for.
pub candidate_hash: CandidateHash,
}
/// Receive a requested available data.
#[derive(Debug, Clone, Encode, Decode)]
pub enum AvailableDataFetchingResponse {
/// The requested data.
#[codec(index = 0)]
AvailableData(AvailableData),
/// Node was not in possession of the requested data.
#[codec(index = 1)]
NoSuchData,
}
impl From<Option<AvailableData>> for AvailableDataFetchingResponse {
fn from(x: Option<AvailableData>) -> Self {
match x {
Some(data) => AvailableDataFetchingResponse::AvailableData(data),
None => AvailableDataFetchingResponse::NoSuchData,
}
}
}
impl IsRequest for AvailableDataFetchingRequest {
type Response = AvailableDataFetchingResponse;
const PROTOCOL: Protocol = Protocol::AvailableDataFetchingV1;
}
/// A dispute request.
///
/// Contains an invalid vote a valid one for a particular candidate in a given session.
#[derive(Clone, Encode, Decode, Debug)]
pub struct DisputeRequest(pub UncheckedDisputeMessage);
impl From<DisputeMessage> for DisputeRequest {
fn from(msg: DisputeMessage) -> Self {
Self(msg.into())
}
}
/// Possible responses to a `DisputeRequest`.
#[derive(Encode, Decode, Debug, PartialEq, Eq)]
pub enum DisputeResponse {
/// Recipient successfully processed the dispute request.
#[codec(index = 0)]
Confirmed,
}
impl IsRequest for DisputeRequest {
type Response = DisputeResponse;
const PROTOCOL: Protocol = Protocol::DisputeSendingV1;
}
@@ -0,0 +1,138 @@
// 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/>.
//! Requests and responses as sent over the wire for the individual protocols.
use codec::{Decode, Encode};
use pezkuwi_node_primitives::ErasureChunk;
use pezkuwi_primitives::{
CandidateHash, CommittedCandidateReceiptV2 as CommittedCandidateReceipt, Hash, Id as ParaId,
PersistedValidationData, UncheckedSignedStatement, ValidatorIndex,
};
use super::{v1, IsRequest, Protocol};
use crate::v3::StatementFilter;
/// Request a candidate with statements.
#[derive(Debug, Clone, Encode, Decode)]
pub struct AttestedCandidateRequest {
/// Hash of the candidate we want to request.
pub candidate_hash: CandidateHash,
/// Statement filter with 'OR' semantics, indicating which validators
/// not to send statements for.
///
/// The filter must have exactly the minimum size required to
/// fit all validators from the backing group.
///
/// The response may not contain any statements masked out by this mask.
pub mask: StatementFilter,
}
/// Response to an `AttestedCandidateRequest`.
#[derive(Debug, Clone, Encode, Decode)]
pub struct AttestedCandidateResponse {
/// The candidate receipt, with commitments.
pub candidate_receipt: CommittedCandidateReceipt,
/// The [`PersistedValidationData`] corresponding to the candidate.
pub persisted_validation_data: PersistedValidationData,
/// All known statements about the candidate, in compact form,
/// omitting `Seconded` statements which were intended to be masked
/// out.
pub statements: Vec<UncheckedSignedStatement>,
}
impl IsRequest for AttestedCandidateRequest {
type Response = AttestedCandidateResponse;
const PROTOCOL: Protocol = Protocol::AttestedCandidateV2;
}
/// Responses as sent by collators.
pub type CollationFetchingResponse = super::v1::CollationFetchingResponse;
/// Request the advertised collation at that relay-parent.
#[derive(Debug, Clone, Encode, Decode)]
pub struct CollationFetchingRequest {
/// Relay parent collation is built on top of.
pub relay_parent: Hash,
/// The `ParaId` of the collation.
pub para_id: ParaId,
/// Candidate hash.
pub candidate_hash: CandidateHash,
}
impl IsRequest for CollationFetchingRequest {
// The response is the same as for V1.
type Response = CollationFetchingResponse;
const PROTOCOL: Protocol = Protocol::CollationFetchingV2;
}
/// Request an availability chunk.
#[derive(Debug, Copy, Clone, Encode, Decode)]
pub struct ChunkFetchingRequest {
/// Hash of candidate we want a chunk for.
pub candidate_hash: CandidateHash,
/// The validator index we are requesting from. This may not be identical to the index of the
/// chunk we'll receive. It's up to the caller to decide whether they need to validate they got
/// the chunk they were expecting.
pub index: ValidatorIndex,
}
/// Receive a requested erasure chunk.
#[derive(Debug, Clone, Encode, Decode)]
pub enum ChunkFetchingResponse {
/// The requested chunk data.
#[codec(index = 0)]
Chunk(ErasureChunk),
/// Node was not in possession of the requested chunk.
#[codec(index = 1)]
NoSuchChunk,
}
impl From<Option<ErasureChunk>> for ChunkFetchingResponse {
fn from(x: Option<ErasureChunk>) -> Self {
match x {
Some(c) => ChunkFetchingResponse::Chunk(c),
None => ChunkFetchingResponse::NoSuchChunk,
}
}
}
impl From<ChunkFetchingResponse> for Option<ErasureChunk> {
fn from(x: ChunkFetchingResponse) -> Self {
match x {
ChunkFetchingResponse::Chunk(c) => Some(c),
ChunkFetchingResponse::NoSuchChunk => None,
}
}
}
impl From<v1::ChunkFetchingRequest> for ChunkFetchingRequest {
fn from(v1::ChunkFetchingRequest { candidate_hash, index }: v1::ChunkFetchingRequest) -> Self {
Self { candidate_hash, index }
}
}
impl From<ChunkFetchingRequest> for v1::ChunkFetchingRequest {
fn from(ChunkFetchingRequest { candidate_hash, index }: ChunkFetchingRequest) -> Self {
Self { candidate_hash, index }
}
}
impl IsRequest for ChunkFetchingRequest {
type Response = ChunkFetchingResponse;
const PROTOCOL: Protocol = Protocol::ChunkFetchingV2;
}