feat: Rebrand Polkadot/Substrate references to PezkuwiChain

This commit systematically rebrands various references from Parity Technologies'
Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk.

Key changes include:
- Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks.
- Modified internal documentation and code comments to reflect PezkuwiChain naming and structure.
- Replaced direct references to  with  or specific paths within the  for XCM, Pezkuwi, and other modules.
- Cleaned up deprecated  issue and PR references in various  and  files, particularly in  and  modules.
- Adjusted image and logo URLs in documentation to point to PezkuwiChain assets.
- Removed or rephrased comments related to external Polkadot/Substrate PRs and issues.

This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
2025-12-14 00:04:10 +03:00
parent e4778b4576
commit 379cb741ed
9082 changed files with 997824 additions and 997542 deletions
@@ -0,0 +1,559 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <http://www.gnu.org/licenses/>.
//! Teyrchain bootnode advertisement.
use crate::config::MAX_ADDRESSES;
use codec::{Compact, CompactRef, Decode, Encode};
use cumulus_primitives_core::{
relay_chain::{Hash as RelayHash, Header as RelayHeader},
ParaId,
};
use cumulus_relay_chain_interface::{RelayChainInterface, RelayChainResult};
use futures::{future::Fuse, pin_mut, FutureExt, StreamExt};
use ip_network::IpNetwork;
use log::{debug, error, trace, warn};
use prost::Message;
use pezsc_network::{
config::OutgoingResponse,
event::{DhtEvent, Event},
multiaddr::Protocol,
request_responses::IncomingRequest,
service::traits::NetworkService,
KademliaKey, Multiaddr,
};
use pezsp_consensus_babe::{digests::CompatibleDigestItem, Epoch, Randomness};
use pezsp_runtime::traits::Header as _;
use std::{collections::HashSet, pin::Pin, sync::Arc};
use tokio::time::Sleep;
/// Log target for this file.
const LOG_TARGET: &str = "bootnodes::advertisement";
/// Delay before retrying the DHT content provider publish operation.
const RETRY_DELAY: std::time::Duration = std::time::Duration::from_secs(30);
/// Teyrchain bootnode advertisement parameters.
pub struct BootnodeAdvertisementParams {
/// Teyrchain ID.
pub para_id: ParaId,
/// Relay chain interface.
pub relay_chain_interface: Arc<dyn RelayChainInterface>,
/// Relay chain node network service.
pub relay_chain_network: Arc<dyn NetworkService>,
/// Bootnode request-response protocol request receiver.
pub request_receiver: async_channel::Receiver<IncomingRequest>,
/// Teyrchain node network service.
pub teyrchain_network: Arc<dyn NetworkService>,
/// Whether to advertise non-global IPs.
pub advertise_non_global_ips: bool,
/// Teyrchain genesis hash.
pub teyrchain_genesis_hash: Vec<u8>,
/// Teyrchain fork ID.
pub teyrchain_fork_id: Option<String>,
/// Teyrchain side public addresses.
pub public_addresses: Vec<Multiaddr>,
}
/// Teyrchain bootnode advertisement service.
pub struct BootnodeAdvertisement {
para_id: ParaId,
para_id_scale_compact: Vec<u8>,
relay_chain_interface: Arc<dyn RelayChainInterface>,
relay_chain_network: Arc<dyn NetworkService>,
current_epoch_key: Option<KademliaKey>,
next_epoch_key: Option<KademliaKey>,
current_epoch_publish_retry: Pin<Box<Fuse<Sleep>>>,
next_epoch_publish_retry: Pin<Box<Fuse<Sleep>>>,
request_receiver: async_channel::Receiver<IncomingRequest>,
teyrchain_network: Arc<dyn NetworkService>,
advertise_non_global_ips: bool,
teyrchain_genesis_hash: Vec<u8>,
teyrchain_fork_id: Option<String>,
public_addresses: Vec<Multiaddr>,
}
impl BootnodeAdvertisement {
/// Create a new bootnode advertisement service.
pub fn new(
BootnodeAdvertisementParams {
para_id,
relay_chain_interface,
relay_chain_network,
request_receiver,
teyrchain_network,
advertise_non_global_ips,
teyrchain_genesis_hash,
teyrchain_fork_id,
public_addresses,
}: BootnodeAdvertisementParams,
) -> Self {
// Discard `/p2p/<peer_id>` from public addresses on initialization to not generate warnings
// on every request for what is an operator mistake.
let local_peer_id = teyrchain_network.local_peer_id();
let public_addresses = public_addresses
.into_iter()
.filter_map(|mut addr| match addr.iter().last() {
Some(Protocol::P2p(peer_id)) if &peer_id == local_peer_id.as_ref() => {
addr.pop();
Some(addr)
},
Some(Protocol::P2p(_)) => {
warn!(
target: LOG_TARGET,
"Discarding public address containing not our peer ID: {addr}",
);
None
},
_ => Some(addr),
})
.collect();
Self {
para_id,
para_id_scale_compact: CompactRef(&para_id).encode(),
relay_chain_interface,
relay_chain_network,
current_epoch_key: None,
next_epoch_key: None,
current_epoch_publish_retry: Box::pin(Fuse::terminated()),
next_epoch_publish_retry: Box::pin(Fuse::terminated()),
request_receiver,
teyrchain_network,
advertise_non_global_ips,
teyrchain_genesis_hash,
teyrchain_fork_id,
public_addresses,
}
}
async fn current_epoch(&self, hash: RelayHash) -> RelayChainResult<Epoch> {
let res = self
.relay_chain_interface
.call_runtime_api("BabeApi_current_epoch", hash, &[])
.await?;
Decode::decode(&mut &*res).map_err(Into::into)
}
async fn next_epoch(&self, hash: RelayHash) -> RelayChainResult<Epoch> {
let res = self
.relay_chain_interface
.call_runtime_api("BabeApi_next_epoch", hash, &[])
.await?;
Decode::decode(&mut &*res).map_err(Into::into)
}
fn epoch_key(&self, randomness: Randomness) -> KademliaKey {
self.para_id_scale_compact
.clone()
.into_iter()
.chain(randomness.into_iter())
.collect::<Vec<_>>()
.into()
}
async fn current_and_next_epoch_keys(
&self,
header: RelayHeader,
) -> (Option<KademliaKey>, Option<KademliaKey>) {
let hash = header.hash();
let number = header.number();
let current_epoch = match self.current_epoch(hash).await {
Ok(epoch) => Some(epoch),
Err(e) => {
warn!(
target: LOG_TARGET,
"Failed to query current epoch for #{number} {hash:?}: {e}",
);
None
},
};
let next_epoch = match self.next_epoch(hash).await {
Ok(epoch) => Some(epoch),
Err(e) => {
warn!(
target: LOG_TARGET,
"Failed to query next epoch for #{number} {hash:?}: {e}",
);
None
},
};
(
current_epoch.map(|epoch| self.epoch_key(epoch.randomness)),
next_epoch.map(|epoch| self.epoch_key(epoch.randomness)),
)
}
async fn handle_import_notification(&mut self, header: RelayHeader) {
if let Some(ref old_current_epoch_key) = self.current_epoch_key {
// Readvertise on start of new epoch only.
let Some(next_epoch_descriptor) =
header.digest().convert_first(|v| v.as_next_epoch_descriptor())
else {
return;
};
let next_epoch_key = self.epoch_key(next_epoch_descriptor.randomness);
if Some(&next_epoch_key) == self.next_epoch_key.as_ref() {
trace!(
target: LOG_TARGET,
"Next epoch descriptor contains the same randomness as the previous one, \
not considering this as epoch change (switched fork?)",
);
return;
}
// Epoch changed, cancel retry attempts.
self.current_epoch_publish_retry = Box::pin(Fuse::terminated());
self.next_epoch_publish_retry = Box::pin(Fuse::terminated());
debug!(target: LOG_TARGET, "New epoch started, readvertising teyrchain bootnode.");
// Stop advertisement of the obsolete key.
debug!(
target: LOG_TARGET,
"Stopping advertisement of bootnode for old current epoch key {}",
hex::encode(old_current_epoch_key.as_ref()),
);
self.relay_chain_network.stop_providing(old_current_epoch_key.clone());
// Advertise current keys.
self.current_epoch_key = self.next_epoch_key.clone();
self.next_epoch_key = Some(next_epoch_key);
if let Some(ref current_epoch_key) = self.current_epoch_key {
debug!(
target: LOG_TARGET,
"Advertising bootnode for current (old next) epoch key {}",
hex::encode(current_epoch_key.as_ref()),
);
self.relay_chain_network.start_providing(current_epoch_key.clone());
}
if let Some(ref next_epoch_key) = self.next_epoch_key {
debug!(
target: LOG_TARGET,
"Advertising bootnode for next epoch key {}",
hex::encode(next_epoch_key.as_ref()),
);
self.relay_chain_network.start_providing(next_epoch_key.clone());
}
} else {
// First advertisement on startup.
let (current_epoch_key, next_epoch_key) =
self.current_and_next_epoch_keys(header).await;
self.current_epoch_key = current_epoch_key.clone();
self.next_epoch_key = next_epoch_key.clone();
if let Some(current_epoch_key) = current_epoch_key {
debug!(
target: LOG_TARGET,
"Initial advertisement of bootnode for current epoch key {}",
hex::encode(current_epoch_key.as_ref()),
);
self.relay_chain_network.start_providing(current_epoch_key);
} else {
warn!(
target: LOG_TARGET,
"Initial advertisement of bootnode for current epoch failed: no key."
);
}
if let Some(next_epoch_key) = next_epoch_key {
debug!(
target: LOG_TARGET,
"Initial advertisement of bootnode for next epoch key {}",
hex::encode(next_epoch_key.as_ref()),
);
self.relay_chain_network.start_providing(next_epoch_key);
} else {
warn!(
target: LOG_TARGET,
"Initial advertisement of bootnode for next epoch failed: no key."
);
}
}
}
/// The list of teyrchain side addresses.
///
/// The addresses are sorted as follows:
/// 1) public addresses provided by the operator
/// 2) global listen addresses
/// 3) discovered external addresses
/// 4) non-global listen addresses
/// 5) loopback listen addresses
fn paranode_addresses(&self) -> Vec<Multiaddr> {
let local_peer_id = self.teyrchain_network.local_peer_id();
// Discard `/p2p/<peer_id>` part. `None` if the address contains foreign peer ID.
let without_p2p = |mut addr: Multiaddr| match addr.iter().last() {
Some(Protocol::P2p(peer_id)) if &peer_id == local_peer_id.as_ref() => {
addr.pop();
Some(addr)
},
Some(Protocol::P2p(_)) => {
warn!(
target: LOG_TARGET,
"Ignoring teyrchain side address containing not our peer ID: {addr}",
);
None
},
_ => Some(addr),
};
// Check if the address is global.
let is_global = |address: &Multiaddr| {
address.iter().all(|protocol| match protocol {
// The `ip_network` library is used because its `is_global()` method is stable,
// while `is_global()` in the standard library currently isn't.
Protocol::Ip4(ip) => IpNetwork::from(ip).is_global(),
Protocol::Ip6(ip) => IpNetwork::from(ip).is_global(),
_ => true,
})
};
// Check if the address is a loopback address.
let is_loopback = |address: &Multiaddr| {
address.iter().any(|protocol| match protocol {
Protocol::Ip4(ip) => IpNetwork::from(ip).is_loopback(),
Protocol::Ip6(ip) => IpNetwork::from(ip).is_loopback(),
_ => false,
})
};
// 1) public addresses provided by the operator
let public_addresses = self.public_addresses.clone().into_iter();
// 2) global listen addresses
let global_listen_addresses =
self.teyrchain_network.listen_addresses().into_iter().filter(is_global);
// 3a) discovered external addresses (global)
let global_external_addresses =
self.teyrchain_network.external_addresses().into_iter().filter(is_global);
// 3b) discovered external addresses (non-global)
let non_global_external_addresses = self
.teyrchain_network
.external_addresses()
.into_iter()
.filter(|addr| !is_global(addr));
// 4) non-global listen addresses
let non_global_listen_addresses = self
.teyrchain_network
.listen_addresses()
.into_iter()
.filter(|addr| !is_global(addr) && !is_loopback(addr));
// 5) loopback listen addresses
let loopback_listen_addresses =
self.teyrchain_network.listen_addresses().into_iter().filter(is_loopback);
let mut seen = HashSet::new();
public_addresses
.chain(global_listen_addresses)
.chain(global_external_addresses)
.chain(
self.advertise_non_global_ips
.then_some(
non_global_external_addresses
.chain(non_global_listen_addresses)
.chain(loopback_listen_addresses),
)
.into_iter()
.flatten(),
)
.filter_map(without_p2p)
// Deduplicate addresses.
.filter(|addr| seen.insert(addr.clone()))
.take(MAX_ADDRESSES)
.collect()
}
fn handle_request(&mut self, req: IncomingRequest) {
if req.payload == self.para_id_scale_compact {
trace!(
target: LOG_TARGET,
"Serving paranode addresses request from {:?} for teyrchain ID {}",
req.peer,
self.para_id,
);
let response = crate::schema::Response {
peer_id: self.teyrchain_network.local_peer_id().to_bytes(),
addrs: self.paranode_addresses().iter().map(|a| a.to_vec()).collect(),
genesis_hash: self.teyrchain_genesis_hash.clone(),
fork_id: self.teyrchain_fork_id.clone(),
};
let _ = req.pending_response.send(OutgoingResponse {
result: Ok(response.encode_to_vec()),
reputation_changes: Vec::new(),
sent_feedback: None,
});
} else {
let payload = req.payload;
match Compact::<ParaId>::decode(&mut &payload[..]) {
Ok(para_id) => {
trace!(
target: LOG_TARGET,
"Ignoring request for teyrchain ID {} != self teyrchain ID {} from {:?}",
para_id.0,
self.para_id,
req.peer,
);
},
Err(e) => {
trace!(
target: LOG_TARGET,
"Cannot decode teyrchain ID in a request from {:?}: {e}",
req.peer,
);
},
}
}
}
fn handle_dht_event(&mut self, event: DhtEvent) {
match event {
DhtEvent::StartedProviding(key) =>
if Some(&key) == self.current_epoch_key.as_ref() {
debug!(
target: LOG_TARGET,
"Successfully published provider for current epoch key {}",
hex::encode(key.as_ref()),
);
} else if Some(&key) == self.next_epoch_key.as_ref() {
debug!(
target: LOG_TARGET,
"Successfully published provider for next epoch key {}",
hex::encode(key.as_ref()),
);
},
DhtEvent::StartProvidingFailed(key) => {
if Some(&key) == self.current_epoch_key.as_ref() {
debug!(
target: LOG_TARGET,
"Failed to publish provider for current epoch key {}. Retrying in {RETRY_DELAY:?}",
hex::encode(key.as_ref()),
);
self.current_epoch_publish_retry =
Box::pin(tokio::time::sleep(RETRY_DELAY).fuse());
} else if Some(&key) == self.next_epoch_key.as_ref() {
debug!(
target: LOG_TARGET,
"Failed to publish provider for next epoch key {}. Retrying in {RETRY_DELAY:?}",
hex::encode(key.as_ref()),
);
self.next_epoch_publish_retry =
Box::pin(tokio::time::sleep(RETRY_DELAY).fuse());
}
},
_ => {},
}
}
fn retry_for_current_epoch(&mut self) {
if let Some(current_epoch_key) = self.current_epoch_key.clone() {
debug!(
target: LOG_TARGET,
"Retrying advertising bootnode for current epoch key {}",
hex::encode(current_epoch_key.as_ref()),
);
self.relay_chain_network.start_providing(current_epoch_key);
} else {
error!(
target: LOG_TARGET,
"Retrying advertising bootnode for current epoch failed: no key. This is a bug."
);
}
}
fn retry_for_next_epoch(&mut self) {
if let Some(next_epoch_key) = self.next_epoch_key.clone() {
debug!(
target: LOG_TARGET,
"Retrying advertising bootnode for next epoch key {}",
hex::encode(next_epoch_key.as_ref()),
);
self.relay_chain_network.start_providing(next_epoch_key);
} else {
error!(
target: LOG_TARGET,
"Retrying advertising bootnode for next epoch failed: no key. This is a bug."
);
}
}
/// Run the bootnode advertisement service.
pub async fn run(mut self) -> RelayChainResult<()> {
let mut import_notification_stream =
self.relay_chain_interface.import_notification_stream().await?;
let dht_event_stream = self
.relay_chain_network
.event_stream("teyrchain-bootnode-discovery")
.filter_map(|e| async move {
match e {
Event::Dht(e) => Some(e),
_ => None,
}
})
.fuse();
pin_mut!(dht_event_stream);
loop {
tokio::select! {
header = import_notification_stream.next() => match header {
Some(header) => self.handle_import_notification(header).await,
None => {
debug!(
target: LOG_TARGET,
"Import notification stream terminated, terminating bootnode advertisement."
);
return Ok(());
}
},
req = self.request_receiver.recv() => match req {
Ok(req) => {
self.handle_request(req);
},
Err(_) => {
debug!(
target: LOG_TARGET,
"Paranode request receiver terminated, terminating bootnode advertisement."
);
return Ok(());
}
},
event = dht_event_stream.select_next_some() => self.handle_dht_event(event),
() = &mut self.current_epoch_publish_retry => self.retry_for_current_epoch(),
() = &mut self.next_epoch_publish_retry => self.retry_for_next_epoch(),
}
}
}
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <http://www.gnu.org/licenses/>.
//! Teyrchain bootnode request-response protocol configuration.
use pezsc_network::{
request_responses::IncomingRequest, service::traits::NetworkBackend, ProtocolName,
};
use pezsp_runtime::traits::Block as BlockT;
use std::time::Duration;
/// Maximum number of addresses allowed in the response.
pub const MAX_ADDRESSES: usize = 32;
/// Expected maximum number of simultaneous requests from remote peers.
/// Should be enough for a testnet with a plenty of nodes starting at the same time.
const INBOUND_CHANNEL_SIZE: usize = 1000;
/// Maximum request size. Should be enough to fit SCALE-compact-encoded `para_id`.
const MAX_REQUEST_SIZE: u64 = 128;
/// Maximum response size as per RFC.
const MAX_RESPONSE_SIZE: u64 = 16 * 1024;
/// Request-response protocol timeout.
const TIMEOUT: Duration = Duration::from_secs(20);
/// Bootnode request-response protocol name given a genesis hash and fork id.
pub fn paranode_protocol_name<Hash: AsRef<[u8]>>(
genesis_hash: Hash,
fork_id: Option<&str>,
) -> ProtocolName {
let genesis_hash = genesis_hash.as_ref();
if let Some(fork_id) = fork_id {
// This is not stated in RFC-0008, but other pezkuwi protocol names are based on `fork_id`
// if it is present, so we also use it here.
format!("/{}/{}/paranode", array_bytes::bytes2hex("", genesis_hash), fork_id)
} else {
format!("/{}/paranode", array_bytes::bytes2hex("", genesis_hash))
}
.into()
}
/// Bootnode request-response protocol config.
pub fn bootnode_request_response_config<
Hash: AsRef<[u8]>,
B: BlockT,
N: NetworkBackend<B, <B as BlockT>::Hash>,
>(
genesis_hash: Hash,
fork_id: Option<&str>,
) -> (N::RequestResponseProtocolConfig, async_channel::Receiver<IncomingRequest>) {
let (inbound_tx, inbound_rx) = async_channel::bounded(INBOUND_CHANNEL_SIZE);
let config = N::request_response_config(
paranode_protocol_name(genesis_hash, fork_id),
Vec::new(),
MAX_REQUEST_SIZE,
MAX_RESPONSE_SIZE,
TIMEOUT,
Some(inbound_tx),
);
(config, inbound_rx)
}
@@ -0,0 +1,468 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <http://www.gnu.org/licenses/>.
//! Teyrchain bootnode discovery.
//!
//! The discovery works as follows:
//! 1. We start teyrchain bootnode content provider discovery on the relay chain DHT in
//! [`BootnodeDiscovery::start_discovery`].
//! 2. We handle every provider discovered in [`BootnodeDiscovery::handle_providers`] and try to
//! request the bootnodes from the provider over a `/paranode` request-response protocol.
//! 3. The request result is handled in [`BootnodeDiscovery::handle_response`]. If the request
//! fails this is a sign of the provider addresses not being cached by the remote / dropped by
//! the networking library (the case with libp2p). In this case we perform a `FIND_NODE` query
//! to get the provider addresses first and repeat the request once we know them.
//! 4. When the request over the `/paranode` protocol succeeds, we add the bootnode addresses as
//! known addresses to the teyrchain networking.
//! 5. If the content provider discovery had completed, all `FIND_NODE` queries finished, and all
//! requests over the `/paranode` protocol succeded or failed, but we have not found any
//! bootnode addresses, we repeat the discovery process after a cooldown period.
use crate::{config::MAX_ADDRESSES, schema::Response};
use codec::{CompactRef, Decode, Encode};
use cumulus_primitives_core::{relay_chain::Hash as RelayHash, ParaId};
use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface, RelayChainResult};
use futures::{
channel::oneshot,
future::{BoxFuture, Fuse, FusedFuture},
pin_mut,
stream::FuturesUnordered,
FutureExt, StreamExt,
};
use log::{debug, error, info, trace, warn};
use prost::Message;
use pezsc_network::{
event::{DhtEvent, Event},
request_responses::{IfDisconnected, RequestFailure},
service::traits::NetworkService,
KademliaKey, Multiaddr, PeerId, ProtocolName,
};
use pezsp_consensus_babe::{Epoch, Randomness};
use std::{collections::HashSet, pin::Pin, sync::Arc, time::Duration};
use tokio::time::{sleep, Sleep};
/// Log target for this file.
const LOG_TARGET: &str = "bootnodes::discovery";
/// Delay before retrying discovery in case of failure. Needed to rate-limit the attempts,
/// especially in small testnets where a discovery attempt can be almost instant.
const RETRY_DELAY: Duration = Duration::from_secs(30);
/// Teyrchain bootnode discovery parameters.
pub struct BootnodeDiscoveryParams {
/// Teyrchain ID.
pub para_id: ParaId,
/// Teyrchain node network service.
pub teyrchain_network: Arc<dyn NetworkService>,
/// Teyrchain genesis hash.
pub teyrchain_genesis_hash: Vec<u8>,
/// Teyrchain fork ID.
pub teyrchain_fork_id: Option<String>,
/// Relay chain interface.
pub relay_chain_interface: Arc<dyn RelayChainInterface>,
/// Relay chain network service.
pub relay_chain_network: Arc<dyn NetworkService>,
/// `/paranode` protocol name.
pub paranode_protocol_name: ProtocolName,
}
/// Teyrchain bootnode discovery service.
pub struct BootnodeDiscovery {
para_id_scale_compact: Vec<u8>,
teyrchain_network: Arc<dyn NetworkService>,
teyrchain_genesis_hash: Vec<u8>,
teyrchain_fork_id: Option<String>,
relay_chain_interface: Arc<dyn RelayChainInterface>,
relay_chain_network: Arc<dyn NetworkService>,
latest_relay_chain_hash: Option<RelayHash>,
key_being_discovered: Option<KademliaKey>,
paranode_protocol_name: ProtocolName,
pending_responses: FuturesUnordered<
BoxFuture<
'static,
(PeerId, Result<Result<(Vec<u8>, ProtocolName), RequestFailure>, oneshot::Canceled>),
>,
>,
direct_requests: HashSet<PeerId>,
find_node_queries: HashSet<PeerId>,
pending_start_discovery: Pin<Box<Fuse<Sleep>>>,
succeeded: bool,
}
impl BootnodeDiscovery {
/// Create a new bootnode discovery service.
pub fn new(
BootnodeDiscoveryParams {
para_id,
teyrchain_network,
teyrchain_genesis_hash,
teyrchain_fork_id,
relay_chain_interface,
relay_chain_network,
paranode_protocol_name,
}: BootnodeDiscoveryParams,
) -> Self {
Self {
para_id_scale_compact: CompactRef(&para_id).encode(),
teyrchain_network,
teyrchain_genesis_hash,
teyrchain_fork_id,
relay_chain_interface,
relay_chain_network,
latest_relay_chain_hash: None,
key_being_discovered: None,
paranode_protocol_name,
pending_responses: FuturesUnordered::default(),
direct_requests: HashSet::new(),
find_node_queries: HashSet::new(),
// Trigger the discovery immediately on startup.
pending_start_discovery: Box::pin(sleep(Duration::ZERO).fuse()),
succeeded: false,
}
}
async fn current_epoch(&mut self, hash: RelayHash) -> RelayChainResult<Epoch> {
let res = self
.relay_chain_interface
.call_runtime_api("BabeApi_current_epoch", hash, &[])
.await?;
Decode::decode(&mut &*res).map_err(Into::into)
}
fn epoch_key(&self, randomness: Randomness) -> KademliaKey {
self.para_id_scale_compact
.clone()
.into_iter()
.chain(randomness.into_iter())
.collect::<Vec<_>>()
.into()
}
/// Start bootnode discovery.
async fn start_discovery(&mut self) -> RelayChainResult<()> {
let Some(hash) = self.latest_relay_chain_hash else {
error!(
target: LOG_TARGET,
"Failed to start bootnode discovery: no relay chain hash available. This is a bug.",
);
// This is a graceful panic via the failure of essential task.
return Err(RelayChainError::GenericError("no relay chain hash available".to_string()));
};
let current_epoch = self.current_epoch(hash).await?;
let current_epoch_key = self.epoch_key(current_epoch.randomness);
self.key_being_discovered = Some(current_epoch_key.clone());
self.relay_chain_network.get_providers(current_epoch_key.clone());
debug!(
target: LOG_TARGET,
"Started discovery of teyrchain bootnode providers for current epoch key {}",
hex::encode(current_epoch_key),
);
Ok(())
}
/// Schedule bootnode discovery if needed. Returns `false` if the discovery event loop should be
/// terminated.
fn maybe_retry_discovery(&mut self) -> bool {
let discovery_in_progress = self.key_being_discovered.is_some() ||
!self.pending_responses.is_empty() ||
!self.find_node_queries.is_empty();
let discovery_scheduled = !self.pending_start_discovery.is_terminated();
if discovery_in_progress || discovery_scheduled {
// Discovery is already in progress or scheduled, just continue the event loop.
true
} else {
if self.succeeded {
// No need to start discovery again if the previous attempt succeeded.
info!(
target: LOG_TARGET,
"Teyrchain bootnode discovery on the relay chain DHT succeeded",
);
false
} else {
debug!(
target: LOG_TARGET,
"Retrying teyrchain bootnode discovery on the relay chain DHT in {RETRY_DELAY:?}",
);
self.pending_start_discovery = Box::pin(sleep(RETRY_DELAY).fuse());
true
}
}
}
fn request_bootnode(&mut self, peer_id: PeerId) {
trace!(
target: LOG_TARGET,
"Requesting teyrchain bootnode from the relay chain {peer_id:?}",
);
let (tx, rx) = oneshot::channel();
self.relay_chain_network.start_request(
peer_id,
self.paranode_protocol_name.clone(),
self.para_id_scale_compact.clone(),
None,
tx,
IfDisconnected::TryConnect,
);
self.pending_responses.push(async move { (peer_id, rx.await) }.boxed());
}
fn handle_providers(&mut self, providers: Vec<PeerId>) {
debug!(
target: LOG_TARGET,
"Found teyrchain bootnode providers on the relay chain: {providers:?}",
);
for peer_id in providers {
if peer_id == self.relay_chain_network.local_peer_id() {
continue;
}
// libp2p may yield the same provider multiple times; skip if we alredy queried it.
if self.direct_requests.contains(&peer_id) || self.find_node_queries.contains(&peer_id)
{
continue;
}
// Directly request a bootnode from the peer without performing a `FIND_NODE` query
// first. With litep2p backend this will likely succeed, because cached provider
// addresses are automatically added to the transport manager known addresses list.
//
// With libp2p backend, or if the remote did not return the cached addresses of the
// provider, the request will fail and we will perform a `FIND_NODE` query.
self.direct_requests.insert(peer_id);
self.request_bootnode(peer_id);
}
}
fn handle_response(
&mut self,
peer_id: PeerId,
res: Result<Result<(Vec<u8>, ProtocolName), RequestFailure>, oneshot::Canceled>,
) {
let direct_request = self.direct_requests.remove(&peer_id);
let response = match res {
Ok(Ok((payload, _))) => match Response::decode(payload.as_slice()) {
Ok(response) => response,
Err(e) => {
warn!(
target: LOG_TARGET,
"Failed to decode teyrchain bootnode response from {peer_id:?}: {e}",
);
return;
},
},
Ok(Err(e)) => {
if direct_request {
// It only makes sense to try to find the node on the DHT in case of "address
// not available" error. Unfortunately, libp2p and litep2p backends report such
// errors differently, and also some network library could break the error
// reporting in the future. So, to be on the safe side and avoid subtle bugs,
// we always try to find the node on the DHT in case of the request failure.
debug!(
target: LOG_TARGET,
"Failed to directly query teyrchain bootnode from {peer_id:?}: {e}. \
Starting FIND_NODE query on the DHT",
);
self.find_node_queries.insert(peer_id);
self.relay_chain_network.find_closest_peers(peer_id);
} else {
debug!(
target: LOG_TARGET,
"Failed to query teyrchain bootnode from {peer_id:?} after finding
the node addresses on the DHT: {e}",
);
}
return;
},
Err(_) => {
debug!(
target: LOG_TARGET,
"Teyrchain bootnode request to {peer_id:?} canceled. \
The node is likely terminating.",
);
return;
},
};
match (response.genesis_hash, response.fork_id) {
(genesis_hash, fork_id)
if genesis_hash == self.teyrchain_genesis_hash &&
fork_id == self.teyrchain_fork_id => {},
(genesis_hash, fork_id) => {
warn!(
target: LOG_TARGET,
"Received invalid teyrchain bootnode response from {peer_id:?}: \
genesis hash {}, fork ID {:?} don't match expected genesis hash {}, fork ID {:?}",
hex::encode(genesis_hash),
fork_id,
hex::encode(&self.teyrchain_genesis_hash),
self.teyrchain_fork_id,
);
return;
},
}
let paranode_peer_id = match PeerId::from_bytes(response.peer_id.as_slice()) {
Ok(peer_id) => peer_id,
Err(e) => {
warn!(
target: LOG_TARGET,
"Failed to decode teyrchain peer ID in response from {peer_id:?}: {e}",
);
return;
},
};
if paranode_peer_id == self.teyrchain_network.local_peer_id() {
warn!(
target: LOG_TARGET,
"Received own teyrchain node peer ID in bootnode response from {peer_id:?}. \
This should not happen as we don't request teyrchain bootnodes from self.",
);
return;
}
let paranode_addresses = response
.addrs
.into_iter()
.map(Multiaddr::try_from)
.take(MAX_ADDRESSES)
.collect::<Result<Vec<_>, _>>();
let paranode_addresses = match paranode_addresses {
Ok(paranode_addresses) => paranode_addresses,
Err(e) => {
warn!(
target: LOG_TARGET,
"Failed to decode teyrchain node addresses in response from {peer_id:?}: {e}",
);
return;
},
};
debug!(
target: LOG_TARGET,
"Discovered teyrchain bootnode {paranode_peer_id:?} with addresses {paranode_addresses:?}",
);
paranode_addresses.into_iter().for_each(|addr| {
self.teyrchain_network.add_known_address(paranode_peer_id, addr);
self.succeeded = true;
});
}
fn handle_dht_event(&mut self, event: DhtEvent) {
match event {
DhtEvent::ProvidersFound(key, providers)
// libp2p generates empty events, so also check if `providers` are not empty.
if Some(key.clone()) == self.key_being_discovered && !providers.is_empty() =>
self.handle_providers(providers),
DhtEvent::NoMoreProviders(key) if Some(key.clone()) == self.key_being_discovered => {
debug!(
target: LOG_TARGET,
"Teyrchain bootnode providers discovery finished for key {}",
hex::encode(key),
);
self.key_being_discovered = None;
},
DhtEvent::ProvidersNotFound(key) if Some(key.clone()) == self.key_being_discovered => {
debug!(
target: LOG_TARGET,
"Teyrchain bootnode providers not found for key {}",
hex::encode(key),
);
self.key_being_discovered = None;
},
DhtEvent::ClosestPeersFound(peer_id, peers) if self.find_node_queries.remove(&peer_id) => {
if let Some((_, addrs)) = peers
.into_iter()
.find(|(peer, addrs)| peer == &peer_id && !addrs.is_empty())
{
trace!(
target: LOG_TARGET,
"Found addresses on the DHT for teyrchain bootnode provider {peer_id:?}: {addrs:?}",
);
for address in addrs {
self.relay_chain_network.add_known_address(peer_id, address);
}
self.request_bootnode(peer_id);
} else {
debug!(
target: LOG_TARGET,
"Failed to find addresses on the DHT for teyrchain bootnode provider {peer_id:?}",
);
}
},
DhtEvent::ClosestPeersNotFound(peer_id) if self.find_node_queries.remove(&peer_id) => {
debug!(
target: LOG_TARGET,
"Failed to find addresses on the DHT for teyrchain bootnode provider {peer_id:?}",
);
},
_ => {},
}
}
/// Run the bootnode discovery service.
pub async fn run(mut self) -> RelayChainResult<()> {
let mut import_notification_stream =
self.relay_chain_interface.import_notification_stream().await?.fuse();
let dht_event_stream = self
.relay_chain_network
.event_stream("teyrchain-bootnode-discovery")
.filter_map(|e| async move {
match e {
Event::Dht(e) => Some(e),
_ => None,
}
})
.fuse();
pin_mut!(dht_event_stream);
// Make sure the relay chain hash is always available before starting the discovery.
let header = import_notification_stream.select_next_some().await;
self.latest_relay_chain_hash = Some(header.hash());
loop {
if !self.maybe_retry_discovery() {
return Ok(());
}
tokio::select! {
_ = &mut self.pending_start_discovery => {
self.start_discovery().await?;
},
header = import_notification_stream.select_next_some() => {
self.latest_relay_chain_hash = Some(header.hash());
},
event = dht_event_stream.select_next_some() => self.handle_dht_event(event),
(peer_id, res) = self.pending_responses.select_next_some(),
if !self.pending_responses.is_empty() =>
self.handle_response(peer_id, res),
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <http://www.gnu.org/licenses/>.
//! Teyrchain bootnodes advertisement and discovery service.
#![warn(missing_docs)]
mod advertisement;
mod config;
mod discovery;
mod task;
mod schema {
include!(concat!(env!("OUT_DIR"), "/response.rs"));
}
pub use config::bootnode_request_response_config;
pub use task::{start_bootnode_tasks, StartBootnodeTasksParams};
@@ -0,0 +1,20 @@
syntax = "proto2";
package response;
message Response {
// Peer ID of the node on the parachain side.
required bytes peer_id = 1;
// Multiaddresses of the parachain side of the node. The list and format are the same as for
// the `listenAddrs` field of the `identify` protocol.
repeated bytes addrs = 2;
// Genesis hash of the parachain. Used to determine the name of the networking protocol
// to connect to the parachain. Untrusted.
required bytes genesis_hash = 3;
// So-called "fork ID" of the parachain. Used to determine the name of the networking protocol
// to connect to the parachain. Untrusted.
optional string fork_id = 4;
};
+201
View File
@@ -0,0 +1,201 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Pezcumulus 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.
// Pezcumulus 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 Pezcumulus. If not, see <http://www.gnu.org/licenses/>.
//! Teyrchain bootnodes advertisement and discovery service.
use crate::{
advertisement::{BootnodeAdvertisement, BootnodeAdvertisementParams},
config::paranode_protocol_name,
discovery::{BootnodeDiscovery, BootnodeDiscoveryParams},
};
use cumulus_primitives_core::{relay_chain::BlockId, ParaId};
use cumulus_relay_chain_interface::RelayChainInterface;
use log::{debug, error};
use num_traits::Zero;
use pezsc_network::{request_responses::IncomingRequest, service::traits::NetworkService, Multiaddr};
use pezsc_service::TaskManager;
use std::sync::Arc;
/// Log target for this crate.
const LOG_TARGET: &str = "bootnodes";
/// Bootnode advertisement task params.
pub struct StartBootnodeTasksParams<'a> {
/// Enable embedded DHT bootnode.
pub embedded_dht_bootnode: bool,
/// Enable DHT bootnode discovery.
pub dht_bootnode_discovery: bool,
/// Teyrchain ID.
pub para_id: ParaId,
/// Task manager.
pub task_manager: &'a mut TaskManager,
/// Relay chain interface.
pub relay_chain_interface: Arc<dyn RelayChainInterface>,
/// Relay chain fork ID.
pub relay_chain_fork_id: Option<String>,
/// Relay chain network service.
pub relay_chain_network: Arc<dyn NetworkService>,
/// `/paranode` protocol request receiver.
pub request_receiver: async_channel::Receiver<IncomingRequest>,
/// Teyrchain node network service.
pub teyrchain_network: Arc<dyn NetworkService>,
/// Whether to advertise non-global IP addresses.
pub advertise_non_global_ips: bool,
/// Teyrchain genesis hash.
pub teyrchain_genesis_hash: Vec<u8>,
/// Teyrchain fork ID.
pub teyrchain_fork_id: Option<String>,
/// Teyrchain public addresses provided by the operator.
pub teyrchain_public_addresses: Vec<Multiaddr>,
}
async fn bootnode_advertisement(
para_id: ParaId,
relay_chain_interface: Arc<dyn RelayChainInterface>,
relay_chain_network: Arc<dyn NetworkService>,
request_receiver: async_channel::Receiver<IncomingRequest>,
teyrchain_network: Arc<dyn NetworkService>,
advertise_non_global_ips: bool,
teyrchain_genesis_hash: Vec<u8>,
teyrchain_fork_id: Option<String>,
public_addresses: Vec<Multiaddr>,
) {
let bootnode_advertisement = BootnodeAdvertisement::new(BootnodeAdvertisementParams {
para_id,
relay_chain_interface,
relay_chain_network,
request_receiver,
teyrchain_network,
advertise_non_global_ips,
teyrchain_genesis_hash,
teyrchain_fork_id,
public_addresses,
});
if let Err(e) = bootnode_advertisement.run().await {
error!(target: LOG_TARGET, "Bootnode advertisement terminated with error: {e}");
}
}
async fn bootnode_discovery(
para_id: ParaId,
teyrchain_network: Arc<dyn NetworkService>,
teyrchain_genesis_hash: Vec<u8>,
teyrchain_fork_id: Option<String>,
relay_chain_interface: Arc<dyn RelayChainInterface>,
relay_chain_fork_id: Option<String>,
relay_chain_network: Arc<dyn NetworkService>,
) {
let relay_chain_genesis_hash =
match relay_chain_interface.header(BlockId::Number(Zero::zero())).await {
Ok(Some(header)) => header.hash().as_bytes().to_vec(),
Ok(None) => {
error!(
target: LOG_TARGET,
"Bootnode discovery: relay chain genesis hash does not exist",
);
// Make essential task fail.
return;
},
Err(e) => {
error!(
target: LOG_TARGET,
"Bootnode discovery: failed to obtain relay chain genesis hash: {e}",
);
// Make essential task fail.
return;
},
};
let paranode_protocol_name =
paranode_protocol_name(relay_chain_genesis_hash, relay_chain_fork_id.as_deref());
let bootnode_discovery = BootnodeDiscovery::new(BootnodeDiscoveryParams {
para_id,
teyrchain_network,
teyrchain_genesis_hash,
teyrchain_fork_id,
relay_chain_interface,
relay_chain_network,
paranode_protocol_name,
});
match bootnode_discovery.run().await {
// Do not terminate the essentil task if bootnode discovery succeeded.
Ok(()) => std::future::pending().await,
Err(e) => error!(target: LOG_TARGET, "Bootnode discovery terminated with error: {e}"),
}
}
/// Start teyrchain bootnode advertisement and discovery tasks.
pub fn start_bootnode_tasks(
StartBootnodeTasksParams {
embedded_dht_bootnode,
dht_bootnode_discovery,
para_id,
task_manager,
relay_chain_interface,
relay_chain_fork_id,
relay_chain_network,
request_receiver,
teyrchain_network,
advertise_non_global_ips,
teyrchain_genesis_hash,
teyrchain_fork_id,
teyrchain_public_addresses,
}: StartBootnodeTasksParams,
) {
debug!(
target: LOG_TARGET,
"Embedded DHT bootnode enabled: {embedded_dht_bootnode}; \
DHT bootnode discovery enabled: {dht_bootnode_discovery}",
);
if embedded_dht_bootnode {
task_manager.spawn_essential_handle().spawn(
"pezcumulus-dht-bootnode-advertisement",
None,
bootnode_advertisement(
para_id,
relay_chain_interface.clone(),
relay_chain_network.clone(),
request_receiver,
teyrchain_network.clone(),
advertise_non_global_ips,
teyrchain_genesis_hash.clone(),
teyrchain_fork_id.clone(),
teyrchain_public_addresses,
),
);
}
if dht_bootnode_discovery {
task_manager.spawn_essential_handle().spawn(
"pezcumulus-dht-bootnode-discovery",
None,
bootnode_discovery(
para_id,
teyrchain_network,
teyrchain_genesis_hash,
teyrchain_fork_id,
relay_chain_interface,
relay_chain_fork_id,
relay_chain_network,
),
);
}
}