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
+48
View File
@@ -0,0 +1,48 @@
[package]
name = "pezcumulus-client-bootnodes"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
description = "Teyrchain bootnodes registration and discovery."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
build = "build.rs"
[lints]
workspace = true
[build-dependencies]
prost-build = { workspace = true }
[dependencies]
array-bytes = { workspace = true, default-features = true }
async-channel = { workspace = true }
codec = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
hex = { workspace = true, default-features = true }
ip_network = { workspace = true }
log = { workspace = true, default-features = true }
num-traits = { workspace = true, default-features = true }
prost = { workspace = true }
tokio = { workspace = true, default-features = true }
# Bizinikiwi
pezsc-network = { workspace = true, default-features = true }
pezsc-service = { workspace = true, default-features = true }
pezsp-consensus-babe = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsc-service/runtime-benchmarks",
"pezsp-consensus-babe/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
+21
View File
@@ -0,0 +1,21 @@
// This file is part of Pezcumulus.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program 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.
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
fn main() {
prost_build::compile_protos(&["src/schema/response.proto"], &["src/schema"]).unwrap();
}
@@ -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,
),
);
}
}
+36
View File
@@ -0,0 +1,36 @@
[package]
name = "pezcumulus-client-cli"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
description = "Teyrchain node CLI utilities."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
clap = { features = ["derive"], workspace = true }
codec = { workspace = true, default-features = true }
url = { workspace = true }
# Bizinikiwi
pezsc-chain-spec = { workspace = true, default-features = true }
pezsc-cli = { workspace = true, default-features = false }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-service = { workspace = true, default-features = false }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezsc-chain-spec/runtime-benchmarks",
"pezsc-cli/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-service/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
+502
View File
@@ -0,0 +1,502 @@
// 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 <https://www.gnu.org/licenses/>.
//! Pezcumulus CLI library.
#![warn(missing_docs)]
use std::{
fs,
io::{self, Write},
path::PathBuf,
sync::Arc,
};
use codec::Encode;
use pezsc_chain_spec::ChainSpec;
use pezsc_cli::RpcEndpoint;
use pezsc_client_api::HeaderBackend;
use pezsc_service::{
config::{PrometheusConfig, RpcBatchRequestConfig, TelemetryEndpoints},
BasePath, TransactionPoolOptions,
};
use pezsp_core::hexdisplay::HexDisplay;
use pezsp_runtime::traits::{Block as BlockT, Zero};
use url::Url;
/// The `purge-chain` command used to remove the whole chain: the teyrchain and the relay chain.
#[derive(Debug, clap::Parser)]
#[group(skip)]
pub struct PurgeChainCmd {
/// The base struct of the purge-chain command.
#[command(flatten)]
pub base: pezsc_cli::PurgeChainCmd,
/// Only delete the para chain database
#[arg(long, aliases = &["para"])]
pub teyrchain: bool,
/// Only delete the relay chain database
#[arg(long, aliases = &["relay"])]
pub relaychain: bool,
}
impl PurgeChainCmd {
/// Run the purge command
pub fn run(
&self,
para_config: pezsc_service::Configuration,
relay_config: pezsc_service::Configuration,
) -> pezsc_cli::Result<()> {
let databases = match (self.teyrchain, self.relaychain) {
(true, true) | (false, false) => {
vec![("teyrchain", para_config.database), ("relaychain", relay_config.database)]
},
(true, false) => vec![("teyrchain", para_config.database)],
(false, true) => vec![("relaychain", relay_config.database)],
};
let db_paths = databases
.iter()
.map(|(chain_label, database)| {
database.path().ok_or_else(|| {
pezsc_cli::Error::Input(format!(
"Cannot purge custom database implementation of: {}",
chain_label,
))
})
})
.collect::<pezsc_cli::Result<Vec<_>>>()?;
if !self.base.yes {
for db_path in &db_paths {
println!("{}", db_path.display());
}
print!("Are you sure to remove? [y/N]: ");
io::stdout().flush().expect("failed to flush stdout");
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
match input.chars().next() {
Some('y') | Some('Y') => {},
_ => {
println!("Aborted");
return Ok(());
},
}
}
for db_path in &db_paths {
match fs::remove_dir_all(db_path) {
Ok(_) => {
println!("{:?} removed.", &db_path);
},
Err(ref err) if err.kind() == io::ErrorKind::NotFound => {
eprintln!("{:?} did not exist.", &db_path);
},
Err(err) => return Err(err.into()),
}
}
Ok(())
}
}
impl pezsc_cli::CliConfiguration for PurgeChainCmd {
fn shared_params(&self) -> &pezsc_cli::SharedParams {
&self.base.shared_params
}
fn database_params(&self) -> Option<&pezsc_cli::DatabaseParams> {
Some(&self.base.database_params)
}
}
/// Get the SCALE encoded genesis header of the teyrchain.
pub fn get_raw_genesis_header<B, C>(client: Arc<C>) -> pezsc_cli::Result<Vec<u8>>
where
B: BlockT,
C: HeaderBackend<B> + 'static,
{
let genesis_hash =
client
.hash(Zero::zero())?
.ok_or(pezsc_cli::Error::Client(pezsp_blockchain::Error::Backend(
"Failed to lookup genesis block hash when exporting genesis head data.".into(),
)))?;
let genesis_header = client.header(genesis_hash)?.ok_or(pezsc_cli::Error::Client(
pezsp_blockchain::Error::Backend(
"Failed to lookup genesis header by hash when exporting genesis head data.".into(),
),
))?;
Ok(genesis_header.encode())
}
/// Command for exporting the genesis head data of the teyrchain
#[derive(Debug, clap::Parser)]
pub struct ExportGenesisHeadCommand {
/// Output file name or stdout if unspecified.
#[arg()]
pub output: Option<PathBuf>,
/// Write output in binary. Default is to write in hex.
#[arg(short, long)]
pub raw: bool,
#[allow(missing_docs)]
#[command(flatten)]
pub shared_params: pezsc_cli::SharedParams,
}
impl ExportGenesisHeadCommand {
/// Run the export-genesis-head command
pub fn run<B, C>(&self, client: Arc<C>) -> pezsc_cli::Result<()>
where
B: BlockT,
C: HeaderBackend<B> + 'static,
{
let raw_header = get_raw_genesis_header(client)?;
let output_buf = if self.raw {
raw_header
} else {
format!("0x{:?}", HexDisplay::from(&raw_header)).into_bytes()
};
if let Some(output) = &self.output {
fs::write(output, output_buf)?;
} else {
io::stdout().write_all(&output_buf)?;
}
Ok(())
}
}
impl pezsc_cli::CliConfiguration for ExportGenesisHeadCommand {
fn shared_params(&self) -> &pezsc_cli::SharedParams {
&self.shared_params
}
fn base_path(&self) -> pezsc_cli::Result<Option<BasePath>> {
// As we are just exporting the genesis wasm a tmp database is enough.
//
// As otherwise we may "pollute" the global base path.
Ok(Some(BasePath::new_temp_dir()?))
}
}
/// Command for exporting the genesis wasm file.
#[derive(Debug, clap::Parser)]
pub struct ExportGenesisWasmCommand {
/// Output file name or stdout if unspecified.
#[arg()]
pub output: Option<PathBuf>,
/// Write output in binary. Default is to write in hex.
#[arg(short, long)]
pub raw: bool,
#[allow(missing_docs)]
#[command(flatten)]
pub shared_params: pezsc_cli::SharedParams,
}
impl ExportGenesisWasmCommand {
/// Run the export-genesis-wasm command
pub fn run(&self, chain_spec: &dyn ChainSpec) -> pezsc_cli::Result<()> {
let raw_wasm_blob = extract_genesis_wasm(chain_spec)?;
let output_buf = if self.raw {
raw_wasm_blob
} else {
format!("0x{:?}", HexDisplay::from(&raw_wasm_blob)).into_bytes()
};
if let Some(output) = &self.output {
fs::write(output, output_buf)?;
} else {
io::stdout().write_all(&output_buf)?;
}
Ok(())
}
}
/// Extract the genesis code from a given ChainSpec.
pub fn extract_genesis_wasm(chain_spec: &dyn ChainSpec) -> pezsc_cli::Result<Vec<u8>> {
let mut storage = chain_spec.build_storage()?;
storage
.top
.remove(pezsp_core::storage::well_known_keys::CODE)
.ok_or_else(|| "Could not find wasm file in genesis state!".into())
}
impl pezsc_cli::CliConfiguration for ExportGenesisWasmCommand {
fn shared_params(&self) -> &pezsc_cli::SharedParams {
&self.shared_params
}
fn base_path(&self) -> pezsc_cli::Result<Option<BasePath>> {
// As we are just exporting the genesis wasm a tmp database is enough.
//
// As otherwise we may "pollute" the global base path.
Ok(Some(BasePath::new_temp_dir()?))
}
}
fn validate_relay_chain_url(arg: &str) -> Result<Url, String> {
let url = Url::parse(arg).map_err(|e| e.to_string())?;
let scheme = url.scheme();
if scheme == "ws" || scheme == "wss" {
Ok(url)
} else {
Err(format!(
"'{}' URL scheme not supported. Only websocket RPC is currently supported",
url.scheme()
))
}
}
/// The `run` command used to run a node.
#[derive(Debug, clap::Parser)]
#[group(skip)]
pub struct RunCmd {
/// The pezcumulus RunCmd inherents from pezsc_cli's
#[command(flatten)]
pub base: pezsc_cli::RunCmd,
/// Run node as collator.
///
/// Note that this is the same as running with `--validator`.
#[arg(long, conflicts_with = "validator")]
pub collator: bool,
/// Creates a less resource-hungry node that retrieves relay chain data from an RPC endpoint.
///
/// The provided URLs should point to RPC endpoints of the relay chain.
/// This node connects to the remote nodes following the order they were specified in. If the
/// connection fails, it attempts to connect to the next endpoint in the list.
///
/// Note: This option doesn't stop the node from connecting to the relay chain network but
/// reduces bandwidth use.
#[arg(
long,
value_parser = validate_relay_chain_url,
num_args = 0..,
alias = "relay-chain-rpc-url"
)]
pub relay_chain_rpc_urls: Vec<Url>,
/// EXPERIMENTAL: This is meant to be used only if collator is overshooting the PoV size, and
/// building blocks that do not fit in the max_pov_size. It is a percentage of the max_pov_size
/// configuration of the relay-chain.
///
/// It will be removed once <https://github.com/pezkuwichain/pezkuwi-sdk/issues/23> is fixed.
#[arg(long)]
pub experimental_max_pov_percentage: Option<u32>,
/// Disable embedded DHT bootnode.
///
/// Do not advertise the node as a teyrchain bootnode on the relay chain DHT.
#[arg(long)]
pub no_dht_bootnode: bool,
/// Disable DHT bootnode discovery.
///
/// Disable discovery of the teyrchain bootnodes via the relay chain DHT.
#[arg(long)]
pub no_dht_bootnode_discovery: bool,
}
impl RunCmd {
/// Create a [`NormalizedRunCmd`] which merges the `collator` cli argument into `validator` to
/// have only one.
pub fn normalize(&self) -> NormalizedRunCmd {
let mut new_base = self.base.clone();
new_base.validator = self.base.validator || self.collator;
NormalizedRunCmd { base: new_base }
}
/// Create [`CollatorOptions`] representing options only relevant to teyrchain collator nodes
pub fn collator_options(&self) -> CollatorOptions {
let relay_chain_mode = if self.relay_chain_rpc_urls.is_empty() {
RelayChainMode::Embedded
} else {
RelayChainMode::ExternalRpc(self.relay_chain_rpc_urls.clone())
};
CollatorOptions {
relay_chain_mode,
embedded_dht_bootnode: !self.no_dht_bootnode,
dht_bootnode_discovery: !self.no_dht_bootnode_discovery,
}
}
}
/// Possible modes for the relay chain to operate in.
#[derive(Clone, Debug)]
pub enum RelayChainMode {
/// Spawn embedded relay chain node
Embedded,
/// Connect to remote relay chain node via websocket RPC
ExternalRpc(Vec<Url>),
}
/// Options only relevant for collator/teyrchain nodes
#[derive(Clone, Debug)]
pub struct CollatorOptions {
/// How this collator retrieves relay chain information
pub relay_chain_mode: RelayChainMode,
/// Enable embedded DHT bootnode.
pub embedded_dht_bootnode: bool,
/// Enable DHT bootnode discovery.
pub dht_bootnode_discovery: bool,
}
/// A non-redundant version of the `RunCmd` that sets the `validator` field when the
/// original `RunCmd` had the `collator` field.
/// This is how we make `--collator` imply `--validator`.
pub struct NormalizedRunCmd {
/// The pezcumulus RunCmd inherents from pezsc_cli's
pub base: pezsc_cli::RunCmd,
}
impl pezsc_cli::CliConfiguration for NormalizedRunCmd {
fn shared_params(&self) -> &pezsc_cli::SharedParams {
self.base.shared_params()
}
fn import_params(&self) -> Option<&pezsc_cli::ImportParams> {
self.base.import_params()
}
fn network_params(&self) -> Option<&pezsc_cli::NetworkParams> {
self.base.network_params()
}
fn keystore_params(&self) -> Option<&pezsc_cli::KeystoreParams> {
self.base.keystore_params()
}
fn offchain_worker_params(&self) -> Option<&pezsc_cli::OffchainWorkerParams> {
self.base.offchain_worker_params()
}
fn node_name(&self) -> pezsc_cli::Result<String> {
self.base.node_name()
}
fn dev_key_seed(&self, is_dev: bool) -> pezsc_cli::Result<Option<String>> {
self.base.dev_key_seed(is_dev)
}
fn telemetry_endpoints(
&self,
chain_spec: &Box<dyn pezsc_cli::ChainSpec>,
) -> pezsc_cli::Result<Option<TelemetryEndpoints>> {
self.base.telemetry_endpoints(chain_spec)
}
fn role(&self, is_dev: bool) -> pezsc_cli::Result<pezsc_cli::Role> {
self.base.role(is_dev)
}
fn force_authoring(&self) -> pezsc_cli::Result<bool> {
self.base.force_authoring()
}
fn prometheus_config(
&self,
default_listen_port: u16,
chain_spec: &Box<dyn pezsc_cli::ChainSpec>,
) -> pezsc_cli::Result<Option<PrometheusConfig>> {
self.base.prometheus_config(default_listen_port, chain_spec)
}
fn disable_grandpa(&self) -> pezsc_cli::Result<bool> {
self.base.disable_grandpa()
}
fn rpc_max_connections(&self) -> pezsc_cli::Result<u32> {
self.base.rpc_max_connections()
}
fn rpc_cors(&self, is_dev: bool) -> pezsc_cli::Result<Option<Vec<String>>> {
self.base.rpc_cors(is_dev)
}
fn rpc_addr(&self, default_listen_port: u16) -> pezsc_cli::Result<Option<Vec<RpcEndpoint>>> {
self.base.rpc_addr(default_listen_port)
}
fn rpc_methods(&self) -> pezsc_cli::Result<pezsc_service::config::RpcMethods> {
self.base.rpc_methods()
}
fn rpc_rate_limit(&self) -> pezsc_cli::Result<Option<std::num::NonZeroU32>> {
Ok(self.base.rpc_params.rpc_rate_limit)
}
fn rpc_rate_limit_whitelisted_ips(&self) -> pezsc_cli::Result<Vec<pezsc_service::config::IpNetwork>> {
Ok(self.base.rpc_params.rpc_rate_limit_whitelisted_ips.clone())
}
fn rpc_rate_limit_trust_proxy_headers(&self) -> pezsc_cli::Result<bool> {
Ok(self.base.rpc_params.rpc_rate_limit_trust_proxy_headers)
}
fn rpc_max_request_size(&self) -> pezsc_cli::Result<u32> {
self.base.rpc_max_request_size()
}
fn rpc_max_response_size(&self) -> pezsc_cli::Result<u32> {
self.base.rpc_max_response_size()
}
fn rpc_max_subscriptions_per_connection(&self) -> pezsc_cli::Result<u32> {
self.base.rpc_max_subscriptions_per_connection()
}
fn rpc_buffer_capacity_per_connection(&self) -> pezsc_cli::Result<u32> {
Ok(self.base.rpc_params.rpc_message_buffer_capacity_per_connection)
}
fn rpc_batch_config(&self) -> pezsc_cli::Result<RpcBatchRequestConfig> {
self.base.rpc_batch_config()
}
fn transaction_pool(&self, is_dev: bool) -> pezsc_cli::Result<TransactionPoolOptions> {
self.base.transaction_pool(is_dev)
}
fn max_runtime_instances(&self) -> pezsc_cli::Result<Option<usize>> {
self.base.max_runtime_instances()
}
fn runtime_cache_size(&self) -> pezsc_cli::Result<u8> {
self.base.runtime_cache_size()
}
fn base_path(&self) -> pezsc_cli::Result<Option<BasePath>> {
self.base.base_path()
}
}
+72
View File
@@ -0,0 +1,72 @@
[package]
name = "pezcumulus-client-collator"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
description = "Common node-side functionality and glue code to collate teyrchain blocks."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
codec = { features = ["derive"], workspace = true, default-features = true }
futures = { workspace = true }
parking_lot = { workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
pezsc-client-api = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-node-primitives = { workspace = true, default-features = true }
pezkuwi-node-subsystem = { workspace = true, default-features = true }
pezkuwi-overseer = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-client-consensus-common = { workspace = true, default-features = true }
pezcumulus-client-network = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
[dev-dependencies]
async-trait = { workspace = true }
# Bizinikiwi
pezsp-maybe-compressed-blob = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-tracing = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-node-subsystem-test-helpers = { workspace = true }
# Pezcumulus
pezcumulus-test-client = { workspace = true }
pezcumulus-test-relay-sproof-builder = { workspace = true, default-features = true }
pezcumulus-test-runtime = { workspace = true }
[features]
runtime-benchmarks = [
"pezcumulus-client-consensus-common/runtime-benchmarks",
"pezcumulus-client-network/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-test-client/runtime-benchmarks",
"pezcumulus-test-relay-sproof-builder/runtime-benchmarks",
"pezcumulus-test-runtime/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem-test-helpers/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
]
+141
View File
@@ -0,0 +1,141 @@
// 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 <https://www.gnu.org/licenses/>.
//! Pezcumulus Collator implementation for Bizinikiwi.
use pezkuwi_node_primitives::CollationGenerationConfig;
use pezkuwi_node_subsystem::messages::{CollationGenerationMessage, CollatorProtocolMessage};
use pezkuwi_overseer::Handle as OverseerHandle;
use pezkuwi_primitives::{CollatorPair, Id as ParaId};
pub mod service;
/// Relay-chain-driven collators are those whose block production is driven purely
/// by new relay chain blocks and the most recently included teyrchain blocks
/// within them.
///
/// This method of driving collators is not suited to anything but the most simple teyrchain
/// consensus mechanisms, and this module may soon be deprecated.
pub mod relay_chain_driven {
use futures::{
channel::{mpsc, oneshot},
prelude::*,
};
use pezkuwi_node_primitives::{CollationGenerationConfig, CollationResult};
use pezkuwi_node_subsystem::messages::{CollationGenerationMessage, CollatorProtocolMessage};
use pezkuwi_overseer::Handle as OverseerHandle;
use pezkuwi_primitives::{CollatorPair, Id as ParaId};
use cumulus_primitives_core::{relay_chain::Hash as PHash, PersistedValidationData};
/// A request to author a collation, based on the advancement of the relay chain.
///
/// See the module docs for more info on relay-chain-driven collators.
pub struct CollationRequest {
relay_parent: PHash,
pvd: PersistedValidationData,
sender: oneshot::Sender<Option<CollationResult>>,
}
impl CollationRequest {
/// Get the relay parent of the collation request.
pub fn relay_parent(&self) -> &PHash {
&self.relay_parent
}
/// Get the [`PersistedValidationData`] for the request.
pub fn persisted_validation_data(&self) -> &PersistedValidationData {
&self.pvd
}
/// Complete the request with a collation, if any.
pub fn complete(self, collation: Option<CollationResult>) {
let _ = self.sender.send(collation);
}
}
/// Initialize the collator with Pezkuwi's collation-generation
/// subsystem, returning a stream of collation requests to handle.
pub async fn init(
key: CollatorPair,
para_id: ParaId,
overseer_handle: OverseerHandle,
) -> mpsc::Receiver<CollationRequest> {
let mut overseer_handle = overseer_handle;
let (stream_tx, stream_rx) = mpsc::channel(0);
let config = CollationGenerationConfig {
key,
para_id,
collator: Some(Box::new(move |relay_parent, validation_data| {
// Cloning the channel on each usage effectively makes the channel
// unbounded. The channel is actually bounded by the block production
// and consensus systems of Pezkuwi, which limits the amount of possible
// blocks.
let mut stream_tx = stream_tx.clone();
let validation_data = validation_data.clone();
Box::pin(async move {
let (this_tx, this_rx) = oneshot::channel();
let request =
CollationRequest { relay_parent, pvd: validation_data, sender: this_tx };
if stream_tx.send(request).await.is_err() {
return None;
}
this_rx.await.ok().flatten()
})
})),
};
overseer_handle
.send_msg(CollationGenerationMessage::Initialize(config), "StartCollator")
.await;
overseer_handle
.send_msg(CollatorProtocolMessage::CollateOn(para_id), "StartCollator")
.await;
stream_rx
}
}
/// Initialize the collation-related subsystems on the relay-chain side.
///
/// This must be done prior to collation, and does not set up any callback for collation.
/// For callback-driven collators, use the [`relay_chain_driven`] module.
pub async fn initialize_collator_subsystems(
overseer_handle: &mut OverseerHandle,
key: CollatorPair,
para_id: ParaId,
reinitialize: bool,
) {
let config = CollationGenerationConfig { key, para_id, collator: None };
if reinitialize {
overseer_handle
.send_msg(CollationGenerationMessage::Reinitialize(config), "StartCollator")
.await;
} else {
overseer_handle
.send_msg(CollationGenerationMessage::Initialize(config), "StartCollator")
.await;
}
overseer_handle
.send_msg(CollatorProtocolMessage::CollateOn(para_id), "StartCollator")
.await;
}
+361
View File
@@ -0,0 +1,361 @@
// 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 <https://www.gnu.org/licenses/>.
//! The Pezcumulus [`CollatorService`] is a utility struct for performing common
//! operations used in teyrchain consensus/authoring.
use cumulus_client_network::WaitToAnnounce;
use cumulus_primitives_core::{CollationInfo, CollectCollationInfo, TeyrchainBlockData};
use pezsc_client_api::BlockBackend;
use pezsp_api::{ApiExt, ProvideRuntimeApi};
use pezsp_consensus::BlockStatus;
use pezsp_core::traits::SpawnNamed;
use pezsp_runtime::traits::{Block as BlockT, HashingFor, Header as HeaderT, Zero};
use cumulus_client_consensus_common::TeyrchainCandidate;
use pezkuwi_node_primitives::{
BlockData, Collation, CollationSecondedSignal, MaybeCompressedPoV, PoV,
};
use codec::Encode;
use futures::channel::oneshot;
use parking_lot::Mutex;
use std::sync::Arc;
/// The logging target.
const LOG_TARGET: &str = "pezcumulus-collator";
/// Utility functions generally applicable to writing collators for Pezcumulus.
pub trait ServiceInterface<Block: BlockT> {
/// Checks the status of the given block hash in the Teyrchain.
///
/// Returns `true` if the block could be found and is good to be build on.
fn check_block_status(&self, hash: Block::Hash, header: &Block::Header) -> bool;
/// Build a full [`Collation`] from a given [`TeyrchainCandidate`]. This requires
/// that the underlying block has been fully imported into the underlying client,
/// as implementations will fetch underlying runtime API data.
///
/// This also returns the unencoded teyrchain block data, in case that is desired.
fn build_collation(
&self,
parent_header: &Block::Header,
block_hash: Block::Hash,
candidate: TeyrchainCandidate<Block>,
) -> Option<(Collation, TeyrchainBlockData<Block>)>;
/// Inform networking systems that the block should be announced after a signal has
/// been received to indicate the block has been seconded by a relay-chain validator.
///
/// This sets up the barrier and returns the sending side of a channel, for the signal
/// to be passed through.
fn announce_with_barrier(
&self,
block_hash: Block::Hash,
) -> oneshot::Sender<CollationSecondedSignal>;
/// Directly announce a block on the network.
fn announce_block(&self, block_hash: Block::Hash, data: Option<Vec<u8>>);
}
/// The [`CollatorService`] provides common utilities for teyrchain consensus and authoring.
///
/// This includes logic for checking the block status of arbitrary teyrchain headers
/// gathered from the relay chain state, creating full [`Collation`]s to be shared with validators,
/// and distributing new teyrchain blocks along the network.
pub struct CollatorService<Block: BlockT, BS, RA> {
block_status: Arc<BS>,
wait_to_announce: Arc<Mutex<WaitToAnnounce<Block>>>,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
runtime_api: Arc<RA>,
}
impl<Block: BlockT, BS, RA> Clone for CollatorService<Block, BS, RA> {
fn clone(&self) -> Self {
Self {
block_status: self.block_status.clone(),
wait_to_announce: self.wait_to_announce.clone(),
announce_block: self.announce_block.clone(),
runtime_api: self.runtime_api.clone(),
}
}
}
impl<Block, BS, RA> CollatorService<Block, BS, RA>
where
Block: BlockT,
BS: BlockBackend<Block>,
RA: ProvideRuntimeApi<Block>,
RA::Api: CollectCollationInfo<Block>,
{
/// Create a new instance.
pub fn new(
block_status: Arc<BS>,
spawner: Arc<dyn SpawnNamed + Send + Sync>,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
runtime_api: Arc<RA>,
) -> Self {
let wait_to_announce =
Arc::new(Mutex::new(WaitToAnnounce::new(spawner, announce_block.clone())));
Self { block_status, wait_to_announce, announce_block, runtime_api }
}
/// Checks the status of the given block hash in the Teyrchain.
///
/// Returns `true` if the block could be found and is good to be build on.
pub fn check_block_status(&self, hash: Block::Hash, header: &Block::Header) -> bool {
match self.block_status.block_status(hash) {
Ok(BlockStatus::Queued) => {
tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Skipping candidate production, because block is still queued for import.",
);
false
},
Ok(BlockStatus::InChainWithState) => true,
Ok(BlockStatus::InChainPruned) => {
tracing::error!(
target: LOG_TARGET,
"Skipping candidate production, because block `{:?}` is already pruned!",
hash,
);
false
},
Ok(BlockStatus::KnownBad) => {
tracing::error!(
target: LOG_TARGET,
block_hash = ?hash,
"Block is tagged as known bad and is included in the relay chain! Skipping candidate production!",
);
false
},
Ok(BlockStatus::Unknown) => {
if header.number().is_zero() {
tracing::error!(
target: LOG_TARGET,
block_hash = ?hash,
"Could not find the header of the genesis block in the database!",
);
} else {
tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Skipping candidate production, because block is unknown.",
);
}
false
},
Err(e) => {
tracing::error!(
target: LOG_TARGET,
block_hash = ?hash,
error = ?e,
"Failed to get block status.",
);
false
},
}
}
/// Fetch the collation info from the runtime.
///
/// Returns `Ok(Some((CollationInfo, ApiVersion)))` on success, `Err(_)` on error or `Ok(None)`
/// if the runtime api isn't implemented by the runtime. `ApiVersion` being the version of the
/// [`CollectCollationInfo`] runtime api.
pub fn fetch_collation_info(
&self,
block_hash: Block::Hash,
header: &Block::Header,
) -> Result<Option<(CollationInfo, u32)>, pezsp_api::ApiError> {
let runtime_api = self.runtime_api.runtime_api();
let api_version =
match runtime_api.api_version::<dyn CollectCollationInfo<Block>>(block_hash)? {
Some(version) => version,
None => {
tracing::error!(
target: LOG_TARGET,
"Could not fetch `CollectCollationInfo` runtime api version."
);
return Ok(None);
},
};
let collation_info = if api_version < 2 {
#[allow(deprecated)]
runtime_api
.collect_collation_info_before_version_2(block_hash)?
.into_latest(header.encode().into())
} else {
runtime_api.collect_collation_info(block_hash, header)?
};
Ok(Some((collation_info, api_version)))
}
/// Build a full [`Collation`] from a given [`TeyrchainCandidate`]. This requires
/// that the underlying block has been fully imported into the underlying client,
/// as it fetches underlying runtime API data.
///
/// This also returns the unencoded teyrchain block data, in case that is desired.
pub fn build_collation(
&self,
parent_header: &Block::Header,
block_hash: Block::Hash,
candidate: TeyrchainCandidate<Block>,
) -> Option<(Collation, TeyrchainBlockData<Block>)> {
let block = candidate.block;
let compact_proof = match candidate
.proof
.into_compact_proof::<HashingFor<Block>>(*parent_header.state_root())
{
Ok(proof) => proof,
Err(e) => {
tracing::error!(target: "pezcumulus-collator", "Failed to compact proof: {:?}", e);
return None;
},
};
// Create the teyrchain block data for the validators.
let (collation_info, _api_version) = self
.fetch_collation_info(block_hash, block.header())
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Failed to collect collation info.",
)
})
.ok()
.flatten()?;
// Workaround for: https://github.com/pezkuwichain/pezkuwi-sdk/issues/98
//
// We are always using the `api_version` of the parent block. The `api_version` can only
// change with a runtime upgrade and this is when we want to observe the old `api_version`.
// Because this old `api_version` is the one used to validate this block. Otherwise we
// already assume the `api_version` is higher than what the relay chain will use and this
// will lead to validation errors.
let api_version = self
.runtime_api
.runtime_api()
.api_version::<dyn CollectCollationInfo<Block>>(parent_header.hash())
.ok()
.flatten()?;
let block_data = TeyrchainBlockData::<Block>::new(vec![block], compact_proof);
let pov = pezkuwi_node_primitives::maybe_compress_pov(PoV {
block_data: BlockData(if api_version >= 3 {
block_data.encode()
} else {
let block_data = block_data.as_v0();
if block_data.is_none() {
tracing::error!(
target: LOG_TARGET,
"Trying to submit a collation with multiple blocks is not supported by the current runtime."
);
}
block_data?.encode()
}),
});
let upward_messages = collation_info
.upward_messages
.try_into()
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Number of upward messages should not be greater than `MAX_UPWARD_MESSAGE_NUM`",
)
})
.ok()?;
let horizontal_messages = collation_info
.horizontal_messages
.try_into()
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Number of horizontal messages should not be greater than `MAX_HORIZONTAL_MESSAGE_NUM`",
)
})
.ok()?;
let collation = Collation {
upward_messages,
new_validation_code: collation_info.new_validation_code,
processed_downward_messages: collation_info.processed_downward_messages,
horizontal_messages,
hrmp_watermark: collation_info.hrmp_watermark,
head_data: collation_info.head_data,
proof_of_validity: MaybeCompressedPoV::Compressed(pov),
};
Some((collation, block_data))
}
/// Inform the networking systems that the block should be announced after an appropriate
/// signal has been received. This returns the sending half of the signal.
pub fn announce_with_barrier(
&self,
block_hash: Block::Hash,
) -> oneshot::Sender<CollationSecondedSignal> {
let (result_sender, signed_stmt_recv) = oneshot::channel();
self.wait_to_announce.lock().wait_to_announce(block_hash, signed_stmt_recv);
result_sender
}
}
impl<Block, BS, RA> ServiceInterface<Block> for CollatorService<Block, BS, RA>
where
Block: BlockT,
BS: BlockBackend<Block>,
RA: ProvideRuntimeApi<Block>,
RA::Api: CollectCollationInfo<Block>,
{
fn check_block_status(&self, hash: Block::Hash, header: &Block::Header) -> bool {
CollatorService::check_block_status(self, hash, header)
}
fn build_collation(
&self,
parent_header: &Block::Header,
block_hash: Block::Hash,
candidate: TeyrchainCandidate<Block>,
) -> Option<(Collation, TeyrchainBlockData<Block>)> {
CollatorService::build_collation(self, parent_header, block_hash, candidate)
}
fn announce_with_barrier(
&self,
block_hash: Block::Hash,
) -> oneshot::Sender<CollationSecondedSignal> {
CollatorService::announce_with_barrier(self, block_hash)
}
fn announce_block(&self, block_hash: Block::Hash, data: Option<Vec<u8>>) {
(self.announce_block)(block_hash, data)
}
}
+104
View File
@@ -0,0 +1,104 @@
[package]
name = "pezcumulus-client-consensus-aura"
description = "AURA consensus algorithm for teyrchains"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-trait = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
futures = { workspace = true }
parking_lot = { workspace = true }
schnellru = { workspace = true }
tokio = { workspace = true, features = ["macros"] }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
prometheus-endpoint = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-consensus = { workspace = true, default-features = true }
pezsc-consensus-aura = { workspace = true, default-features = true }
pezsc-consensus-babe = { workspace = true, default-features = true }
pezsc-consensus-slots = { workspace = true, default-features = true }
pezsc-network-types = { workspace = true, default-features = true }
pezsc-telemetry = { workspace = true, default-features = true }
pezsc-utils = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-application-crypto = { workspace = true, default-features = true }
pezsp-block-builder = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-consensus-aura = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-inherents = { workspace = true, default-features = true }
pezsp-keystore = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-timestamp = { workspace = true, default-features = true }
pezsp-trie = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-client-collator = { workspace = true, default-features = true }
pezcumulus-client-consensus-common = { workspace = true, default-features = true }
pezcumulus-client-consensus-proposer = { workspace = true, default-features = true }
pezcumulus-client-teyrchain-inherent = { workspace = true, default-features = true }
pezcumulus-primitives-aura = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-node-primitives = { workspace = true, default-features = true }
pezkuwi-node-subsystem = { workspace = true, default-features = true }
pezkuwi-node-subsystem-util = { workspace = true, default-features = true }
pezkuwi-overseer = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
[dev-dependencies]
pezcumulus-test-client = { workspace = true }
pezcumulus-test-relay-sproof-builder = { workspace = true }
rstest = { workspace = true }
pezsp-keyring = { workspace = true }
pezsp-tracing = { workspace = true }
pezsp-version = { workspace = true }
[features]
runtime-benchmarks = [
"pezcumulus-client-collator/runtime-benchmarks",
"pezcumulus-client-consensus-common/runtime-benchmarks",
"pezcumulus-client-consensus-proposer/runtime-benchmarks",
"pezcumulus-client-teyrchain-inherent/runtime-benchmarks",
"pezcumulus-primitives-aura/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezcumulus-test-client/runtime-benchmarks",
"pezcumulus-test-relay-sproof-builder/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem-util/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-consensus-aura/runtime-benchmarks",
"pezsc-consensus-babe/runtime-benchmarks",
"pezsc-consensus-slots/runtime-benchmarks",
"pezsc-consensus/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-block-builder/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus-aura/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-inherents/runtime-benchmarks",
"pezsp-keyring/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
"pezsp-timestamp/runtime-benchmarks",
"pezsp-trie/runtime-benchmarks",
"pezsp-version/runtime-benchmarks",
]
@@ -0,0 +1,442 @@
// 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 <https://www.gnu.org/licenses/>.
//! The core collator logic for Aura - slot claiming, block proposing, and collation
//! packaging.
//!
//! The [`Collator`] struct exposed here is meant to be a component of higher-level logic
//! which actually manages the control flow of the collator - which slots to claim, how
//! many collations to build, when to work, etc.
//!
//! This module also exposes some standalone functions for common operations when building
//! aura-based collators.
use codec::Codec;
use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
use cumulus_client_consensus_common::{
self as consensus_common, TeyrchainBlockImportMarker, TeyrchainCandidate,
};
use cumulus_client_consensus_proposer::ProposerInterface;
use cumulus_client_teyrchain_inherent::{TeyrchainInherentData, TeyrchainInherentDataProvider};
use cumulus_primitives_core::{
relay_chain::Hash as PHash, DigestItem, PersistedValidationData, TeyrchainBlockData,
};
use cumulus_relay_chain_interface::RelayChainInterface;
use pezkuwi_node_primitives::{Collation, MaybeCompressedPoV};
use pezkuwi_primitives::{Header as PHeader, Id as ParaId};
use crate::collators::RelayParentData;
use futures::prelude::*;
use pezsc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy, StateAction};
use pezsc_consensus_aura::standalone as aura_internal;
use pezsc_network_types::PeerId;
use pezsp_api::ProvideRuntimeApi;
use pezsp_application_crypto::AppPublic;
use pezsp_consensus::BlockOrigin;
use pezsp_consensus_aura::{AuraApi, Slot, SlotDuration};
use pezsp_core::crypto::Pair;
use pezsp_inherents::{CreateInherentDataProviders, InherentData, InherentDataProvider};
use pezsp_keystore::KeystorePtr;
use pezsp_runtime::{
generic::Digest,
traits::{Block as BlockT, HashingFor, Header as HeaderT, Member},
};
use pezsp_state_machine::StorageChanges;
use pezsp_timestamp::Timestamp;
use std::{error::Error, time::Duration};
/// Parameters for instantiating a [`Collator`].
pub struct Params<BI, CIDP, RClient, Proposer, CS> {
/// A builder for inherent data builders.
pub create_inherent_data_providers: CIDP,
/// The block import handle.
pub block_import: BI,
/// An interface to the relay-chain client.
pub relay_client: RClient,
/// The keystore handle used for accessing teyrchain key material.
pub keystore: KeystorePtr,
/// The collator network peer id.
pub collator_peer_id: PeerId,
/// The identifier of the teyrchain within the relay-chain.
pub para_id: ParaId,
/// The block proposer used for building blocks.
pub proposer: Proposer,
/// The collator service used for bundling proposals into collations and announcing
/// to the network.
pub collator_service: CS,
}
/// A utility struct for writing collation logic that makes use of Aura entirely
/// or in part. See module docs for more details.
pub struct Collator<Block, P, BI, CIDP, RClient, Proposer, CS> {
create_inherent_data_providers: CIDP,
block_import: BI,
relay_client: RClient,
keystore: KeystorePtr,
para_id: ParaId,
proposer: Proposer,
collator_service: CS,
_marker: std::marker::PhantomData<(Block, Box<dyn Fn(P) + Send + Sync + 'static>)>,
}
impl<Block, P, BI, CIDP, RClient, Proposer, CS> Collator<Block, P, BI, CIDP, RClient, Proposer, CS>
where
Block: BlockT,
RClient: RelayChainInterface,
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
Proposer: ProposerInterface<Block>,
CS: CollatorServiceInterface<Block>,
P: Pair,
P::Public: AppPublic + Member,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
{
/// Instantiate a new instance of the `Aura` manager.
pub fn new(params: Params<BI, CIDP, RClient, Proposer, CS>) -> Self {
Collator {
create_inherent_data_providers: params.create_inherent_data_providers,
block_import: params.block_import,
relay_client: params.relay_client,
keystore: params.keystore,
para_id: params.para_id,
proposer: params.proposer,
collator_service: params.collator_service,
_marker: std::marker::PhantomData,
}
}
/// Explicitly creates the inherent data for teyrchain block authoring and overrides
/// the timestamp inherent data with the one provided, if any. Additionally allows to specify
/// relay parent descendants that can be used to prevent authoring at the tip of the relay
/// chain.
pub async fn create_inherent_data_with_rp_offset(
&self,
relay_parent: PHash,
validation_data: &PersistedValidationData,
parent_hash: Block::Hash,
timestamp: impl Into<Option<Timestamp>>,
relay_parent_descendants: Option<RelayParentData>,
collator_peer_id: PeerId,
) -> Result<(TeyrchainInherentData, InherentData), Box<dyn Error + Send + Sync + 'static>> {
let paras_inherent_data = TeyrchainInherentDataProvider::create_at(
relay_parent,
&self.relay_client,
validation_data,
self.para_id,
relay_parent_descendants
.map(RelayParentData::into_inherent_descendant_list)
.unwrap_or_default(),
Vec::new(),
collator_peer_id,
)
.await;
let paras_inherent_data = match paras_inherent_data {
Some(p) => p,
None =>
return Err(
format!("Could not create paras inherent data at {:?}", relay_parent).into()
),
};
let mut other_inherent_data = self
.create_inherent_data_providers
.create_inherent_data_providers(parent_hash, ())
.map_err(|e| e as Box<dyn Error + Send + Sync + 'static>)
.await?
.create_inherent_data()
.await
.map_err(Box::new)?;
if let Some(timestamp) = timestamp.into() {
other_inherent_data.replace_data(pezsp_timestamp::INHERENT_IDENTIFIER, &timestamp);
}
Ok((paras_inherent_data, other_inherent_data))
}
/// Explicitly creates the inherent data for teyrchain block authoring and overrides
/// the timestamp inherent data with the one provided, if any.
pub async fn create_inherent_data(
&self,
relay_parent: PHash,
validation_data: &PersistedValidationData,
parent_hash: Block::Hash,
timestamp: impl Into<Option<Timestamp>>,
collator_peer_id: PeerId,
) -> Result<(TeyrchainInherentData, InherentData), Box<dyn Error + Send + Sync + 'static>> {
self.create_inherent_data_with_rp_offset(
relay_parent,
validation_data,
parent_hash,
timestamp,
None,
collator_peer_id,
)
.await
}
/// Build and import a teyrchain block on the given parent header, using the given slot claim.
pub async fn build_block_and_import(
&mut self,
parent_header: &Block::Header,
slot_claim: &SlotClaim<P::Public>,
additional_pre_digest: impl Into<Option<Vec<DigestItem>>>,
inherent_data: (TeyrchainInherentData, InherentData),
proposal_duration: Duration,
max_pov_size: usize,
) -> Result<Option<TeyrchainCandidate<Block>>, Box<dyn Error + Send + 'static>> {
let mut digest = additional_pre_digest.into().unwrap_or_default();
digest.push(slot_claim.pre_digest.clone());
let maybe_proposal = self
.proposer
.propose(
&parent_header,
&inherent_data.0,
inherent_data.1,
Digest { logs: digest },
proposal_duration,
Some(max_pov_size),
)
.await
.map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
let proposal = match maybe_proposal {
None => return Ok(None),
Some(p) => p,
};
let sealed_importable = seal::<_, P>(
proposal.block,
proposal.storage_changes,
&slot_claim.author_pub,
&self.keystore,
)
.map_err(|e| e as Box<dyn Error + Send>)?;
let block = Block::new(
sealed_importable.post_header(),
sealed_importable
.body
.as_ref()
.expect("body always created with this `propose` fn; qed")
.clone(),
);
self.block_import
.import_block(sealed_importable)
.map_err(|e| Box::new(e) as Box<dyn Error + Send>)
.await?;
Ok(Some(TeyrchainCandidate { block, proof: proposal.proof }))
}
/// Propose, seal, import a block and packaging it into a collation.
///
/// Provide the slot to build at as well as any other necessary pre-digest logs,
/// the inherent data, and the proposal duration and PoV size limits.
///
/// The Aura pre-digest should not be explicitly provided and is set internally.
///
/// This does not announce the collation to the teyrchain network or the relay chain.
pub async fn collate(
&mut self,
parent_header: &Block::Header,
slot_claim: &SlotClaim<P::Public>,
additional_pre_digest: impl Into<Option<Vec<DigestItem>>>,
inherent_data: (TeyrchainInherentData, InherentData),
proposal_duration: Duration,
max_pov_size: usize,
) -> Result<Option<(Collation, TeyrchainBlockData<Block>)>, Box<dyn Error + Send + 'static>> {
let maybe_candidate = self
.build_block_and_import(
parent_header,
slot_claim,
additional_pre_digest,
inherent_data,
proposal_duration,
max_pov_size,
)
.await?;
let Some(candidate) = maybe_candidate else { return Ok(None) };
let hash = candidate.block.header().hash();
if let Some((collation, block_data)) =
self.collator_service.build_collation(parent_header, hash, candidate)
{
block_data.log_size_info();
if let MaybeCompressedPoV::Compressed(ref pov) = collation.proof_of_validity {
tracing::info!(
target: crate::LOG_TARGET,
"Compressed PoV size: {}kb",
pov.block_data.0.len() as f64 / 1024f64,
);
}
Ok(Some((collation, block_data)))
} else {
Err(Box::<dyn Error + Send + Sync>::from("Unable to produce collation"))
}
}
/// Get the underlying collator service.
pub fn collator_service(&self) -> &CS {
&self.collator_service
}
}
/// A claim on an Aura slot.
pub struct SlotClaim<Pub> {
author_pub: Pub,
pre_digest: DigestItem,
slot: Slot,
timestamp: Timestamp,
}
impl<Pub> SlotClaim<Pub> {
/// Create a slot-claim from the given author public key, slot, and timestamp.
///
/// This does not check whether the author actually owns the slot or the timestamp
/// falls within the slot.
pub fn unchecked<P>(author_pub: Pub, slot: Slot, timestamp: Timestamp) -> Self
where
P: Pair<Public = Pub>,
P::Public: Codec,
P::Signature: Codec,
{
SlotClaim { author_pub, timestamp, pre_digest: aura_internal::pre_digest::<P>(slot), slot }
}
/// Get the author's public key.
pub fn author_pub(&self) -> &Pub {
&self.author_pub
}
/// Get the Aura pre-digest for this slot.
pub fn pre_digest(&self) -> &DigestItem {
&self.pre_digest
}
/// Get the slot assigned to this claim.
pub fn slot(&self) -> Slot {
self.slot
}
/// Get the timestamp corresponding to the relay-chain slot this claim was
/// generated against.
pub fn timestamp(&self) -> Timestamp {
self.timestamp
}
}
/// Attempt to claim a slot derived from the given relay-parent header's slot.
pub async fn claim_slot<B, C, P>(
client: &C,
parent_hash: B::Hash,
relay_parent_header: &PHeader,
slot_duration: SlotDuration,
relay_chain_slot_duration: Duration,
keystore: &KeystorePtr,
) -> Result<Option<SlotClaim<P::Public>>, Box<dyn Error>>
where
B: BlockT,
C: ProvideRuntimeApi<B> + Send + Sync + 'static,
C::Api: AuraApi<B, P::Public>,
P: Pair,
P::Public: Codec,
P::Signature: Codec,
{
// load authorities
let authorities = client.runtime_api().authorities(parent_hash).map_err(Box::new)?;
// Determine the current slot and timestamp based on the relay-parent's.
let (slot_now, timestamp) = match consensus_common::relay_slot_and_timestamp(
relay_parent_header,
relay_chain_slot_duration,
) {
Some((r_s, t)) => {
let our_slot = Slot::from_timestamp(t, slot_duration);
tracing::debug!(
target: crate::LOG_TARGET,
relay_slot = ?r_s,
para_slot = ?our_slot,
timestamp = ?t,
?slot_duration,
?relay_chain_slot_duration,
"Adjusted relay-chain slot to teyrchain slot"
);
(our_slot, t)
},
None => return Ok(None),
};
// Try to claim the slot locally.
let author_pub = {
let res = aura_internal::claim_slot::<P>(slot_now, &authorities, keystore).await;
match res {
Some(p) => p,
None => return Ok(None),
}
};
Ok(Some(SlotClaim::unchecked::<P>(author_pub, slot_now, timestamp)))
}
/// Seal a block with a signature in the header.
pub fn seal<B: BlockT, P>(
pre_sealed: B,
storage_changes: StorageChanges<HashingFor<B>>,
author_pub: &P::Public,
keystore: &KeystorePtr,
) -> Result<BlockImportParams<B>, Box<dyn Error + Send + Sync + 'static>>
where
P: Pair,
P::Signature: Codec + TryFrom<Vec<u8>>,
P::Public: AppPublic,
{
let (pre_header, body) = pre_sealed.deconstruct();
let pre_hash = pre_header.hash();
let block_number = *pre_header.number();
// seal the block.
let block_import_params = {
let seal_digest =
aura_internal::seal::<_, P>(&pre_hash, &author_pub, keystore).map_err(Box::new)?;
let mut block_import_params = BlockImportParams::new(BlockOrigin::Own, pre_header);
block_import_params.post_digests.push(seal_digest);
block_import_params.body = Some(body);
block_import_params.state_action =
StateAction::ApplyChanges(pezsc_consensus::StorageChanges::Changes(storage_changes));
block_import_params.fork_choice = Some(ForkChoiceStrategy::LongestChain);
block_import_params
};
let post_hash = block_import_params.post_hash();
tracing::info!(
target: crate::LOG_TARGET,
"🔖 Pre-sealed block for proposal at {}. Hash now {:?}, previously {:?}.",
block_number,
post_hash,
pre_hash,
);
Ok(block_import_params)
}
@@ -0,0 +1,277 @@
// 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 <https://www.gnu.org/licenses/>.
//! This provides the option to run a basic relay-chain driven Aura implementation.
//!
//! This collator only builds on top of the most recently included block, limiting the
//! block time to a maximum of two times the relay-chain block time, and requiring the
//! block to be built and distributed to validators between two relay-chain blocks.
//!
//! For more information about AuRa, the Bizinikiwi crate should be checked.
use codec::{Codec, Decode};
use cumulus_client_collator::{
relay_chain_driven::CollationRequest, service::ServiceInterface as CollatorServiceInterface,
};
use cumulus_client_consensus_common::TeyrchainBlockImportMarker;
use cumulus_client_consensus_proposer::ProposerInterface;
use cumulus_primitives_core::{relay_chain::BlockId as RBlockId, CollectCollationInfo};
use cumulus_relay_chain_interface::RelayChainInterface;
use pezkuwi_node_primitives::CollationResult;
use pezkuwi_overseer::Handle as OverseerHandle;
use pezkuwi_primitives::{CollatorPair, Id as ParaId, ValidationCode};
use futures::{channel::mpsc::Receiver, prelude::*};
use pezsc_client_api::{backend::AuxStore, BlockBackend, BlockOf};
use pezsc_consensus::BlockImport;
use pezsc_network_types::PeerId;
use pezsp_api::{CallApiAt, ProvideRuntimeApi};
use pezsp_application_crypto::AppPublic;
use pezsp_blockchain::HeaderBackend;
use pezsp_consensus_aura::AuraApi;
use pezsp_core::crypto::Pair;
use pezsp_inherents::CreateInherentDataProviders;
use pezsp_keystore::KeystorePtr;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, Member};
use pezsp_state_machine::Backend as _;
use std::{sync::Arc, time::Duration};
use crate::collator as collator_util;
/// Parameters for [`run`].
pub struct Params<BI, CIDP, Client, RClient, Proposer, CS> {
/// Inherent data providers. Only non-consensus inherent data should be provided, i.e.
/// the timestamp, slot, and paras inherents should be omitted, as they are set by this
/// collator.
pub create_inherent_data_providers: CIDP,
/// Used to actually import blocks.
pub block_import: BI,
/// The underlying para client.
pub para_client: Arc<Client>,
/// A handle to the relay-chain client.
pub relay_client: RClient,
/// The underlying keystore, which should contain Aura consensus keys.
pub keystore: KeystorePtr,
/// The collator key used to sign collations before submitting to validators.
pub collator_key: CollatorPair,
/// The collator network peer id.
pub collator_peer_id: PeerId,
/// The para's ID.
pub para_id: ParaId,
/// A handle to the relay-chain client's "Overseer" or task orchestrator.
pub overseer_handle: OverseerHandle,
/// The length of slots in the relay chain.
pub relay_chain_slot_duration: Duration,
/// The underlying block proposer this should call into.
pub proposer: Proposer,
/// The generic collator service used to plug into this consensus engine.
pub collator_service: CS,
/// The amount of time to spend authoring each block.
pub authoring_duration: Duration,
/// Receiver for collation requests. If `None`, Aura consensus will establish a new receiver.
/// Should be used when a chain migrates from a different consensus algorithm and was already
/// processing collation requests before initializing Aura.
pub collation_request_receiver: Option<Receiver<CollationRequest>>,
}
/// Run bare Aura consensus as a relay-chain-driven collator.
pub fn run<Block, P, BI, CIDP, Client, RClient, Proposer, CS>(
params: Params<BI, CIDP, Client, RClient, Proposer, CS>,
) -> impl Future<Output = ()> + Send + 'static
where
Block: BlockT + Send,
Client: ProvideRuntimeApi<Block>
+ BlockOf
+ AuxStore
+ HeaderBackend<Block>
+ BlockBackend<Block>
+ CallApiAt<Block>
+ Send
+ Sync
+ 'static,
Client::Api: AuraApi<Block, P::Public> + CollectCollationInfo<Block>,
RClient: RelayChainInterface + Send + Clone + 'static,
CIDP: CreateInherentDataProviders<Block, ()> + Send + 'static,
CIDP::InherentDataProviders: Send,
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
P: Pair,
P::Public: AppPublic + Member + Codec,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
{
async move {
let mut collation_requests = match params.collation_request_receiver {
Some(receiver) => receiver,
None =>
cumulus_client_collator::relay_chain_driven::init(
params.collator_key,
params.para_id,
params.overseer_handle,
)
.await,
};
let mut collator = {
let params = collator_util::Params {
create_inherent_data_providers: params.create_inherent_data_providers,
block_import: params.block_import,
relay_client: params.relay_client.clone(),
keystore: params.keystore.clone(),
collator_peer_id: params.collator_peer_id,
para_id: params.para_id,
proposer: params.proposer,
collator_service: params.collator_service,
};
collator_util::Collator::<Block, P, _, _, _, _, _>::new(params)
};
let mut last_processed_slot = 0;
let mut last_relay_chain_block = Default::default();
while let Some(request) = collation_requests.next().await {
macro_rules! reject_with_error {
($err:expr) => {{
request.complete(None);
tracing::error!(target: crate::LOG_TARGET, err = ?{ $err });
continue;
}};
}
macro_rules! try_request {
($x:expr) => {{
match $x {
Ok(x) => x,
Err(e) => reject_with_error!(e),
}
}};
}
let validation_data = request.persisted_validation_data();
let parent_header =
try_request!(Block::Header::decode(&mut &validation_data.parent_head.0[..]));
let parent_hash = parent_header.hash();
if !collator.collator_service().check_block_status(parent_hash, &parent_header) {
continue;
}
let Ok(Some(code)) =
params.para_client.state_at(parent_hash).map_err(drop).and_then(|s| {
s.storage(&pezsp_core::storage::well_known_keys::CODE).map_err(drop)
})
else {
continue;
};
super::check_validation_code_or_log(
&ValidationCode::from(code).hash(),
params.para_id,
&params.relay_client,
*request.relay_parent(),
)
.await;
let relay_parent_header =
match params.relay_client.header(RBlockId::hash(*request.relay_parent())).await {
Err(e) => reject_with_error!(e),
Ok(None) => continue, // sanity: would be inconsistent to get `None` here
Ok(Some(h)) => h,
};
let slot_duration = match params.para_client.runtime_api().slot_duration(parent_hash) {
Ok(d) => d,
Err(e) => reject_with_error!(e),
};
let claim = match collator_util::claim_slot::<_, _, P>(
&*params.para_client,
parent_hash,
&relay_parent_header,
slot_duration,
params.relay_chain_slot_duration,
&params.keystore,
)
.await
{
Ok(None) => continue,
Ok(Some(c)) => c,
Err(e) => reject_with_error!(e),
};
// With async backing this function will be called every relay chain block.
//
// Most teyrchains currently run with 12 seconds slots and thus, they would try to
// produce multiple blocks per slot which very likely would fail on chain. Thus, we have
// this "hack" to only produce one block per slot per relay chain fork.
//
// With https://github.com/pezkuwichain/pezkuwi-sdk/issues/127 this implementation will be
// obsolete and also the underlying issue will be fixed.
if last_processed_slot >= *claim.slot() &&
last_relay_chain_block < *relay_parent_header.number()
{
continue;
}
let (teyrchain_inherent_data, other_inherent_data) = try_request!(
collator
.create_inherent_data(
*request.relay_parent(),
&validation_data,
parent_hash,
claim.timestamp(),
params.collator_peer_id,
)
.await
);
let allowed_pov_size = (validation_data.max_pov_size / 2) as usize;
let maybe_collation = try_request!(
collator
.collate(
&parent_header,
&claim,
None,
(teyrchain_inherent_data, other_inherent_data),
params.authoring_duration,
allowed_pov_size,
)
.await
);
if let Some((collation, block_data)) = maybe_collation {
let Some(block_hash) = block_data.blocks().first().map(|b| b.hash()) else {
continue;
};
let result_sender =
Some(collator.collator_service().announce_with_barrier(block_hash));
request.complete(Some(CollationResult { collation, result_sender }));
} else {
request.complete(None);
tracing::debug!(target: crate::LOG_TARGET, "No block proposal");
}
last_processed_slot = *claim.slot();
last_relay_chain_block = *relay_parent_header.number();
}
}
}
@@ -0,0 +1,511 @@
// 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 <https://www.gnu.org/licenses/>.
//! A collator for Aura that looks ahead of the most recently included teyrchain block
//! when determining what to build upon.
//!
//! This collator also builds additional blocks when the maximum backlog is not saturated.
//! The size of the backlog is determined by invoking a runtime API. If that runtime API
//! is not supported, this assumes a maximum backlog size of 1.
//!
//! This takes more advantage of asynchronous backing, though not complete advantage.
//! When the backlog is not saturated, this approach lets the backlog temporarily 'catch up'
//! with periods of higher throughput. When the backlog is saturated, we typically
//! fall back to the limited cadence of a single teyrchain block per relay-chain block.
//!
//! Despite this, the fact that there is a backlog at all allows us to spend more time
//! building the block, as there is some buffer before it can get posted to the relay-chain.
//! The main limitation is block propagation time - i.e. the new blocks created by an author
//! must be propagated to the next author before their turn.
use codec::{Codec, Encode};
use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
use cumulus_client_consensus_common::{self as consensus_common, TeyrchainBlockImportMarker};
use cumulus_client_consensus_proposer::ProposerInterface;
use cumulus_primitives_aura::AuraUnincludedSegmentApi;
use cumulus_primitives_core::{CollectCollationInfo, PersistedValidationData};
use cumulus_relay_chain_interface::RelayChainInterface;
use pezkuwi_node_primitives::SubmitCollationParams;
use pezkuwi_node_subsystem::messages::CollationGenerationMessage;
use pezkuwi_overseer::Handle as OverseerHandle;
use pezkuwi_primitives::{CollatorPair, Id as ParaId, OccupiedCoreAssumption};
use crate::{
collator as collator_util,
collators::{claim_queue_at, BackingGroupConnectionHelper},
export_pov_to_path,
};
use futures::prelude::*;
use pezsc_client_api::{backend::AuxStore, BlockBackend, BlockOf};
use pezsc_consensus::BlockImport;
use pezsc_network_types::PeerId;
use pezsp_api::ProvideRuntimeApi;
use pezsp_application_crypto::AppPublic;
use pezsp_blockchain::HeaderBackend;
use pezsp_consensus_aura::{AuraApi, Slot};
use pezsp_core::crypto::Pair;
use pezsp_inherents::CreateInherentDataProviders;
use pezsp_keystore::KeystorePtr;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, Member};
use pezsp_timestamp::Timestamp;
use std::{path::PathBuf, sync::Arc, time::Duration};
/// Parameters for [`run`].
pub struct Params<BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS> {
/// Inherent data providers. Only non-consensus inherent data should be provided, i.e.
/// the timestamp, slot, and paras inherents should be omitted, as they are set by this
/// collator.
pub create_inherent_data_providers: CIDP,
/// Used to actually import blocks.
pub block_import: BI,
/// The underlying para client.
pub para_client: Arc<Client>,
/// The para client's backend, used to access the database.
pub para_backend: Arc<Backend>,
/// A handle to the relay-chain client.
pub relay_client: RClient,
/// A validation code hash provider, used to get the current validation code hash.
pub code_hash_provider: CHP,
/// The underlying keystore, which should contain Aura consensus keys.
pub keystore: KeystorePtr,
/// The collator key used to sign collations before submitting to validators.
pub collator_key: CollatorPair,
/// The collator network peer id.
pub collator_peer_id: PeerId,
/// The para's ID.
pub para_id: ParaId,
/// A handle to the relay-chain client's "Overseer" or task orchestrator.
pub overseer_handle: OverseerHandle,
/// The length of slots in the relay chain.
pub relay_chain_slot_duration: Duration,
/// The underlying block proposer this should call into.
pub proposer: Proposer,
/// The generic collator service used to plug into this consensus engine.
pub collator_service: CS,
/// The amount of time to spend authoring each block.
pub authoring_duration: Duration,
/// Whether we should reinitialize the collator config (i.e. we are transitioning to aura).
pub reinitialize: bool,
/// The maximum percentage of the maximum PoV size that the collator can use.
/// It will be removed once <https://github.com/pezkuwichain/pezkuwi-sdk/issues/23> is fixed.
pub max_pov_percentage: Option<u32>,
}
/// Get the current teyrchain slot from a given block hash.
///
/// Returns the teyrchain slot, relay chain slot, and timestamp.
fn get_teyrchain_slot<Block, Client, P>(
para_client: &Client,
block_hash: Block::Hash,
relay_parent_header: &pezkuwi_primitives::Header,
relay_chain_slot_duration: Duration,
) -> Option<(Slot, Slot, Timestamp)>
where
Block: BlockT,
Client: ProvideRuntimeApi<Block>,
Client::Api: AuraApi<Block, P>,
P: Codec,
{
let slot_duration =
match pezsc_consensus_aura::standalone::slot_duration_at(para_client, block_hash) {
Ok(sd) => sd,
Err(err) => {
tracing::error!(target: crate::LOG_TARGET, ?err, "Failed to acquire teyrchain slot duration");
return None;
},
};
tracing::debug!(target: crate::LOG_TARGET, ?slot_duration, ?block_hash, "Teyrchain slot duration acquired");
let (relay_slot, timestamp) =
consensus_common::relay_slot_and_timestamp(relay_parent_header, relay_chain_slot_duration)?;
let slot_now = Slot::from_timestamp(timestamp, slot_duration);
tracing::debug!(
target: crate::LOG_TARGET,
?relay_slot,
para_slot = ?slot_now,
?timestamp,
?slot_duration,
?relay_chain_slot_duration,
"Adjusted relay-chain slot to teyrchain slot"
);
Some((slot_now, relay_slot, timestamp))
}
/// Run async-backing-friendly Aura.
pub fn run<Block, P, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS>(
params: Params<BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS>,
) -> impl Future<Output = ()> + Send + 'static
where
Block: BlockT,
Client: ProvideRuntimeApi<Block>
+ BlockOf
+ AuxStore
+ HeaderBackend<Block>
+ BlockBackend<Block>
+ Send
+ Sync
+ 'static,
Client::Api:
AuraApi<Block, P::Public> + CollectCollationInfo<Block> + AuraUnincludedSegmentApi<Block>,
Backend: pezsc_client_api::Backend<Block> + 'static,
RClient: RelayChainInterface + Clone + 'static,
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
CIDP::InherentDataProviders: Send,
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
CHP: consensus_common::ValidationCodeHashProvider<Block::Hash> + Send + 'static,
P: Pair + Send + Sync + 'static,
P::Public: AppPublic + Member + Codec,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
{
run_with_export::<_, P, _, _, _, _, _, _, _, _>(ParamsWithExport { params, export_pov: None })
}
/// Parameters for [`run_with_export`].
pub struct ParamsWithExport<BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS> {
/// The parameters.
pub params: Params<BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS>,
/// When set, the collator will export every produced `POV` to this folder.
pub export_pov: Option<PathBuf>,
}
/// Run async-backing-friendly Aura.
///
/// This is exactly the same as [`run`], but it supports the optional export of each produced `POV`
/// to the file system.
pub fn run_with_export<Block, P, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS>(
ParamsWithExport { mut params, export_pov }: ParamsWithExport<
BI,
CIDP,
Client,
Backend,
RClient,
CHP,
Proposer,
CS,
>,
) -> impl Future<Output = ()> + Send + 'static
where
Block: BlockT,
Client: ProvideRuntimeApi<Block>
+ BlockOf
+ AuxStore
+ HeaderBackend<Block>
+ BlockBackend<Block>
+ Send
+ Sync
+ 'static,
Client::Api:
AuraApi<Block, P::Public> + CollectCollationInfo<Block> + AuraUnincludedSegmentApi<Block>,
Backend: pezsc_client_api::Backend<Block> + 'static,
RClient: RelayChainInterface + Clone + 'static,
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
CIDP::InherentDataProviders: Send,
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
CHP: consensus_common::ValidationCodeHashProvider<Block::Hash> + Send + 'static,
P: Pair + Send + Sync + 'static,
P::Public: AppPublic + Member + Codec,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
{
async move {
cumulus_client_collator::initialize_collator_subsystems(
&mut params.overseer_handle,
params.collator_key,
params.para_id,
params.reinitialize,
)
.await;
let mut import_notifications = match params.relay_client.import_notification_stream().await
{
Ok(s) => s,
Err(err) => {
tracing::error!(
target: crate::LOG_TARGET,
?err,
"Failed to initialize consensus: no relay chain import notification stream"
);
return;
},
};
let mut collator = {
let params = collator_util::Params {
create_inherent_data_providers: params.create_inherent_data_providers,
block_import: params.block_import,
relay_client: params.relay_client.clone(),
keystore: params.keystore.clone(),
collator_peer_id: params.collator_peer_id,
para_id: params.para_id,
proposer: params.proposer,
collator_service: params.collator_service,
};
collator_util::Collator::<Block, P, _, _, _, _, _>::new(params)
};
let mut connection_helper = BackingGroupConnectionHelper::new(
params.keystore.clone(),
params.overseer_handle.clone(),
);
while let Some(relay_parent_header) = import_notifications.next().await {
let relay_parent = relay_parent_header.hash();
let Some(core_index) = claim_queue_at(relay_parent, &mut params.relay_client)
.await
.iter_claims_at_depth_for_para(0, params.para_id)
.next()
else {
tracing::trace!(
target: crate::LOG_TARGET,
?relay_parent,
?params.para_id,
"Para is not scheduled on any core, skipping import notification",
);
continue;
};
let max_pov_size = match params
.relay_client
.persisted_validation_data(
relay_parent,
params.para_id,
OccupiedCoreAssumption::Included,
)
.await
{
Ok(None) => continue,
Ok(Some(pvd)) => pvd.max_pov_size,
Err(err) => {
tracing::error!(target: crate::LOG_TARGET, ?err, "Failed to gather information from relay-client");
continue;
},
};
let (included_block, initial_parent) = match crate::collators::find_parent(
relay_parent,
params.para_id,
&*params.para_backend,
&params.relay_client,
)
.await
{
Some(value) => value,
None => continue,
};
let para_client = &*params.para_client;
let keystore = &params.keystore;
let can_build_upon = |block_hash| {
let (slot_now, relay_slot, timestamp) = get_teyrchain_slot::<_, _, P::Public>(
para_client,
block_hash,
&relay_parent_header,
params.relay_chain_slot_duration,
)?;
Some(super::can_build_upon::<_, _, P>(
slot_now,
relay_slot,
timestamp,
block_hash,
included_block.hash(),
para_client,
&keystore,
))
};
// Build in a loop until not allowed. Note that the authorities can change
// at any block, so we need to re-claim our slot every time.
let mut parent_hash = initial_parent.hash;
let mut parent_header = initial_parent.header;
let overseer_handle = &mut params.overseer_handle;
// Do not try to build upon an unknown, pruned or bad block
if !collator.collator_service().check_block_status(parent_hash, &parent_header) {
continue;
}
// Trigger pre-conect to backing groups if necessary.
if let (Some((slot_now, _relay_slot, _timestamp)), Ok(authorities)) = (
get_teyrchain_slot::<_, _, P::Public>(
para_client,
parent_hash,
&relay_parent_header,
params.relay_chain_slot_duration,
),
para_client.runtime_api().authorities(parent_hash),
) {
connection_helper.update::<P>(slot_now, &authorities).await;
}
// This needs to change to support elastic scaling, but for continuously
// scheduled chains this ensures that the backlog will grow steadily.
for n_built in 0..2 {
let slot_claim = match can_build_upon(parent_hash) {
Some(fut) => match fut.await {
None => break,
Some(c) => c,
},
None => break,
};
tracing::debug!(
target: crate::LOG_TARGET,
?relay_parent,
unincluded_segment_len = initial_parent.depth + n_built,
"Slot claimed. Building"
);
let validation_data = PersistedValidationData {
parent_head: parent_header.encode().into(),
relay_parent_number: *relay_parent_header.number(),
relay_parent_storage_root: *relay_parent_header.state_root(),
max_pov_size,
};
// Build and announce collations recursively until
// `can_build_upon` fails or building a collation fails.
let (teyrchain_inherent_data, other_inherent_data) = match collator
.create_inherent_data(
relay_parent,
&validation_data,
parent_hash,
slot_claim.timestamp(),
params.collator_peer_id,
)
.await
{
Err(err) => {
tracing::error!(target: crate::LOG_TARGET, ?err);
break;
},
Ok(x) => x,
};
let Some(validation_code_hash) =
params.code_hash_provider.code_hash_at(parent_hash)
else {
tracing::error!(target: crate::LOG_TARGET, ?parent_hash, "Could not fetch validation code hash");
break;
};
super::check_validation_code_or_log(
&validation_code_hash,
params.para_id,
&params.relay_client,
relay_parent,
)
.await;
let allowed_pov_size = if let Some(max_pov_percentage) = params.max_pov_percentage {
validation_data.max_pov_size * max_pov_percentage / 100
} else {
// Set the block limit to 85% of the maximum PoV size.
//
// Once https://github.com/pezkuwichain/pezkuwi-sdk/issues/23 issue is
// fixed, the reservation should be removed.
validation_data.max_pov_size * 85 / 100
} as usize;
match collator
.collate(
&parent_header,
&slot_claim,
None,
(teyrchain_inherent_data, other_inherent_data),
params.authoring_duration,
allowed_pov_size,
)
.await
{
Ok(Some((collation, block_data))) => {
let Some(new_block_header) =
block_data.blocks().first().map(|b| b.header().clone())
else {
tracing::error!(target: crate::LOG_TARGET, "Produced PoV doesn't contain any blocks");
break;
};
let new_block_hash = new_block_header.hash();
// Here we are assuming that the import logic protects against equivocations
// and provides sybil-resistance, as it should.
collator.collator_service().announce_block(new_block_hash, None);
if let Some(ref export_pov) = export_pov {
export_pov_to_path::<Block>(
export_pov.clone(),
collation.proof_of_validity.clone().into_compressed(),
new_block_hash,
*new_block_header.number(),
parent_header.clone(),
*relay_parent_header.state_root(),
*relay_parent_header.number(),
validation_data.max_pov_size,
);
}
// Send a submit-collation message to the collation generation subsystem,
// which then distributes this to validators.
//
// Here we are assuming that the leaf is imported, as we've gotten an
// import notification.
overseer_handle
.send_msg(
CollationGenerationMessage::SubmitCollation(
SubmitCollationParams {
relay_parent,
collation,
parent_head: parent_header.encode().into(),
validation_code_hash,
result_sender: None,
core_index,
},
),
"SubmitCollation",
)
.await;
parent_hash = new_block_hash;
parent_header = new_block_header;
},
Ok(None) => {
tracing::debug!(target: crate::LOG_TARGET, "No block proposal");
break;
},
Err(err) => {
tracing::error!(target: crate::LOG_TARGET, ?err);
break;
},
}
}
}
}
}
@@ -0,0 +1,709 @@
// 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 <https://www.gnu.org/licenses/>.
//! Stock, pure Aura collators.
//!
//! This includes the [`basic`] collator, which only builds on top of the most recently
//! included teyrchain block, as well as the [`lookahead`] collator, which prospectively
//! builds on teyrchain blocks which have not yet been included in the relay chain.
use crate::collator::SlotClaim;
use codec::Codec;
use cumulus_client_consensus_common::{self as consensus_common, ParentSearchParams};
use cumulus_primitives_aura::{AuraUnincludedSegmentApi, Slot};
use cumulus_primitives_core::{relay_chain::Header as RelayHeader, BlockT};
use cumulus_relay_chain_interface::{OverseerHandle, RelayChainInterface};
use pezkuwi_node_subsystem::messages::{CollatorProtocolMessage, RuntimeApiRequest};
use pezkuwi_node_subsystem_util::runtime::ClaimQueueSnapshot;
use pezkuwi_primitives::{
Hash as RelayHash, Id as ParaId, OccupiedCoreAssumption, ValidationCodeHash,
DEFAULT_SCHEDULING_LOOKAHEAD,
};
use pezsc_consensus_aura::{standalone as aura_internal, AuraApi};
use pezsp_api::{ApiExt, ProvideRuntimeApi, RuntimeApiInfo};
use pezsp_core::Pair;
use pezsp_keystore::KeystorePtr;
use pezsp_timestamp::Timestamp;
pub mod basic;
pub mod lookahead;
pub mod slot_based;
// This is an arbitrary value which is guaranteed to exceed the required depth for 500ms blocks
// built with a relay parent offset of 1. It must be larger than the unincluded segment capacity.
//
// The formula we use to compute the capacity of the unincluded segment in the teyrchain runtime
// is:
// UNINCLUDED_SEGMENT_CAPACITY = (2 + RELAY_PARENT_OFFSET) * BLOCK_PROCESSING_VELOCITY + 1.
//
// Since we only search for parent blocks which have already been imported,
// we can guarantee that all imported blocks respect the unincluded segment
// rules specified by the teyrchain's runtime and thus will never be too deep. This is just an extra
// sanity check.
const PARENT_SEARCH_DEPTH: usize = 40;
// Helper to pre-connect to the backing group we got assigned to and keep the connection
// open until backing group changes or own slot ends.
struct BackingGroupConnectionHelper {
keystore: pezsp_keystore::KeystorePtr,
overseer_handle: OverseerHandle,
our_slot: Option<Slot>,
}
impl BackingGroupConnectionHelper {
pub fn new(keystore: pezsp_keystore::KeystorePtr, overseer_handle: OverseerHandle) -> Self {
Self { keystore, overseer_handle, our_slot: None }
}
async fn send_subsystem_message(&mut self, message: CollatorProtocolMessage) {
self.overseer_handle.send_msg(message, "BackingGroupConnectionHelper").await;
}
/// Update the current slot and initiate connections to backing groups if needed.
pub async fn update<P>(&mut self, current_slot: Slot, authorities: &[P::Public])
where
P: pezsp_core::Pair + Send + Sync,
P::Public: Codec,
{
if Some(current_slot) <= self.our_slot {
// Current slot or next slot is ours.
// We already sent pre-connect message, no need to proceed further.
return;
}
let next_slot = current_slot + 1;
let next_slot_is_ours =
aura_internal::claim_slot::<P>(next_slot, authorities, &self.keystore)
.await
.is_some();
if next_slot_is_ours {
// Only send message if we were not connected. This avoids sending duplicate messages
// when running with a single collator.
if self.our_slot.is_none() {
// Next slot is ours, send connect message.
tracing::debug!(target: crate::LOG_TARGET, "Our slot {} is next, connecting to backing groups", next_slot);
self.send_subsystem_message(CollatorProtocolMessage::ConnectToBackingGroups)
.await;
}
self.our_slot = Some(next_slot);
} else if self.our_slot.take().is_some() {
// Next slot is not ours, send disconnect only if we had a slot before.
tracing::debug!(target: crate::LOG_TARGET, "Current slot = {}, disconnecting from backing groups", current_slot);
self.send_subsystem_message(CollatorProtocolMessage::DisconnectFromBackingGroups)
.await;
}
}
}
/// Check the `local_validation_code_hash` against the validation code hash in the relay chain
/// state.
///
/// If the code hashes do not match, it prints a warning.
async fn check_validation_code_or_log(
local_validation_code_hash: &ValidationCodeHash,
para_id: ParaId,
relay_client: &impl RelayChainInterface,
relay_parent: RelayHash,
) {
let state_validation_code_hash = match relay_client
.validation_code_hash(relay_parent, para_id, OccupiedCoreAssumption::Included)
.await
{
Ok(hash) => hash,
Err(error) => {
tracing::debug!(
target: super::LOG_TARGET,
%error,
?relay_parent,
%para_id,
"Failed to fetch validation code hash",
);
return;
},
};
match state_validation_code_hash {
Some(state) =>
if state != *local_validation_code_hash {
tracing::warn!(
target: super::LOG_TARGET,
%para_id,
?relay_parent,
?local_validation_code_hash,
relay_validation_code_hash = ?state,
"Teyrchain code doesn't match validation code stored in the relay chain state.",
);
},
None => {
tracing::warn!(
target: super::LOG_TARGET,
%para_id,
?relay_parent,
"Could not find validation code for teyrchain in the relay chain state.",
);
},
}
}
/// Fetch scheduling lookahead at given relay parent.
async fn scheduling_lookahead(
relay_parent: RelayHash,
relay_client: &impl RelayChainInterface,
) -> Option<u32> {
let runtime_api_version = relay_client
.version(relay_parent)
.await
.map_err(|e| {
tracing::error!(
target: super::LOG_TARGET,
error = ?e,
"Failed to fetch relay chain runtime version.",
)
})
.ok()?;
let teyrchain_host_runtime_api_version = runtime_api_version
.api_version(
&<dyn pezkuwi_primitives::runtime_api::TeyrchainHost<pezkuwi_primitives::Block>>::ID,
)
.unwrap_or_default();
if teyrchain_host_runtime_api_version <
RuntimeApiRequest::SCHEDULING_LOOKAHEAD_RUNTIME_REQUIREMENT
{
return None;
}
match relay_client.scheduling_lookahead(relay_parent).await {
Ok(scheduling_lookahead) => Some(scheduling_lookahead),
Err(err) => {
tracing::error!(
target: crate::LOG_TARGET,
?err,
?relay_parent,
"Failed to fetch scheduling lookahead from relay chain",
);
None
},
}
}
// Returns the claim queue at the given relay parent.
async fn claim_queue_at(
relay_parent: RelayHash,
relay_client: &impl RelayChainInterface,
) -> ClaimQueueSnapshot {
// Get `ClaimQueue` from runtime
match relay_client.claim_queue(relay_parent).await {
Ok(claim_queue) => claim_queue.into(),
Err(error) => {
tracing::error!(
target: crate::LOG_TARGET,
?error,
?relay_parent,
"Failed to query claim queue runtime API",
);
Default::default()
},
}
}
// Checks if we own the slot at the given block and whether there
// is space in the unincluded segment.
async fn can_build_upon<Block: BlockT, Client, P>(
para_slot: Slot,
relay_slot: Slot,
timestamp: Timestamp,
parent_hash: Block::Hash,
included_block: Block::Hash,
client: &Client,
keystore: &KeystorePtr,
) -> Option<SlotClaim<P::Public>>
where
Client: ProvideRuntimeApi<Block>,
Client::Api: AuraApi<Block, P::Public> + AuraUnincludedSegmentApi<Block> + ApiExt<Block>,
P: Pair,
P::Public: Codec,
P::Signature: Codec,
{
let runtime_api = client.runtime_api();
let authorities = runtime_api.authorities(parent_hash).ok()?;
let author_pub = aura_internal::claim_slot::<P>(para_slot, &authorities, keystore).await?;
// This function is typically called when we want to build block N. At that point, the
// unincluded segment in the runtime is unaware of the hash of block N-1. If the unincluded
// segment in the runtime is full, but block N-1 is the included block, the unincluded segment
// should have length 0 and we can build. Since the hash is not available to the runtime
// however, we need this extra check here.
if parent_hash == included_block {
return Some(SlotClaim::unchecked::<P>(author_pub, para_slot, timestamp));
}
let api_version = runtime_api
.api_version::<dyn AuraUnincludedSegmentApi<Block>>(parent_hash)
.ok()
.flatten()?;
let slot = if api_version > 1 { relay_slot } else { para_slot };
runtime_api
.can_build_upon(parent_hash, included_block, slot)
.ok()?
.then(|| SlotClaim::unchecked::<P>(author_pub, para_slot, timestamp))
}
/// Use [`cumulus_client_consensus_common::find_potential_parents`] to find teyrchain blocks that
/// we can build on. Once a list of potential parents is retrieved, return the last one of the
/// longest chain.
async fn find_parent<Block>(
relay_parent: RelayHash,
para_id: ParaId,
para_backend: &impl pezsc_client_api::Backend<Block>,
relay_client: &impl RelayChainInterface,
) -> Option<(<Block as BlockT>::Header, consensus_common::PotentialParent<Block>)>
where
Block: BlockT,
{
let parent_search_params = ParentSearchParams {
relay_parent,
para_id,
ancestry_lookback: scheduling_lookahead(relay_parent, relay_client)
.await
.unwrap_or(DEFAULT_SCHEDULING_LOOKAHEAD)
.saturating_sub(1) as usize,
max_depth: PARENT_SEARCH_DEPTH,
ignore_alternative_branches: true,
};
let potential_parents = cumulus_client_consensus_common::find_potential_parents::<Block>(
parent_search_params,
para_backend,
relay_client,
)
.await;
let potential_parents = match potential_parents {
Err(e) => {
tracing::error!(
target: crate::LOG_TARGET,
?relay_parent,
err = ?e,
"Could not fetch potential parents to build upon"
);
return None;
},
Ok(x) => x,
};
let included_block = potential_parents.iter().find(|x| x.depth == 0)?.header.clone();
potential_parents
.into_iter()
.max_by_key(|a| a.depth)
.map(|parent| (included_block, parent))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::collators::{can_build_upon, BackingGroupConnectionHelper};
use codec::Encode;
use cumulus_primitives_aura::Slot;
use cumulus_primitives_core::BlockT;
use cumulus_relay_chain_interface::PHash;
use cumulus_test_client::{
runtime::{Block, Hash},
Client, DefaultTestClientBuilderExt, InitBlockBuilder, TestClientBuilder,
TestClientBuilderExt,
};
use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder;
use futures::StreamExt;
use pezkuwi_overseer::{Event, Handle};
use pezkuwi_primitives::HeadData;
use pezsc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy};
use pezsp_consensus::BlockOrigin;
use pezsp_keystore::{Keystore, KeystorePtr};
use pezsp_timestamp::Timestamp;
use std::sync::{Arc, Mutex};
async fn import_block<I: BlockImport<Block>>(
importer: &I,
block: Block,
origin: BlockOrigin,
import_as_best: bool,
) {
let (header, body) = block.deconstruct();
let mut block_import_params = BlockImportParams::new(origin, header);
block_import_params.fork_choice = Some(ForkChoiceStrategy::Custom(import_as_best));
block_import_params.body = Some(body);
importer.import_block(block_import_params).await.unwrap();
}
fn sproof_with_parent_by_hash(client: &Client, hash: PHash) -> RelayStateSproofBuilder {
let header = client.header(hash).ok().flatten().expect("No header for parent block");
let included = HeadData(header.encode());
let mut builder = RelayStateSproofBuilder::default();
builder.para_id = cumulus_test_client::runtime::TEYRCHAIN_ID.into();
builder.included_para_head = Some(included);
builder
}
async fn build_and_import_block(client: &Client, included: Hash) -> Block {
let sproof = sproof_with_parent_by_hash(client, included);
let block_builder = client.init_block_builder(None, sproof).block_builder;
let block = block_builder.build().unwrap().block;
let origin = BlockOrigin::NetworkInitialSync;
import_block(client, block.clone(), origin, true).await;
block
}
fn set_up_components(num_authorities: usize) -> (Arc<Client>, KeystorePtr) {
let keystore = Arc::new(pezsp_keystore::testing::MemoryKeystore::new()) as Arc<_>;
for key in pezsp_keyring::Sr25519Keyring::iter().take(num_authorities) {
Keystore::sr25519_generate_new(
&*keystore,
pezsp_application_crypto::key_types::AURA,
Some(&key.to_seed()),
)
.expect("Can insert key into MemoryKeyStore");
}
(Arc::new(TestClientBuilder::new().build()), keystore)
}
/// This tests a special scenario where the unincluded segment in the runtime
/// is full. We are calling `can_build_upon`, passing the last built block as the
/// included one. In the runtime we will not find the hash of the included block in the
/// unincluded segment. The `can_build_upon` runtime API would therefore return `false`, but
/// we are ensuring on the node side that we are are always able to build on the included block.
#[tokio::test]
async fn test_can_build_upon() {
let (client, keystore) = set_up_components(6);
let genesis_hash = client.chain_info().genesis_hash;
let mut last_hash = genesis_hash;
// Fill up the unincluded segment tracker in the runtime.
while can_build_upon::<_, _, pezsp_consensus_aura::sr25519::AuthorityPair>(
Slot::from(u64::MAX),
Slot::from(u64::MAX),
Timestamp::default(),
last_hash,
genesis_hash,
&*client,
&keystore,
)
.await
.is_some()
{
let block = build_and_import_block(&client, genesis_hash).await;
last_hash = block.header().hash();
}
// Blocks were built with the genesis hash set as included block.
// We call `can_build_upon` with the last built block as the included block.
let result = can_build_upon::<_, _, pezsp_consensus_aura::sr25519::AuthorityPair>(
Slot::from(u64::MAX),
Slot::from(u64::MAX),
Timestamp::default(),
last_hash,
last_hash,
&*client,
&keystore,
)
.await;
assert!(result.is_some());
}
/// Helper to create a mock overseer handle and message recorder
fn create_overseer_handle() -> (OverseerHandle, Arc<Mutex<Vec<CollatorProtocolMessage>>>) {
let messages = Arc::new(Mutex::new(Vec::new()));
let messages_clone = messages.clone();
let (tx, mut rx) = pezkuwi_node_subsystem_util::metered::channel(100);
// Spawn a task to receive and record overseer messages
tokio::spawn(async move {
while let Some(event) = rx.next().await {
if let Event::MsgToSubsystem { msg, .. } = event {
if let pezkuwi_node_subsystem::AllMessages::CollatorProtocol(cp_msg) = msg {
messages_clone.lock().unwrap().push(cp_msg);
}
}
}
});
(Handle::new(tx), messages)
}
#[tokio::test]
async fn preconnect_when_next_slot_is_ours() {
let (client, keystore) = set_up_components(1);
let genesis_hash = client.chain_info().genesis_hash;
let (overseer_handle, messages_recorder) = create_overseer_handle();
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
// Fetch authorities for the update call
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
// Update with slot 5, next slot (6) should be ours
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
.await;
// Give time for message to be processed
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let messages = messages_recorder.lock().unwrap();
assert_eq!(messages.len(), 1);
assert!(matches!(messages[0], CollatorProtocolMessage::ConnectToBackingGroups));
assert_eq!(helper.our_slot, Some(Slot::from(6)));
}
#[tokio::test]
async fn preconnect_no_duplicate_connect_message() {
let (client, keystore) = set_up_components(1);
let genesis_hash = client.chain_info().genesis_hash;
let (overseer_handle, messages_recorder) = create_overseer_handle();
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
// Fetch authorities for the update calls
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
// Update with slot 5, next slot (6) is ours
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
.await;
// Give time for message to be processed
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
assert_eq!(messages_recorder.lock().unwrap().len(), 1);
messages_recorder.lock().unwrap().clear();
// Update with slot 5 again - should not send another message
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
assert_eq!(messages_recorder.lock().unwrap().len(), 0);
// Update with slot 1 (our slot) - should not send another message
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(6), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
assert_eq!(messages_recorder.lock().unwrap().len(), 0);
}
#[tokio::test]
async fn preconnect_disconnect_when_slot_passes() {
let (client, keystore) = set_up_components(1);
let genesis_hash = client.chain_info().genesis_hash;
let (overseer_handle, messages_recorder) = create_overseer_handle();
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
// Fetch authorities for the update calls
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
// Slot 0 -> Alice, Slot 1 -> Bob, Slot 2 -> Charlie, Slot 3 -> Dave, Slot 4 -> Eve,
// Slot 5 -> Ferdie, Slot 6 -> Alice
// Update with slot 5, next slot (6) is ours -> should connect
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
assert_eq!(helper.our_slot, Some(Slot::from(6)));
messages_recorder.lock().unwrap().clear();
// Update with slot 8, next slot (9) is Charlie's -> should disconnect
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(8), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
{
let messages = messages_recorder.lock().unwrap();
assert_eq!(messages.len(), 1, "Expected exactly one disconnect message");
assert!(matches!(messages[0], CollatorProtocolMessage::DisconnectFromBackingGroups));
assert_eq!(helper.our_slot, None);
}
messages_recorder.lock().unwrap().clear();
// Update again with slot 8, next slot (9) is Charlie's -> should not send another
// disconnect message
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(8), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let messages = messages_recorder.lock().unwrap();
assert_eq!(messages.len(), 0, "Expected no messages");
assert_eq!(helper.our_slot, None);
}
#[tokio::test]
async fn preconnect_no_disconnect_without_previous_connection() {
let (client, keystore) = set_up_components(1);
let genesis_hash = client.chain_info().genesis_hash;
let (overseer_handle, messages_recorder) = create_overseer_handle();
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
// Fetch authorities for the update call
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
// Slot 0 -> Alice, Slot 1 -> Bob, Slot 2 -> Charlie, Slot 3 -> Dave, Slot 4 -> Eve,
// Slot 5 -> Ferdie
// Update with slot 1 (Bob's slot), next slot (2) is Charlie's
// Since we never connected before (our_slot is None), we should not send disconnect
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(1), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
// Should not send any message since we never connected
assert_eq!(messages_recorder.lock().unwrap().len(), 0);
assert_eq!(helper.our_slot, None);
}
#[tokio::test]
async fn preconnect_multiple_cycles() {
let (client, keystore) = set_up_components(1);
let genesis_hash = client.chain_info().genesis_hash;
let (overseer_handle, messages_recorder) = create_overseer_handle();
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
// Fetch authorities for the update calls
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
// Slot 0 -> Alice, Slot 1 -> Bob, Slot 2 -> Charlie, Slot 3 -> Dave, Slot 4 -> Eve,
// Slot 5 -> Ferdie, Slot 6 -> Alice, Slot 7 -> Bob, ...
// Cycle 1: Connect at slot 5, next slot (6) is ours
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
{
let messages = messages_recorder.lock().unwrap();
assert_eq!(messages.len(), 1);
assert!(matches!(messages[0], CollatorProtocolMessage::ConnectToBackingGroups));
}
assert_eq!(helper.our_slot, Some(Slot::from(6)));
messages_recorder.lock().unwrap().clear();
// Cycle 1: Disconnect at slot 7, next slot (8) is Charlie's
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(7), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
{
let messages = messages_recorder.lock().unwrap();
assert_eq!(messages.len(), 1);
assert!(matches!(messages[0], CollatorProtocolMessage::DisconnectFromBackingGroups));
}
assert_eq!(helper.our_slot, None);
messages_recorder.lock().unwrap().clear();
// Cycle 2: Connect again at slot 11, next slot (12) is ours
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(11), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
{
let messages = messages_recorder.lock().unwrap();
assert_eq!(messages.len(), 1);
assert!(matches!(messages[0], CollatorProtocolMessage::ConnectToBackingGroups));
}
assert_eq!(helper.our_slot, Some(Slot::from(12)));
}
#[tokio::test]
async fn preconnect_handles_empty_authorities() {
let keystore = Arc::new(pezsp_keystore::testing::MemoryKeystore::new()) as Arc<_>;
let (overseer_handle, messages_recorder) = create_overseer_handle();
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
// Pass empty authorities list
let authorities = vec![];
helper
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(0), &authorities)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
// Should not send any message if authorities list is empty
assert_eq!(messages_recorder.lock().unwrap().len(), 0);
}
}
/// Holds a relay parent and its descendants.
pub struct RelayParentData {
/// The relay parent block header
relay_parent: RelayHeader,
/// Ordered collection of descendant block headers, from oldest to newest
descendants: Vec<RelayHeader>,
}
impl RelayParentData {
/// Creates a new instance with the given relay parent and no descendants.
pub fn new(relay_parent: RelayHeader) -> Self {
Self { relay_parent, descendants: Default::default() }
}
/// Creates a new instance with the given relay parent and descendants.
pub fn new_with_descendants(relay_parent: RelayHeader, descendants: Vec<RelayHeader>) -> Self {
Self { relay_parent, descendants }
}
/// Returns a reference to the relay parent header.
pub fn relay_parent(&self) -> &RelayHeader {
&self.relay_parent
}
/// Returns the number of descendants.
#[cfg(test)]
pub fn descendants_len(&self) -> usize {
self.descendants.len()
}
/// Consumes the structure and returns a vector containing the relay parent followed by its
/// descendants in chronological order. The resulting list should be provided to the teyrchain
/// inherent data.
pub fn into_inherent_descendant_list(self) -> Vec<RelayHeader> {
let Self { relay_parent, mut descendants } = self;
if descendants.is_empty() {
return Default::default();
}
let mut result = vec![relay_parent];
result.append(&mut descendants);
result
}
}
@@ -0,0 +1,647 @@
// 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 <https://www.gnu.org/licenses/>.
use codec::{Codec, Encode};
use super::CollatorMessage;
use crate::{
collator as collator_util,
collators::{
check_validation_code_or_log,
slot_based::{
relay_chain_data_cache::{RelayChainData, RelayChainDataCache},
slot_timer::{SlotInfo, SlotTimer},
},
BackingGroupConnectionHelper, RelayParentData,
},
LOG_TARGET,
};
use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
use cumulus_client_consensus_common::{self as consensus_common, TeyrchainBlockImportMarker};
use cumulus_client_consensus_proposer::ProposerInterface;
use cumulus_primitives_aura::{AuraUnincludedSegmentApi, Slot};
use cumulus_primitives_core::{
extract_relay_parent, rpsr_digest, ClaimQueueOffset, CoreInfo, CoreSelector, CumulusDigestItem,
PersistedValidationData, RelayParentOffsetApi,
};
use cumulus_relay_chain_interface::RelayChainInterface;
use futures::prelude::*;
use pezkuwi_primitives::{
Block as RelayBlock, CoreIndex, Hash as RelayHash, Header as RelayHeader, Id as ParaId,
};
use pezsc_client_api::{backend::AuxStore, BlockBackend, BlockOf, UsageProvider};
use pezsc_consensus::BlockImport;
use pezsc_consensus_aura::SlotDuration;
use pezsc_network_types::PeerId;
use pezsp_api::ProvideRuntimeApi;
use pezsp_application_crypto::AppPublic;
use pezsp_blockchain::HeaderBackend;
use pezsp_consensus_aura::AuraApi;
use pezsp_core::crypto::Pair;
use pezsp_inherents::CreateInherentDataProviders;
use pezsp_keystore::KeystorePtr;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, Member, Zero};
use std::{collections::VecDeque, sync::Arc, time::Duration};
/// Parameters for [`run_block_builder`].
pub struct BuilderTaskParams<
Block: BlockT,
BI,
CIDP,
Client,
Backend,
RelayClient,
CHP,
Proposer,
CS,
> {
/// Inherent data providers. Only non-consensus inherent data should be provided, i.e.
/// the timestamp, slot, and paras inherents should be omitted, as they are set by this
/// collator.
pub create_inherent_data_providers: CIDP,
/// Used to actually import blocks.
pub block_import: BI,
/// The underlying para client.
pub para_client: Arc<Client>,
/// The para client's backend, used to access the database.
pub para_backend: Arc<Backend>,
/// A handle to the relay-chain client.
pub relay_client: RelayClient,
/// A validation code hash provider, used to get the current validation code hash.
pub code_hash_provider: CHP,
/// The underlying keystore, which should contain Aura consensus keys.
pub keystore: KeystorePtr,
/// The collator network peer id.
pub collator_peer_id: PeerId,
/// The para's ID.
pub para_id: ParaId,
/// The underlying block proposer this should call into.
pub proposer: Proposer,
/// The generic collator service used to plug into this consensus engine.
pub collator_service: CS,
/// The amount of time to spend authoring each block.
pub authoring_duration: Duration,
/// Channel to send built blocks to the collation task.
pub collator_sender: pezsc_utils::mpsc::TracingUnboundedSender<CollatorMessage<Block>>,
/// Slot duration of the relay chain.
pub relay_chain_slot_duration: Duration,
/// Offset all time operations by this duration.
///
/// This is a time quantity that is subtracted from the actual timestamp when computing
/// the time left to enter a new slot. In practice, this *left-shifts* the clock time with the
/// intent to keep our "clock" slightly behind the relay chain one and thus reducing the
/// likelihood of encountering unfavorable notification arrival timings (i.e. we don't want to
/// wait for relay chain notifications because we woke up too early).
pub slot_offset: Duration,
/// The maximum percentage of the maximum PoV size that the collator can use.
/// It will be removed once https://github.com/pezkuwichain/pezkuwi-sdk/issues/23 is fixed.
pub max_pov_percentage: Option<u32>,
}
/// Run block-builder.
pub fn run_block_builder<Block, P, BI, CIDP, Client, Backend, RelayClient, CHP, Proposer, CS>(
params: BuilderTaskParams<Block, BI, CIDP, Client, Backend, RelayClient, CHP, Proposer, CS>,
) -> impl Future<Output = ()> + Send + 'static
where
Block: BlockT,
Client: ProvideRuntimeApi<Block>
+ UsageProvider<Block>
+ BlockOf
+ AuxStore
+ HeaderBackend<Block>
+ BlockBackend<Block>
+ Send
+ Sync
+ 'static,
Client::Api:
AuraApi<Block, P::Public> + RelayParentOffsetApi<Block> + AuraUnincludedSegmentApi<Block>,
Backend: pezsc_client_api::Backend<Block> + 'static,
RelayClient: RelayChainInterface + Clone + 'static,
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
CIDP::InherentDataProviders: Send,
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
CHP: consensus_common::ValidationCodeHashProvider<Block::Hash> + Send + 'static,
P: Pair + Send + Sync + 'static,
P::Public: AppPublic + Member + Codec,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
{
async move {
tracing::info!(target: LOG_TARGET, "Starting slot-based block-builder task.");
let BuilderTaskParams {
relay_client,
create_inherent_data_providers,
para_client,
keystore,
block_import,
collator_peer_id,
para_id,
proposer,
collator_service,
collator_sender,
code_hash_provider,
authoring_duration,
relay_chain_slot_duration,
para_backend,
slot_offset,
max_pov_percentage,
} = params;
let mut slot_timer = SlotTimer::<_, _, P>::new_with_offset(
para_client.clone(),
slot_offset,
relay_chain_slot_duration,
);
let mut collator = {
let params = collator_util::Params {
create_inherent_data_providers,
block_import,
relay_client: relay_client.clone(),
keystore: keystore.clone(),
collator_peer_id,
para_id,
proposer,
collator_service,
};
collator_util::Collator::<Block, P, _, _, _, _, _>::new(params)
};
let mut relay_chain_data_cache = RelayChainDataCache::new(relay_client.clone(), para_id);
let mut connection_helper = BackingGroupConnectionHelper::new(
keystore.clone(),
relay_client
.overseer_handle()
// Should never fail. If it fails, then providing collations to relay chain
// doesn't work either. So it is fine to panic here.
.expect("Relay chain interface must provide overseer handle."),
);
loop {
// We wait here until the next slot arrives.
if slot_timer.wait_until_next_slot().await.is_err() {
tracing::error!(target: LOG_TARGET, "Unable to wait for next slot.");
return;
};
let Ok(relay_best_hash) = relay_client.best_block_hash().await else {
tracing::warn!(target: crate::LOG_TARGET, "Unable to fetch latest relay chain block hash.");
continue;
};
let best_hash = para_client.info().best_hash;
let relay_parent_offset =
para_client.runtime_api().relay_parent_offset(best_hash).unwrap_or_default();
let Ok(para_slot_duration) = crate::slot_duration(&*para_client) else {
tracing::error!(target: LOG_TARGET, "Failed to fetch slot duration from runtime.");
continue;
};
let Ok(Some(rp_data)) = offset_relay_parent_find_descendants(
&mut relay_chain_data_cache,
relay_best_hash,
relay_parent_offset,
)
.await
else {
continue;
};
let Some(para_slot) = adjust_para_to_relay_parent_slot(
rp_data.relay_parent(),
relay_chain_slot_duration,
para_slot_duration,
) else {
continue;
};
let relay_parent = rp_data.relay_parent().hash();
let relay_parent_header = rp_data.relay_parent().clone();
let Some((included_header, parent)) =
crate::collators::find_parent(relay_parent, para_id, &*para_backend, &relay_client)
.await
else {
continue;
};
let parent_hash = parent.hash;
let parent_header = &parent.header;
// Retrieve the core.
let core = match determine_core(
&mut relay_chain_data_cache,
&relay_parent_header,
para_id,
parent_header,
relay_parent_offset,
)
.await
{
Err(()) => {
tracing::debug!(
target: LOG_TARGET,
?relay_parent,
"Failed to determine core"
);
continue;
},
Ok(Some(cores)) => {
tracing::debug!(
target: LOG_TARGET,
?relay_parent,
core_selector = ?cores.selector,
claim_queue_offset = ?cores.claim_queue_offset,
"Going to claim core",
);
cores
},
Ok(None) => {
tracing::debug!(
target: LOG_TARGET,
?relay_parent,
"No core scheduled"
);
continue;
},
};
let Ok(RelayChainData { max_pov_size, last_claimed_core_selector, .. }) =
relay_chain_data_cache.get_mut_relay_chain_data(relay_parent).await
else {
continue;
};
slot_timer.update_scheduling(core.total_cores().into());
// We mainly call this to inform users at genesis if there is a mismatch with the
// on-chain data.
collator.collator_service().check_block_status(parent_hash, parent_header);
let Ok(relay_slot) =
pezsc_consensus_babe::find_pre_digest::<RelayBlock>(&relay_parent_header)
.map(|babe_pre_digest| babe_pre_digest.slot())
else {
tracing::error!(target: crate::LOG_TARGET, "Relay chain does not contain babe slot. This should never happen.");
continue;
};
let included_header_hash = included_header.hash();
if let Ok(authorities) = para_client.runtime_api().authorities(parent_hash) {
connection_helper.update::<P>(para_slot.slot, &authorities).await;
}
let slot_claim = match crate::collators::can_build_upon::<_, _, P>(
para_slot.slot,
relay_slot,
para_slot.timestamp,
parent_hash,
included_header_hash,
&*para_client,
&keystore,
)
.await
{
Some(slot) => slot,
None => {
tracing::debug!(
target: crate::LOG_TARGET,
unincluded_segment_len = parent.depth,
relay_parent = ?relay_parent,
relay_parent_num = %relay_parent_header.number(),
included_hash = ?included_header_hash,
included_num = %included_header.number(),
parent = ?parent_hash,
slot = ?para_slot.slot,
"Not building block."
);
continue;
},
};
tracing::debug!(
target: crate::LOG_TARGET,
unincluded_segment_len = parent.depth,
relay_parent = %relay_parent,
relay_parent_num = %relay_parent_header.number(),
relay_parent_offset,
included_hash = %included_header_hash,
included_num = %included_header.number(),
parent = %parent_hash,
slot = ?para_slot.slot,
"Building block."
);
let validation_data = PersistedValidationData {
parent_head: parent_header.encode().into(),
relay_parent_number: *relay_parent_header.number(),
relay_parent_storage_root: *relay_parent_header.state_root(),
max_pov_size: *max_pov_size,
};
let (teyrchain_inherent_data, other_inherent_data) = match collator
.create_inherent_data_with_rp_offset(
relay_parent,
&validation_data,
parent_hash,
slot_claim.timestamp(),
Some(rp_data),
collator_peer_id,
)
.await
{
Err(err) => {
tracing::error!(target: crate::LOG_TARGET, ?err);
break;
},
Ok(x) => x,
};
let validation_code_hash = match code_hash_provider.code_hash_at(parent_hash) {
None => {
tracing::error!(target: crate::LOG_TARGET, ?parent_hash, "Could not fetch validation code hash");
break;
},
Some(v) => v,
};
check_validation_code_or_log(
&validation_code_hash,
para_id,
&relay_client,
relay_parent,
)
.await;
let allowed_pov_size = if let Some(max_pov_percentage) = max_pov_percentage {
validation_data.max_pov_size * max_pov_percentage / 100
} else {
// Set the block limit to 85% of the maximum PoV size.
//
// Once https://github.com/pezkuwichain/pezkuwi-sdk/issues/23 issue is
// fixed, this should be removed.
validation_data.max_pov_size * 85 / 100
} as usize;
let adjusted_authoring_duration =
slot_timer.adjust_authoring_duration(authoring_duration);
tracing::debug!(target: crate::LOG_TARGET, duration = ?adjusted_authoring_duration, "Adjusted proposal duration.");
let Some(adjusted_authoring_duration) = adjusted_authoring_duration else {
tracing::debug!(
target: crate::LOG_TARGET,
unincluded_segment_len = parent.depth,
relay_parent = ?relay_parent,
relay_parent_num = %relay_parent_header.number(),
included_hash = ?included_header_hash,
included_num = %included_header.number(),
parent = ?parent_hash,
slot = ?para_slot.slot,
"Not building block due to insufficient authoring duration."
);
continue;
};
let Ok(Some(candidate)) = collator
.build_block_and_import(
&parent_header,
&slot_claim,
Some(vec![CumulusDigestItem::CoreInfo(core.core_info()).to_digest_item()]),
(teyrchain_inherent_data, other_inherent_data),
adjusted_authoring_duration,
allowed_pov_size,
)
.await
else {
tracing::error!(target: crate::LOG_TARGET, "Unable to build block at slot.");
continue;
};
let new_block_hash = candidate.block.header().hash();
// Announce the newly built block to our peers.
collator.collator_service().announce_block(new_block_hash, None);
*last_claimed_core_selector = Some(core.core_selector());
if let Err(err) = collator_sender.unbounded_send(CollatorMessage {
relay_parent,
parent_header: parent_header.clone(),
teyrchain_candidate: candidate,
validation_code_hash,
core_index: core.core_index(),
max_pov_size: validation_data.max_pov_size,
}) {
tracing::error!(target: crate::LOG_TARGET, ?err, "Unable to send block to collation task.");
return;
}
}
}
}
/// Translate the slot of the relay parent to the slot of the teyrchain.
fn adjust_para_to_relay_parent_slot(
relay_header: &RelayHeader,
relay_chain_slot_duration: Duration,
para_slot_duration: SlotDuration,
) -> Option<SlotInfo> {
let relay_slot = pezsc_consensus_babe::find_pre_digest::<RelayBlock>(&relay_header)
.map(|babe_pre_digest| babe_pre_digest.slot())
.ok()?;
let new_slot = Slot::from_timestamp(
relay_slot
.timestamp(SlotDuration::from_millis(relay_chain_slot_duration.as_millis() as u64))?,
para_slot_duration,
);
let para_slot = SlotInfo { slot: new_slot, timestamp: new_slot.timestamp(para_slot_duration)? };
tracing::debug!(
target: LOG_TARGET,
timestamp = ?para_slot.timestamp,
slot = ?para_slot.slot,
"Teyrchain slot adjusted to relay chain.",
);
Some(para_slot)
}
/// Finds a relay chain parent block at a specified offset from the best block, collecting its
/// descendants.
///
/// # Returns
/// * `Ok(RelayParentData)` - Contains the target relay parent and its ordered list of descendants
/// * `Err(())` - If any relay chain block header cannot be retrieved
///
/// The function traverses backwards from the best block until it finds the block at the specified
/// offset, collecting all blocks in between to maintain the chain of ancestry.
pub(crate) async fn offset_relay_parent_find_descendants<RelayClient>(
relay_chain_data_cache: &mut RelayChainDataCache<RelayClient>,
relay_best_block: RelayHash,
relay_parent_offset: u32,
) -> Result<Option<RelayParentData>, ()>
where
RelayClient: RelayChainInterface + Clone + 'static,
{
let Ok(mut relay_header) = relay_chain_data_cache
.get_mut_relay_chain_data(relay_best_block)
.await
.map(|d| d.relay_parent_header.clone())
else {
tracing::error!(target: LOG_TARGET, ?relay_best_block, "Unable to fetch best relay chain block header.");
return Err(());
};
if relay_parent_offset == 0 {
return Ok(Some(RelayParentData::new(relay_header)));
}
if pezsc_consensus_babe::contains_epoch_change::<RelayBlock>(&relay_header) {
tracing::debug!(target: LOG_TARGET, ?relay_best_block, relay_best_block_number = relay_header.number(), "Relay parent is in previous session.");
return Ok(None);
}
let mut required_ancestors: VecDeque<RelayHeader> = Default::default();
required_ancestors.push_front(relay_header.clone());
while required_ancestors.len() < relay_parent_offset as usize {
let next_header = relay_chain_data_cache
.get_mut_relay_chain_data(*relay_header.parent_hash())
.await?
.relay_parent_header
.clone();
if pezsc_consensus_babe::contains_epoch_change::<RelayBlock>(&next_header) {
tracing::debug!(target: LOG_TARGET, ?relay_best_block, ancestor = %next_header.hash(), ancestor_block_number = next_header.number(), "Ancestor of best block is in previous session.");
return Ok(None);
}
required_ancestors.push_front(next_header.clone());
relay_header = next_header;
}
let relay_parent = relay_chain_data_cache
.get_mut_relay_chain_data(*relay_header.parent_hash())
.await?
.relay_parent_header
.clone();
tracing::debug!(
target: LOG_TARGET,
relay_parent_hash = %relay_parent.hash(),
relay_parent_num = relay_parent.number(),
num_descendants = required_ancestors.len(),
"Relay parent descendants."
);
Ok(Some(RelayParentData::new_with_descendants(relay_parent, required_ancestors.into())))
}
/// Return value of [`determine_core`].
pub(crate) struct Core {
selector: CoreSelector,
claim_queue_offset: ClaimQueueOffset,
core_index: CoreIndex,
number_of_cores: u16,
}
impl Core {
/// Returns the current [`CoreInfo`].
fn core_info(&self) -> CoreInfo {
CoreInfo {
selector: self.selector,
claim_queue_offset: self.claim_queue_offset,
number_of_cores: self.number_of_cores.into(),
}
}
/// Returns the current [`CoreSelector`].
pub(crate) fn core_selector(&self) -> CoreSelector {
self.selector
}
/// Returns the current [`CoreIndex`].
pub(crate) fn core_index(&self) -> CoreIndex {
self.core_index
}
/// Returns the total number of cores.
pub(crate) fn total_cores(&self) -> u16 {
self.number_of_cores
}
}
/// Determine the core for the given `para_id`.
pub(crate) async fn determine_core<H: HeaderT, RI: RelayChainInterface + 'static>(
relay_chain_data_cache: &mut RelayChainDataCache<RI>,
relay_parent: &RelayHeader,
para_id: ParaId,
para_parent: &H,
relay_parent_offset: u32,
) -> Result<Option<Core>, ()> {
let cores_at_offset = &relay_chain_data_cache
.get_mut_relay_chain_data(relay_parent.hash())
.await?
.claim_queue
.iter_claims_at_depth_for_para(relay_parent_offset as usize, para_id)
.collect::<Vec<_>>();
let is_new_relay_parent = if para_parent.number().is_zero() {
true
} else {
match extract_relay_parent(para_parent.digest()) {
Some(last_relay_parent) => last_relay_parent != relay_parent.hash(),
None =>
rpsr_digest::extract_relay_parent_storage_root(para_parent.digest())
.ok_or(())?
.0 != *relay_parent.state_root(),
}
};
let core_info = CumulusDigestItem::find_core_info(para_parent.digest());
// If we are using a new relay parent, we can start over from the start.
let (selector, core_index) = if is_new_relay_parent {
let Some(core_index) = cores_at_offset.get(0) else { return Ok(None) };
(0, *core_index)
} else if let Some(core_info) = core_info {
let selector = core_info.selector.0 as usize + 1;
let Some(core_index) = cores_at_offset.get(selector) else { return Ok(None) };
(selector, *core_index)
} else {
let last_claimed_core_selector = relay_chain_data_cache
.get_mut_relay_chain_data(relay_parent.hash())
.await?
.last_claimed_core_selector;
let selector = last_claimed_core_selector.map_or(0, |cs| cs.0 as usize) + 1;
let Some(core_index) = cores_at_offset.get(selector) else { return Ok(None) };
(selector, *core_index)
};
Ok(Some(Core {
selector: CoreSelector(selector as u8),
core_index,
claim_queue_offset: ClaimQueueOffset(relay_parent_offset as u8),
number_of_cores: cores_at_offset.len() as u16,
}))
}
@@ -0,0 +1,145 @@
// 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 <https://www.gnu.org/licenses/>.
use futures::{stream::FusedStream, StreamExt};
use pezsc_consensus::{BlockImport, StateAction};
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
use pezsp_api::{ApiExt, CallApiAt, CallContext, Core, ProvideRuntimeApi, StorageProof};
use pezsp_runtime::traits::{Block as BlockT, Header as _};
use pezsp_trie::proof_size_extension::ProofSizeExt;
use std::sync::Arc;
/// Handle for receiving the block and the storage proof from the [`SlotBasedBlockImport`].
///
/// This handle should be passed to [`Params`](super::Params) or can also be dropped if the node is
/// not running as collator.
pub struct SlotBasedBlockImportHandle<Block> {
receiver: TracingUnboundedReceiver<(Block, StorageProof)>,
}
impl<Block> SlotBasedBlockImportHandle<Block> {
/// Returns the next item.
///
/// The future will never return when the internal channel is closed.
pub async fn next(&mut self) -> (Block, StorageProof) {
loop {
if self.receiver.is_terminated() {
futures::pending!()
} else if let Some(res) = self.receiver.next().await {
return res;
}
}
}
}
/// Special block import for the slot based collator.
pub struct SlotBasedBlockImport<Block, BI, Client> {
inner: BI,
client: Arc<Client>,
sender: TracingUnboundedSender<(Block, StorageProof)>,
}
impl<Block, BI, Client> SlotBasedBlockImport<Block, BI, Client> {
/// Create a new instance.
///
/// The returned [`SlotBasedBlockImportHandle`] needs to be passed to the
/// [`Params`](super::Params), so that this block import instance can communicate with the
/// collation task. If the node is not running as a collator, just dropping the handle is fine.
pub fn new(inner: BI, client: Arc<Client>) -> (Self, SlotBasedBlockImportHandle<Block>) {
let (sender, receiver) = tracing_unbounded("SlotBasedBlockImportChannel", 1000);
(Self { sender, client, inner }, SlotBasedBlockImportHandle { receiver })
}
}
impl<Block, BI: Clone, Client> Clone for SlotBasedBlockImport<Block, BI, Client> {
fn clone(&self) -> Self {
Self { inner: self.inner.clone(), client: self.client.clone(), sender: self.sender.clone() }
}
}
#[async_trait::async_trait]
impl<Block, BI, Client> BlockImport<Block> for SlotBasedBlockImport<Block, BI, Client>
where
Block: BlockT,
BI: BlockImport<Block> + Send + Sync,
BI::Error: Into<pezsp_consensus::Error>,
Client: ProvideRuntimeApi<Block> + CallApiAt<Block> + Send + Sync,
Client::StateBackend: Send,
Client::Api: Core<Block>,
{
type Error = pezsp_consensus::Error;
async fn check_block(
&self,
block: pezsc_consensus::BlockCheckParams<Block>,
) -> Result<pezsc_consensus::ImportResult, Self::Error> {
self.inner.check_block(block).await.map_err(Into::into)
}
async fn import_block(
&self,
mut params: pezsc_consensus::BlockImportParams<Block>,
) -> Result<pezsc_consensus::ImportResult, Self::Error> {
// If the channel exists and it is required to execute the block, we will execute the block
// here. This is done to collect the storage proof and to prevent re-execution, we push
// downwards the state changes. `StateAction::ApplyChanges` is ignored, because it either
// means that the node produced the block itself or the block was imported via state sync.
if !self.sender.is_closed() && !matches!(params.state_action, StateAction::ApplyChanges(_))
{
let mut runtime_api = self.client.runtime_api();
runtime_api.set_call_context(CallContext::Onchain);
runtime_api.record_proof();
let recorder = runtime_api
.proof_recorder()
.expect("Proof recording is enabled in the line above; qed.");
runtime_api.register_extension(ProofSizeExt::new(recorder));
let parent_hash = *params.header.parent_hash();
let block = Block::new(params.header.clone(), params.body.clone().unwrap_or_default());
runtime_api
.execute_block(parent_hash, block.clone().into())
.map_err(|e| Box::new(e) as Box<_>)?;
let storage_proof =
runtime_api.extract_proof().expect("Proof recording was enabled above; qed");
let state = self.client.state_at(parent_hash).map_err(|e| Box::new(e) as Box<_>)?;
let gen_storage_changes = runtime_api
.into_storage_changes(&state, parent_hash)
.map_err(pezsp_consensus::Error::ChainLookup)?;
if params.header.state_root() != &gen_storage_changes.transaction_storage_root {
return Err(pezsp_consensus::Error::Other(Box::new(
pezsp_blockchain::Error::InvalidStateRoot,
)));
}
params.state_action = StateAction::ApplyChanges(pezsc_consensus::StorageChanges::Changes(
gen_storage_changes,
));
let _ = self.sender.unbounded_send((block, storage_proof));
}
self.inner.import_block(params).await.map_err(Into::into)
}
}
@@ -0,0 +1,189 @@
// 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 <https://www.gnu.org/licenses/>.
use codec::Encode;
use std::path::PathBuf;
use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
use cumulus_relay_chain_interface::RelayChainInterface;
use pezkuwi_node_primitives::{MaybeCompressedPoV, SubmitCollationParams};
use pezkuwi_node_subsystem::messages::CollationGenerationMessage;
use pezkuwi_overseer::Handle as OverseerHandle;
use pezkuwi_primitives::{CollatorPair, Id as ParaId};
use cumulus_primitives_core::relay_chain::BlockId;
use futures::prelude::*;
use crate::export_pov_to_path;
use pezsc_utils::mpsc::TracingUnboundedReceiver;
use pezsp_runtime::traits::{Block as BlockT, Header};
use super::CollatorMessage;
const LOG_TARGET: &str = "aura::pezcumulus::collation_task";
/// Parameters for the collation task.
pub struct Params<Block: BlockT, RClient, CS> {
/// A handle to the relay-chain client.
pub relay_client: RClient,
/// The collator key used to sign collations before submitting to validators.
pub collator_key: CollatorPair,
/// The para's ID.
pub para_id: ParaId,
/// Whether we should reinitialize the collator config (i.e. we are transitioning to aura).
pub reinitialize: bool,
/// Collator service interface
pub collator_service: CS,
/// Receiver channel for communication with the block builder task.
pub collator_receiver: TracingUnboundedReceiver<CollatorMessage<Block>>,
/// The handle from the special slot based block import.
pub block_import_handle: super::SlotBasedBlockImportHandle<Block>,
/// When set, the collator will export every produced `POV` to this folder.
pub export_pov: Option<PathBuf>,
}
/// Asynchronously executes the collation task for a teyrchain.
///
/// This function initializes the collator subsystems necessary for producing and submitting
/// collations to the relay chain. It listens for new best relay chain block notifications and
/// handles collator messages. If our teyrchain is scheduled on a core and we have a candidate,
/// the task will build a collation and send it to the relay chain.
pub async fn run_collation_task<Block, RClient, CS>(
Params {
relay_client,
collator_key,
para_id,
reinitialize,
collator_service,
mut collator_receiver,
mut block_import_handle,
export_pov,
}: Params<Block, RClient, CS>,
) where
Block: BlockT,
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
RClient: RelayChainInterface + Clone + 'static,
{
let Ok(mut overseer_handle) = relay_client.overseer_handle() else {
tracing::error!(target: LOG_TARGET, "Failed to get overseer handle.");
return;
};
cumulus_client_collator::initialize_collator_subsystems(
&mut overseer_handle,
collator_key,
para_id,
reinitialize,
)
.await;
loop {
futures::select! {
collator_message = collator_receiver.next() => {
let Some(message) = collator_message else {
return;
};
handle_collation_message(message, &collator_service, &mut overseer_handle,relay_client.clone(),export_pov.clone()).await;
},
block_import_msg = block_import_handle.next().fuse() => {
// TODO: Implement me.
// Issue: https://github.com/pezkuwichain/pezkuwi-sdk/issues/24
let _ = block_import_msg;
}
}
}
}
/// Handle an incoming collation message from the block builder task.
/// This builds the collation from the [`CollatorMessage`] and submits it to
/// the collation-generation subsystem of the relay chain.
async fn handle_collation_message<Block: BlockT, RClient: RelayChainInterface + Clone + 'static>(
message: CollatorMessage<Block>,
collator_service: &impl CollatorServiceInterface<Block>,
overseer_handle: &mut OverseerHandle,
relay_client: RClient,
export_pov: Option<PathBuf>,
) {
let CollatorMessage {
parent_header,
teyrchain_candidate,
validation_code_hash,
relay_parent,
core_index,
max_pov_size,
} = message;
let hash = teyrchain_candidate.block.header().hash();
let number = *teyrchain_candidate.block.header().number();
let (collation, block_data) =
match collator_service.build_collation(&parent_header, hash, teyrchain_candidate) {
Some(collation) => collation,
None => {
tracing::warn!(target: LOG_TARGET, %hash, ?number, ?core_index, "Unable to build collation.");
return;
},
};
block_data.log_size_info();
if let MaybeCompressedPoV::Compressed(ref pov) = collation.proof_of_validity {
if let Some(pov_path) = export_pov {
if let Ok(Some(relay_parent_header)) =
relay_client.header(BlockId::Hash(relay_parent)).await
{
if let Some(header) = block_data.blocks().first().map(|b| b.header()) {
export_pov_to_path::<Block>(
pov_path.clone(),
pov.clone(),
header.hash(),
*header.number(),
parent_header.clone(),
relay_parent_header.state_root,
relay_parent_header.number,
max_pov_size,
);
}
} else {
tracing::error!(target: LOG_TARGET, "Failed to get relay parent header from hash: {relay_parent:?}");
}
}
tracing::info!(
target: LOG_TARGET,
"Compressed PoV size: {}kb",
pov.block_data.0.len() as f64 / 1024f64,
);
}
tracing::debug!(target: LOG_TARGET, ?core_index, ?hash, %number, "Submitting collation for core.");
overseer_handle
.send_msg(
CollationGenerationMessage::SubmitCollation(SubmitCollationParams {
relay_parent,
collation,
parent_head: parent_header.encode().into(),
validation_code_hash,
core_index,
result_sender: None,
}),
"SubmitCollation",
)
.await;
}
@@ -0,0 +1,270 @@
// 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 <https://www.gnu.org/licenses/>.
//! # Architecture Overview
//!
//! The block building mechanism operates through two coordinated tasks:
//!
//! 1. **Block Builder Task**: Orchestrates the timing and execution of teyrchain block production
//! 2. **Collator Task**: Processes built blocks into collations for relay chain submission
//!
//! # Block Builder Task Details
//!
//! The block builder task manages block production timing and execution through an iterative
//! process:
//!
//! 1. Awaits the next production signal from the internal timer
//! 2. Retrieves the current best relay chain block and identifies a valid parent block (see
//! [find_potential_parents][cumulus_client_consensus_common::find_potential_parents] for parent
//! selection criteria)
//! 3. Validates that:
//! - The teyrchain has an assigned core on the relay chain
//! - No block has been previously built on the target core
//! 4. Executes block building and import operations
//! 5. Transmits the completed block to the collator task
//!
//! # Block Production Timing
//!
//! When a block is produced is determined by the following parameters:
//!
//! - Teyrchain slot duration
//! - Number of assigned teyrchain cores
//! - Teyrchain runtime configuration
//!
//! ## Timing Examples
//!
//! The following table demonstrates various timing configurations and their effects. The "AURA
//! Slot" column shows which author is responsible for the block.
//!
//! | Slot Duration (ms) | Cores | Production Attempts (ms) | AURA Slot |
//! |-------------------|--------|-------------------------|------------|
//! | 2000 | 3 | 0, 2000, 4000, 6000 | 0, 1, 2, 3 |
//! | 6000 | 1 | 0, 6000, 12000, 18000 | 0, 1, 2, 3 |
//! | 6000 | 3 | 0, 2000, 4000, 6000 | 0, 0, 0, 1 |
//! | 12000 | 1 | 0, 6000, 12000, 18000 | 0, 0, 1, 1 |
//! | 12000 | 3 | 0, 2000, 4000, 6000 | 0, 0, 0, 0 |
//!
//! # Collator Task Details
//!
//! The collator task receives built blocks from the block builder task and performs two primary
//! functions:
//!
//! 1. Block compression
//! 2. Submission to the collation-generation subsystem
use self::{block_builder_task::run_block_builder, collation_task::run_collation_task};
pub use block_import::{SlotBasedBlockImport, SlotBasedBlockImportHandle};
use codec::Codec;
use consensus_common::TeyrchainCandidate;
use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
use cumulus_client_consensus_common::{self as consensus_common, TeyrchainBlockImportMarker};
use cumulus_client_consensus_proposer::ProposerInterface;
use cumulus_primitives_aura::AuraUnincludedSegmentApi;
use cumulus_primitives_core::RelayParentOffsetApi;
use cumulus_relay_chain_interface::RelayChainInterface;
use futures::FutureExt;
use pezkuwi_primitives::{
CollatorPair, CoreIndex, Hash as RelayHash, Id as ParaId, ValidationCodeHash,
};
use pezsc_client_api::{backend::AuxStore, BlockBackend, BlockOf, UsageProvider};
use pezsc_consensus::BlockImport;
use pezsc_network_types::PeerId;
use pezsc_utils::mpsc::tracing_unbounded;
use pezsp_api::ProvideRuntimeApi;
use pezsp_application_crypto::AppPublic;
use pezsp_blockchain::HeaderBackend;
use pezsp_consensus_aura::AuraApi;
use pezsp_core::{crypto::Pair, traits::SpawnEssentialNamed};
use pezsp_inherents::CreateInherentDataProviders;
use pezsp_keystore::KeystorePtr;
use pezsp_runtime::traits::{Block as BlockT, Member};
use std::{path::PathBuf, sync::Arc, time::Duration};
mod block_builder_task;
mod block_import;
mod collation_task;
mod relay_chain_data_cache;
mod slot_timer;
#[cfg(test)]
mod tests;
/// Parameters for [`run`].
pub struct Params<Block, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS, Spawner> {
/// Inherent data providers. Only non-consensus inherent data should be provided, i.e.
/// the timestamp, slot, and paras inherents should be omitted, as they are set by this
/// collator.
pub create_inherent_data_providers: CIDP,
/// Used to actually import blocks.
pub block_import: BI,
/// The underlying para client.
pub para_client: Arc<Client>,
/// The para client's backend, used to access the database.
pub para_backend: Arc<Backend>,
/// A handle to the relay-chain client.
pub relay_client: RClient,
/// A validation code hash provider, used to get the current validation code hash.
pub code_hash_provider: CHP,
/// The underlying keystore, which should contain Aura consensus keys.
pub keystore: KeystorePtr,
/// The collator key used to sign collations before submitting to validators.
pub collator_key: CollatorPair,
/// The collator network peer id.
pub collator_peer_id: PeerId,
/// The para's ID.
pub para_id: ParaId,
/// The underlying block proposer this should call into.
pub proposer: Proposer,
/// The generic collator service used to plug into this consensus engine.
pub collator_service: CS,
/// The amount of time to spend authoring each block.
pub authoring_duration: Duration,
/// Whether we should reinitialize the collator config (i.e. we are transitioning to aura).
pub reinitialize: bool,
/// Offset slots by a fixed duration. This can be used to create more preferrable authoring
/// timings.
pub slot_offset: Duration,
/// The handle returned by [`SlotBasedBlockImport`].
pub block_import_handle: SlotBasedBlockImportHandle<Block>,
/// Spawner for spawning futures.
pub spawner: Spawner,
/// Slot duration of the relay chain
pub relay_chain_slot_duration: Duration,
/// When set, the collator will export every produced `POV` to this folder.
pub export_pov: Option<PathBuf>,
/// The maximum percentage of the maximum PoV size that the collator can use.
/// It will be removed once <https://github.com/pezkuwichain/pezkuwi-sdk/issues/23> is fixed.
pub max_pov_percentage: Option<u32>,
}
/// Run aura-based block building and collation task.
pub fn run<Block, P, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS, Spawner>(
params: Params<Block, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS, Spawner>,
) where
Block: BlockT,
Client: ProvideRuntimeApi<Block>
+ BlockOf
+ AuxStore
+ HeaderBackend<Block>
+ BlockBackend<Block>
+ UsageProvider<Block>
+ Send
+ Sync
+ 'static,
Client::Api:
AuraApi<Block, P::Public> + AuraUnincludedSegmentApi<Block> + RelayParentOffsetApi<Block>,
Backend: pezsc_client_api::Backend<Block> + 'static,
RClient: RelayChainInterface + Clone + 'static,
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
CIDP::InherentDataProviders: Send,
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
CS: CollatorServiceInterface<Block> + Send + Sync + Clone + 'static,
CHP: consensus_common::ValidationCodeHashProvider<Block::Hash> + Send + 'static,
P: Pair + Send + Sync + 'static,
P::Public: AppPublic + Member + Codec,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
Spawner: SpawnEssentialNamed + Clone + 'static,
{
let Params {
create_inherent_data_providers,
block_import,
para_client,
para_backend,
relay_client,
code_hash_provider,
keystore,
collator_key,
collator_peer_id,
para_id,
proposer,
collator_service,
authoring_duration,
reinitialize,
slot_offset,
block_import_handle,
spawner,
export_pov,
relay_chain_slot_duration,
max_pov_percentage,
} = params;
let (tx, rx) = tracing_unbounded("mpsc_builder_to_collator", 100);
let collator_task_params = collation_task::Params {
relay_client: relay_client.clone(),
collator_key,
para_id,
reinitialize,
collator_service: collator_service.clone(),
collator_receiver: rx,
block_import_handle,
export_pov,
};
let collation_task_fut = run_collation_task::<Block, _, _>(collator_task_params);
let block_builder_params = block_builder_task::BuilderTaskParams {
create_inherent_data_providers,
block_import,
para_client,
para_backend,
relay_client,
code_hash_provider,
keystore,
collator_peer_id,
para_id,
proposer,
collator_service,
authoring_duration,
collator_sender: tx,
relay_chain_slot_duration,
slot_offset,
max_pov_percentage,
};
let block_builder_fut =
run_block_builder::<Block, P, _, _, _, _, _, _, _, _>(block_builder_params);
spawner.spawn_essential_blocking(
"slot-based-block-builder",
Some("slot-based-collator"),
block_builder_fut.boxed(),
);
spawner.spawn_essential_blocking(
"slot-based-collation",
Some("slot-based-collator"),
collation_task_fut.boxed(),
);
}
/// Message to be sent from the block builder to the collation task.
///
/// Contains all data necessary to submit a collation to the relay chain.
struct CollatorMessage<Block: BlockT> {
/// The hash of the relay chain block that provides the context for the teyrchain block.
pub relay_parent: RelayHash,
/// The header of the parent block.
pub parent_header: Block::Header,
/// The teyrchain block candidate.
pub teyrchain_candidate: TeyrchainCandidate<Block>,
/// The validation code hash at the parent block.
pub validation_code_hash: ValidationCodeHash,
/// Core index that this block should be submitted on
pub core_index: CoreIndex,
/// Maximum pov size. Currently needed only for exporting PoV.
pub max_pov_size: u32,
}
@@ -0,0 +1,122 @@
// 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 <https://www.gnu.org/licenses/>.
//! Utility for caching [`RelayChainData`] for different relay blocks.
use crate::collators::claim_queue_at;
use cumulus_primitives_core::CoreSelector;
use cumulus_relay_chain_interface::RelayChainInterface;
use pezkuwi_node_subsystem_util::runtime::ClaimQueueSnapshot;
use pezkuwi_primitives::{
Hash as RelayHash, Header as RelayHeader, Id as ParaId, OccupiedCoreAssumption,
};
use pezsp_runtime::generic::BlockId;
/// Contains relay chain data necessary for teyrchain block building.
#[derive(Clone, Debug)]
pub struct RelayChainData {
/// Current relay chain parent header.
pub relay_parent_header: RelayHeader,
/// The claim queue at the relay parent.
pub claim_queue: ClaimQueueSnapshot,
/// Maximum configured PoV size on the relay chain.
pub max_pov_size: u32,
/// The last [`CoreSelector`] we used.
pub last_claimed_core_selector: Option<CoreSelector>,
}
/// Simple helper to fetch relay chain data and cache it based on the current relay chain best block
/// hash.
pub struct RelayChainDataCache<RI> {
relay_client: RI,
para_id: ParaId,
cached_data: schnellru::LruMap<RelayHash, RelayChainData>,
}
impl<RI> RelayChainDataCache<RI>
where
RI: RelayChainInterface + 'static,
{
pub fn new(relay_client: RI, para_id: ParaId) -> Self {
Self {
relay_client,
para_id,
// 50 cached relay chain blocks should be more than enough.
cached_data: schnellru::LruMap::new(schnellru::ByLength::new(50)),
}
}
/// Fetch required [`RelayChainData`] from the relay chain.
/// If this data has been fetched in the past for the incoming hash, it will reuse
/// cached data.
pub async fn get_mut_relay_chain_data(
&mut self,
relay_parent: RelayHash,
) -> Result<&mut RelayChainData, ()> {
let insert_data = if self.cached_data.peek(&relay_parent).is_some() {
tracing::trace!(target: crate::LOG_TARGET, %relay_parent, "Using cached data for relay parent.");
None
} else {
tracing::trace!(target: crate::LOG_TARGET, %relay_parent, "Relay chain best block changed, fetching new data from relay chain.");
Some(self.update_for_relay_parent(relay_parent).await?)
};
Ok(self
.cached_data
.get_or_insert(relay_parent, || {
insert_data.expect("`insert_data` exists if not cached yet; qed")
})
.expect("There is space for at least one element; qed"))
}
/// Fetch fresh data from the relay chain for the given relay parent hash.
async fn update_for_relay_parent(&self, relay_parent: RelayHash) -> Result<RelayChainData, ()> {
let claim_queue = claim_queue_at(relay_parent, &self.relay_client).await;
let Ok(Some(relay_parent_header)) =
self.relay_client.header(BlockId::Hash(relay_parent)).await
else {
tracing::warn!(target: crate::LOG_TARGET, "Unable to fetch latest relay chain block header.");
return Err(());
};
let max_pov_size = match self
.relay_client
.persisted_validation_data(relay_parent, self.para_id, OccupiedCoreAssumption::Included)
.await
{
Ok(None) => return Err(()),
Ok(Some(pvd)) => pvd.max_pov_size,
Err(err) => {
tracing::error!(target: crate::LOG_TARGET, ?err, "Failed to gather information from relay-client");
return Err(());
},
};
Ok(RelayChainData {
relay_parent_header,
claim_queue,
max_pov_size,
last_claimed_core_selector: None,
})
}
#[cfg(test)]
pub(crate) fn insert_test_data(&mut self, relay_parent: RelayHash, data: RelayChainData) {
self.cached_data.insert(relay_parent, data);
}
}
@@ -0,0 +1,647 @@
// 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 <https://www.gnu.org/licenses/>.
use crate::LOG_TARGET;
use codec::Codec;
use cumulus_primitives_aura::Slot;
use cumulus_primitives_core::BlockT;
use pezsc_client_api::UsageProvider;
use pezsc_consensus_aura::SlotDuration;
use pezsp_api::ProvideRuntimeApi;
use pezsp_application_crypto::AppPublic;
use pezsp_consensus_aura::AuraApi;
use pezsp_core::Pair;
use pezsp_runtime::traits::Member;
use pezsp_timestamp::Timestamp;
use std::{
cmp::{max, min},
sync::Arc,
time::Duration,
};
/// Lower limits of allowed block production interval.
/// Defensive mechanism, corresponds to 12 cores at 6 second block time.
const BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS: Duration = Duration::from_millis(500);
/// Theoretically, the block production is capped at `BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS`.
/// In practice, there might be slight deviations due to timing inaccuracies and delays.
///
/// This constant is taken into account while adjusting the authoring duration to fit into the slot.
/// Therefore, it will only reduce the authoring duration if we are within the
/// `BLOCK_PRODUCTION_ADJUSTMENT_MS` threshold of the next slot.
///
/// ### 12 cores 500ms blocks
///
/// For example, for 12 cores 500ms blocks: the next slot is scheduled in 490ms due to delays.
/// In that case, we still want to attempt producing the block, as missing the slot would be worse
/// than producing slightly too fast.
const BLOCK_PRODUCTION_THRESHOLD_MS: Duration = Duration::from_millis(100);
/// The amount of time the authoring duration of the last block production attempt
/// should be reduced by to fit into the slot timing.
const BLOCK_PRODUCTION_ADJUSTMENT_MS: Duration = Duration::from_millis(1000);
#[derive(Debug)]
pub(crate) struct SlotInfo {
pub timestamp: Timestamp,
pub slot: Slot,
}
/// Manages block-production timings based on chain parameters and assigned cores.
#[derive(Debug)]
pub(crate) struct SlotTimer<Block, Client, P> {
/// Teyrchain client that is used for runtime calls
client: Arc<Client>,
/// Offset the current time by this duration.
time_offset: Duration,
/// Last reported core count.
last_reported_core_num: Option<u32>,
/// Slot duration of the relay chain. This is used to compute how man block-production
/// attempts we should trigger per relay chain block.
relay_slot_duration: Duration,
/// Stores the latest slot that was reported by [`Self::wait_until_next_slot`].
last_reported_slot: Option<Slot>,
_marker: std::marker::PhantomData<(Block, Box<dyn Fn(P) + Send + Sync + 'static>)>,
}
/// Compute when to try block-authoring next.
/// The exact time point is determined by the slot duration of relay- and teyrchain as
/// well as the last observed core count. If more cores are available, we attempt to author blocks
/// for them.
///
/// Returns a tuple with:
/// - `Duration`: How long to wait until the next slot.
/// - `Slot`: The AURA slot used for authoring
fn compute_next_wake_up_time(
para_slot_duration: SlotDuration,
relay_slot_duration: Duration,
core_count: Option<u32>,
time_now: Duration,
time_offset: Duration,
) -> (Duration, Slot) {
let para_slots_per_relay_block =
(relay_slot_duration.as_millis() / para_slot_duration.as_millis() as u128) as u32;
let assigned_core_num = core_count.unwrap_or(1);
// Trigger at least once per relay block, if we have for example 12 second slot duration,
// we should still produce two blocks if we are scheduled on every relay block.
let mut block_production_interval = min(para_slot_duration.as_duration(), relay_slot_duration);
if assigned_core_num > para_slots_per_relay_block &&
para_slot_duration.as_duration() >= relay_slot_duration
{
block_production_interval =
max(relay_slot_duration / assigned_core_num, BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS);
tracing::debug!(
target: LOG_TARGET,
?block_production_interval,
"Expected to produce for {assigned_core_num} cores but only have {para_slots_per_relay_block} slots. Attempting to produce multiple blocks per slot."
);
}
let (duration, timestamp) =
time_until_next_attempt(time_now, block_production_interval, time_offset);
let aura_slot = Slot::from_timestamp(timestamp, para_slot_duration);
(duration, aura_slot)
}
/// Compute the time until the next slot changes.
///
/// Returns None if the next slot cannot be computed.
fn compute_time_until_next_slot_change(
para_slot_duration: SlotDuration,
time_now: Duration,
time_offset: Duration,
last_reported_slot: Slot,
) -> Option<(Duration, Slot)> {
let now = time_now.saturating_sub(time_offset);
let next_slot = last_reported_slot + Slot::from(1);
let Some(next_slot_timestamp) = next_slot.timestamp(para_slot_duration) else {
return None;
};
let remaining_time = next_slot_timestamp.as_duration().saturating_sub(now);
Some((remaining_time, next_slot))
}
/// Returns current duration since Unix epoch.
fn duration_now() -> Duration {
use std::time::SystemTime;
let now = SystemTime::now();
now.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_else(|e| {
panic!("Current time {:?} is before Unix epoch. Something is wrong: {:?}", now, e)
})
}
/// Adjust the authoring duration.
fn adjust_authoring_duration(
mut authoring_duration: Duration,
next_block: (Duration, Slot),
next_slot_change: (Duration, Slot),
different_authors: bool,
) -> Option<Duration> {
let (duration, next_block_slot) = next_block;
let (duration_until_next_slot, next_slot) = next_slot_change;
// The authoring of blocks must stop 1 second before the slot ends.
let duration_until_deadline =
duration_until_next_slot.saturating_sub(BLOCK_PRODUCTION_ADJUSTMENT_MS);
tracing::debug!(
target: LOG_TARGET,
?authoring_duration,
?duration,
?next_block_slot,
?duration_until_next_slot,
?next_slot,
?duration_until_deadline,
?different_authors,
"Adjusting authoring duration for slot.",
);
// Ensure no blocks are produced in the last second of the slot,
// regardless of authoring duration.
if duration_until_deadline == Duration::ZERO {
if different_authors {
tracing::warn!(
target: LOG_TARGET,
?duration_until_next_slot,
?next_slot,
"Not enough time left in the slot to adjust authoring duration. Skipping block production for the slot."
);
return None;
}
// If authors are the same, we can still attempt producing the block
// considering the next block duration.
return Some(authoring_duration.min(duration));
}
// Clamp the authoring duration to fit into the slot deadline only if authors are different.
// For most cases, the deadline is farther in the future than the authoring duration.
if different_authors && authoring_duration >= duration_until_deadline {
authoring_duration = duration_until_deadline;
// Ensure we are not going below the minimum interval within a reasonable threshold.
// For 12 cores, we might have a scenario where the last 3 blocks are skipped:
// - Block 10: next slot change in 1.493s:
// - After adjusting the deadline: 1.493s - 1s = 0.493s the block could be produced
// without issues.
// - Block 11: next slot change in 0.993s - skipped by the deadline
// - Block 12: next slot change in 0.493s - skipped by the deadline
if authoring_duration <
BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS.saturating_sub(BLOCK_PRODUCTION_THRESHOLD_MS)
{
tracing::debug!(
target: LOG_TARGET,
?authoring_duration,
?next_slot,
"Authoring duration is below minimum. Skipping block production for the slot."
);
return None;
}
}
// The `duration` intends to slightly adjust when then block production
// attempt happens. This goes slightly below the `BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS`
// threshold.
Some(authoring_duration.min(duration))
}
/// Returns the duration until the next block production should be attempted.
/// Returns:
/// - Duration: The duration until the next attempt.
fn time_until_next_attempt(
now: Duration,
block_production_interval: Duration,
offset: Duration,
) -> (Duration, Timestamp) {
let now = now.as_millis().saturating_sub(offset.as_millis());
let next_slot_time = ((now + block_production_interval.as_millis()) /
block_production_interval.as_millis()) *
block_production_interval.as_millis();
let remaining_millis = next_slot_time - now;
(Duration::from_millis(remaining_millis as u64), Timestamp::from(next_slot_time as u64))
}
impl<Block, Client, P> SlotTimer<Block, Client, P>
where
Block: BlockT,
Client: ProvideRuntimeApi<Block> + UsageProvider<Block> + Send + Sync + 'static,
Client::Api: AuraApi<Block, P::Public>,
P: Pair,
P::Public: AppPublic + Member + Codec,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
{
/// Create a new slot timer.
pub fn new_with_offset(
client: Arc<Client>,
time_offset: Duration,
relay_slot_duration: Duration,
) -> Self {
Self {
client,
time_offset,
last_reported_core_num: None,
relay_slot_duration,
last_reported_slot: Default::default(),
_marker: Default::default(),
}
}
/// Inform the slot timer about the last seen number of cores.
pub fn update_scheduling(&mut self, num_cores_next_block: u32) {
self.last_reported_core_num = Some(num_cores_next_block);
}
/// Returns the slot and how much time left until the next block production attempt.
pub fn time_until_next_block(&mut self, slot_duration: SlotDuration) -> (Duration, Slot) {
compute_next_wake_up_time(
slot_duration,
self.relay_slot_duration,
self.last_reported_core_num,
duration_now(),
self.time_offset,
)
}
/// Compute the time until the next slot changes.
fn time_until_next_slot_change(
&mut self,
slot_duration: SlotDuration,
) -> Option<(Duration, Slot)> {
compute_time_until_next_slot_change(
slot_duration,
duration_now(),
self.time_offset,
self.last_reported_slot.unwrap_or_default(),
)
}
/// Check if two slots have different authors based on AURA round-robin algorithm.
///
/// Returns true if the authors for the two slots are different.
fn check_different_slot_authors(&self, slot: Slot, next_slot: Slot) -> bool {
let best_hash = self.client.usage_info().chain.best_hash;
let Ok(authorities) = self.client.runtime_api().authorities(best_hash) else {
tracing::warn!(target: LOG_TARGET, "Failed to fetch authorities for slot author comparison");
// Presume they are different, this will adjust the slot authoring duration more
// conservatively.
return true;
};
let authorities_len = authorities.len() as u64;
if authorities_len <= 1 {
return false;
}
let author1_idx = *slot % authorities_len;
let author2_idx = *next_slot % authorities_len;
author1_idx != author2_idx
}
/// Adjust the authoring duration to fit into the slot timing.
///
/// Returns the adjusted authoring duration and the slot that it corresponds to.
pub fn adjust_authoring_duration(&mut self, authoring_duration: Duration) -> Option<Duration> {
let Ok(slot_duration) = crate::slot_duration(&*self.client) else {
tracing::error!(target: LOG_TARGET, "Failed to fetch slot duration from runtime.");
return None;
};
let next_block = self.time_until_next_block(slot_duration);
let Some(next_slot_change) = self.time_until_next_slot_change(slot_duration) else {
tracing::error!(
target: LOG_TARGET,
"Failed to compute time until next slot change. Using unadjusted authoring duration."
);
return Some(authoring_duration);
};
// Check if authors at current and next slots are different
let current_slot = self.last_reported_slot.unwrap_or(next_block.1);
let different_authors = self.check_different_slot_authors(current_slot, next_slot_change.1);
adjust_authoring_duration(
authoring_duration,
next_block,
next_slot_change,
different_authors,
)
}
/// Returns a future that resolves when the next block production should be attempted.
pub async fn wait_until_next_slot(&mut self) -> Result<(), ()> {
let slot_duration = match crate::slot_duration(&*self.client) {
Ok(d) => d,
Err(error) => {
tracing::error!(target: LOG_TARGET, %error, "Failed to fetch slot duration from runtime.");
return Err(());
},
};
let (time_until_next_attempt, mut next_aura_slot) =
self.time_until_next_block(slot_duration);
tracing::trace!(
target: LOG_TARGET,
?time_until_next_attempt,
aura_slot = ?next_aura_slot,
last_reported = ?self.last_reported_slot,
"Determined next block production opportunity."
);
match self.last_reported_slot {
// If we already reported a slot, we don't want to skip a slot. But we also don't want
// to go through all the slots if a node was halted for some reason.
Some(ls) if ls + 1 < next_aura_slot && next_aura_slot <= ls + 3 => {
next_aura_slot = ls + 1u64;
},
None | Some(_) => {
tracing::trace!(target: LOG_TARGET, ?time_until_next_attempt, "Sleeping until the next slot.");
tokio::time::sleep(time_until_next_attempt).await;
},
}
tracing::debug!(
target: LOG_TARGET,
?slot_duration,
aura_slot = ?next_aura_slot,
"New block production opportunity."
);
self.last_reported_slot = Some(next_aura_slot);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use pezsc_consensus_aura::SlotDuration;
const RELAY_CHAIN_SLOT_DURATION: u64 = 6000;
#[rstest]
// Test that different now timestamps have correct impact
// ||||
#[case(6000, Some(1), 1000, 0, 5000)]
#[case(6000, Some(1), 0, 0, 6000)]
#[case(6000, Some(1), 6000, 0, 6000)]
#[case(6000, Some(0), 6000, 0, 6000)]
// Test that `None` core defaults to 1
// ||||
#[case(6000, None, 1000, 0, 5000)]
#[case(6000, None, 0, 0, 6000)]
#[case(6000, None, 6000, 0, 6000)]
// Test that offset affects the current time correctly
// ||||
#[case(6000, Some(1), 1000, 1000, 6000)]
#[case(6000, Some(1), 12000, 2000, 2000)]
#[case(6000, Some(1), 12000, 6000, 6000)]
#[case(6000, Some(1), 12000, 7000, 1000)]
// Test that number of cores affects the block production interval
// |||||||
#[case(6000, Some(3), 12000, 0, 2000)]
#[case(6000, Some(2), 12000, 0, 3000)]
#[case(6000, Some(3), 11999, 0, 1)]
// High core count
// ||||||||
#[case(6000, Some(12), 0, 0, 500)]
/// Test that the minimum block interval is respected
/// at high core counts.
/// |||||||||
#[case(6000, Some(100), 0, 0, 500)]
// Test that slot_duration works correctly
// ||||
#[case(2000, Some(1), 1000, 0, 1000)]
#[case(2000, Some(1), 3000, 0, 1000)]
#[case(2000, Some(1), 10000, 0, 2000)]
#[case(2000, Some(2), 1000, 0, 1000)]
// Cores are ignored if relay_slot_duration != para_slot_duration
// |||||||
#[case(2000, Some(3), 3000, 0, 1000)]
// For long slot durations, we should still check
// every relay chain block for the slot.
// |||||
#[case(12000, None, 0, 0, 6000)]
#[case(12000, None, 6100, 0, 5900)]
#[case(12000, None, 6000, 2000, 2000)]
#[case(12000, Some(2), 6000, 0, 3000)]
#[case(12000, Some(3), 6000, 0, 2000)]
#[case(12000, Some(3), 8100, 0, 1900)]
fn test_get_next_slot(
#[case] para_slot_millis: u64,
#[case] core_count: Option<u32>,
#[case] time_now: u64,
#[case] offset_millis: u64,
#[case] expected_wait_duration: u128,
) {
let para_slot_duration = SlotDuration::from_millis(para_slot_millis); // 6 second slots
let relay_slot_duration = Duration::from_millis(RELAY_CHAIN_SLOT_DURATION);
let time_now = Duration::from_millis(time_now); // 1 second passed
let offset = Duration::from_millis(offset_millis);
let (wait_duration, _) = compute_next_wake_up_time(
para_slot_duration,
relay_slot_duration,
core_count,
time_now,
offset,
);
assert_eq!(wait_duration.as_millis(), expected_wait_duration, "Wait time mismatch.");
// Should wait 5 seconds
}
#[rstest]
// Basic slot change scenarios
#[case(6000, 0, 0, Slot::from(0), 6000, Slot::from(1))]
#[case(6000, 1000, 0, Slot::from(0), 5000, Slot::from(1))]
#[case(6000, 6000, 0, Slot::from(1), 6000, Slot::from(2))]
#[case(6000, 12000, 0, Slot::from(2), 6000, Slot::from(3))]
// Test with offset
#[case(6000, 1000, 1000, Slot::from(0), 6000, Slot::from(1))]
#[case(6000, 2000, 1000, Slot::from(0), 5000, Slot::from(1))]
#[case(6000, 6000, 3000, Slot::from(0), 3000, Slot::from(1))]
// Different slot durations
#[case(3000, 1000, 0, Slot::from(0), 2000, Slot::from(1))]
#[case(3000, 3000, 0, Slot::from(1), 3000, Slot::from(2))]
#[case(12000, 6000, 0, Slot::from(0), 6000, Slot::from(1))]
#[case(12000, 12000, 0, Slot::from(1), 12000, Slot::from(2))]
// Edge cases - at slot boundary
#[case(6000, 5999, 0, Slot::from(0), 1, Slot::from(1))]
#[case(6000, 11999, 0, Slot::from(1), 1, Slot::from(2))]
fn test_compute_time_until_next_slot_change(
#[case] para_slot_millis: u64,
#[case] time_now: u64,
#[case] offset_millis: u64,
#[case] last_reported_slot: Slot,
#[case] expected_duration: u128,
#[case] expected_next_slot: Slot,
) {
let para_slot_duration = SlotDuration::from_millis(para_slot_millis);
let time_now = Duration::from_millis(time_now);
let offset = Duration::from_millis(offset_millis);
let result = compute_time_until_next_slot_change(
para_slot_duration,
time_now,
offset,
last_reported_slot,
);
assert!(result.is_some(), "Expected result to be Some");
let (duration, next_slot) = result.unwrap();
assert_eq!(duration.as_millis(), expected_duration, "Duration mismatch");
assert_eq!(next_slot, expected_next_slot, "Next slot mismatch");
}
#[rstest]
// Various scenarios for 2s block production adjustment.
#[case::blocks_2s_fits_next_block(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(2000), Slot::from(1)), // Next block
(Duration::from_millis(4000), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(2000)), // Expected
)]
#[case::blocks_2s_closer_next_slot(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(1950), Slot::from(1)), // Next block
(Duration::from_millis(4000), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(1950)), // Expected
)]
#[case::blocks_2s_closer_next_slot_bigger(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(1500), Slot::from(1)), // Next block
(Duration::from_millis(4000), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(1500)), // Expected
)]
#[case::blocks_2s_reduce_by_1s(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(2000), Slot::from(1)), // Next block
(Duration::from_millis(2000), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(1000)), // Expected
)]
#[case::blocks_2s_reduce_by_1s_plus_offset(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(1950), Slot::from(1)), // Next block
(Duration::from_millis(1950), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(950)), // Expected
)]
#[case::blocks_2s_reduce_to_minimum(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(1400), Slot::from(1)), // Next block
(Duration::from_millis(1400), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(400)), // Expected
)]
#[case::blocks_2s_reduce_below_minimum(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(1300), Slot::from(1)), // Next block
(Duration::from_millis(1300), Slot::from(2)), // Next slot change
true, // Different authors
None, // Expected to reduce below minimum
)]
#[case::blocks_2s_same_author(
Duration::from_millis(2000), // Authoring duration
(Duration::from_millis(1400), Slot::from(1)), // Next block
(Duration::from_millis(1400), Slot::from(2)), // Next slot change
false, // Different authors
Some(Duration::from_millis(1400)), // Expected no adjustment for last second.
)]
// Various scenarios for 500ms block production adjustment.
#[case::blocks_500ms_fits_next_block(
Duration::from_millis(500), // Authoring duration
(Duration::from_millis(500), Slot::from(1)), // Next block
(Duration::from_millis(2000), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(500)), // Expected
)]
#[case::blocks_500ms_closer_next_slot(
Duration::from_millis(500), // Authoring duration
(Duration::from_millis(450), Slot::from(1)), // Next block
(Duration::from_millis(2000), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(450)), // Expected
)]
#[case::blocks_500ms_closer_next_slot_bigger(
Duration::from_millis(500), // Authoring duration
(Duration::from_millis(400), Slot::from(1)), // Next block
(Duration::from_millis(1500), Slot::from(2)), // Next slot change
true, // Different authors
Some(Duration::from_millis(400)), // Expected
)]
#[case::blocks_500ms_reduce_by_1s(
Duration::from_millis(500), // Authoring duration
(Duration::from_millis(500), Slot::from(1)), // Next block
(Duration::from_millis(1000), Slot::from(2)), // Next slot change
true, // Different authors
None, // Expected
)]
#[case::blocks_500ms_reduce_by_1s_closer(
Duration::from_millis(500), // Authoring duration
(Duration::from_millis(500), Slot::from(1)), // Next block
(Duration::from_millis(500), Slot::from(2)), // Next slot change
true, // Different authors
None, // Expected
)]
// If we are producing with 1 collator for 500ms authoring duration,
// we must produce the last two slots and ignore the 1s adjustment.
#[case::blocks_500ms_same_author(
Duration::from_millis(500), // Authoring duration
(Duration::from_millis(410), Slot::from(1)), // Next block
(Duration::from_millis(1000), Slot::from(2)), // Next slot change
false, // Different authors
Some(Duration::from_millis(410)), // Expected no adjustment for last second.
)]
#[case::blocks_500ms_same_author_closer(
Duration::from_millis(500), // Authoring duration
(Duration::from_millis(400), Slot::from(1)), // Next block
(Duration::from_millis(400), Slot::from(2)), // Next slot change
false, // Different authors
Some(Duration::from_millis(400)), // Expected no adjustment for last second.
)]
fn test_adjust_authoring_duration(
#[case] authoring_duration: Duration,
#[case] next_block: (Duration, Slot),
#[case] next_slot_change: (Duration, Slot),
#[case] different_authors: bool,
#[case] expected: Option<Duration>,
) {
pezsp_tracing::init_for_tests();
let result = adjust_authoring_duration(
authoring_duration,
next_block,
next_slot_change,
different_authors,
);
tracing::debug!("Adjusted authoring duration: {:?}", result);
assert_eq!(result, expected);
}
}
@@ -0,0 +1,730 @@
// 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 <https://www.gnu.org/licenses/>.
use super::{
block_builder_task::{determine_core, offset_relay_parent_find_descendants},
relay_chain_data_cache::{RelayChainData, RelayChainDataCache},
};
use async_trait::async_trait;
use codec::Encode;
use cumulus_primitives_core::{ClaimQueueOffset, CoreInfo, CoreSelector, CumulusDigestItem};
use cumulus_relay_chain_interface::*;
use futures::Stream;
use pezkuwi_node_subsystem_util::runtime::ClaimQueueSnapshot;
use pezkuwi_primitives::{
CandidateEvent, CommittedCandidateReceiptV2, CoreIndex, Hash as RelayHash,
Header as RelayHeader, Id as ParaId,
};
use rstest::rstest;
use pezsc_consensus_babe::{
AuthorityId, ConsensusLog as BabeConsensusLog, NextEpochDescriptor, BABE_ENGINE_ID,
};
use pezsp_core::sr25519;
use pezsp_runtime::{generic::BlockId, testing::Header as TestHeader, traits::Header};
use pezsp_version::RuntimeVersion;
use std::{
collections::{BTreeMap, HashMap, VecDeque},
pin::Pin,
};
#[tokio::test]
async fn offset_test_zero_offset() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
let result = offset_relay_parent_find_descendants(&mut cache, best_hash, 0).await;
assert!(result.is_ok());
let data = result.unwrap().unwrap();
assert_eq!(data.descendants_len(), 0);
assert_eq!(data.relay_parent().hash(), best_hash);
assert!(data.into_inherent_descendant_list().is_empty());
}
#[tokio::test]
async fn offset_test_two_offset() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
let result = offset_relay_parent_find_descendants(&mut cache, best_hash, 2).await;
assert!(result.is_ok());
let data = result.unwrap().unwrap();
assert_eq!(data.descendants_len(), 2);
assert_eq!(*data.relay_parent().number(), 98);
let descendant_list = data.into_inherent_descendant_list();
assert_eq!(descendant_list.len(), 3);
assert_eq!(*descendant_list.first().unwrap().number(), 98);
assert_eq!(*descendant_list.last().unwrap().number(), 100);
}
#[tokio::test]
async fn offset_test_five_offset() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
let result = offset_relay_parent_find_descendants(&mut cache, best_hash, 5).await;
assert!(result.is_ok());
let data = result.unwrap().unwrap();
assert_eq!(data.descendants_len(), 5);
assert_eq!(*data.relay_parent().number(), 95);
let descendant_list = data.into_inherent_descendant_list();
assert_eq!(descendant_list.len(), 6);
assert_eq!(*descendant_list.first().unwrap().number(), 95);
assert_eq!(*descendant_list.last().unwrap().number(), 100);
}
#[tokio::test]
async fn offset_test_too_long() {
let (headers, _best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
let result = offset_relay_parent_find_descendants(&mut cache, _best_hash, 200).await;
assert!(result.is_err());
let result = offset_relay_parent_find_descendants(&mut cache, _best_hash, 101).await;
assert!(result.is_err());
}
#[derive(PartialEq)]
enum HasEpochChange {
Yes,
No,
}
#[rstest]
#[case::in_best(
&[HasEpochChange::No, HasEpochChange::No, HasEpochChange::Yes],
)]
#[case::in_first_ancestor(
&[HasEpochChange::No, HasEpochChange::Yes, HasEpochChange::No],
)]
#[case::in_second_ancestor(
&[HasEpochChange::Yes, HasEpochChange::No, HasEpochChange::No],
)]
#[tokio::test]
async fn offset_returns_none_when_epoch_change_encountered(#[case] flags: &[HasEpochChange]) {
let (headers, best_hash) = build_headers_with_epoch_flags(flags);
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
let result = offset_relay_parent_find_descendants(&mut cache, best_hash, 3).await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn determine_core_new_relay_parent() {
let (headers, _best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
// Create a test relay parent header
let relay_parent = RelayHeader {
parent_hash: Default::default(),
number: 100,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
// Create a test para parent header at block 0 (genesis)
let para_parent = TestHeader::new_from_number(0);
// Setup claim queue data for the cache
cache.set_test_data(relay_parent.clone(), vec![CoreIndex(0), CoreIndex(1)]);
let result = determine_core(&mut cache, &relay_parent, 1.into(), &para_parent, 0).await;
let core = result.unwrap();
let core = core.unwrap();
assert_eq!(core.core_selector(), CoreSelector(0));
assert_eq!(core.core_index(), CoreIndex(0));
assert_eq!(core.total_cores(), 2);
}
#[tokio::test]
async fn determine_core_with_core_info() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
// Create a test relay parent header
let relay_parent = RelayHeader {
parent_hash: best_hash,
number: 101,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
// Create a para parent header with core info in digest
let core_info = CoreInfo {
selector: CoreSelector(0),
claim_queue_offset: ClaimQueueOffset(0),
number_of_cores: 3.into(),
};
let mut digest = pezsp_runtime::generic::Digest::default();
digest.push(CumulusDigestItem::CoreInfo(core_info).to_digest_item());
// Add relay parent storage root to make it a non-new relay parent
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
*relay_parent.state_root(),
*relay_parent.number(),
));
let para_parent = TestHeader {
parent_hash: best_hash.into(),
number: 1,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest,
};
// Setup claim queue data for the cache
cache.set_test_data(relay_parent.clone(), vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)]);
let result = determine_core(&mut cache, &relay_parent, 1.into(), &para_parent, 0).await;
match result {
Ok(Some(core)) => {
assert_eq!(core.core_selector(), CoreSelector(1)); // Should be next selector (0 + 1)
assert_eq!(core.core_index(), CoreIndex(1));
assert_eq!(core.total_cores(), 3);
},
Ok(None) => panic!("Expected Some core, got None"),
Err(()) => panic!("determine_core returned error"),
}
}
#[tokio::test]
async fn determine_core_no_cores_available() {
let (headers, _best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
// Create a test relay parent header
let relay_parent = RelayHeader {
parent_hash: Default::default(),
number: 100,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
// Create a test para parent header at block 0 (genesis)
let para_parent = TestHeader::new_from_number(0);
// Setup empty claim queue
cache.set_test_data(relay_parent.clone(), vec![]);
let result = determine_core(&mut cache, &relay_parent, 1.into(), &para_parent, 0).await;
let core = result.unwrap();
assert!(core.is_none());
}
#[tokio::test]
async fn determine_core_selector_overflow() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
// Create a test relay parent header
let relay_parent = RelayHeader {
parent_hash: best_hash,
number: 101,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
let core_info = CoreInfo {
selector: CoreSelector(1),
claim_queue_offset: ClaimQueueOffset(0),
number_of_cores: 2.into(),
};
let mut digest = pezsp_runtime::generic::Digest::default();
digest.push(CumulusDigestItem::CoreInfo(core_info).to_digest_item());
// Add relay parent storage root to make it a non-new relay parent
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
*relay_parent.state_root(),
*relay_parent.number(),
));
let para_parent = TestHeader {
parent_hash: best_hash.into(),
number: 1,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest,
};
// Setup claim queue with only 2 cores
cache.set_test_data(relay_parent.clone(), vec![CoreIndex(0), CoreIndex(1)]);
let result = determine_core(&mut cache, &relay_parent, 1.into(), &para_parent, 0).await;
let core = result.unwrap();
assert!(core.is_none()); // Should return None when selector overflows
}
#[tokio::test]
async fn determine_core_uses_last_claimed_core_selector() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
// Create a test relay parent header
let relay_parent = RelayHeader {
parent_hash: best_hash,
number: 101,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
// Create a para parent header without core info in digest (non-genesis)
// Need to add relay parent storage root to digest to make it a non-new relay parent
let mut digest = pezsp_runtime::generic::Digest::default();
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
*relay_parent.state_root(),
*relay_parent.number(),
));
let para_parent = TestHeader {
parent_hash: best_hash.into(),
number: 1,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest,
};
// Setup claim queue data with last_claimed_core_selector set to 1
cache.set_test_data_with_last_selector(
relay_parent.clone(),
vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)],
Some(CoreSelector(1)),
);
let result = determine_core(&mut cache, &relay_parent, 1.into(), &para_parent, 0).await;
match result {
Ok(Some(core)) => {
// Should use last_claimed_core_selector (1) + 1 = 2
assert_eq!(core.core_selector(), CoreSelector(2));
assert_eq!(core.core_index(), CoreIndex(2));
assert_eq!(core.total_cores(), 3);
},
Ok(None) => panic!("Expected Some core, got None"),
Err(()) => panic!("determine_core returned error"),
}
}
#[tokio::test]
async fn determine_core_uses_last_claimed_core_selector_wraps_around() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
// Create a test relay parent header
let relay_parent = RelayHeader {
parent_hash: best_hash,
number: 101,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
// Create a para parent header without core info in digest (non-genesis)
// Need to add relay parent storage root to digest to make it a non-new relay parent
let mut digest = pezsp_runtime::generic::Digest::default();
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
*relay_parent.state_root(),
*relay_parent.number(),
));
let para_parent = TestHeader {
parent_hash: best_hash.into(),
number: 1,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest,
};
// Setup claim queue data with last_claimed_core_selector set to 2 (last index)
// Next selector should wrap around to out of bounds and return None
cache.set_test_data_with_last_selector(
relay_parent.clone(),
vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)],
Some(CoreSelector(2)),
);
let result = determine_core(&mut cache, &relay_parent, 1.into(), &para_parent, 0).await;
match result {
Ok(Some(_)) => panic!("Expected None due to selector overflow"),
Ok(None) => {
// This is expected - selector 2 + 1 = 3, but only cores 0,1,2 available
},
Err(()) => panic!("determine_core returned error"),
}
}
#[tokio::test]
async fn determine_core_no_last_claimed_core_selector() {
let (headers, best_hash) = create_header_chain();
let client = TestRelayClient::new(headers);
let mut cache = RelayChainDataCache::new(client, 1.into());
// Create a test relay parent header
let relay_parent = RelayHeader {
parent_hash: best_hash,
number: 101,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
// Create a para parent header without core info in digest (non-genesis)
// Need to add relay parent storage root to digest to make it a non-new relay parent
let mut digest = pezsp_runtime::generic::Digest::default();
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
*relay_parent.state_root(),
*relay_parent.number(),
));
let para_parent = TestHeader {
parent_hash: best_hash.into(),
number: 1,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest,
};
// Setup claim queue data with no last_claimed_core_selector (None)
cache.set_test_data_with_last_selector(
relay_parent.clone(),
vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)],
None,
);
let result = determine_core(&mut cache, &relay_parent, 1.into(), &para_parent, 0).await;
match result {
Ok(Some(core)) => {
// Should start from selector 0 + 1 = 1 when no last selector
assert_eq!(core.core_selector(), CoreSelector(1));
assert_eq!(core.core_index(), CoreIndex(1));
assert_eq!(core.total_cores(), 3);
},
Ok(None) => panic!("Expected Some core, got None"),
Err(()) => panic!("determine_core returned error"),
}
}
#[derive(Clone)]
struct TestRelayClient {
headers: HashMap<RelayHash, RelayHeader>,
}
impl TestRelayClient {
fn new(headers: HashMap<RelayHash, RelayHeader>) -> Self {
Self { headers }
}
}
#[async_trait]
impl RelayChainInterface for TestRelayClient {
async fn validators(&self, _: RelayHash) -> RelayChainResult<Vec<ValidatorId>> {
unimplemented!("Not needed for test")
}
async fn best_block_hash(&self) -> RelayChainResult<RelayHash> {
unimplemented!("Not needed for test")
}
async fn finalized_block_hash(&self) -> RelayChainResult<RelayHash> {
unimplemented!("Not needed for test")
}
async fn retrieve_dmq_contents(
&self,
_: ParaId,
_: RelayHash,
) -> RelayChainResult<Vec<InboundDownwardMessage>> {
unimplemented!("Not needed for test")
}
async fn retrieve_all_inbound_hrmp_channel_contents(
&self,
_: ParaId,
_: RelayHash,
) -> RelayChainResult<BTreeMap<ParaId, Vec<InboundHrmpMessage>>> {
unimplemented!("Not needed for test")
}
async fn persisted_validation_data(
&self,
_: RelayHash,
_: ParaId,
_: OccupiedCoreAssumption,
) -> RelayChainResult<Option<PersistedValidationData>> {
use cumulus_primitives_core::PersistedValidationData;
Ok(Some(PersistedValidationData {
parent_head: Default::default(),
relay_parent_number: 100,
relay_parent_storage_root: Default::default(),
max_pov_size: 1024 * 1024,
}))
}
async fn validation_code_hash(
&self,
_: RelayHash,
_: ParaId,
_: OccupiedCoreAssumption,
) -> RelayChainResult<Option<ValidationCodeHash>> {
unimplemented!("Not needed for test")
}
async fn candidate_pending_availability(
&self,
_: RelayHash,
_: ParaId,
) -> RelayChainResult<Option<CommittedCandidateReceiptV2>> {
unimplemented!("Not needed for test")
}
async fn candidates_pending_availability(
&self,
_: RelayHash,
_: ParaId,
) -> RelayChainResult<Vec<CommittedCandidateReceiptV2>> {
unimplemented!("Not needed for test")
}
async fn session_index_for_child(&self, _: RelayHash) -> RelayChainResult<SessionIndex> {
unimplemented!("Not needed for test")
}
async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
unimplemented!("Not needed for test")
}
async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
unimplemented!("Not needed for test")
}
async fn is_major_syncing(&self) -> RelayChainResult<bool> {
unimplemented!("Not needed for test")
}
fn overseer_handle(&self) -> RelayChainResult<OverseerHandle> {
unimplemented!("Not needed for test")
}
async fn get_storage_by_key(
&self,
_: RelayHash,
_: &[u8],
) -> RelayChainResult<Option<StorageValue>> {
unimplemented!("Not needed for test")
}
async fn prove_read(
&self,
_: RelayHash,
_: &Vec<Vec<u8>>,
) -> RelayChainResult<pezsc_client_api::StorageProof> {
unimplemented!("Not needed for test")
}
async fn wait_for_block(&self, _: RelayHash) -> RelayChainResult<()> {
unimplemented!("Not needed for test")
}
async fn new_best_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
unimplemented!("Not needed for test")
}
async fn header(
&self,
block_id: BlockId<pezkuwi_primitives::Block>,
) -> RelayChainResult<Option<PHeader>> {
let hash = match block_id {
BlockId::Hash(hash) => hash,
BlockId::Number(_) => unimplemented!("Not needed for test"),
};
let header = self.headers.get(&hash);
Ok(header.cloned())
}
async fn availability_cores(
&self,
_relay_parent: RelayHash,
) -> RelayChainResult<Vec<CoreState<RelayHash, BlockNumber>>> {
unimplemented!("Not needed for test");
}
async fn version(&self, _: RelayHash) -> RelayChainResult<RuntimeVersion> {
unimplemented!("Not needed for test");
}
async fn claim_queue(
&self,
_: RelayHash,
) -> RelayChainResult<BTreeMap<CoreIndex, VecDeque<ParaId>>> {
// Return empty claim queue for offset tests
Ok(BTreeMap::new())
}
async fn call_runtime_api(
&self,
_method_name: &'static str,
_hash: RelayHash,
_payload: &[u8],
) -> RelayChainResult<Vec<u8>> {
unimplemented!("Not needed for test")
}
async fn scheduling_lookahead(&self, _: RelayHash) -> RelayChainResult<u32> {
unimplemented!("Not needed for test")
}
async fn candidate_events(&self, _: RelayHash) -> RelayChainResult<Vec<CandidateEvent>> {
unimplemented!("Not needed for test")
}
}
/// Build a consecutive set of relay headers whose digest entries optionally carry a BABE
/// epoch-change marker, returning the underlying map and the hash of the last header.
fn build_headers_with_epoch_flags(
flags: &[HasEpochChange],
) -> (HashMap<RelayHash, RelayHeader>, RelayHash) {
let mut headers = HashMap::new();
let mut parent_hash = RelayHash::default();
let mut last_hash = RelayHash::default();
for (index, has_epoch_change) in flags.iter().enumerate() {
let digest = if *has_epoch_change == HasEpochChange::Yes {
babe_epoch_change_digest()
} else {
Default::default()
};
let header = RelayHeader {
parent_hash,
number: (index as u32 + 1),
state_root: Default::default(),
extrinsics_root: Default::default(),
digest,
};
let hash = header.hash();
headers.insert(hash, header);
parent_hash = hash;
last_hash = hash;
}
(headers, last_hash)
}
/// Create a digest containing a single BABE `NextEpochData` item for use in tests.
fn babe_epoch_change_digest() -> pezsp_runtime::generic::Digest {
let mut digest = pezsp_runtime::generic::Digest::default();
let authority_id = AuthorityId::from(sr25519::Public::from_raw([1u8; 32]));
let next_epoch =
NextEpochDescriptor { authorities: vec![(authority_id, 1u64)], randomness: [0u8; 32] };
let log = BabeConsensusLog::NextEpochData(next_epoch);
digest.push(pezsp_runtime::generic::DigestItem::Consensus(BABE_ENGINE_ID, log.encode()));
digest
}
fn create_header_chain() -> (HashMap<RelayHash, RelayHeader>, RelayHash) {
let mut headers = HashMap::new();
let mut current_parent = None;
let mut header_hash = RelayHash::repeat_byte(0x1);
// Create chain from highest to lowest number
for number in 1..=100 {
let mut header = RelayHeader {
parent_hash: Default::default(),
number,
state_root: Default::default(),
extrinsics_root: Default::default(),
digest: Default::default(),
};
if let Some(hash) = current_parent {
header.parent_hash = hash;
}
header_hash = header.hash();
// Store header and update parent for next iteration
headers.insert(header_hash, header.clone());
current_parent = Some(header_hash);
}
(headers, header_hash)
}
// Test extension for RelayChainDataCache
impl RelayChainDataCache<TestRelayClient> {
fn set_test_data(&mut self, relay_parent_header: RelayHeader, cores: Vec<CoreIndex>) {
self.set_test_data_with_last_selector(relay_parent_header, cores, None);
}
fn set_test_data_with_last_selector(
&mut self,
relay_parent_header: RelayHeader,
cores: Vec<CoreIndex>,
last_claimed_core_selector: Option<CoreSelector>,
) {
let relay_parent_hash = relay_parent_header.hash();
let mut claim_queue = BTreeMap::new();
for core_index in cores {
claim_queue.insert(core_index, [ParaId::from(1)].into());
}
let claim_queue_snapshot = ClaimQueueSnapshot::from(claim_queue);
let data = RelayChainData {
relay_parent_header,
claim_queue: claim_queue_snapshot,
max_pov_size: 1024 * 1024,
last_claimed_core_selector,
};
self.insert_test_data(relay_parent_hash, data);
}
}
@@ -0,0 +1,392 @@
// 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 <https://www.gnu.org/licenses/>.
/// An import queue which provides some equivocation resistance with lenient trait bounds.
///
/// Equivocation resistance in general is a hard problem, as different nodes in the network
/// may see equivocations in a different order, and therefore may not agree on which blocks
/// should be thrown out and which ones should be kept.
use codec::Codec;
use cumulus_client_consensus_common::TeyrchainBlockImportMarker;
use cumulus_primitives_core::{CumulusDigestItem, RelayBlockIdentifier};
use parking_lot::Mutex;
use pezkuwi_primitives::Hash as RHash;
use pezsc_consensus::{
import_queue::{BasicQueue, Verifier as VerifierT},
BlockImport, BlockImportParams, ForkChoiceStrategy,
};
use pezsc_consensus_aura::{standalone as aura_internal, AuthoritiesTracker};
use pezsc_telemetry::{telemetry, TelemetryHandle, CONSENSUS_DEBUG, CONSENSUS_TRACE};
use schnellru::{ByLength, LruMap};
use pezsp_api::ProvideRuntimeApi;
use pezsp_block_builder::BlockBuilder as BlockBuilderApi;
use pezsp_blockchain::{HeaderBackend, HeaderMetadata};
use pezsp_consensus::{error::Error as ConsensusError, BlockOrigin};
use pezsp_consensus_aura::{AuraApi, Slot, SlotDuration};
use pezsp_core::crypto::Pair;
use pezsp_inherents::CreateInherentDataProviders;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
use std::{fmt::Debug, sync::Arc};
const LRU_WINDOW: u32 = 512;
const EQUIVOCATION_LIMIT: usize = 16;
struct NaiveEquivocationDefender<N> {
/// We distinguish blocks by `(Slot, BlockNumber, RelayParent)`.
cache: LruMap<(u64, N, RHash), usize>,
}
impl<N: std::hash::Hash + PartialEq> Default for NaiveEquivocationDefender<N> {
fn default() -> Self {
NaiveEquivocationDefender { cache: LruMap::new(ByLength::new(LRU_WINDOW)) }
}
}
impl<N: std::hash::Hash + PartialEq> NaiveEquivocationDefender<N> {
// Returns `true` if equivocation is beyond the limit.
fn insert_and_check(&mut self, slot: Slot, block_number: N, relay_chain_parent: RHash) -> bool {
let val = self
.cache
.get_or_insert((*slot, block_number, relay_chain_parent), || 0)
.expect("insertion with ByLength limiter always succeeds; qed");
if *val == EQUIVOCATION_LIMIT {
true
} else {
*val += 1;
false
}
}
}
/// A teyrchain block import verifier that checks for equivocation limits within each slot.
pub struct Verifier<P: Pair, Client, Block: BlockT, CIDP> {
client: Arc<Client>,
create_inherent_data_providers: CIDP,
defender: Mutex<NaiveEquivocationDefender<NumberFor<Block>>>,
telemetry: Option<TelemetryHandle>,
// Unused for now. Will be plugged in with a later PR.
_authorities_tracker: AuthoritiesTracker<P, Block, Client>,
}
impl<P, Client, Block, CIDP> Verifier<P, Client, Block, CIDP>
where
P: Pair,
P::Signature: Codec,
P::Public: Codec + Debug,
Block: BlockT,
Client: ProvideRuntimeApi<Block> + Send + Sync,
<Client as ProvideRuntimeApi<Block>>::Api: BlockBuilderApi<Block> + AuraApi<Block, P::Public>,
CIDP: CreateInherentDataProviders<Block, ()>,
{
/// Creates a new Verifier instance for handling teyrchain block import verification in Aura
/// consensus.
pub fn new(
client: Arc<Client>,
inherent_data_provider: CIDP,
telemetry: Option<TelemetryHandle>,
) -> Self {
Self {
client: client.clone(),
create_inherent_data_providers: inherent_data_provider,
defender: Mutex::new(NaiveEquivocationDefender::default()),
telemetry,
_authorities_tracker: AuthoritiesTracker::new(client),
}
}
}
#[async_trait::async_trait]
impl<P, Client, Block, CIDP> VerifierT<Block> for Verifier<P, Client, Block, CIDP>
where
P: Pair,
P::Signature: Codec,
P::Public: Codec + Debug,
Block: BlockT,
Client: HeaderBackend<Block>
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ ProvideRuntimeApi<Block>
+ Send
+ Sync,
<Client as ProvideRuntimeApi<Block>>::Api: BlockBuilderApi<Block> + AuraApi<Block, P::Public>,
CIDP: CreateInherentDataProviders<Block, ()>,
{
async fn verify(
&self,
mut block_params: BlockImportParams<Block>,
) -> Result<BlockImportParams<Block>, String> {
// Skip checks that include execution, if being told so, or when importing only state.
//
// This is done for example when gap syncing and it is expected that the block after the gap
// was checked/chosen properly, e.g. by warp syncing to this block using a finality proof.
if block_params.state_action.skip_execution_checks() || block_params.with_state() {
block_params.fork_choice = Some(ForkChoiceStrategy::Custom(block_params.with_state()));
return Ok(block_params);
}
let post_hash = block_params.header.hash();
let parent_hash = *block_params.header.parent_hash();
// check seal and update pre-hash/post-hash
{
let authorities = aura_internal::fetch_authorities(self.client.as_ref(), parent_hash)
.map_err(|e| {
format!("Could not fetch authorities at {:?}: {}", parent_hash, e)
})?;
let slot_duration = self
.client
.runtime_api()
.slot_duration(parent_hash)
.map_err(|e| e.to_string())?;
let slot_now = slot_now(slot_duration);
let res = aura_internal::check_header_slot_and_seal::<Block, P>(
slot_now,
block_params.header,
&authorities,
);
match res {
Ok((pre_header, slot, seal_digest)) => {
telemetry!(
self.telemetry;
CONSENSUS_TRACE;
"aura.checked_and_importing";
"pre_header" => ?pre_header,
);
// We need some kind of identifier for the relay parent, in the worst case we
// take the all `0` hash.
let relay_parent =
match CumulusDigestItem::find_relay_block_identifier(pre_header.digest()) {
None => Default::default(),
Some(RelayBlockIdentifier::ByHash(h)) |
Some(RelayBlockIdentifier::ByStorageRoot {
storage_root: h, ..
}) => h,
};
block_params.header = pre_header;
block_params.post_digests.push(seal_digest);
block_params.fork_choice = Some(ForkChoiceStrategy::LongestChain);
block_params.post_hash = Some(post_hash);
// Check for and reject egregious amounts of equivocations.
//
// If the `origin` is `ConsensusBroadcast`, we ignore the result of the
// equivocation check. This `origin` is for example used by pov-recovery.
if self.defender.lock().insert_and_check(
slot,
*block_params.header.number(),
relay_parent,
) && !matches!(block_params.origin, BlockOrigin::ConsensusBroadcast)
{
return Err(format!(
"Rejecting block {:?} due to excessive equivocations at slot",
post_hash,
));
}
},
Err(aura_internal::SealVerificationError::Deferred(hdr, slot)) => {
telemetry!(
self.telemetry;
CONSENSUS_DEBUG;
"aura.header_too_far_in_future";
"hash" => ?post_hash,
"a" => ?hdr,
"b" => ?slot,
);
return Err(format!(
"Rejecting block ({:?}) from future slot {:?}",
post_hash, slot
));
},
Err(e) =>
return Err(format!(
"Rejecting block ({:?}) with invalid seal ({:?})",
post_hash, e
)),
}
}
// Check inherents.
if let Some(body) = block_params.body.clone() {
let block = Block::new(block_params.header.clone(), body);
let create_inherent_data_providers = self
.create_inherent_data_providers
.create_inherent_data_providers(parent_hash, ())
.await
.map_err(|e| format!("Could not create inherent data {:?}", e))?;
pezsp_block_builder::check_inherents(
self.client.clone(),
parent_hash,
block,
&create_inherent_data_providers,
)
.await
.map_err(|e| format!("Error checking block inherents {:?}", e))?;
}
Ok(block_params)
}
}
fn slot_now(slot_duration: SlotDuration) -> Slot {
let timestamp = pezsp_timestamp::InherentDataProvider::from_system_time().timestamp();
Slot::from_timestamp(timestamp, slot_duration)
}
/// Start an import queue for a Pezcumulus node which checks blocks' seals and inherent data.
///
/// Pass in only inherent data providers which don't include aura or teyrchain consensus inherents,
/// e.g. things like timestamp and custom inherents for the runtime.
///
/// The others are generated explicitly internally.
///
/// This should only be used for runtimes where the runtime does not check all inherents and
/// seals in `execute_block` (see <https://github.com/pezkuwichain/kurdistan-sdk/issues/91>)
pub fn fully_verifying_import_queue<P, Client, Block: BlockT, I, CIDP>(
client: Arc<Client>,
block_import: I,
create_inherent_data_providers: CIDP,
spawner: &impl pezsp_core::traits::SpawnEssentialNamed,
registry: Option<&prometheus_endpoint::Registry>,
telemetry: Option<TelemetryHandle>,
) -> BasicQueue<Block>
where
P: Pair + 'static,
P::Signature: Codec,
P::Public: Codec + Debug,
I: BlockImport<Block, Error = ConsensusError>
+ TeyrchainBlockImportMarker
+ Send
+ Sync
+ 'static,
Client: HeaderBackend<Block>
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ ProvideRuntimeApi<Block>
+ Send
+ Sync
+ 'static,
<Client as ProvideRuntimeApi<Block>>::Api: BlockBuilderApi<Block> + AuraApi<Block, P::Public>,
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
{
let verifier = Verifier::<P, _, _, _> {
client: client.clone(),
create_inherent_data_providers,
defender: Mutex::new(NaiveEquivocationDefender::default()),
telemetry,
_authorities_tracker: AuthoritiesTracker::new(client.clone()),
};
BasicQueue::new(verifier, Box::new(block_import), None, spawner, registry)
}
#[cfg(test)]
mod test {
use super::*;
use codec::Encode;
use cumulus_test_client::{
runtime::Block, seal_block, Client, InitBlockBuilder, TestClientBuilder,
TestClientBuilderExt,
};
use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder;
use futures::FutureExt;
use pezkuwi_primitives::{HeadData, PersistedValidationData};
use pezsc_client_api::HeaderBackend;
use pezsp_consensus_aura::sr25519;
use pezsp_tracing::try_init_simple;
use std::{collections::HashSet, sync::Arc};
#[test]
fn import_equivocated_blocks_from_recovery() {
try_init_simple();
let client = Arc::new(TestClientBuilder::default().build());
let verifier = Verifier::<sr25519::AuthorityPair, Client, Block, _> {
client: client.clone(),
create_inherent_data_providers: |_, _| async move {
Ok(pezsp_timestamp::InherentDataProvider::from_system_time())
},
defender: Mutex::new(NaiveEquivocationDefender::default()),
telemetry: None,
_authorities_tracker: AuthoritiesTracker::new(client.clone()),
};
let genesis = client.info().best_hash;
let mut sproof = RelayStateSproofBuilder::default();
sproof.included_para_head = Some(HeadData(client.header(genesis).unwrap().encode()));
sproof.para_id = cumulus_test_client::runtime::TEYRCHAIN_ID.into();
let validation_data = PersistedValidationData {
relay_parent_number: 1,
parent_head: client.header(genesis).unwrap().encode().into(),
..Default::default()
};
let block_builder = client.init_block_builder(Some(validation_data), sproof);
let block = block_builder.block_builder.build().unwrap();
let mut blocks = Vec::new();
for _ in 0..EQUIVOCATION_LIMIT + 1 {
blocks.push(seal_block(block.block.clone(), &client))
}
// sr25519 should generate a different signature every time you sign something and thus, all
// blocks get a different hash (even if they are the same block).
assert_eq!(blocks.iter().map(|b| b.hash()).collect::<HashSet<_>>().len(), blocks.len());
blocks.iter().take(EQUIVOCATION_LIMIT).for_each(|block| {
let mut params =
BlockImportParams::new(BlockOrigin::NetworkBroadcast, block.header().clone());
params.body = Some(block.extrinsics().to_vec());
verifier.verify(params).now_or_never().unwrap().unwrap();
});
// Now let's try some previously verified block and a block we have not verified yet.
//
// Verify should fail, because we are above the limit. However, when we change the origin to
// `ConsensusBroadcast`, it should work.
let extra_blocks =
vec![blocks[EQUIVOCATION_LIMIT / 2].clone(), blocks.last().unwrap().clone()];
extra_blocks.into_iter().for_each(|block| {
let mut params =
BlockImportParams::new(BlockOrigin::NetworkBroadcast, block.header().clone());
params.body = Some(block.extrinsics().to_vec());
assert!(verifier
.verify(params)
.now_or_never()
.unwrap()
.map(drop)
.unwrap_err()
.contains("excessive equivocations at slot"));
// When it comes from `pov-recovery`, we will accept it
let mut params =
BlockImportParams::new(BlockOrigin::ConsensusBroadcast, block.header().clone());
params.body = Some(block.extrinsics().to_vec());
assert!(verifier.verify(params).now_or_never().unwrap().is_ok());
});
}
}
@@ -0,0 +1,126 @@
// 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 <https://www.gnu.org/licenses/>.
//! Teyrchain specific wrapper for the AuRa import queue.
use codec::Codec;
use cumulus_client_consensus_common::TeyrchainBlockImportMarker;
use prometheus_endpoint::Registry;
use pezsc_client_api::{backend::AuxStore, BlockOf, UsageProvider};
use pezsc_consensus::{import_queue::DefaultImportQueue, BlockImport};
use pezsc_consensus_aura::{AuraVerifier, CompatibilityMode};
use pezsc_consensus_slots::InherentDataProviderExt;
use pezsc_telemetry::TelemetryHandle;
use pezsp_api::{ApiExt, ProvideRuntimeApi};
use pezsp_block_builder::BlockBuilder as BlockBuilderApi;
use pezsp_blockchain::{HeaderBackend, HeaderMetadata};
use pezsp_consensus::Error as ConsensusError;
use pezsp_consensus_aura::AuraApi;
use pezsp_core::crypto::Pair;
use pezsp_inherents::CreateInherentDataProviders;
use pezsp_runtime::traits::Block as BlockT;
use std::{fmt::Debug, sync::Arc};
/// Parameters for [`import_queue`].
pub struct ImportQueueParams<'a, I, C, CIDP, S> {
/// The block import to use.
pub block_import: I,
/// The client to interact with the chain.
pub client: Arc<C>,
/// The inherent data providers, to create the inherent data.
pub create_inherent_data_providers: CIDP,
/// The spawner to spawn background tasks.
pub spawner: &'a S,
/// The prometheus registry.
pub registry: Option<&'a Registry>,
/// The telemetry handle.
pub telemetry: Option<TelemetryHandle>,
}
/// Start an import queue for the Aura consensus algorithm.
pub fn import_queue<P, Block, I, C, S, CIDP>(
ImportQueueParams {
block_import,
client,
create_inherent_data_providers,
spawner,
registry,
telemetry,
}: ImportQueueParams<'_, I, C, CIDP, S>,
) -> Result<DefaultImportQueue<Block>, pezsp_consensus::Error>
where
Block: BlockT,
C::Api: BlockBuilderApi<Block> + AuraApi<Block, P::Public> + ApiExt<Block>,
C: 'static
+ ProvideRuntimeApi<Block>
+ BlockOf
+ Send
+ Sync
+ AuxStore
+ UsageProvider<Block>
+ HeaderBackend<Block>
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>,
I: BlockImport<Block, Error = ConsensusError>
+ TeyrchainBlockImportMarker
+ Send
+ Sync
+ 'static,
P: Pair + 'static,
P::Public: Debug + Codec,
P::Signature: Codec,
S: pezsp_core::traits::SpawnEssentialNamed,
CIDP: CreateInherentDataProviders<Block, ()> + Sync + Send + 'static,
CIDP::InherentDataProviders: InherentDataProviderExt + Send + Sync,
{
pezsc_consensus_aura::import_queue::<P, _, _, _, _, _>(pezsc_consensus_aura::ImportQueueParams {
block_import,
justification_import: None,
client,
create_inherent_data_providers,
spawner,
registry,
check_for_equivocation: pezsc_consensus_aura::CheckForEquivocation::No,
telemetry,
compatibility_mode: CompatibilityMode::None,
})
}
/// Parameters of [`build_verifier`].
pub struct BuildVerifierParams<C, CIDP> {
/// The client to interact with the chain.
pub client: Arc<C>,
/// The inherent data providers, to create the inherent data.
pub create_inherent_data_providers: CIDP,
/// The telemetry handle.
pub telemetry: Option<TelemetryHandle>,
}
/// Build the [`AuraVerifier`].
pub fn build_verifier<P: Pair, C, CIDP, B: BlockT>(
BuildVerifierParams { client, create_inherent_data_providers, telemetry }: BuildVerifierParams<
C,
CIDP,
>,
) -> AuraVerifier<C, P, CIDP, B> {
pezsc_consensus_aura::build_verifier(pezsc_consensus_aura::BuildVerifierParams {
client,
create_inherent_data_providers,
telemetry,
check_for_equivocation: pezsc_consensus_aura::CheckForEquivocation::No,
compatibility_mode: CompatibilityMode::None,
})
}
@@ -0,0 +1,86 @@
// 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 <https://www.gnu.org/licenses/>.
//! The AuRa consensus algorithm for teyrchains.
//!
//! This extends the Bizinikiwi provided AuRa consensus implementation to make it compatible for
//! teyrchains.
//!
//! For more information about AuRa, the Bizinikiwi crate should be checked.
use codec::Encode;
use cumulus_primitives_core::PersistedValidationData;
use cumulus_primitives_core::relay_chain::HeadData;
use pezkuwi_primitives::{BlockNumber as RBlockNumber, Hash as RHash};
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
use std::{fs, fs::File, path::PathBuf};
mod import_queue;
pub use import_queue::{build_verifier, import_queue, BuildVerifierParams, ImportQueueParams};
use pezkuwi_node_primitives::PoV;
pub use pezsc_consensus_aura::{
slot_duration, standalone::slot_duration_at, AuraVerifier, BuildAuraWorkerParams,
SlotProportion,
};
pub use pezsc_consensus_slots::InherentDataProviderExt;
pub mod collator;
pub mod collators;
pub mod equivocation_import_queue;
const LOG_TARGET: &str = "aura::pezcumulus";
/// Export the given `pov` to the file system at `path`.
///
/// The file will be named `block_hash_block_number.pov`.
///
/// The `parent_header`, `relay_parent_storage_root` and `relay_parent_number` will also be
/// stored in the file alongside the `pov`. This enables stateless validation of the `pov`.
pub(crate) fn export_pov_to_path<Block: BlockT>(
path: PathBuf,
pov: PoV,
block_hash: Block::Hash,
block_number: NumberFor<Block>,
parent_header: Block::Header,
relay_parent_storage_root: RHash,
relay_parent_number: RBlockNumber,
max_pov_size: u32,
) {
if let Err(error) = fs::create_dir_all(&path) {
tracing::error!(target: LOG_TARGET, %error, path = %path.display(), "Failed to create PoV export directory");
return;
}
let mut file = match File::create(path.join(format!("{block_hash:?}_{block_number}.pov"))) {
Ok(f) => f,
Err(error) => {
tracing::error!(target: LOG_TARGET, %error, "Failed to export PoV.");
return;
},
};
pov.encode_to(&mut file);
PersistedValidationData {
parent_head: HeadData(parent_header.encode()),
relay_parent_number,
relay_parent_storage_root,
max_pov_size,
}
.encode_to(&mut file);
}
@@ -0,0 +1,78 @@
[package]
name = "pezcumulus-client-consensus-common"
description = "Pezcumulus specific common consensus implementations"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-trait = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
dyn-clone = { workspace = true }
futures = { workspace = true }
log = { workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
prometheus-endpoint = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-consensus = { workspace = true, default-features = true }
pezsc-consensus-babe = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-consensus-slots = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-timestamp = { workspace = true, default-features = true }
pezsp-trie = { workspace = true, default-features = true }
pezsp-version = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-primitives = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-client-pov-recovery = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
pezcumulus-relay-chain-streams = { workspace = true, default-features = true }
schnellru = { workspace = true }
[dev-dependencies]
futures-timer = { workspace = true }
tokio = { features = ["macros"], workspace = true }
# Bizinikiwi
pezsp-tracing = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-test-client = { workspace = true }
pezcumulus-test-relay-sproof-builder = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-client-pov-recovery/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezcumulus-relay-chain-streams/runtime-benchmarks",
"pezcumulus-test-client/runtime-benchmarks",
"pezcumulus-test-relay-sproof-builder/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-consensus-babe/runtime-benchmarks",
"pezsc-consensus/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus-slots/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-timestamp/runtime-benchmarks",
"pezsp-trie/runtime-benchmarks",
"pezsp-version/runtime-benchmarks",
]
@@ -0,0 +1,77 @@
// 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 <https://www.gnu.org/licenses/>.
//! (unstable) Composable utilities for constructing import queues for teyrchains.
//!
//! Unlike standalone chains, teyrchains have the requirement that all consensus logic
//! must be checked within the runtime. This property means that work which is normally
//! done in the import queue per-block, such as checking signatures, quorums, and whether
//! inherent extrinsics were constructed faithfully do not need to be done, per se.
//!
//! It may seem that it would be beneficial for the client to do these checks regardless,
//! but in practice this means that clients would just reject blocks which are _valid_ according
//! to their Teyrchain Validation Function, which is the ultimate source of consensus truth.
//!
//! However, teyrchain runtimes expose two different access points for executing blocks
//! in full nodes versus executing those blocks in the teyrchain validation environment.
//! At the time of writing, the inherent and consensus checks in most Pezcumulus runtimes
//! are only performed during teyrchain validation, not full node block execution.
//!
//! See <https://github.com/pezkuwichain/kurdistan-sdk/issues/91> for details.
use pezsp_consensus::error::Error as ConsensusError;
use pezsp_runtime::traits::Block as BlockT;
use pezsc_consensus::{
block_import::{BlockImport, BlockImportParams},
import_queue::{BasicQueue, Verifier},
};
use crate::TeyrchainBlockImportMarker;
/// A [`Verifier`] for blocks which verifies absolutely nothing.
///
/// This should only be used when the runtime is responsible for checking block seals and inherents.
pub struct VerifyNothing;
#[async_trait::async_trait]
impl<Block: BlockT> Verifier<Block> for VerifyNothing {
async fn verify(
&self,
params: BlockImportParams<Block>,
) -> Result<BlockImportParams<Block>, String> {
Ok(params)
}
}
/// An import queue which does no verification.
///
/// This should only be used when the runtime is responsible for checking block seals and inherents.
pub fn verify_nothing_import_queue<Block: BlockT, I>(
block_import: I,
spawner: &impl pezsp_core::traits::SpawnEssentialNamed,
registry: Option<&prometheus_endpoint::Registry>,
) -> BasicQueue<Block>
where
I: BlockImport<Block, Error = ConsensusError>
+ TeyrchainBlockImportMarker
+ Send
+ Sync
+ 'static,
{
BasicQueue::new(VerifyNothing, Box::new(block_import), None, spawner, registry)
}
@@ -0,0 +1,407 @@
// 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 <https://www.gnu.org/licenses/>.
use pezsc_client_api::{blockchain::Backend as _, Backend, HeaderBackend as _};
use pezsp_blockchain::{HashAndNumber, HeaderMetadata, TreeRoute};
use pezsp_runtime::traits::{Block as BlockT, NumberFor, One, Saturating, UniqueSaturatedInto, Zero};
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
const LOG_TARGET: &str = "level-monitor";
/// Value good enough to be used with teyrchains using the current backend implementation
/// that ships with Bizinikiwi. This value may change in the future.
pub const MAX_LEAVES_PER_LEVEL_SENSIBLE_DEFAULT: usize = 32;
// Counter threshold after which we are going to eventually cleanup our internal data.
const CLEANUP_THRESHOLD: u32 = 32;
/// Upper bound to the number of leaves allowed for each level of the blockchain.
///
/// If the limit is set and more leaves are detected on block import, then the older ones are
/// dropped to make space for the fresh blocks.
///
/// In environments where blocks confirmations from the relay chain may be "slow", then
/// setting an upper bound helps keeping the chain health by dropping old (presumably) stale
/// leaves and prevents discarding new blocks because we've reached the backend max value.
pub enum LevelLimit {
/// Limit set to [`MAX_LEAVES_PER_LEVEL_SENSIBLE_DEFAULT`].
Default,
/// No explicit limit, however a limit may be implicitly imposed by the backend implementation.
None,
/// Custom value.
Some(usize),
}
/// Support structure to constrain the number of leaves at each level.
pub struct LevelMonitor<Block: BlockT, BE> {
/// Max number of leaves for each level.
level_limit: usize,
/// Monotonic counter used to keep track of block freshness.
pub(crate) import_counter: NumberFor<Block>,
/// Map between blocks hashes and freshness.
pub(crate) freshness: HashMap<Block::Hash, NumberFor<Block>>,
/// Blockchain levels cache.
pub(crate) levels: HashMap<NumberFor<Block>, HashSet<Block::Hash>>,
/// Lower level number stored by the levels map.
lowest_level: NumberFor<Block>,
/// Backend reference to remove blocks on level saturation.
backend: Arc<BE>,
}
/// Contains information about the target scheduled for removal.
struct TargetInfo<Block: BlockT> {
/// Index of freshest leaf in the leaves array.
freshest_leaf_idx: usize,
/// Route from target to its freshest leaf.
freshest_route: TreeRoute<Block>,
}
impl<Block, BE> LevelMonitor<Block, BE>
where
Block: BlockT,
BE: Backend<Block>,
{
/// Instance a new monitor structure.
pub fn new(level_limit: usize, backend: Arc<BE>) -> Self {
let mut monitor = LevelMonitor {
level_limit,
import_counter: Zero::zero(),
freshness: HashMap::new(),
levels: HashMap::new(),
lowest_level: Zero::zero(),
backend,
};
monitor.restore();
monitor
}
/// Restore the structure using the backend.
///
/// Blocks freshness values are inferred from the height and not from the effective import
/// moment. This is a not accurate but "good-enough" best effort solution.
///
/// Level limits are not enforced during this phase.
fn restore(&mut self) {
let info = self.backend.blockchain().info();
log::debug!(
target: LOG_TARGET,
"Restoring chain level monitor from last finalized block: {} {}",
info.finalized_number,
info.finalized_hash
);
self.lowest_level = info.finalized_number;
self.import_counter = info.finalized_number;
for leaf in self.backend.blockchain().leaves().unwrap_or_default() {
let Ok(mut meta) = self.backend.blockchain().header_metadata(leaf) else {
log::debug!(
target: LOG_TARGET,
"Could not fetch header metadata for leaf: {leaf:?}",
);
continue;
};
self.import_counter = self.import_counter.max(meta.number);
// Populate the monitor until we don't hit an already imported branch
while !self.freshness.contains_key(&meta.hash) {
self.freshness.insert(meta.hash, meta.number);
self.levels.entry(meta.number).or_default().insert(meta.hash);
if meta.number <= self.lowest_level {
break;
}
meta = match self.backend.blockchain().header_metadata(meta.parent) {
Ok(m) => m,
Err(_) => {
// This can happen after we have warp synced a node.
log::debug!(
target: LOG_TARGET,
"Could not fetch header metadata for parent: {:?}",
meta.parent,
);
break;
},
}
}
}
log::debug!(
target: LOG_TARGET,
"Restored chain level monitor up to height {}",
self.import_counter
);
}
/// Check and enforce the limit bound at the given height.
///
/// In practice this will enforce the given height in having a number of blocks less than
/// the limit passed to the constructor.
///
/// If the given level is found to have a number of blocks greater than or equal the limit
/// then the limit is enforced by choosing one (or more) blocks to remove.
///
/// The removal strategy is driven by the block freshness.
///
/// A block freshness is determined by the most recent leaf freshness descending from the block
/// itself. In other words its freshness is equal to its more "fresh" descendant.
///
/// The least "fresh" blocks are eventually removed.
pub fn enforce_limit(&mut self, number: NumberFor<Block>) {
let level_len = self.levels.get(&number).map(|l| l.len()).unwrap_or_default();
if level_len < self.level_limit {
return;
}
// Sort leaves by freshness only once (less fresh first) and keep track of
// leaves that were invalidated on removal.
let mut leaves = self.backend.blockchain().leaves().unwrap_or_default();
leaves.sort_unstable_by(|a, b| self.freshness.get(a).cmp(&self.freshness.get(b)));
let mut invalidated_leaves = HashSet::new();
// This may not be the most efficient way to remove **multiple** entries, but is the easy
// one :-). Should be considered that in "normal" conditions the number of blocks to remove
// is 0 or 1, it is not worth to complicate the code too much. One condition that may
// trigger multiple removals (2+) is if we restart the node using an existing db and a
// smaller limit wrt the one previously used.
let remove_count = level_len - self.level_limit + 1;
log::debug!(
target: LOG_TARGET,
"Detected leaves overflow at height {number}, removing {remove_count} obsolete blocks",
);
(0..remove_count).all(|_| {
self.find_target(number, &leaves, &invalidated_leaves).map_or(false, |target| {
self.remove_target(target, number, &leaves, &mut invalidated_leaves);
true
})
});
}
// Helper function to find the best candidate to be removed.
//
// Given a set of blocks with height equal to `number` (potential candidates)
// 1. For each candidate fetch all the leaves that are descending from it.
// 2. Set the candidate freshness equal to the fresher of its descending leaves.
// 3. The target is set as the candidate that is less fresh.
//
// Input `leaves` are assumed to be already ordered by "freshness" (less fresh first).
//
// Returns the index of the target fresher leaf within `leaves` and the route from target to
// such leaf.
fn find_target(
&self,
number: NumberFor<Block>,
leaves: &[Block::Hash],
invalidated_leaves: &HashSet<usize>,
) -> Option<TargetInfo<Block>> {
let mut target_info: Option<TargetInfo<Block>> = None;
let blockchain = self.backend.blockchain();
let best_hash = blockchain.info().best_hash;
// Leaves that where already assigned to some node and thus can be skipped
// during the search.
let mut assigned_leaves = HashSet::new();
let level = self.levels.get(&number)?;
for blk_hash in level.iter().filter(|hash| **hash != best_hash) {
// Search for the fresher leaf information for this block
let candidate_info = leaves
.iter()
.enumerate()
.filter(|(leaf_idx, _)| {
!assigned_leaves.contains(leaf_idx) && !invalidated_leaves.contains(leaf_idx)
})
.rev()
.find_map(|(leaf_idx, leaf_hash)| {
if blk_hash == leaf_hash {
let entry = HashAndNumber { number, hash: *blk_hash };
TreeRoute::new(vec![entry], 0).ok().map(|freshest_route| TargetInfo {
freshest_leaf_idx: leaf_idx,
freshest_route,
})
} else {
match pezsp_blockchain::tree_route(blockchain, *blk_hash, *leaf_hash) {
Ok(route) if route.retracted().is_empty() => Some(TargetInfo {
freshest_leaf_idx: leaf_idx,
freshest_route: route,
}),
Err(err) => {
log::warn!(
target: LOG_TARGET,
"(Lookup) Unable getting route from {:?} to {:?}: {}",
blk_hash,
leaf_hash,
err,
);
None
},
_ => None,
}
}
});
let candidate_info = match candidate_info {
Some(candidate_info) => {
assigned_leaves.insert(candidate_info.freshest_leaf_idx);
candidate_info
},
None => {
// This should never happen
log::error!(
target: LOG_TARGET,
"Unable getting route to any leaf from {:?} (this is a bug)",
blk_hash,
);
continue;
},
};
// Found fresher leaf for this candidate.
// This candidate is set as the new target if:
// 1. its fresher leaf is less fresh than the previous target fresher leaf AND
// 2. best block is not in its route
let is_less_fresh = || {
target_info
.as_ref()
.map(|ti| candidate_info.freshest_leaf_idx < ti.freshest_leaf_idx)
.unwrap_or(true)
};
let not_contains_best = || {
candidate_info
.freshest_route
.enacted()
.iter()
.all(|entry| entry.hash != best_hash)
};
if is_less_fresh() && not_contains_best() {
let early_stop = candidate_info.freshest_leaf_idx == 0;
target_info = Some(candidate_info);
if early_stop {
// We will never find a candidate with an worst freshest leaf than this.
break;
}
}
}
target_info
}
// Remove the target block and all its descendants.
//
// Leaves should have already been ordered by "freshness" (less fresh first).
fn remove_target(
&mut self,
target: TargetInfo<Block>,
number: NumberFor<Block>,
leaves: &[Block::Hash],
invalidated_leaves: &mut HashSet<usize>,
) {
let mut remove_leaf = |number, hash| {
log::debug!(target: LOG_TARGET, "Removing block (@{}) {:?}", number, hash);
if let Err(err) = self.backend.remove_leaf_block(hash) {
log::debug!(target: LOG_TARGET, "Remove not possible for {}: {}", hash, err);
return false;
}
self.levels.get_mut(&number).map(|level| level.remove(&hash));
self.freshness.remove(&hash);
true
};
invalidated_leaves.insert(target.freshest_leaf_idx);
// Takes care of route removal. Starts from the leaf and stops as soon as an error is
// encountered. In this case an error is interpreted as the block being not a leaf
// and it will be removed while removing another route from the same block but to a
// different leaf.
let mut remove_route = |route: TreeRoute<Block>| {
route.enacted().iter().rev().all(|elem| remove_leaf(elem.number, elem.hash));
};
let target_hash = target.freshest_route.common_block().hash;
debug_assert_eq!(
target.freshest_route.common_block().number,
number,
"This is a bug in LevelMonitor::find_target() or the Backend is corrupted"
);
// Remove freshest (cached) route first.
remove_route(target.freshest_route);
// Don't bother trying with leaves we already found to not be our descendants.
let to_skip = leaves.len() - target.freshest_leaf_idx;
leaves.iter().enumerate().rev().skip(to_skip).for_each(|(leaf_idx, leaf_hash)| {
if invalidated_leaves.contains(&leaf_idx) {
return;
}
match pezsp_blockchain::tree_route(self.backend.blockchain(), target_hash, *leaf_hash) {
Ok(route) if route.retracted().is_empty() => {
invalidated_leaves.insert(leaf_idx);
remove_route(route);
},
Err(err) => {
log::warn!(
target: LOG_TARGET,
"(Removal) unable getting route from {:?} to {:?}: {}",
target_hash,
leaf_hash,
err,
);
},
_ => (),
};
});
remove_leaf(number, target_hash);
}
/// Add a new imported block information to the monitor.
pub fn block_imported(&mut self, number: NumberFor<Block>, hash: Block::Hash) {
let finalized_num = self.backend.blockchain().info().finalized_number;
if number > finalized_num {
// Only blocks above the last finalized block should be added to the monitor
self.import_counter += One::one();
self.freshness.insert(hash, self.import_counter);
self.levels.entry(number).or_default().insert(hash);
}
let delta: u32 = finalized_num.saturating_sub(self.lowest_level).unique_saturated_into();
if delta >= CLEANUP_THRESHOLD {
// Do cleanup once in a while, we are allowed to have some obsolete information.
for i in 0..delta {
let number = self.lowest_level + i.unique_saturated_into();
self.levels.remove(&number).map(|level| {
level.iter().for_each(|hash| {
self.freshness.remove(hash);
})
});
}
self.lowest_level = finalized_num;
}
}
}
@@ -0,0 +1,220 @@
// 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 <https://www.gnu.org/licenses/>.
use codec::Decode;
use pezkuwi_primitives::{Block as PBlock, Hash as PHash, Header as PHeader, ValidationCodeHash};
use cumulus_primitives_core::{relay_chain, AbridgedHostConfiguration};
use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface};
use pezsc_client_api::Backend;
use pezsc_consensus::{shared_data::SharedData, BlockImport, ImportResult};
use pezsp_consensus_slots::Slot;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
use pezsp_timestamp::Timestamp;
use std::{sync::Arc, time::Duration};
mod level_monitor;
mod parent_search;
#[cfg(test)]
mod tests;
mod teyrchain_consensus;
pub use parent_search::*;
pub use cumulus_relay_chain_streams::finalized_heads;
pub use teyrchain_consensus::spawn_teyrchain_consensus_tasks;
use level_monitor::LevelMonitor;
pub use level_monitor::{LevelLimit, MAX_LEAVES_PER_LEVEL_SENSIBLE_DEFAULT};
pub mod import_queue;
/// Provides the hash of validation code used for authoring/execution of blocks at a given
/// hash.
pub trait ValidationCodeHashProvider<Hash> {
fn code_hash_at(&self, at: Hash) -> Option<ValidationCodeHash>;
}
impl<F, Hash> ValidationCodeHashProvider<Hash> for F
where
F: Fn(Hash) -> Option<ValidationCodeHash>,
{
fn code_hash_at(&self, at: Hash) -> Option<ValidationCodeHash> {
(self)(at)
}
}
/// The result from building a collation.
pub struct TeyrchainCandidate<B> {
/// The block that was built for this candidate.
pub block: B,
/// The proof that was recorded while building the block.
pub proof: pezsp_trie::StorageProof,
}
/// Teyrchain specific block import.
///
/// Specialized block import for teyrchains. It supports to delay setting the best block until the
/// relay chain has included a candidate in its best block. By default the delayed best block
/// setting is disabled. The block import also monitors the imported blocks and prunes by default if
/// there are too many blocks at the same height. Too many blocks at the same height can for example
/// happen if the relay chain is rejecting the teyrchain blocks in the validation.
pub struct TeyrchainBlockImport<Block: BlockT, BI, BE> {
inner: BI,
monitor: Option<SharedData<LevelMonitor<Block, BE>>>,
delayed_best_block: bool,
}
impl<Block: BlockT, BI, BE: Backend<Block>> TeyrchainBlockImport<Block, BI, BE> {
/// Create a new instance.
///
/// The number of leaves per level limit is set to `LevelLimit::Default`.
pub fn new(inner: BI, backend: Arc<BE>) -> Self {
Self::new_with_limit(inner, backend, LevelLimit::Default)
}
/// Create a new instance with an explicit limit to the number of leaves per level.
///
/// This function alone doesn't enforce the limit on levels for old imported blocks,
/// the limit is eventually enforced only when new blocks are imported.
pub fn new_with_limit(inner: BI, backend: Arc<BE>, level_leaves_max: LevelLimit) -> Self {
let level_limit = match level_leaves_max {
LevelLimit::None => None,
LevelLimit::Some(limit) => Some(limit),
LevelLimit::Default => Some(MAX_LEAVES_PER_LEVEL_SENSIBLE_DEFAULT),
};
let monitor =
level_limit.map(|level_limit| SharedData::new(LevelMonitor::new(level_limit, backend)));
Self { inner, monitor, delayed_best_block: false }
}
/// Create a new instance which delays setting the best block.
///
/// The number of leaves per level limit is set to `LevelLimit::Default`.
pub fn new_with_delayed_best_block(inner: BI, backend: Arc<BE>) -> Self {
Self {
delayed_best_block: true,
..Self::new_with_limit(inner, backend, LevelLimit::Default)
}
}
}
impl<Block: BlockT, I: Clone, BE> Clone for TeyrchainBlockImport<Block, I, BE> {
fn clone(&self) -> Self {
TeyrchainBlockImport {
inner: self.inner.clone(),
monitor: self.monitor.clone(),
delayed_best_block: self.delayed_best_block,
}
}
}
#[async_trait::async_trait]
impl<Block, BI, BE> BlockImport<Block> for TeyrchainBlockImport<Block, BI, BE>
where
Block: BlockT,
BI: BlockImport<Block> + Send + Sync,
BE: Backend<Block>,
{
type Error = BI::Error;
async fn check_block(
&self,
block: pezsc_consensus::BlockCheckParams<Block>,
) -> Result<pezsc_consensus::ImportResult, Self::Error> {
self.inner.check_block(block).await
}
async fn import_block(
&self,
mut params: pezsc_consensus::BlockImportParams<Block>,
) -> Result<pezsc_consensus::ImportResult, Self::Error> {
// Blocks are stored within the backend by using POST hash.
let hash = params.post_hash();
let number = *params.header.number();
if params.with_state() {
// Force imported state finality.
// Required for warp sync. We assume that preconditions have been
// checked properly and we are importing a finalized block with state.
params.finalized = true;
}
if self.delayed_best_block {
// Best block is determined by the relay chain, or if we are doing the initial sync
// we import all blocks as new best.
params.fork_choice = Some(pezsc_consensus::ForkChoiceStrategy::Custom(
params.origin == pezsp_consensus::BlockOrigin::NetworkInitialSync,
));
}
let maybe_lock = self.monitor.as_ref().map(|monitor_lock| {
let mut monitor = monitor_lock.shared_data_locked();
monitor.enforce_limit(number);
monitor.release_mutex()
});
let res = self.inner.import_block(params).await?;
if let (Some(mut monitor_lock), ImportResult::Imported(_)) = (maybe_lock, &res) {
let mut monitor = monitor_lock.upgrade();
monitor.block_imported(number, hash);
}
Ok(res)
}
}
/// Marker trait denoting a block import type that fits the teyrchain requirements.
pub trait TeyrchainBlockImportMarker {}
impl<B: BlockT, BI, BE> TeyrchainBlockImportMarker for TeyrchainBlockImport<B, BI, BE> {}
/// Get the relay-parent slot and timestamp from a header.
pub fn relay_slot_and_timestamp(
relay_parent_header: &PHeader,
relay_chain_slot_duration: Duration,
) -> Option<(Slot, Timestamp)> {
pezsc_consensus_babe::find_pre_digest::<PBlock>(relay_parent_header)
.map(|babe_pre_digest| {
let slot = babe_pre_digest.slot();
let t = Timestamp::new(relay_chain_slot_duration.as_millis() as u64 * *slot);
(slot, t)
})
.ok()
}
/// Reads abridged host configuration from the relay chain storage at the given relay parent.
pub async fn load_abridged_host_configuration(
relay_parent: PHash,
relay_client: &impl RelayChainInterface,
) -> Result<Option<AbridgedHostConfiguration>, RelayChainError> {
relay_client
.get_storage_by_key(relay_parent, relay_chain::well_known_keys::ACTIVE_CONFIG)
.await?
.map(|bytes| {
AbridgedHostConfiguration::decode(&mut &bytes[..])
.map_err(RelayChainError::DeserializationError)
})
.transpose()
}
@@ -0,0 +1,419 @@
// 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 <https://www.gnu.org/licenses/>.
use codec::Decode;
use pezkuwi_primitives::Hash as RelayHash;
use cumulus_primitives_core::{
relay_chain::{BlockId as RBlockId, OccupiedCoreAssumption},
ParaId,
};
use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface};
use pezsc_client_api::{Backend, HeaderBackend};
use pezsp_blockchain::{Backend as BlockchainBackend, TreeRoute};
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
const PARENT_SEARCH_LOG_TARGET: &str = "consensus::common::find_potential_parents";
/// Parameters when searching for suitable parents to build on top of.
#[derive(Debug)]
pub struct ParentSearchParams {
/// The relay-parent that is intended to be used.
pub relay_parent: RelayHash,
/// The ID of the teyrchain.
pub para_id: ParaId,
/// A limitation on the age of relay parents for teyrchain blocks that are being
/// considered. This is relative to the `relay_parent` number.
pub ancestry_lookback: usize,
/// How "deep" parents can be relative to the included teyrchain block at the relay-parent.
/// The included block has depth 0.
pub max_depth: usize,
/// Whether to only ignore "alternative" branches, i.e. branches of the chain
/// which do not contain the block pending availability.
pub ignore_alternative_branches: bool,
}
/// A potential parent block returned from [`find_potential_parents`]
#[derive(PartialEq)]
pub struct PotentialParent<B: BlockT> {
/// The hash of the block.
pub hash: B::Hash,
/// The header of the block.
pub header: B::Header,
/// The depth of the block with respect to the included block.
pub depth: usize,
/// Whether the block is the included block, is itself pending on-chain, or descends
/// from the block pending availability.
pub aligned_with_pending: bool,
}
impl<B: BlockT> std::fmt::Debug for PotentialParent<B> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PotentialParent")
.field("hash", &self.hash)
.field("depth", &self.depth)
.field("aligned_with_pending", &self.aligned_with_pending)
.field("number", &self.header.number())
.finish()
}
}
/// Perform a recursive search through blocks to find potential
/// parent blocks for a new block.
///
/// This accepts a relay-chain block to be used as an anchor and a maximum search depth,
/// along with some arguments for filtering teyrchain blocks and performs a recursive search
/// for teyrchain blocks. The search begins at the last included teyrchain block and returns
/// a set of [`PotentialParent`]s which could be potential parents of a new block with this
/// relay-parent according to the search parameters.
///
/// A teyrchain block is a potential parent if it is either the last included teyrchain block, the
/// pending teyrchain block (when `max_depth` >= 1), or all of the following hold:
/// * its parent is a potential parent
/// * its relay-parent is within `ancestry_lookback` of the targeted relay-parent.
/// * its relay-parent is within the same session as the targeted relay-parent.
/// * the block number is within `max_depth` blocks of the included block
pub async fn find_potential_parents<B: BlockT>(
params: ParentSearchParams,
backend: &impl Backend<B>,
relay_client: &impl RelayChainInterface,
) -> Result<Vec<PotentialParent<B>>, RelayChainError> {
tracing::trace!("Parent search parameters: {params:?}");
// Get the included block.
let Some((included_header, included_hash)) =
fetch_included_from_relay_chain(relay_client, backend, params.para_id, params.relay_parent)
.await?
else {
return Ok(Default::default());
};
let only_included = vec![PotentialParent {
hash: included_hash,
header: included_header.clone(),
depth: 0,
aligned_with_pending: true,
}];
if params.max_depth == 0 {
return Ok(only_included);
};
// Pending header and hash.
let maybe_pending = {
// Fetch the most recent pending header from the relay chain. We use
// `OccupiedCoreAssumption::Included` so the candidate pending availability gets enacted
// before being returned to us.
let pending_header = relay_client
.persisted_validation_data(
params.relay_parent,
params.para_id,
OccupiedCoreAssumption::Included,
)
.await?
.and_then(|p| B::Header::decode(&mut &p.parent_head.0[..]).ok())
.filter(|x| x.hash() != included_hash);
// If the pending block is not locally known, we can't do anything.
if let Some(header) = pending_header {
let pending_hash = header.hash();
match backend.blockchain().header(pending_hash) {
// We are supposed to ignore branches that don't contain the pending block, but we
// do not know the pending block locally.
Ok(None) | Err(_) if params.ignore_alternative_branches => {
tracing::warn!(
target: PARENT_SEARCH_LOG_TARGET,
%pending_hash,
"Failed to get header for pending block.",
);
return Ok(Default::default());
},
Ok(Some(_)) => Some((header, pending_hash)),
_ => None,
}
} else {
None
}
};
let maybe_route_to_last_pending = maybe_pending
.as_ref()
.map(|(_, pending)| {
pezsp_blockchain::tree_route(backend.blockchain(), included_hash, *pending)
})
.transpose()?;
// If we want to ignore alternative branches there is no reason to start
// the parent search at the included block. We can add the included block and
// the path to the pending block to the potential parents directly (limited by max_depth).
let (frontier, potential_parents) = match (
&maybe_pending,
params.ignore_alternative_branches,
&maybe_route_to_last_pending,
) {
(Some((pending_header, pending_hash)), true, Some(ref route_to_pending)) => {
let mut potential_parents = only_included;
// This is a defensive check, should never happen.
if !route_to_pending.retracted().is_empty() {
tracing::warn!(target: PARENT_SEARCH_LOG_TARGET, "Included block not an ancestor of pending block. This should not happen.");
return Ok(Default::default());
}
// Add all items on the path included -> pending - 1 to the potential parents, but
// not more than `max_depth`.
let num_parents_on_path =
route_to_pending.enacted().len().saturating_sub(1).min(params.max_depth);
for (num, block) in
route_to_pending.enacted().iter().take(num_parents_on_path).enumerate()
{
let Ok(Some(header)) = backend.blockchain().header(block.hash) else { continue };
potential_parents.push(PotentialParent {
hash: block.hash,
header,
depth: 1 + num,
aligned_with_pending: true,
});
}
// The search for additional potential parents should now start at the children of
// the pending block.
(
vec![PotentialParent {
hash: *pending_hash,
header: pending_header.clone(),
depth: route_to_pending.enacted().len(),
aligned_with_pending: true,
}],
potential_parents,
)
},
_ => (only_included, Default::default()),
};
if potential_parents.len() > params.max_depth {
return Ok(potential_parents);
}
// Build up the ancestry record of the relay chain to compare against.
let rp_ancestry =
build_relay_parent_ancestry(params.ancestry_lookback, params.relay_parent, relay_client)
.await?;
Ok(search_child_branches_for_parents(
frontier,
maybe_route_to_last_pending,
included_header,
maybe_pending.map(|(_, hash)| hash),
backend,
params.max_depth,
params.ignore_alternative_branches,
rp_ancestry,
potential_parents,
))
}
/// Fetch the included block from the relay chain.
async fn fetch_included_from_relay_chain<B: BlockT>(
relay_client: &impl RelayChainInterface,
backend: &impl Backend<B>,
para_id: ParaId,
relay_parent: RelayHash,
) -> Result<Option<(B::Header, B::Hash)>, RelayChainError> {
// Fetch the pending header from the relay chain. We use `OccupiedCoreAssumption::TimedOut`
// so that even if there is a pending candidate, we assume it is timed out and we get the
// included head.
let included_header = relay_client
.persisted_validation_data(relay_parent, para_id, OccupiedCoreAssumption::TimedOut)
.await?;
let included_header = match included_header {
Some(pvd) => pvd.parent_head,
None => return Ok(None), // this implies the para doesn't exist.
};
let included_header = match B::Header::decode(&mut &included_header.0[..]).ok() {
None => return Ok(None),
Some(x) => x,
};
let included_hash = included_header.hash();
// If the included block is not locally known, we can't do anything.
match backend.blockchain().header(included_hash) {
Ok(None) => {
tracing::warn!(
target: PARENT_SEARCH_LOG_TARGET,
%included_hash,
"Failed to get header for included block.",
);
return Ok(None);
},
Err(e) => {
tracing::warn!(
target: PARENT_SEARCH_LOG_TARGET,
%included_hash,
%e,
"Failed to get header for included block.",
);
return Ok(None);
},
_ => {},
};
Ok(Some((included_header, included_hash)))
}
/// Build an ancestry of relay parents that are acceptable.
///
/// An acceptable relay parent is one that is no more than `ancestry_lookback` + 1 blocks below the
/// relay parent we want to build on. Teyrchain blocks anchored on relay parents older than that can
/// not be considered potential parents for block building. They have no chance of still getting
/// included, so our newly build teyrchain block would also not get included.
///
/// On success, returns a vector of `(header_hash, state_root)` of the relevant relay chain
/// ancestry blocks.
async fn build_relay_parent_ancestry(
ancestry_lookback: usize,
relay_parent: RelayHash,
relay_client: &impl RelayChainInterface,
) -> Result<Vec<(RelayHash, RelayHash)>, RelayChainError> {
let mut ancestry = Vec::with_capacity(ancestry_lookback + 1);
let mut current_rp = relay_parent;
let mut required_session = None;
while ancestry.len() <= ancestry_lookback {
let Some(header) = relay_client.header(RBlockId::hash(current_rp)).await? else { break };
let session = relay_client.session_index_for_child(current_rp).await?;
if required_session.get_or_insert(session) != &session {
// Respect the relay-chain rule not to cross session boundaries.
break;
}
ancestry.push((current_rp, *header.state_root()));
current_rp = *header.parent_hash();
// don't iterate back into the genesis block.
if header.number == 1 {
break;
}
}
Ok(ancestry)
}
/// Start search for child blocks that can be used as parents.
pub fn search_child_branches_for_parents<Block: BlockT>(
mut frontier: Vec<PotentialParent<Block>>,
maybe_route_to_last_pending: Option<TreeRoute<Block>>,
included_header: Block::Header,
pending_hash: Option<Block::Hash>,
backend: &impl Backend<Block>,
max_depth: usize,
ignore_alternative_branches: bool,
rp_ancestry: Vec<(RelayHash, RelayHash)>,
mut potential_parents: Vec<PotentialParent<Block>>,
) -> Vec<PotentialParent<Block>> {
let included_hash = included_header.hash();
let is_hash_in_ancestry = |hash| rp_ancestry.iter().any(|x| x.0 == hash);
let is_root_in_ancestry = |root| rp_ancestry.iter().any(|x| x.1 == root);
// The distance between pending and included block. Is later used to check if a child
// is aligned with pending when it is between pending and included block.
let pending_distance = maybe_route_to_last_pending.as_ref().map(|route| route.enacted().len());
// If a block is on the path included -> pending, we consider it `aligned_with_pending`.
let is_child_pending = |hash| {
maybe_route_to_last_pending
.as_ref()
.map_or(true, |route| route.enacted().iter().any(|x| x.hash == hash))
};
tracing::trace!(
target: PARENT_SEARCH_LOG_TARGET,
?included_hash,
included_num = ?included_header.number(),
?pending_hash ,
?rp_ancestry,
"Searching relay chain ancestry."
);
while let Some(entry) = frontier.pop() {
let is_pending = pending_hash.as_ref().map_or(false, |h| &entry.hash == h);
let is_included = included_hash == entry.hash;
// note: even if the pending block or included block have a relay parent
// outside of the expected part of the relay chain, they are always allowed
// because they have already been posted on chain.
let is_potential = is_pending || is_included || {
let digest = entry.header.digest();
let is_hash_in_ancestry_check = cumulus_primitives_core::extract_relay_parent(digest)
.map_or(false, is_hash_in_ancestry);
let is_root_in_ancestry_check =
cumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(digest)
.map(|(r, _n)| r)
.map_or(false, is_root_in_ancestry);
is_hash_in_ancestry_check || is_root_in_ancestry_check
};
let parent_aligned_with_pending = entry.aligned_with_pending;
let child_depth = entry.depth + 1;
let hash = entry.hash;
tracing::trace!(
target: PARENT_SEARCH_LOG_TARGET,
?hash,
is_potential,
is_pending,
is_included,
"Checking potential parent."
);
if is_potential {
potential_parents.push(entry);
}
if !is_potential || child_depth > max_depth {
continue;
}
// push children onto search frontier.
for child in backend.blockchain().children(hash).ok().into_iter().flatten() {
tracing::trace!(target: PARENT_SEARCH_LOG_TARGET, ?child, child_depth, ?pending_distance, "Looking at child.");
let aligned_with_pending = parent_aligned_with_pending &&
(pending_distance.map_or(true, |dist| child_depth > dist) ||
is_child_pending(child));
if ignore_alternative_branches && !aligned_with_pending {
tracing::trace!(target: PARENT_SEARCH_LOG_TARGET, ?child, "Child is not aligned with pending block.");
continue;
}
let Ok(Some(header)) = backend.blockchain().header(child) else { continue };
frontier.push(PotentialParent {
hash: child,
header,
depth: child_depth,
aligned_with_pending,
});
}
}
potential_parents
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,544 @@
// 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 <https://www.gnu.org/licenses/>.
use cumulus_relay_chain_streams::{finalized_heads, new_best_heads};
use pezsc_client_api::{
Backend, BlockBackend, BlockImportNotification, BlockchainEvents, Finalizer, UsageProvider,
};
use pezsc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy};
use schnellru::{ByLength, LruMap};
use pezsp_blockchain::Error as ClientError;
use pezsp_consensus::{BlockOrigin, BlockStatus};
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
use cumulus_client_pov_recovery::{RecoveryKind, RecoveryRequest};
use cumulus_relay_chain_interface::RelayChainInterface;
use pezkuwi_primitives::Id as ParaId;
use codec::Decode;
use futures::{
channel::mpsc::{Sender, UnboundedSender},
pin_mut, select, FutureExt, SinkExt, Stream, StreamExt,
};
use pezsp_core::traits::SpawnEssentialNamed;
use std::sync::Arc;
const LOG_TARGET: &str = "pezcumulus-consensus";
const FINALIZATION_CACHE_SIZE: u32 = 40;
fn handle_new_finalized_head<P, Block, B>(
teyrchain: &Arc<P>,
header: Block::Header,
last_seen_finalized_hashes: &mut LruMap<Block::Hash, ()>,
) where
Block: BlockT,
B: Backend<Block>,
P: Finalizer<Block, B> + UsageProvider<Block> + BlockchainEvents<Block>,
{
let hash = header.hash();
last_seen_finalized_hashes.insert(hash, ());
// Only finalize if we are below the incoming finalized teyrchain head
if teyrchain.usage_info().chain.finalized_number < *header.number() {
tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Attempting to finalize header.",
);
if let Err(e) = teyrchain.finalize_block(hash, None, true) {
match e {
ClientError::UnknownBlock(_) => tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Could not finalize block because it is unknown.",
),
_ => tracing::warn!(
target: LOG_TARGET,
error = ?e,
block_hash = ?hash,
"Failed to finalize block",
),
}
}
}
}
/// Streams finalized teyrchain heads from the relay chain.
///
/// This worker continuously monitors the relay chain for finalized blocks and extracts
/// the corresponding teyrchain head data for the given `para_id`. The extracted head
/// data is sent through the provided channel for consumption by the consensus system.
///
/// This is necessary because finalization of blocks can take a long
/// time. During this blocking operation, we should not keep references to finality notifications,
/// because that prevents the corresponding blocks from getting pruned.
pub async fn finalized_head_stream_worker<R: RelayChainInterface + Clone, Block: BlockT>(
mut tx: UnboundedSender<Block::Header>,
para_id: ParaId,
relay_chain: R,
) {
let finalized_heads = match finalized_heads(relay_chain.clone(), para_id).await {
Ok(finalized_heads_stream) => finalized_heads_stream.fuse(),
Err(err) => {
tracing::error!(target: LOG_TARGET, error = ?err, "Unable to retrieve finalized heads stream.");
return;
},
};
pin_mut!(finalized_heads);
loop {
if let Some((head_data, _)) = finalized_heads.next().await {
let header = match Block::Header::decode(&mut &head_data[..]) {
Ok(header) => header,
Err(err) => {
tracing::debug!(
target: LOG_TARGET,
error = ?err,
"Could not decode teyrchain header while following finalized heads.",
);
continue;
},
};
if let Err(e) = tx.send(header).await {
tracing::error!(target: LOG_TARGET, ?e, "Error while sending finalized head.");
return;
};
}
}
}
/// Follow the finalized head of the given teyrchain.
///
/// For every finalized block of the relay chain, it will get the included teyrchain header
/// corresponding to `para_id` and will finalize it in the teyrchain.
async fn follow_finalized_head<P, Block, B>(
teyrchain: Arc<P>,
finalized_head_stream: Box<impl Stream<Item = Block::Header> + Unpin + Send>,
) where
Block: BlockT,
P: Finalizer<Block, B> + UsageProvider<Block> + BlockchainEvents<Block>,
B: Backend<Block>,
{
let mut imported_blocks = teyrchain.import_notification_stream().fuse();
let mut finalized_head_stream = finalized_head_stream.fuse();
// We use this cache to finalize blocks that are imported late.
// For example, a block that has been recovered via PoV-Recovery
// on a full node can have several minutes delay. With this cache
// we have some "memory" of recently finalized blocks.
let mut last_seen_finalized_hashes = LruMap::new(ByLength::new(FINALIZATION_CACHE_SIZE));
loop {
select! {
fin = finalized_head_stream.next() => {
match fin {
Some(finalized_head) =>
handle_new_finalized_head(&teyrchain, finalized_head, &mut last_seen_finalized_hashes),
None => {
tracing::debug!(target: LOG_TARGET, "Stopping following finalized head.");
return
}
}
},
imported = imported_blocks.next() => {
match imported {
Some(imported_block) => {
// When we see a block import that is already finalized, we immediately finalize it.
if last_seen_finalized_hashes.peek(&imported_block.hash).is_some() {
tracing::debug!(
target: LOG_TARGET,
block_hash = ?imported_block.hash,
"Setting newly imported block as finalized.",
);
if let Err(e) = teyrchain.finalize_block(imported_block.hash, None, true) {
match e {
ClientError::UnknownBlock(_) => tracing::debug!(
target: LOG_TARGET,
block_hash = ?imported_block.hash,
"Could not finalize block because it is unknown.",
),
_ => tracing::warn!(
target: LOG_TARGET,
error = ?e,
block_hash = ?imported_block.hash,
"Failed to finalize block",
),
}
}
}
},
None => {
tracing::debug!(
target: LOG_TARGET,
"Stopping following imported blocks.",
);
return
}
}
}
}
}
}
/// Spawns the essential finalization tasks for teyrchain consensus.
///
/// This function creates and spawns two critical background tasks:
/// 1. A finalized head stream worker that monitors relay chain finality and extracts included
/// headers
/// 2. The main teyrchain consensus task that handles finalization and best block updates
pub fn spawn_teyrchain_consensus_tasks<P, R, Block, B, S>(
para_id: ParaId,
teyrchain: Arc<P>,
relay_chain: R,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
recovery_chan_tx: Option<Sender<RecoveryRequest<Block>>>,
spawn_handle: S,
) where
Block: BlockT,
P: Finalizer<Block, B>
+ UsageProvider<Block>
+ Send
+ Sync
+ BlockBackend<Block>
+ BlockchainEvents<Block>
+ 'static,
for<'a> &'a P: BlockImport<Block>,
R: RelayChainInterface + Clone + 'static,
S: SpawnEssentialNamed + 'static,
B: Backend<Block> + 'static,
{
let (tx, rx) = futures::channel::mpsc::unbounded();
let worker = finalized_head_stream_worker::<_, Block>(tx, para_id, relay_chain.clone());
let consensus = run_teyrchain_consensus(
para_id,
teyrchain,
relay_chain,
announce_block,
Box::new(rx),
recovery_chan_tx,
);
spawn_handle.spawn_essential_blocking("pezcumulus-consensus", None, Box::pin(consensus));
spawn_handle.spawn_essential_blocking(
"pezcumulus-consensus-finality-stream",
None,
Box::pin(worker),
);
}
/// Run the teyrchain consensus.
///
/// This will follow the given `relay_chain` to act as consensus for the teyrchain that corresponds
/// to the given `para_id`. It will set the new best block of the teyrchain as it gets aware of it.
/// The same happens for the finalized block.
///
/// # Note
///
/// This will access the backend of the teyrchain and thus, this future should be spawned as
/// blocking task.
pub async fn run_teyrchain_consensus<P, R, Block, B>(
para_id: ParaId,
teyrchain: Arc<P>,
relay_chain: R,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
finalized_head_stream: Box<impl Stream<Item = Block::Header> + Unpin + Send>,
recovery_chan_tx: Option<Sender<RecoveryRequest<Block>>>,
) where
Block: BlockT,
P: Finalizer<Block, B>
+ UsageProvider<Block>
+ Send
+ Sync
+ BlockBackend<Block>
+ BlockchainEvents<Block>,
for<'a> &'a P: BlockImport<Block>,
R: RelayChainInterface + Clone,
B: Backend<Block>,
{
let follow_new_best = follow_new_best(
para_id,
teyrchain.clone(),
relay_chain.clone(),
announce_block,
recovery_chan_tx,
);
let follow_finalized_head = follow_finalized_head(teyrchain, finalized_head_stream);
select! {
_ = follow_new_best.fuse() => {},
_ = follow_finalized_head.fuse() => {},
}
}
/// Follow the relay chain new best head, to update the Teyrchain new best head.
async fn follow_new_best<P, R, Block, B>(
para_id: ParaId,
teyrchain: Arc<P>,
relay_chain: R,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
mut recovery_chan_tx: Option<Sender<RecoveryRequest<Block>>>,
) where
Block: BlockT,
P: Finalizer<Block, B>
+ UsageProvider<Block>
+ Send
+ Sync
+ BlockBackend<Block>
+ BlockchainEvents<Block>,
for<'a> &'a P: BlockImport<Block>,
R: RelayChainInterface + Clone,
B: Backend<Block>,
{
let new_best_heads = match new_best_heads(relay_chain, para_id).await {
Ok(best_heads_stream) => best_heads_stream.fuse(),
Err(err) => {
tracing::error!(target: LOG_TARGET, error = ?err, "Unable to retrieve best heads stream.");
return;
},
};
pin_mut!(new_best_heads);
let mut imported_blocks = teyrchain.import_notification_stream().fuse();
// The unset best header of the teyrchain. Will be `Some(_)` when we have imported a relay chain
// block before the associated teyrchain block. In this case we need to wait for this block to
// be imported to set it as new best.
let mut unset_best_header = None;
loop {
select! {
h = new_best_heads.next() => {
match h {
Some(h) => handle_new_best_teyrchain_head(
h,
&*teyrchain,
&mut unset_best_header,
recovery_chan_tx.as_mut(),
).await,
None => {
tracing::debug!(
target: LOG_TARGET,
"Stopping following new best.",
);
return
}
}
},
i = imported_blocks.next() => {
match i {
Some(i) => handle_new_block_imported(
i,
&mut unset_best_header,
&*teyrchain,
&*announce_block,
).await,
None => {
tracing::debug!(
target: LOG_TARGET,
"Stopping following imported blocks.",
);
return
}
}
},
}
}
}
/// Handle a new import block of the teyrchain.
async fn handle_new_block_imported<Block, P>(
notification: BlockImportNotification<Block>,
unset_best_header_opt: &mut Option<Block::Header>,
teyrchain: &P,
announce_block: &(dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync),
) where
Block: BlockT,
P: UsageProvider<Block> + Send + Sync + BlockBackend<Block>,
for<'a> &'a P: BlockImport<Block>,
{
// HACK
//
// Remove after https://github.com/pezkuwichain/kurdistan-sdk/issues/76 or similar is merged
if notification.origin != BlockOrigin::Own {
announce_block(notification.hash, None);
}
let unset_best_header = match (notification.is_new_best, &unset_best_header_opt) {
// If this is the new best block or we don't have any unset block, we can end it here.
(true, _) | (_, None) => return,
(false, Some(ref u)) => u,
};
let unset_hash = if notification.header.number() < unset_best_header.number() {
return;
} else if notification.header.number() == unset_best_header.number() {
let unset_hash = unset_best_header.hash();
if unset_hash != notification.hash {
return;
} else {
unset_hash
}
} else {
unset_best_header.hash()
};
match teyrchain.block_status(unset_hash) {
Ok(BlockStatus::InChainWithState) => {
let unset_best_header = unset_best_header_opt
.take()
.expect("We checked above that the value is set; qed");
tracing::debug!(
target: LOG_TARGET,
?unset_hash,
"Importing block as new best for teyrchain.",
);
import_block_as_new_best(unset_hash, unset_best_header, teyrchain).await;
},
state => tracing::debug!(
target: LOG_TARGET,
?unset_best_header,
?notification.header,
?state,
"Unexpected state for unset best header.",
),
}
}
/// Handle the new best teyrchain head as extracted from the new best relay chain.
async fn handle_new_best_teyrchain_head<Block, P>(
head: Vec<u8>,
teyrchain: &P,
unset_best_header: &mut Option<Block::Header>,
mut recovery_chan_tx: Option<&mut Sender<RecoveryRequest<Block>>>,
) where
Block: BlockT,
P: UsageProvider<Block> + Send + Sync + BlockBackend<Block>,
for<'a> &'a P: BlockImport<Block>,
{
let teyrchain_head = match <<Block as BlockT>::Header>::decode(&mut &head[..]) {
Ok(header) => header,
Err(err) => {
tracing::debug!(
target: LOG_TARGET,
error = ?err,
"Could not decode Teyrchain header while following best heads.",
);
return;
},
};
let hash = teyrchain_head.hash();
if teyrchain.usage_info().chain.best_hash == hash {
tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Skipping set new best block, because block is already the best.",
);
return;
}
// Make sure the block is already known or otherwise we skip setting new best.
match teyrchain.block_status(hash) {
Ok(BlockStatus::InChainWithState) => {
unset_best_header.take();
tracing::debug!(
target: LOG_TARGET,
included = ?hash,
"Importing block as new best for teyrchain.",
);
import_block_as_new_best(hash, teyrchain_head, teyrchain).await;
},
Ok(BlockStatus::InChainPruned) => {
tracing::error!(
target: LOG_TARGET,
block_hash = ?hash,
"Trying to set pruned block as new best!",
);
},
Ok(BlockStatus::Unknown) => {
*unset_best_header = Some(teyrchain_head);
tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Teyrchain block not yet imported, waiting for import to enact as best block.",
);
if let Some(ref mut recovery_chan_tx) = recovery_chan_tx {
// Best effort channel to actively encourage block recovery.
// An error here is not fatal; the relay chain continuously re-announces
// the best block, thus we will have other opportunities to retry.
let req = RecoveryRequest { hash, kind: RecoveryKind::Full };
if let Err(err) = recovery_chan_tx.try_send(req) {
tracing::warn!(
target: LOG_TARGET,
block_hash = ?hash,
error = ?err,
"Unable to notify block recovery subsystem"
)
}
}
},
Err(e) => {
tracing::error!(
target: LOG_TARGET,
block_hash = ?hash,
error = ?e,
"Failed to get block status of block.",
);
},
_ => {},
}
}
async fn import_block_as_new_best<Block, P>(hash: Block::Hash, header: Block::Header, teyrchain: &P)
where
Block: BlockT,
P: UsageProvider<Block> + Send + Sync + BlockBackend<Block>,
for<'a> &'a P: BlockImport<Block>,
{
let best_number = teyrchain.usage_info().chain.best_number;
if *header.number() < best_number {
tracing::debug!(
target: LOG_TARGET,
%best_number,
block_number = %header.number(),
"Skipping importing block as new best block, because there already exists a \
best block with an higher number",
);
return;
}
// Make it the new best block
let mut block_import_params = BlockImportParams::new(BlockOrigin::ConsensusBroadcast, header);
block_import_params.fork_choice = Some(ForkChoiceStrategy::Custom(true));
block_import_params.import_existing = true;
if let Err(err) = teyrchain.import_block(block_import_params).await {
tracing::warn!(
target: LOG_TARGET,
block_hash = ?hash,
error = ?err,
"Failed to set new best block.",
);
}
}
@@ -0,0 +1,45 @@
[package]
name = "pezcumulus-client-consensus-proposer"
description = "A Bizinikiwi `Proposer` for building teyrchain blocks"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true, default-features = true }
async-trait = { workspace = true }
thiserror = { workspace = true }
# Bizinikiwi
pezsc-basic-authorship = { workspace = true }
pezsc-block-builder = { workspace = true }
pezsc-transaction-pool-api = { workspace = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-inherents = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-primitives-teyrchain-inherent = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-primitives-teyrchain-inherent/runtime-benchmarks",
"pezsc-basic-authorship/runtime-benchmarks",
"pezsc-block-builder/runtime-benchmarks",
"pezsc-transaction-pool-api/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-inherents/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
]
@@ -0,0 +1,126 @@
// 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 <https://www.gnu.org/licenses/>.
//! The Pezcumulus [`ProposerInterface`] is an extension of the Bizinikiwi [`ProposerFactory`]
//! for creating new teyrchain blocks.
//!
//! This utility is designed to be composed within any collator consensus algorithm.
use async_trait::async_trait;
use cumulus_primitives_teyrchain_inherent::TeyrchainInherentData;
use pezsc_basic_authorship::{ProposeArgs, ProposerFactory};
use pezsc_block_builder::BlockBuilderApi;
use pezsc_transaction_pool_api::TransactionPool;
use pezsp_api::{ApiExt, CallApiAt, ProvideRuntimeApi};
use pezsp_blockchain::HeaderBackend;
use pezsp_consensus::{EnableProofRecording, Environment, Proposal};
use pezsp_inherents::{InherentData, InherentDataProvider};
use pezsp_runtime::{traits::Block as BlockT, Digest};
use pezsp_state_machine::StorageProof;
use std::{fmt::Debug, time::Duration};
/// Errors that can occur when proposing a teyrchain block.
#[derive(thiserror::Error, Debug)]
#[error(transparent)]
pub struct Error {
inner: anyhow::Error,
}
impl Error {
/// Create an error tied to the creation of a proposer.
pub fn proposer_creation(err: impl Into<anyhow::Error>) -> Self {
Error { inner: err.into().context("Proposer Creation") }
}
/// Create an error tied to the proposing logic itself.
pub fn proposing(err: impl Into<anyhow::Error>) -> Self {
Error { inner: err.into().context("Proposing") }
}
}
/// A type alias for easily referring to the type of a proposal produced by a specific
/// [`ProposerInterface`].
pub type ProposalOf<B> = Proposal<B, StorageProof>;
/// An interface for proposers.
#[async_trait]
pub trait ProposerInterface<Block: BlockT> {
/// Propose a collation using the supplied `InherentData` and the provided
/// `TeyrchainInherentData`.
///
/// Also specify any required inherent digests, the maximum proposal duration,
/// and the block size limit in bytes. See the documentation on
/// [`pezsp_consensus::Proposer::propose`] for more details on how to interpret these parameters.
///
/// The `InherentData` and `Digest` are left deliberately general in order to accommodate
/// all possible collator selection algorithms or inherent creation mechanisms,
/// while the `TeyrchainInherentData` is made explicit so it will be constructed appropriately.
///
/// If the `InherentData` passed into this function already has a `TeyrchainInherentData`,
/// this should throw an error.
async fn propose(
&mut self,
parent_header: &Block::Header,
paras_inherent_data: &TeyrchainInherentData,
other_inherent_data: InherentData,
inherent_digests: Digest,
max_duration: Duration,
block_size_limit: Option<usize>,
) -> Result<Option<Proposal<Block, StorageProof>>, Error>;
}
#[async_trait]
impl<Block, A, C> ProposerInterface<Block> for ProposerFactory<A, C, EnableProofRecording>
where
A: TransactionPool<Block = Block> + 'static,
C: HeaderBackend<Block> + ProvideRuntimeApi<Block> + CallApiAt<Block> + Send + Sync + 'static,
C::Api: ApiExt<Block> + BlockBuilderApi<Block>,
Block: pezsp_runtime::traits::Block,
{
async fn propose(
&mut self,
parent_header: &Block::Header,
paras_inherent_data: &TeyrchainInherentData,
other_inherent_data: InherentData,
inherent_digests: Digest,
max_duration: Duration,
block_size_limit: Option<usize>,
) -> Result<Option<Proposal<Block, StorageProof>>, Error> {
let proposer = self
.init(parent_header)
.await
.map_err(|e| Error::proposer_creation(anyhow::Error::new(e)))?;
let mut inherent_data = other_inherent_data;
paras_inherent_data
.provide_inherent_data(&mut inherent_data)
.await
.map_err(|e| Error::proposing(anyhow::Error::new(e)))?;
proposer
.propose_block(ProposeArgs {
inherent_data,
inherent_digests,
max_duration,
block_size_limit,
ignored_nodes_by_proof_recording: None,
})
.await
.map(Some)
.map_err(|e| Error::proposing(anyhow::Error::new(e)).into())
}
}
@@ -0,0 +1,48 @@
[package]
name = "pezcumulus-client-consensus-relay-chain"
description = "The relay-chain provided consensus algorithm"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-trait = { workspace = true }
futures = { workspace = true }
parking_lot = { workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
prometheus-endpoint = { workspace = true, default-features = true }
pezsc-consensus = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-block-builder = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-inherents = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-client-consensus-common = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-client-consensus-common/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezsc-consensus/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-block-builder/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-inherents/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
@@ -0,0 +1,88 @@
// 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 <https://www.gnu.org/licenses/>.
use std::{marker::PhantomData, sync::Arc};
use pezsc_consensus::{import_queue::Verifier as VerifierT, BlockImportParams};
use pezsp_api::ProvideRuntimeApi;
use pezsp_block_builder::BlockBuilder as BlockBuilderApi;
use pezsp_inherents::CreateInherentDataProviders;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
/// A verifier that just checks the inherents.
pub struct Verifier<Client, Block, CIDP> {
client: Arc<Client>,
create_inherent_data_providers: CIDP,
_marker: PhantomData<Block>,
}
impl<Client, Block, CIDP> Verifier<Client, Block, CIDP> {
/// Create a new instance.
pub fn new(client: Arc<Client>, create_inherent_data_providers: CIDP) -> Self {
Self { client, create_inherent_data_providers, _marker: PhantomData }
}
}
#[async_trait::async_trait]
impl<Client, Block, CIDP> VerifierT<Block> for Verifier<Client, Block, CIDP>
where
Block: BlockT,
Client: ProvideRuntimeApi<Block> + Send + Sync,
<Client as ProvideRuntimeApi<Block>>::Api: BlockBuilderApi<Block>,
CIDP: CreateInherentDataProviders<Block, ()>,
{
async fn verify(
&self,
mut block_params: BlockImportParams<Block>,
) -> Result<BlockImportParams<Block>, String> {
block_params.fork_choice = Some(pezsc_consensus::ForkChoiceStrategy::Custom(
block_params.origin == pezsp_consensus::BlockOrigin::NetworkInitialSync,
));
// Skip checks that include execution, if being told so, or when importing only state.
//
// This is done for example when gap syncing and it is expected that the block after the gap
// was checked/chosen properly, e.g. by warp syncing to this block using a finality proof.
if block_params.state_action.skip_execution_checks() || block_params.with_state() {
return Ok(block_params);
}
if let Some(inner_body) = block_params.body.take() {
let inherent_data_providers = self
.create_inherent_data_providers
.create_inherent_data_providers(*block_params.header.parent_hash(), ())
.await
.map_err(|e| e.to_string())?;
let block = Block::new(block_params.header.clone(), inner_body);
pezsp_block_builder::check_inherents(
self.client.clone(),
*block.header().parent_hash(),
block.clone(),
&inherent_data_providers,
)
.await
.map_err(|e| format!("Error checking block inherents {:?}", e))?;
let (_, inner_body) = block.deconstruct();
block_params.body = Some(inner_body);
}
block_params.post_hash = Some(block_params.header.hash());
Ok(block_params)
}
}
@@ -0,0 +1,39 @@
// 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 <https://www.gnu.org/licenses/>.
//! The relay-chain provided consensus algorithm for teyrchains.
//!
//! This is the simplest consensus algorithm you can use when developing a teyrchain. It is a
//! permission-less consensus algorithm that doesn't require any staking or similar to join as a
//! collator. In this algorithm the consensus is provided by the relay-chain. This works in the
//! following way.
//!
//! 1. Each node that sees itself as a collator is free to build a teyrchain candidate.
//!
//! 2. This teyrchain candidate is send to the teyrchain validators that are part of the relay
//! chain.
//!
//! 3. The teyrchain validators validate at most X different teyrchain candidates, where X is the
//! total number of teyrchain validators.
//!
//! 4. The teyrchain candidate that is backed by the most validators is chosen by the relay-chain
//! block producer to be added as backed candidate on chain.
//!
//! 5. After the teyrchain candidate got backed and included, all collators start at 1.
mod import_queue;
pub use import_queue::Verifier;
+80
View File
@@ -0,0 +1,80 @@
[package]
name = "pezcumulus-client-network"
version = "0.7.0"
authors.workspace = true
description = "Pezcumulus-specific networking protocol"
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-trait = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
futures = { workspace = true }
futures-timer = { workspace = true }
parking_lot = { workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
pezsc-client-api = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-version = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-node-primitives = { workspace = true, default-features = true }
pezkuwi-node-subsystem = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
pezkuwi-primitives-test-helpers = { workspace = true, default-features = true }
pezkuwi-teyrchain-primitives = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
[dev-dependencies]
rstest = { workspace = true }
tokio = { features = ["macros"], workspace = true, default-features = true }
# Bizinikiwi
pezsp-keyring = { workspace = true, default-features = true }
pezsp-keystore = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-test-client = { workspace = true }
# Pezcumulus
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-inprocess-interface = { workspace = true, default-features = true }
pezcumulus-test-service = { workspace = true }
[features]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-inprocess-interface/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezcumulus-test-service/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-primitives-test-helpers/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezkuwi-test-client/runtime-benchmarks",
"pezkuwi-teyrchain-primitives/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-keyring/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
"pezsp-version/runtime-benchmarks",
]
+543
View File
@@ -0,0 +1,543 @@
// 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 <https://www.gnu.org/licenses/>.
//! Teyrchain specific networking
//!
//! Provides a custom block announcement implementation for teyrchains
//! that use the relay chain provided consensus. See [`RequireSecondedInBlockAnnounce`]
//! and [`WaitToAnnounce`] for more information about this implementation.
use pezsp_api::RuntimeApiInfo;
use pezsp_consensus::block_validation::{
BlockAnnounceValidator as BlockAnnounceValidatorT, Validation,
};
use pezsp_core::traits::SpawnNamed;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
use cumulus_relay_chain_interface::RelayChainInterface;
use pezkuwi_node_primitives::{CollationSecondedSignal, Statement};
use pezkuwi_node_subsystem::messages::RuntimeApiRequest;
use pezkuwi_primitives::{
CandidateReceiptV2 as CandidateReceipt, CompactStatement, Hash as PHash, Id as ParaId,
OccupiedCoreAssumption, SigningContext, UncheckedSigned,
};
use pezkuwi_teyrchain_primitives::primitives::HeadData;
use codec::{Decode, DecodeAll, Encode};
use futures::{channel::oneshot, future::FutureExt, Future};
use std::{fmt, marker::PhantomData, pin::Pin, sync::Arc};
#[cfg(test)]
mod tests;
const LOG_TARGET: &str = "sync::pezcumulus";
type BoxedError = Box<dyn std::error::Error + Send>;
#[derive(Debug)]
struct BlockAnnounceError(String);
impl std::error::Error for BlockAnnounceError {}
impl fmt::Display for BlockAnnounceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
/// The data that we attach to a block announcement.
///
/// This will be used to prove that a header belongs to a block that is probably being backed by
/// the relay chain.
#[derive(Encode, Debug)]
pub struct BlockAnnounceData {
/// The receipt identifying the candidate.
receipt: CandidateReceipt,
/// The seconded statement issued by a relay chain validator that approves the candidate.
statement: UncheckedSigned<CompactStatement>,
/// The relay parent that was used as context to sign the [`Self::statement`].
relay_parent: PHash,
}
impl Decode for BlockAnnounceData {
fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
let receipt = CandidateReceipt::decode(input)?;
let statement = UncheckedSigned::<CompactStatement>::decode(input)?;
let relay_parent = match PHash::decode(input) {
Ok(p) => p,
// For being backwards compatible, we support missing relay-chain parent.
Err(_) => receipt.descriptor.relay_parent(),
};
Ok(Self { receipt, statement, relay_parent })
}
}
impl BlockAnnounceData {
/// Validate that the receipt, statement and announced header match.
///
/// This will not check the signature, for this you should use
/// [`BlockAnnounceData::check_signature`].
fn validate(&self, encoded_header: Vec<u8>) -> Result<(), Validation> {
let candidate_hash =
if let CompactStatement::Seconded(h) = self.statement.unchecked_payload() {
h
} else {
tracing::debug!(target: LOG_TARGET, "`CompactStatement` isn't the candidate variant!",);
return Err(Validation::Failure { disconnect: true });
};
if *candidate_hash != self.receipt.hash() {
tracing::debug!(
target: LOG_TARGET,
"Receipt candidate hash doesn't match candidate hash in statement",
);
return Err(Validation::Failure { disconnect: true });
}
if HeadData(encoded_header).hash() != self.receipt.descriptor.para_head() {
tracing::debug!(
target: LOG_TARGET,
"Receipt para head hash doesn't match the hash of the header in the block announcement",
);
return Err(Validation::Failure { disconnect: true });
}
Ok(())
}
/// Check the signature of the statement.
///
/// Returns an `Err(_)` if it failed.
async fn check_signature<RCInterface>(
self,
relay_chain_client: &RCInterface,
) -> Result<Validation, BlockAnnounceError>
where
RCInterface: RelayChainInterface + 'static,
{
let validator_index = self.statement.unchecked_validator_index();
let session_index =
match relay_chain_client.session_index_for_child(self.relay_parent).await {
Ok(r) => r,
Err(e) => return Err(BlockAnnounceError(format!("{:?}", e))),
};
let signing_context = SigningContext { parent_hash: self.relay_parent, session_index };
// Check that the signer is a legit validator.
let authorities = match relay_chain_client.validators(self.relay_parent).await {
Ok(r) => r,
Err(e) => return Err(BlockAnnounceError(format!("{:?}", e))),
};
let signer = match authorities.get(validator_index.0 as usize) {
Some(r) => r,
None => {
tracing::debug!(
target: LOG_TARGET,
"Block announcement justification signer is a validator index out of bound",
);
return Ok(Validation::Failure { disconnect: true });
},
};
// Check statement is correctly signed.
if self.statement.try_into_checked(&signing_context, signer).is_err() {
tracing::debug!(
target: LOG_TARGET,
"Block announcement justification signature is invalid.",
);
return Ok(Validation::Failure { disconnect: true });
}
Ok(Validation::Success { is_new_best: true })
}
}
impl TryFrom<&'_ CollationSecondedSignal> for BlockAnnounceData {
type Error = ();
fn try_from(signal: &CollationSecondedSignal) -> Result<BlockAnnounceData, ()> {
let receipt = if let Statement::Seconded(receipt) = signal.statement.payload() {
receipt.to_plain()
} else {
return Err(());
};
Ok(BlockAnnounceData {
receipt,
statement: signal.statement.convert_payload().into(),
relay_parent: signal.relay_parent,
})
}
}
/// A type alias for the [`RequireSecondedInBlockAnnounce`] validator.
#[deprecated = "This has been renamed to RequireSecondedInBlockAnnounce"]
pub type BlockAnnounceValidator<Block, RCInterface> =
RequireSecondedInBlockAnnounce<Block, RCInterface>;
/// Teyrchain specific block announce validator.
///
/// This is not required when the collation mechanism itself is sybil-resistant, as it is a spam
/// protection mechanism used to prevent nodes from dealing with unbounded numbers of blocks. For
/// sybil-resistant collation mechanisms, this will only slow things down.
///
/// This block announce validator is required if the teyrchain is running
/// with the relay chain provided consensus to make sure each node only
/// imports a reasonable number of blocks per round. The relay chain provided
/// consensus doesn't have any authorities and so it could happen that without
/// this special block announce validator a node would need to import *millions*
/// of blocks per round, which is clearly not doable.
///
/// To solve this problem, each block announcement is delayed until a collator
/// has received a [`Statement::Seconded`] for its `PoV`. This message tells the
/// collator that its `PoV` was validated successfully by a teyrchain validator and
/// that it is very likely that this `PoV` will be included in the relay chain. Every
/// collator that doesn't receive the message for its `PoV` will not announce its block.
/// For more information on the block announcement, see [`WaitToAnnounce`].
///
/// For each block announcement that is received, the generic block announcement validation
/// will call this validator and provides the extra data that was attached to the announcement.
/// We call this extra data `justification`.
/// It is expected that the attached data is a SCALE encoded [`BlockAnnounceData`]. The
/// statement is checked to be a [`CompactStatement::Seconded`] and that it is signed by an active
/// teyrchain validator.
///
/// If no justification was provided we check if the block announcement is at the tip of the known
/// chain. If it is at the tip, it is required to provide a justification or otherwise we reject
/// it. However, if the announcement is for a block below the tip the announcement is accepted
/// as it probably comes from a node that is currently syncing the chain.
#[derive(Clone)]
pub struct RequireSecondedInBlockAnnounce<Block, RCInterface> {
phantom: PhantomData<Block>,
relay_chain_interface: RCInterface,
para_id: ParaId,
}
impl<Block, RCInterface> RequireSecondedInBlockAnnounce<Block, RCInterface>
where
RCInterface: Clone,
{
/// Create a new [`RequireSecondedInBlockAnnounce`].
pub fn new(relay_chain_interface: RCInterface, para_id: ParaId) -> Self {
Self { phantom: Default::default(), relay_chain_interface, para_id }
}
}
impl<Block: BlockT, RCInterface> RequireSecondedInBlockAnnounce<Block, RCInterface>
where
RCInterface: RelayChainInterface + Clone,
{
/// Get the included block of the given teyrchain in the relay chain.
async fn included_block(
relay_chain_interface: &RCInterface,
hash: PHash,
para_id: ParaId,
) -> Result<Block::Header, BoxedError> {
let validation_data = relay_chain_interface
.persisted_validation_data(hash, para_id, OccupiedCoreAssumption::TimedOut)
.await
.map_err(|e| Box::new(BlockAnnounceError(format!("{:?}", e))) as Box<_>)?
.ok_or_else(|| {
Box::new(BlockAnnounceError("Could not find teyrchain head in relay chain".into()))
as Box<_>
})?;
let para_head =
Block::Header::decode(&mut &validation_data.parent_head.0[..]).map_err(|e| {
Box::new(BlockAnnounceError(format!("Failed to decode teyrchain head: {:?}", e)))
as Box<_>
})?;
Ok(para_head)
}
/// Get the backed block hashes of the given teyrchain in the relay chain.
async fn backed_block_hashes(
relay_chain_interface: &RCInterface,
hash: PHash,
para_id: ParaId,
) -> Result<impl Iterator<Item = PHash>, BoxedError> {
let runtime_api_version = relay_chain_interface
.version(hash)
.await
.map_err(|e| Box::new(BlockAnnounceError(format!("{:?}", e))) as Box<_>)?;
let teyrchain_host_runtime_api_version =
runtime_api_version
.api_version(
&<dyn pezkuwi_primitives::runtime_api::TeyrchainHost<
pezkuwi_primitives::Block,
>>::ID,
)
.unwrap_or_default();
// If the relay chain runtime does not support the new runtime API, fallback to the
// deprecated one.
let candidate_receipts = if teyrchain_host_runtime_api_version <
RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT
{
#[allow(deprecated)]
relay_chain_interface
.candidate_pending_availability(hash, para_id)
.await
.map(|c| c.into_iter().collect::<Vec<_>>())
} else {
relay_chain_interface.candidates_pending_availability(hash, para_id).await
}
.map_err(|e| Box::new(BlockAnnounceError(format!("{:?}", e))) as Box<_>)?;
Ok(candidate_receipts.into_iter().map(|cr| cr.descriptor.para_head()))
}
/// Handle a block announcement with empty data (no statement) attached to it.
async fn handle_empty_block_announce_data(
&self,
header: Block::Header,
) -> Result<Validation, BoxedError> {
let relay_chain_interface = self.relay_chain_interface.clone();
let para_id = self.para_id;
// Check if block is equal or higher than best (this requires a justification)
let relay_chain_best_hash = relay_chain_interface
.best_block_hash()
.await
.map_err(|e| Box::new(e) as Box<_>)?;
let block_number = header.number();
let best_head =
Self::included_block(&relay_chain_interface, relay_chain_best_hash, para_id).await?;
let known_best_number = best_head.number();
if best_head == header {
tracing::debug!(target: LOG_TARGET, "Announced block matches best block.",);
return Ok(Validation::Success { is_new_best: true });
}
let mut backed_blocks =
Self::backed_block_hashes(&relay_chain_interface, relay_chain_best_hash, para_id)
.await?;
let head_hash = HeadData(header.encode()).hash();
if backed_blocks.any(|block_hash| block_hash == head_hash) {
tracing::debug!(target: LOG_TARGET, "Announced block matches latest backed block.",);
Ok(Validation::Success { is_new_best: true })
} else if block_number >= known_best_number {
tracing::debug!(
target: LOG_TARGET,
"Validation failed because a justification is needed if the block at the top of the chain."
);
Ok(Validation::Failure { disconnect: false })
} else {
Ok(Validation::Success { is_new_best: false })
}
}
}
impl<Block: BlockT, RCInterface> BlockAnnounceValidatorT<Block>
for RequireSecondedInBlockAnnounce<Block, RCInterface>
where
RCInterface: RelayChainInterface + Clone + 'static,
{
fn validate(
&mut self,
header: &Block::Header,
data: &[u8],
) -> Pin<Box<dyn Future<Output = Result<Validation, BoxedError>> + Send>> {
let relay_chain_interface = self.relay_chain_interface.clone();
let data = data.to_vec();
let header = header.clone();
let header_encoded = header.encode();
let block_announce_validator = self.clone();
async move {
let relay_chain_is_syncing = relay_chain_interface
.is_major_syncing()
.await
.map_err(
|e| tracing::error!(target: LOG_TARGET, "Unable to determine sync status. {}", e),
)
.unwrap_or(false);
if relay_chain_is_syncing {
return Ok(Validation::Success { is_new_best: false });
}
if data.is_empty() {
return block_announce_validator.handle_empty_block_announce_data(header).await;
}
let block_announce_data = match BlockAnnounceData::decode_all(&mut data.as_slice()) {
Ok(r) => r,
Err(err) =>
return Err(Box::new(BlockAnnounceError(format!(
"Can not decode the `BlockAnnounceData`: {:?}",
err
))) as Box<_>),
};
if let Err(e) = block_announce_data.validate(header_encoded) {
return Ok(e);
}
let relay_parent = block_announce_data.receipt.descriptor.relay_parent();
relay_chain_interface
.wait_for_block(relay_parent)
.await
.map_err(|e| Box::new(BlockAnnounceError(e.to_string())) as Box<_>)?;
block_announce_data
.check_signature(&relay_chain_interface)
.await
.map_err(|e| Box::new(e) as Box<_>)
}
.boxed()
}
}
/// Wait before announcing a block that a candidate message has been received for this block, then
/// add this message as justification for the block announcement.
///
/// This object will spawn a new task every time the method `wait_to_announce` is called and cancel
/// the previous task running.
pub struct WaitToAnnounce<Block: BlockT> {
spawner: Arc<dyn SpawnNamed + Send + Sync>,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
}
impl<Block: BlockT> WaitToAnnounce<Block> {
/// Create the `WaitToAnnounce` object
pub fn new(
spawner: Arc<dyn SpawnNamed + Send + Sync>,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
) -> WaitToAnnounce<Block> {
WaitToAnnounce { spawner, announce_block }
}
/// Wait for a candidate message for the block, then announce the block. The candidate
/// message will be added as justification to the block announcement.
pub fn wait_to_announce(
&mut self,
block_hash: <Block as BlockT>::Hash,
signed_stmt_recv: oneshot::Receiver<CollationSecondedSignal>,
) {
let announce_block = self.announce_block.clone();
self.spawner.spawn(
"pezcumulus-wait-to-announce",
None,
async move {
tracing::debug!(
target: "pezcumulus-network",
"waiting for announce block in a background task...",
);
wait_to_announce::<Block>(block_hash, announce_block, signed_stmt_recv).await;
tracing::debug!(
target: "pezcumulus-network",
"block announcement finished",
);
}
.boxed(),
);
}
}
async fn wait_to_announce<Block: BlockT>(
block_hash: <Block as BlockT>::Hash,
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
signed_stmt_recv: oneshot::Receiver<CollationSecondedSignal>,
) {
let signal = match signed_stmt_recv.await {
Ok(s) => s,
Err(_) => {
tracing::debug!(
target: "pezcumulus-network",
block = ?block_hash,
"Wait to announce stopped, because sender was dropped.",
);
return;
},
};
if let Ok(data) = BlockAnnounceData::try_from(&signal) {
announce_block(block_hash, Some(data.encode()));
} else {
tracing::debug!(
target: "pezcumulus-network",
?signal,
block = ?block_hash,
"Received invalid statement while waiting to announce block.",
);
}
}
/// A [`BlockAnnounceValidator`] which accepts all block announcements, as it assumes
/// sybil resistance is handled elsewhere.
#[derive(Debug, Clone)]
pub struct AssumeSybilResistance(bool);
impl AssumeSybilResistance {
/// Instantiate this block announcement validator while permissively allowing (but ignoring)
/// announcements which come tagged with seconded messages.
///
/// This is useful for backwards compatibility when upgrading nodes: old nodes will continue
/// to broadcast announcements with seconded messages, so these announcements shouldn't be
/// rejected and the peers not punished.
pub fn allow_seconded_messages() -> Self {
AssumeSybilResistance(true)
}
/// Instantiate this block announcement validator while rejecting announcements that come with
/// data.
pub fn reject_seconded_messages() -> Self {
AssumeSybilResistance(false)
}
}
impl<Block: BlockT> BlockAnnounceValidatorT<Block> for AssumeSybilResistance {
fn validate(
&mut self,
_header: &Block::Header,
data: &[u8],
) -> Pin<Box<dyn Future<Output = Result<Validation, BoxedError>> + Send>> {
let allow_seconded_messages = self.0;
let data = data.to_vec();
async move {
Ok(if data.is_empty() {
Validation::Success { is_new_best: false }
} else if !allow_seconded_messages {
Validation::Failure { disconnect: false }
} else {
match BlockAnnounceData::decode_all(&mut data.as_slice()) {
Ok(_) => Validation::Success { is_new_best: false },
Err(_) => Validation::Failure { disconnect: true },
}
})
}
.boxed()
}
}
+689
View File
@@ -0,0 +1,689 @@
// 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 <https://www.gnu.org/licenses/>.
use super::*;
use async_trait::async_trait;
use cumulus_primitives_core::relay_chain::{BlockId, CoreIndex};
use cumulus_relay_chain_inprocess_interface::{check_block_in_chain, BlockCheckStatus};
use cumulus_relay_chain_interface::{
OverseerHandle, PHeader, ParaId, RelayChainError, RelayChainResult,
};
use cumulus_test_service::runtime::{Block, Hash, Header};
use futures::{executor::block_on, poll, task::Poll, FutureExt, Stream, StreamExt};
use parking_lot::Mutex;
use pezkuwi_node_primitives::{SignedFullStatement, Statement};
use pezkuwi_primitives::{
BlockNumber, CandidateCommitments, CandidateDescriptorV2, CandidateEvent, CollatorPair,
CommittedCandidateReceiptV2, CoreState, Hash as PHash, HeadData, InboundDownwardMessage,
InboundHrmpMessage, OccupiedCoreAssumption, PersistedValidationData, SessionIndex,
SigningContext, ValidationCodeHash, ValidatorId,
};
use pezkuwi_primitives_test_helpers::{CandidateDescriptor, CommittedCandidateReceipt};
use pezkuwi_test_client::{
Client as PClient, ClientBlockImportExt, DefaultTestClientBuilderExt, FullBackend as PBackend,
InitPezkuwiBlockBuilder, TestClientBuilder, TestClientBuilderExt,
};
use rstest::rstest;
use pezsc_client_api::{Backend, BlockchainEvents};
use pezsp_blockchain::HeaderBackend;
use pezsp_consensus::BlockOrigin;
use pezsp_core::{Pair, H256};
use pezsp_keyring::Sr25519Keyring;
use pezsp_keystore::{testing::MemoryKeystore, Keystore, KeystorePtr};
use pezsp_runtime::RuntimeAppPublic;
use pezsp_state_machine::StorageValue;
use pezsp_version::RuntimeVersion;
use std::{
borrow::Cow,
collections::{BTreeMap, VecDeque},
time::Duration,
};
fn check_error(error: crate::BoxedError, check_error: impl Fn(&BlockAnnounceError) -> bool) {
let error = *error
.downcast::<BlockAnnounceError>()
.expect("Downcasts error to `ClientError`");
if !check_error(&error) {
panic!("Invalid error: {:?}", error);
}
}
fn dummy_candidate() -> CommittedCandidateReceiptV2 {
CommittedCandidateReceiptV2 {
descriptor: CandidateDescriptorV2::new(
0u32.into(),
PHash::random(),
0.into(),
1,
PHash::random(),
PHash::random(),
PHash::random(),
pezkuwi_teyrchain_primitives::primitives::HeadData(default_header().encode()).hash(),
ValidationCodeHash::from(PHash::random()),
),
commitments: CandidateCommitments {
upward_messages: Default::default(),
horizontal_messages: Default::default(),
new_validation_code: None,
head_data: HeadData(Vec::new()),
processed_downward_messages: 0,
hrmp_watermark: 0,
},
}
}
#[derive(Clone)]
struct DummyRelayChainInterface {
data: Arc<Mutex<ApiData>>,
relay_client: Arc<PClient>,
relay_backend: Arc<PBackend>,
}
impl DummyRelayChainInterface {
fn new() -> Self {
let builder = TestClientBuilder::new();
let relay_backend = builder.backend();
Self {
data: Arc::new(Mutex::new(ApiData {
validators: vec![Sr25519Keyring::Alice.public().into()],
has_pending_availability: false,
runtime_version:
RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT,
})),
relay_client: Arc::new(builder.build()),
relay_backend,
}
}
}
#[async_trait]
impl RelayChainInterface for DummyRelayChainInterface {
async fn validators(&self, _: PHash) -> RelayChainResult<Vec<ValidatorId>> {
Ok(self.data.lock().validators.clone())
}
async fn best_block_hash(&self) -> RelayChainResult<PHash> {
Ok(self.relay_backend.blockchain().info().best_hash)
}
async fn finalized_block_hash(&self) -> RelayChainResult<PHash> {
Ok(self.relay_backend.blockchain().info().finalized_hash)
}
async fn retrieve_dmq_contents(
&self,
_: ParaId,
_: PHash,
) -> RelayChainResult<Vec<InboundDownwardMessage>> {
unimplemented!("Not needed for test")
}
async fn retrieve_all_inbound_hrmp_channel_contents(
&self,
_: ParaId,
_: PHash,
) -> RelayChainResult<BTreeMap<ParaId, Vec<InboundHrmpMessage>>> {
Ok(BTreeMap::new())
}
async fn persisted_validation_data(
&self,
_: PHash,
_: ParaId,
_: OccupiedCoreAssumption,
) -> RelayChainResult<Option<PersistedValidationData>> {
Ok(Some(PersistedValidationData {
parent_head: HeadData(default_header().encode()),
..Default::default()
}))
}
async fn validation_code_hash(
&self,
_: PHash,
_: ParaId,
_: OccupiedCoreAssumption,
) -> RelayChainResult<Option<ValidationCodeHash>> {
unimplemented!("Not needed for test")
}
async fn candidate_pending_availability(
&self,
_: PHash,
_: ParaId,
) -> RelayChainResult<Option<CommittedCandidateReceiptV2>> {
if self.data.lock().runtime_version >=
RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT
{
panic!("Should have used candidates_pending_availability instead");
}
if self.data.lock().has_pending_availability {
Ok(Some(dummy_candidate()))
} else {
Ok(None)
}
}
async fn candidates_pending_availability(
&self,
_: PHash,
_: ParaId,
) -> RelayChainResult<Vec<CommittedCandidateReceiptV2>> {
if self.data.lock().runtime_version <
RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT
{
panic!("Should have used candidate_pending_availability instead");
}
if self.data.lock().has_pending_availability {
Ok(vec![dummy_candidate()])
} else {
Ok(vec![])
}
}
async fn session_index_for_child(&self, _: PHash) -> RelayChainResult<SessionIndex> {
Ok(0)
}
async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
Ok(Box::pin(
self.relay_client
.import_notification_stream()
.map(|notification| notification.header),
))
}
async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
Ok(Box::pin(
self.relay_client
.finality_notification_stream()
.map(|notification| notification.header),
))
}
async fn is_major_syncing(&self) -> RelayChainResult<bool> {
Ok(false)
}
fn overseer_handle(&self) -> RelayChainResult<OverseerHandle> {
unimplemented!("Not needed for test")
}
async fn get_storage_by_key(
&self,
_: PHash,
_: &[u8],
) -> RelayChainResult<Option<StorageValue>> {
unimplemented!("Not needed for test")
}
async fn prove_read(
&self,
_: PHash,
_: &Vec<Vec<u8>>,
) -> RelayChainResult<pezsc_client_api::StorageProof> {
unimplemented!("Not needed for test")
}
async fn wait_for_block(&self, hash: PHash) -> RelayChainResult<()> {
let mut listener = match check_block_in_chain(
self.relay_backend.clone(),
self.relay_client.clone(),
hash,
)? {
BlockCheckStatus::InChain => return Ok(()),
BlockCheckStatus::Unknown(listener) => listener,
};
let mut timeout = futures_timer::Delay::new(Duration::from_secs(10)).fuse();
loop {
futures::select! {
_ = timeout => return Err(RelayChainError::WaitTimeout(hash)),
evt = listener.next() => match evt {
Some(evt) if evt.hash == hash => return Ok(()),
// Not the event we waited on.
Some(_) => continue,
None => return Err(RelayChainError::ImportListenerClosed(hash)),
}
}
}
}
async fn new_best_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
let notifications_stream =
self.relay_client
.import_notification_stream()
.filter_map(|notification| async move {
if notification.is_new_best {
Some(notification.header)
} else {
None
}
});
Ok(Box::pin(notifications_stream))
}
async fn header(&self, block_id: BlockId) -> RelayChainResult<Option<PHeader>> {
let hash = match block_id {
BlockId::Hash(hash) => hash,
BlockId::Number(num) =>
if let Some(hash) = self.relay_client.hash(num)? {
hash
} else {
return Ok(None);
},
};
let header = self.relay_client.header(hash)?;
Ok(header)
}
async fn availability_cores(
&self,
_relay_parent: PHash,
) -> RelayChainResult<Vec<CoreState<PHash, BlockNumber>>> {
unimplemented!("Not needed for test");
}
async fn version(&self, _: PHash) -> RelayChainResult<RuntimeVersion> {
let version = self.data.lock().runtime_version;
let apis = pezsp_version::create_apis_vec!([(
<dyn pezkuwi_primitives::runtime_api::TeyrchainHost<pezkuwi_primitives::Block>>::ID,
version
)])
.into_owned()
.to_vec();
Ok(RuntimeVersion {
spec_name: Cow::Borrowed("test"),
impl_name: Cow::Borrowed("test"),
authoring_version: 1,
spec_version: 1,
impl_version: 0,
apis: Cow::Owned(apis),
transaction_version: 5,
system_version: 1,
})
}
async fn claim_queue(
&self,
_: PHash,
) -> RelayChainResult<BTreeMap<CoreIndex, VecDeque<ParaId>>> {
unimplemented!("Not needed for test");
}
async fn call_runtime_api(
&self,
_method_name: &'static str,
_hash: PHash,
_payload: &[u8],
) -> RelayChainResult<Vec<u8>> {
unimplemented!("Not needed for test")
}
async fn scheduling_lookahead(&self, _: PHash) -> RelayChainResult<u32> {
unimplemented!("Not needed for test")
}
async fn candidate_events(&self, _: PHash) -> RelayChainResult<Vec<CandidateEvent>> {
unimplemented!("Not needed for test")
}
}
fn make_validator_and_api() -> (
RequireSecondedInBlockAnnounce<Block, Arc<DummyRelayChainInterface>>,
Arc<DummyRelayChainInterface>,
) {
let relay_chain_interface = Arc::new(DummyRelayChainInterface::new());
(
RequireSecondedInBlockAnnounce::new(relay_chain_interface.clone(), ParaId::from(56)),
relay_chain_interface,
)
}
fn default_header() -> Header {
Header {
number: 1,
digest: Default::default(),
extrinsics_root: Default::default(),
parent_hash: Default::default(),
state_root: Default::default(),
}
}
/// Same as [`make_gossip_message_and_header`], but using the genesis header as relay parent.
async fn make_gossip_message_and_header_using_genesis(
api: Arc<DummyRelayChainInterface>,
validator_index: u32,
) -> (CollationSecondedSignal, Header) {
let relay_parent = api.relay_client.hash(0).ok().flatten().expect("Genesis hash exists");
make_gossip_message_and_header(api, relay_parent, validator_index).await
}
async fn make_gossip_message_and_header(
relay_chain_interface: Arc<DummyRelayChainInterface>,
relay_parent: H256,
validator_index: u32,
) -> (CollationSecondedSignal, Header) {
let keystore: KeystorePtr = Arc::new(MemoryKeystore::new());
let alice_public = Keystore::sr25519_generate_new(
&*keystore,
ValidatorId::ID,
Some(&Sr25519Keyring::Alice.to_seed()),
)
.unwrap();
let session_index = relay_chain_interface.session_index_for_child(relay_parent).await.unwrap();
let signing_context = SigningContext { parent_hash: relay_parent, session_index };
let header = default_header();
let candidate_receipt = CommittedCandidateReceipt {
commitments: CandidateCommitments {
head_data: header.encode().into(),
..Default::default()
},
descriptor: CandidateDescriptor {
para_id: 0u32.into(),
relay_parent,
collator: CollatorPair::generate().0.public(),
persisted_validation_data_hash: PHash::random(),
pov_hash: PHash::random(),
erasure_root: PHash::random(),
signature: pezsp_core::sr25519::Signature::default().into(),
para_head: pezkuwi_teyrchain_primitives::primitives::HeadData(header.encode()).hash(),
validation_code_hash: ValidationCodeHash::from(PHash::random()),
},
};
let statement = Statement::Seconded(candidate_receipt.into());
let signed = SignedFullStatement::sign(
&keystore,
statement,
&signing_context,
validator_index.into(),
&alice_public.into(),
)
.ok()
.flatten()
.expect("Signing statement");
(CollationSecondedSignal { statement: signed, relay_parent }, header)
}
#[test]
fn valid_if_no_data_and_less_than_best_known_number() {
let mut validator = make_validator_and_api().0;
let header = Header { number: 0, ..default_header() };
let res = block_on(validator.validate(&header, &[]));
assert_eq!(
res.unwrap(),
Validation::Success { is_new_best: false },
"validating without data with block number < best known number is always a success",
);
}
#[test]
fn invalid_if_no_data_exceeds_best_known_number() {
let mut validator = make_validator_and_api().0;
let header = Header { number: 1, state_root: Hash::random(), ..default_header() };
let res = block_on(validator.validate(&header, &[]));
assert_eq!(
res.unwrap(),
Validation::Failure { disconnect: false },
"validation fails if no justification and block number >= best known number",
);
}
#[test]
fn valid_if_no_data_and_block_matches_best_known_block() {
let mut validator = make_validator_and_api().0;
let res = block_on(validator.validate(&default_header(), &[]));
assert_eq!(
res.unwrap(),
Validation::Success { is_new_best: true },
"validation is successful when the block hash matches the best known block",
);
}
#[test]
fn check_statement_is_encoded_correctly() {
let mut validator = make_validator_and_api().0;
let header = default_header();
let res = block_on(validator.validate(&header, &[0x42]))
.expect_err("Should fail on invalid encoded statement");
check_error(res, |error| {
matches!(
error,
BlockAnnounceError(x) if x.contains("Can not decode the `BlockAnnounceData`")
)
});
}
#[test]
fn block_announce_data_decoding_should_reject_extra_data() {
let (mut validator, api) = make_validator_and_api();
let (signal, header) = block_on(make_gossip_message_and_header_using_genesis(api, 1));
let mut data = BlockAnnounceData::try_from(&signal).unwrap().encode();
data.push(0x42);
let res = block_on(validator.validate(&header, &data)).expect_err("Should return an error ");
check_error(res, |error| {
matches!(
error,
BlockAnnounceError(x) if x.contains("Input buffer has still data left after decoding!")
)
});
}
#[derive(Encode, Decode, Debug)]
struct LegacyBlockAnnounceData {
receipt: CandidateReceipt,
statement: UncheckedSigned<CompactStatement>,
}
#[test]
fn legacy_block_announce_data_handling() {
let (_, api) = make_validator_and_api();
let (signal, _) = block_on(make_gossip_message_and_header_using_genesis(api, 1));
let receipt = if let Statement::Seconded(receipt) = signal.statement.payload() {
receipt.to_plain()
} else {
panic!("Invalid")
};
let legacy = LegacyBlockAnnounceData {
receipt: receipt.clone(),
statement: signal.statement.convert_payload().into(),
};
let data = legacy.encode();
let block_data =
BlockAnnounceData::decode(&mut &data[..]).expect("Decoding works from legacy works");
assert_eq!(receipt.descriptor.relay_parent(), block_data.relay_parent);
let data = block_data.encode();
LegacyBlockAnnounceData::decode(&mut &data[..]).expect("Decoding works");
}
#[test]
fn check_signer_is_legit_validator() {
let (mut validator, api) = make_validator_and_api();
let (signal, header) = block_on(make_gossip_message_and_header_using_genesis(api, 1));
let data = BlockAnnounceData::try_from(&signal).unwrap().encode();
let res = block_on(validator.validate(&header, &data));
assert_eq!(Validation::Failure { disconnect: true }, res.unwrap());
}
#[test]
fn check_statement_is_correctly_signed() {
let (mut validator, api) = make_validator_and_api();
let (signal, header) = block_on(make_gossip_message_and_header_using_genesis(api, 0));
let mut data = BlockAnnounceData::try_from(&signal).unwrap().encode();
// The signature comes at the end of the type, so change a bit to make the signature invalid.
let last = data.len() - 1;
data[last] = data[last].wrapping_add(1);
let res = block_on(validator.validate(&header, &data));
assert_eq!(Validation::Failure { disconnect: true }, res.unwrap());
}
#[tokio::test]
async fn check_statement_seconded() {
let (mut validator, relay_chain_interface) = make_validator_and_api();
let header = default_header();
let relay_parent = H256::from_low_u64_be(1);
let keystore: KeystorePtr = Arc::new(MemoryKeystore::new());
let alice_public = Keystore::sr25519_generate_new(
&*keystore,
ValidatorId::ID,
Some(&Sr25519Keyring::Alice.to_seed()),
)
.unwrap();
let session_index = relay_chain_interface.session_index_for_child(relay_parent).await.unwrap();
let signing_context = SigningContext { parent_hash: relay_parent, session_index };
let statement = Statement::Valid(Default::default());
let signed_statement = SignedFullStatement::sign(
&keystore,
statement,
&signing_context,
0.into(),
&alice_public.into(),
)
.ok()
.flatten()
.expect("Signs statement");
let data = BlockAnnounceData {
receipt: CandidateReceipt {
commitments_hash: PHash::random(),
descriptor: CandidateDescriptor {
para_head: HeadData(Vec::new()).hash(),
para_id: 0u32.into(),
relay_parent: PHash::random(),
collator: CollatorPair::generate().0.public(),
persisted_validation_data_hash: PHash::random(),
pov_hash: PHash::random(),
erasure_root: PHash::random(),
signature: pezsp_core::sr25519::Signature::default().into(),
validation_code_hash: ValidationCodeHash::from(PHash::random()),
}
.into(),
},
statement: signed_statement.convert_payload().into(),
relay_parent,
}
.encode();
let res = block_on(validator.validate(&header, &data));
assert_eq!(Validation::Failure { disconnect: true }, res.unwrap());
}
#[test]
fn check_header_match_candidate_receipt_header() {
let (mut validator, api) = make_validator_and_api();
let (signal, mut header) = block_on(make_gossip_message_and_header_using_genesis(api, 0));
let data = BlockAnnounceData::try_from(&signal).unwrap().encode();
header.number = 300;
let res = block_on(validator.validate(&header, &data));
assert_eq!(Validation::Failure { disconnect: true }, res.unwrap());
}
/// Test that ensures that we postpone the block announce verification until
/// a relay chain block is imported. This is important for when we receive a
/// block announcement before we have imported the associated relay chain block
/// which can happen on slow nodes or nodes with a slow network connection.
#[test]
fn relay_parent_not_imported_when_block_announce_is_processed() {
block_on(async move {
let (mut validator, api) = make_validator_and_api();
let client = api.relay_client.clone();
let block = client.init_pezkuwi_block_builder().build().expect("Build new block").block;
let (signal, header) = make_gossip_message_and_header(api, block.hash(), 0).await;
let data = BlockAnnounceData::try_from(&signal).unwrap().encode();
let mut validation = validator.validate(&header, &data);
// The relay chain block is not available yet, so the first poll should return
// that the future is still pending.
assert!(poll!(&mut validation).is_pending());
client.import(BlockOrigin::Own, block).await.expect("Imports the block");
assert!(matches!(
poll!(validation),
Poll::Ready(Ok(Validation::Success { is_new_best: true }))
));
});
}
/// Ensures that when we receive a block announcement without a statement included, while the block
/// is not yet included by the node checking the announcement, but the node is already backed.
#[rstest]
#[case(RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT)]
#[case(10)]
fn block_announced_without_statement_and_block_only_backed(#[case] runtime_version: u32) {
block_on(async move {
let (mut validator, api) = make_validator_and_api();
api.data.lock().has_pending_availability = true;
api.data.lock().runtime_version = runtime_version;
let header = default_header();
let validation = validator.validate(&header, &[]);
assert!(matches!(validation.await, Ok(Validation::Success { is_new_best: true })));
});
}
#[derive(Default)]
struct ApiData {
validators: Vec<ValidatorId>,
has_pending_availability: bool,
runtime_version: u32,
}
+72
View File
@@ -0,0 +1,72 @@
[package]
name = "pezcumulus-client-pov-recovery"
version = "0.7.0"
authors.workspace = true
description = "Teyrchain PoV recovery"
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
codec = { features = ["derive"], workspace = true, default-features = true }
futures = { workspace = true }
futures-timer = { workspace = true }
rand = { workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
pezsc-client-api = { workspace = true, default-features = true }
pezsc-consensus = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-maybe-compressed-blob = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-version = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-node-primitives = { workspace = true, default-features = true }
pezkuwi-node-subsystem = { workspace = true, default-features = true }
pezkuwi-overseer = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
# Pezcumulus
async-trait = { workspace = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
pezcumulus-relay-chain-streams = { workspace = true, default-features = true }
[dev-dependencies]
assert_matches = { workspace = true }
rstest = { workspace = true }
pezsc-utils = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-tracing = { workspace = true, default-features = true }
tokio = { features = ["macros"], workspace = true, default-features = true }
# Pezcumulus
pezcumulus-test-client = { workspace = true }
[features]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezcumulus-relay-chain-streams/runtime-benchmarks",
"pezcumulus-test-client/runtime-benchmarks",
"pezkuwi-node-primitives/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-consensus/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-version/runtime-benchmarks",
]
@@ -0,0 +1,103 @@
// 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 <https://www.gnu.org/licenses/>.
use pezsp_runtime::traits::Block as BlockT;
use pezkuwi_node_primitives::PoV;
use pezkuwi_node_subsystem::messages::AvailabilityRecoveryMessage;
use futures::{channel::oneshot, stream::FuturesUnordered, Future, FutureExt, StreamExt};
use std::{pin::Pin, sync::Arc};
use crate::RecoveryHandle;
/// The active candidate recovery.
///
/// This handles the candidate recovery and tracks the activate recoveries.
pub(crate) struct ActiveCandidateRecovery<Block: BlockT> {
/// The recoveries that are currently being executed.
recoveries:
FuturesUnordered<Pin<Box<dyn Future<Output = (Block::Hash, Option<Arc<PoV>>)> + Send>>>,
recovery_handle: Box<dyn RecoveryHandle>,
}
impl<Block: BlockT> ActiveCandidateRecovery<Block> {
pub fn new(recovery_handle: Box<dyn RecoveryHandle>) -> Self {
Self { recoveries: Default::default(), recovery_handle }
}
/// Recover the given `candidate`.
pub async fn recover_candidate(
&mut self,
block_hash: Block::Hash,
candidate: &crate::Candidate<Block>,
) {
let (tx, rx) = oneshot::channel();
self.recovery_handle
.send_recovery_msg(
AvailabilityRecoveryMessage::RecoverAvailableData(
candidate.receipt.clone(),
candidate.session_index,
None,
None,
tx,
),
"ActiveCandidateRecovery",
)
.await;
self.recoveries.push(
async move {
match rx.await {
Ok(Ok(res)) => (block_hash, Some(res.pov)),
Ok(Err(error)) => {
tracing::debug!(
target: crate::LOG_TARGET,
?error,
?block_hash,
"Availability recovery failed",
);
(block_hash, None)
},
Err(_) => {
tracing::debug!(
target: crate::LOG_TARGET,
"Availability recovery oneshot channel closed",
);
(block_hash, None)
},
}
}
.boxed(),
);
}
/// Waits for the next recovery.
///
/// If the returned [`PoV`] is `None`, it means that the recovery failed.
pub async fn wait_for_recovery(&mut self) -> (Block::Hash, Option<Arc<PoV>>) {
loop {
if let Some(res) = self.recoveries.next().await {
return res;
} else {
futures::pending!()
}
}
}
}
+662
View File
@@ -0,0 +1,662 @@
// 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 <https://www.gnu.org/licenses/>.
//! Teyrchain PoV recovery
//!
//! A teyrchain needs to build PoVs that are send to the relay chain to progress. These PoVs are
//! erasure encoded and one piece of it is stored by each relay chain validator. As the relay chain
//! decides on which PoV per teyrchain to include and thus, to progress the teyrchain it can happen
//! that the block corresponding to this PoV isn't propagated in the teyrchain network. This can
//! have several reasons, either a malicious collator that managed to include its own PoV and
//! doesn't want to share it with the rest of the network or maybe a collator went down before it
//! could distribute the block in the network. When something like this happens we can use the PoV
//! recovery algorithm implemented in this crate to recover a PoV and to propagate it with the rest
//! of the network.
//!
//! It works in the following way:
//!
//! 1. For every included relay chain block we note the backed candidate of our teyrchain. If the
//! block belonging to the PoV is already known, we do nothing. Otherwise we start a timer that
//! waits for a randomized time inside a specified interval before starting to
//! recover the PoV.
//!
//! 2. If between starting and firing the timer the block is imported, we skip the recovery of the
//! PoV.
//!
//! 3. If the timer fired we recover the PoV using the relay chain PoV recovery protocol.
//!
//! 4a. After it is recovered, we restore the block and import it.
//!
//! 4b. Since we are trying to recover pending candidates, availability is not guaranteed. If the
//! block PoV is not yet available, we retry.
//!
//! If we need to recover multiple PoV blocks (which should hopefully not happen in real life), we
//! make sure that the blocks are imported in the correct order.
use pezsc_client_api::{BlockBackend, BlockchainEvents, UsageProvider};
use pezsc_consensus::import_queue::{ImportQueueService, IncomingBlock};
use pezsp_consensus::{BlockOrigin, BlockStatus, SyncOracle};
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
use pezkuwi_node_primitives::{PoV, POV_BOMB_LIMIT};
use pezkuwi_node_subsystem::messages::AvailabilityRecoveryMessage;
use pezkuwi_overseer::Handle as OverseerHandle;
use pezkuwi_primitives::{
CandidateReceiptV2 as CandidateReceipt,
CommittedCandidateReceiptV2 as CommittedCandidateReceipt, Id as ParaId, SessionIndex,
};
use cumulus_primitives_core::TeyrchainBlockData;
use cumulus_relay_chain_interface::RelayChainInterface;
use cumulus_relay_chain_streams::pending_candidates;
use codec::{Decode, DecodeAll};
use futures::{
channel::mpsc::Receiver, select, stream::FuturesUnordered, Future, FutureExt, StreamExt,
};
use futures_timer::Delay;
use rand::{distributions::Uniform, prelude::Distribution, thread_rng};
use std::{
collections::{HashMap, HashSet, VecDeque},
pin::Pin,
sync::Arc,
time::Duration,
};
#[cfg(test)]
mod tests;
mod active_candidate_recovery;
use active_candidate_recovery::ActiveCandidateRecovery;
const LOG_TARGET: &str = "pezcumulus-pov-recovery";
/// Test-friendly wrapper trait for the overseer handle.
/// Can be used to simulate failing recovery requests.
#[async_trait::async_trait]
pub trait RecoveryHandle: Send {
async fn send_recovery_msg(
&mut self,
message: AvailabilityRecoveryMessage,
origin: &'static str,
);
}
#[async_trait::async_trait]
impl RecoveryHandle for OverseerHandle {
async fn send_recovery_msg(
&mut self,
message: AvailabilityRecoveryMessage,
origin: &'static str,
) {
self.send_msg(message, origin).await;
}
}
/// Type of recovery to trigger.
#[derive(Debug, PartialEq)]
pub enum RecoveryKind {
/// Single block recovery.
Simple,
/// Full ancestry recovery.
Full,
}
/// Structure used to trigger an explicit recovery request via `PoVRecovery`.
pub struct RecoveryRequest<Block: BlockT> {
/// Hash of the last block to recover.
pub hash: Block::Hash,
/// Recovery type.
pub kind: RecoveryKind,
}
/// The delay between observing an unknown block and triggering the recovery of a block.
/// Randomizing the start of the recovery within this interval
/// can be used to prevent self-DOSing if the recovery request is part of a
/// distributed protocol and there is the possibility that multiple actors are
/// requiring to perform the recovery action at approximately the same time.
#[derive(Clone, Copy)]
pub struct RecoveryDelayRange {
/// Start recovering after `min` delay.
pub min: Duration,
/// Start recovering before `max` delay.
pub max: Duration,
}
impl RecoveryDelayRange {
/// Produce a randomized duration between `min` and `max`.
fn duration(&self) -> Duration {
Uniform::from(self.min..=self.max).sample(&mut thread_rng())
}
}
/// Represents an outstanding block candidate.
struct Candidate<Block: BlockT> {
receipt: CandidateReceipt,
session_index: SessionIndex,
block_number: NumberFor<Block>,
parent_hash: Block::Hash,
// Lazy recovery has been submitted.
// Should be true iff a block is either queued to be recovered or
// recovery is currently in progress.
waiting_recovery: bool,
}
/// Queue that is used to decide when to start PoV-recovery operations.
struct RecoveryQueue<Block: BlockT> {
recovery_delay_range: RecoveryDelayRange,
// Queue that keeps the hashes of blocks to be recovered.
recovery_queue: VecDeque<Block::Hash>,
// Futures that resolve when a new recovery should be started.
signaling_queue: FuturesUnordered<Pin<Box<dyn Future<Output = ()> + Send>>>,
}
impl<Block: BlockT> RecoveryQueue<Block> {
pub fn new(recovery_delay_range: RecoveryDelayRange) -> Self {
Self {
recovery_delay_range,
recovery_queue: Default::default(),
signaling_queue: Default::default(),
}
}
/// Add hash of a block that should go to the end of the recovery queue.
/// A new recovery will be signaled after `delay` has passed.
pub fn push_recovery(&mut self, hash: Block::Hash) {
let delay = self.recovery_delay_range.duration();
tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Adding block to queue and adding new recovery slot in {:?} sec",
delay.as_secs(),
);
self.recovery_queue.push_back(hash);
self.signaling_queue.push(
async move {
Delay::new(delay).await;
}
.boxed(),
);
}
/// Get the next hash for block recovery.
pub async fn next_recovery(&mut self) -> Block::Hash {
loop {
if self.signaling_queue.next().await.is_some() {
if let Some(hash) = self.recovery_queue.pop_front() {
return hash;
} else {
tracing::error!(
target: LOG_TARGET,
"Recovery was signaled, but no candidate hash available. This is a bug."
);
};
}
futures::pending!()
}
}
}
/// Encapsulates the logic of the pov recovery.
pub struct PoVRecovery<Block: BlockT, PC, RC> {
/// All the pending candidates that we are waiting for to be imported or that need to be
/// recovered when `next_candidate_to_recover` tells us to do so.
candidates: HashMap<Block::Hash, Candidate<Block>>,
/// A stream of futures that resolve to hashes of candidates that need to be recovered.
///
/// The candidates to the hashes are stored in `candidates`. If a candidate is not
/// available anymore in this map, it means that it was already imported.
candidate_recovery_queue: RecoveryQueue<Block>,
active_candidate_recovery: ActiveCandidateRecovery<Block>,
/// Blocks that wait that the parent is imported.
///
/// Uses parent -> blocks mapping.
waiting_for_parent: HashMap<Block::Hash, Vec<Block>>,
teyrchain_client: Arc<PC>,
teyrchain_import_queue: Box<dyn ImportQueueService<Block>>,
relay_chain_interface: RC,
para_id: ParaId,
/// Explicit block recovery requests channel.
recovery_chan_rx: Receiver<RecoveryRequest<Block>>,
/// Blocks that we are retrying currently
candidates_in_retry: HashSet<Block::Hash>,
teyrchain_sync_service: Arc<dyn SyncOracle + Sync + Send>,
}
impl<Block: BlockT, PC, RCInterface> PoVRecovery<Block, PC, RCInterface>
where
PC: BlockBackend<Block> + BlockchainEvents<Block> + UsageProvider<Block>,
RCInterface: RelayChainInterface + Clone,
{
/// Create a new instance.
pub fn new(
recovery_handle: Box<dyn RecoveryHandle>,
recovery_delay_range: RecoveryDelayRange,
teyrchain_client: Arc<PC>,
teyrchain_import_queue: Box<dyn ImportQueueService<Block>>,
relay_chain_interface: RCInterface,
para_id: ParaId,
recovery_chan_rx: Receiver<RecoveryRequest<Block>>,
teyrchain_sync_service: Arc<dyn SyncOracle + Sync + Send>,
) -> Self {
Self {
candidates: HashMap::new(),
candidate_recovery_queue: RecoveryQueue::new(recovery_delay_range),
active_candidate_recovery: ActiveCandidateRecovery::new(recovery_handle),
waiting_for_parent: HashMap::new(),
teyrchain_client,
teyrchain_import_queue,
relay_chain_interface,
para_id,
candidates_in_retry: HashSet::new(),
recovery_chan_rx,
teyrchain_sync_service,
}
}
/// Handle a new pending candidate.
fn handle_pending_candidate(
&mut self,
receipt: CommittedCandidateReceipt,
session_index: SessionIndex,
) {
let header = match Block::Header::decode(&mut &receipt.commitments.head_data.0[..]) {
Ok(header) => header,
Err(e) => {
tracing::warn!(
target: LOG_TARGET,
error = ?e,
"Failed to decode teyrchain header from pending candidate",
);
return;
},
};
if *header.number() <= self.teyrchain_client.usage_info().chain.finalized_number {
return;
}
let hash = header.hash();
if self.candidates.contains_key(&hash) {
return;
}
tracing::debug!(target: LOG_TARGET, block_hash = ?hash, "Adding outstanding candidate");
self.candidates.insert(
hash,
Candidate {
block_number: *header.number(),
receipt: receipt.to_plain(),
session_index,
parent_hash: *header.parent_hash(),
waiting_recovery: false,
},
);
// If required, triggers a lazy recovery request that will eventually be blocked
// if in the meantime the block is imported.
self.recover(RecoveryRequest { hash, kind: RecoveryKind::Simple });
}
/// Block is no longer waiting for recovery
fn clear_waiting_recovery(&mut self, block_hash: &Block::Hash) {
if let Some(candidate) = self.candidates.get_mut(block_hash) {
// Prevents triggering an already enqueued recovery request
candidate.waiting_recovery = false;
}
}
/// Handle a finalized block with the given `block_number`.
fn handle_block_finalized(&mut self, block_number: NumberFor<Block>) {
self.candidates.retain(|_, pc| pc.block_number > block_number);
}
/// Recover the candidate for the given `block_hash`.
async fn recover_candidate(&mut self, block_hash: Block::Hash) {
match self.candidates.get(&block_hash) {
Some(candidate) if candidate.waiting_recovery => {
tracing::debug!(target: LOG_TARGET, ?block_hash, "Issuing recovery request");
self.active_candidate_recovery.recover_candidate(block_hash, candidate).await;
},
_ => (),
}
}
/// Clear `waiting_for_parent` and `waiting_recovery` for the candidate with `hash`.
/// Also clears children blocks waiting for this parent.
fn reset_candidate(&mut self, hash: Block::Hash) {
let mut blocks_to_delete = vec![hash];
while let Some(delete) = blocks_to_delete.pop() {
if let Some(children) = self.waiting_for_parent.remove(&delete) {
blocks_to_delete.extend(children.iter().map(BlockT::hash));
}
}
self.clear_waiting_recovery(&hash);
}
/// Try to decode [`TeyrchainBlockData`] from `data`.
///
/// Internally it will handle the decoding of the different versions.
fn decode_teyrchain_block_data(
data: &[u8],
expected_block_hash: Block::Hash,
) -> Option<TeyrchainBlockData<Block>> {
match TeyrchainBlockData::<Block>::decode_all(&mut &data[..]) {
Ok(block_data) => {
if block_data.blocks().last().map_or(false, |b| b.hash() == expected_block_hash) {
return Some(block_data);
}
tracing::debug!(
target: LOG_TARGET,
?expected_block_hash,
"Could not find the expected block hash as latest block in `TeyrchainBlockData`"
);
},
Err(error) => {
tracing::debug!(
target: LOG_TARGET,
?expected_block_hash,
?error,
"Could not decode `TeyrchainBlockData` from recovered PoV",
);
},
}
None
}
/// Handle a recovered candidate.
async fn handle_candidate_recovered(&mut self, block_hash: Block::Hash, pov: Option<&PoV>) {
let pov = match pov {
Some(pov) => {
self.candidates_in_retry.remove(&block_hash);
pov
},
None =>
if self.candidates_in_retry.insert(block_hash) {
tracing::debug!(target: LOG_TARGET, ?block_hash, "Recovery failed, retrying.");
self.candidate_recovery_queue.push_recovery(block_hash);
return;
} else {
tracing::warn!(
target: LOG_TARGET,
?block_hash,
"Unable to recover block after retry.",
);
self.candidates_in_retry.remove(&block_hash);
self.reset_candidate(block_hash);
return;
},
};
let raw_block_data =
match pezsp_maybe_compressed_blob::decompress(&pov.block_data.0, POV_BOMB_LIMIT) {
Ok(r) => r,
Err(error) => {
tracing::debug!(target: LOG_TARGET, ?error, "Failed to decompress PoV");
self.reset_candidate(block_hash);
return;
},
};
let Some(block_data) = Self::decode_teyrchain_block_data(&raw_block_data, block_hash)
else {
self.reset_candidate(block_hash);
return;
};
let blocks = block_data.into_blocks();
let Some(parent) = blocks.first().map(|b| *b.header().parent_hash()) else {
tracing::debug!(
target: LOG_TARGET,
?block_hash,
"Recovered candidate doesn't contain any blocks.",
);
self.reset_candidate(block_hash);
return;
};
match self.teyrchain_client.block_status(parent) {
Ok(BlockStatus::Unknown) => {
// If the parent block is currently being recovered or is scheduled to be recovered,
// we want to wait for the parent.
let parent_scheduled_for_recovery =
self.candidates.get(&parent).map_or(false, |parent| parent.waiting_recovery);
if parent_scheduled_for_recovery {
tracing::debug!(
target: LOG_TARGET,
?block_hash,
parent_hash = ?parent,
parent_scheduled_for_recovery,
waiting_blocks = self.waiting_for_parent.len(),
"Waiting for recovery of parent.",
);
blocks.into_iter().for_each(|b| {
self.waiting_for_parent
.entry(*b.header().parent_hash())
.or_default()
.push(b);
});
return;
} else {
tracing::debug!(
target: LOG_TARGET,
?block_hash,
parent_hash = ?parent,
"Parent not found while trying to import recovered block.",
);
self.reset_candidate(block_hash);
return;
}
},
Err(error) => {
tracing::debug!(
target: LOG_TARGET,
block_hash = ?parent,
?error,
"Error while checking block status",
);
self.reset_candidate(block_hash);
return;
},
// Any other status is fine to "ignore/accept"
_ => (),
}
self.import_blocks(blocks.into_iter());
}
/// Import the given `blocks`.
///
/// This will also recursively drain `waiting_for_parent` and import them as well.
fn import_blocks(&mut self, blocks: impl Iterator<Item = Block>) {
let mut blocks = VecDeque::from_iter(blocks);
tracing::debug!(
target: LOG_TARGET,
blocks = ?blocks.iter().map(|b| b.hash()),
"Importing blocks retrieved using pov_recovery",
);
let mut incoming_blocks = Vec::new();
while let Some(block) = blocks.pop_front() {
let block_hash = block.hash();
let (header, body) = block.deconstruct();
incoming_blocks.push(IncomingBlock {
hash: block_hash,
header: Some(header),
body: Some(body),
import_existing: false,
allow_missing_state: false,
justifications: None,
origin: None,
skip_execution: false,
state: None,
indexed_body: None,
});
if let Some(waiting) = self.waiting_for_parent.remove(&block_hash) {
blocks.extend(waiting);
}
}
self.teyrchain_import_queue
// Use `ConsensusBroadcast` to inform the import pipeline that this blocks needs to be
// imported.
.import_blocks(BlockOrigin::ConsensusBroadcast, incoming_blocks);
}
/// Attempts an explicit recovery of one or more blocks.
pub fn recover(&mut self, req: RecoveryRequest<Block>) {
let RecoveryRequest { mut hash, kind } = req;
let mut to_recover = Vec::new();
loop {
let candidate = match self.candidates.get_mut(&hash) {
Some(candidate) => candidate,
None => {
tracing::debug!(
target: LOG_TARGET,
block_hash = ?hash,
"Could not recover. Block was never announced as candidate"
);
return;
},
};
match self.teyrchain_client.block_status(hash) {
Ok(BlockStatus::Unknown) if !candidate.waiting_recovery => {
candidate.waiting_recovery = true;
to_recover.push(hash);
},
Ok(_) => break,
Err(e) => {
tracing::error!(
target: LOG_TARGET,
error = ?e,
block_hash = ?hash,
"Failed to get block status",
);
for hash in to_recover {
self.clear_waiting_recovery(&hash);
}
return;
},
}
if kind == RecoveryKind::Simple {
break;
}
hash = candidate.parent_hash;
}
for hash in to_recover.into_iter().rev() {
self.candidate_recovery_queue.push_recovery(hash);
}
}
/// Run the pov-recovery.
pub async fn run(mut self) {
let mut imported_blocks = self.teyrchain_client.import_notification_stream().fuse();
let mut finalized_blocks = self.teyrchain_client.finality_notification_stream().fuse();
let pending_candidates = match pending_candidates(
self.relay_chain_interface.clone(),
self.para_id,
self.teyrchain_sync_service.clone(),
)
.await
{
Ok(pending_candidates_stream) => pending_candidates_stream.fuse(),
Err(err) => {
tracing::error!(target: LOG_TARGET, error = ?err, "Unable to retrieve pending candidate stream.");
return;
},
};
futures::pin_mut!(pending_candidates);
loop {
select! {
next_pending_candidates = pending_candidates.next() => {
if let Some((candidates, session_index, _)) = next_pending_candidates {
for candidate in candidates {
self.handle_pending_candidate(candidate, session_index);
}
} else {
tracing::debug!(target: LOG_TARGET, "Pending candidates stream ended");
return;
}
},
recovery_req = self.recovery_chan_rx.next() => {
if let Some(req) = recovery_req {
self.recover(req);
} else {
tracing::debug!(target: LOG_TARGET, "Recovery channel stream ended");
return;
}
},
imported = imported_blocks.next() => {
if let Some(imported) = imported {
self.clear_waiting_recovery(&imported.hash);
// We need to double check that no blocks are waiting for this block.
// Can happen when a waiting child block is queued to wait for parent while the parent block is still
// in the import queue.
if let Some(waiting_blocks) = self.waiting_for_parent.remove(&imported.hash) {
for block in waiting_blocks {
tracing::debug!(target: LOG_TARGET, block_hash = ?block.hash(), resolved_parent = ?imported.hash, "Found new waiting child block during import, queuing.");
self.import_blocks(std::iter::once(block));
}
};
} else {
tracing::debug!(target: LOG_TARGET, "Imported blocks stream ended");
return;
}
},
finalized = finalized_blocks.next() => {
if let Some(finalized) = finalized {
self.handle_block_finalized(*finalized.header.number());
} else {
tracing::debug!(target: LOG_TARGET, "Finalized blocks stream ended");
return;
}
},
next_to_recover = self.candidate_recovery_queue.next_recovery().fuse() => {
self.recover_candidate(next_to_recover).await;
},
(block_hash, pov) =
self.active_candidate_recovery.wait_for_recovery().fuse() =>
{
self.handle_candidate_recovered(block_hash, pov.as_deref()).await;
},
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,70 @@
[package]
authors.workspace = true
name = "pezcumulus-relay-chain-inprocess-interface"
version = "0.7.0"
edition.workspace = true
description = "Implementation of the RelayChainInterface trait for Pezkuwi full-nodes."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-channel = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
futures-timer = { workspace = true }
# Bizinikiwi
pezsc-cli = { workspace = true, default-features = false }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsc-sysinfo = { workspace = true, default-features = true }
pezsc-telemetry = { workspace = true, default-features = true }
pezsc-tracing = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-cli = { features = ["cli"], workspace = true }
pezkuwi-primitives = { workspace = true, default-features = true }
pezkuwi-service = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-client-bootnodes = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
[dev-dependencies]
# Bizinikiwi
pezsp-keyring = { workspace = true, default-features = true }
# Pezkuwi
metered = { features = ["futures_channel"], workspace = true }
pezkuwi-test-client = { workspace = true }
[features]
runtime-benchmarks = [
"pezcumulus-client-bootnodes/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezkuwi-cli/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezkuwi-service/runtime-benchmarks",
"pezkuwi-test-client/runtime-benchmarks",
"pezsc-cli/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsc-sysinfo/runtime-benchmarks",
"pezsc-tracing/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-keyring/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
]
@@ -0,0 +1,601 @@
// 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 <https://www.gnu.org/licenses/>.
use std::{
collections::{BTreeMap, HashSet, VecDeque},
pin::Pin,
sync::Arc,
time::Duration,
};
use async_trait::async_trait;
use cumulus_client_bootnodes::bootnode_request_response_config;
use cumulus_primitives_core::{
relay_chain::{
runtime_api::TeyrchainHost, Block as PBlock, BlockId, BlockNumber,
CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CoreIndex, CoreState,
Hash as PHash, Header as PHeader, InboundHrmpMessage, OccupiedCoreAssumption, SessionIndex,
ValidationCodeHash, ValidatorId,
},
InboundDownwardMessage, ParaId, PersistedValidationData,
};
use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface, RelayChainResult};
use futures::{FutureExt, Stream, StreamExt};
use pezkuwi_primitives::CandidateEvent;
use pezkuwi_service::{
builder::PezkuwiServiceBuilder, CollatorOverseerGen, CollatorPair, Configuration, FullBackend,
FullClient, Handle, NewFull, NewFullParams, TaskManager,
};
use pezsc_cli::{RuntimeVersion, BizinikiwiCli};
use pezsc_client_api::{
blockchain::BlockStatus, Backend, BlockchainEvents, HeaderBackend, ImportNotifications,
StorageProof, TrieCacheContext,
};
use pezsc_network::{
config::NetworkBackendType,
request_responses::IncomingRequest,
service::traits::{NetworkBackend, NetworkService},
};
use pezsc_telemetry::TelemetryWorkerHandle;
use pezsp_api::{CallApiAt, CallApiAtParams, CallContext, ProvideRuntimeApi};
use pezsp_consensus::SyncOracle;
use pezsp_core::Pair;
use pezsp_state_machine::{Backend as StateBackend, StorageValue};
/// The timeout in seconds after that the waiting for a block should be aborted.
const TIMEOUT_IN_SECONDS: u64 = 6;
/// Provides an implementation of the [`RelayChainInterface`] using a local in-process relay chain
/// node.
#[derive(Clone)]
pub struct RelayChainInProcessInterface {
full_client: Arc<FullClient>,
backend: Arc<FullBackend>,
sync_oracle: Arc<dyn SyncOracle + Send + Sync>,
overseer_handle: Handle,
}
impl RelayChainInProcessInterface {
/// Create a new instance of [`RelayChainInProcessInterface`]
pub fn new(
full_client: Arc<FullClient>,
backend: Arc<FullBackend>,
sync_oracle: Arc<dyn SyncOracle + Send + Sync>,
overseer_handle: Handle,
) -> Self {
Self { full_client, backend, sync_oracle, overseer_handle }
}
}
#[async_trait]
impl RelayChainInterface for RelayChainInProcessInterface {
async fn version(&self, relay_parent: PHash) -> RelayChainResult<RuntimeVersion> {
Ok(self.full_client.runtime_version_at(relay_parent)?)
}
async fn retrieve_dmq_contents(
&self,
para_id: ParaId,
relay_parent: PHash,
) -> RelayChainResult<Vec<InboundDownwardMessage>> {
Ok(self.full_client.runtime_api().dmq_contents(relay_parent, para_id)?)
}
async fn retrieve_all_inbound_hrmp_channel_contents(
&self,
para_id: ParaId,
relay_parent: PHash,
) -> RelayChainResult<BTreeMap<ParaId, Vec<InboundHrmpMessage>>> {
Ok(self
.full_client
.runtime_api()
.inbound_hrmp_channels_contents(relay_parent, para_id)?)
}
async fn header(&self, block_id: BlockId) -> RelayChainResult<Option<PHeader>> {
let hash = match block_id {
BlockId::Hash(hash) => hash,
BlockId::Number(num) =>
if let Some(hash) = self.full_client.hash(num)? {
hash
} else {
return Ok(None);
},
};
let header = self.full_client.header(hash)?;
Ok(header)
}
async fn persisted_validation_data(
&self,
hash: PHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> RelayChainResult<Option<PersistedValidationData>> {
Ok(self.full_client.runtime_api().persisted_validation_data(
hash,
para_id,
occupied_core_assumption,
)?)
}
async fn validation_code_hash(
&self,
hash: PHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> RelayChainResult<Option<ValidationCodeHash>> {
Ok(self.full_client.runtime_api().validation_code_hash(
hash,
para_id,
occupied_core_assumption,
)?)
}
async fn candidate_pending_availability(
&self,
hash: PHash,
para_id: ParaId,
) -> RelayChainResult<Option<CommittedCandidateReceipt>> {
Ok(self
.full_client
.runtime_api()
.candidate_pending_availability(hash, para_id)?
.map(|receipt| receipt.into()))
}
async fn session_index_for_child(&self, hash: PHash) -> RelayChainResult<SessionIndex> {
Ok(self.full_client.runtime_api().session_index_for_child(hash)?)
}
async fn validators(&self, hash: PHash) -> RelayChainResult<Vec<ValidatorId>> {
Ok(self.full_client.runtime_api().validators(hash)?)
}
async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
let notification_stream = self
.full_client
.import_notification_stream()
.map(|notification| notification.header);
Ok(Box::pin(notification_stream))
}
async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
let notification_stream = self
.full_client
.finality_notification_stream()
.map(|notification| notification.header);
Ok(Box::pin(notification_stream))
}
async fn best_block_hash(&self) -> RelayChainResult<PHash> {
Ok(self.backend.blockchain().info().best_hash)
}
async fn finalized_block_hash(&self) -> RelayChainResult<PHash> {
Ok(self.backend.blockchain().info().finalized_hash)
}
async fn call_runtime_api(
&self,
method_name: &'static str,
hash: PHash,
payload: &[u8],
) -> RelayChainResult<Vec<u8>> {
Ok(self.full_client.call_api_at(CallApiAtParams {
at: hash,
function: method_name,
arguments: payload.to_vec(),
overlayed_changes: &Default::default(),
call_context: CallContext::Offchain,
recorder: &None,
extensions: &Default::default(),
})?)
}
async fn is_major_syncing(&self) -> RelayChainResult<bool> {
Ok(self.sync_oracle.is_major_syncing())
}
fn overseer_handle(&self) -> RelayChainResult<Handle> {
Ok(self.overseer_handle.clone())
}
async fn get_storage_by_key(
&self,
relay_parent: PHash,
key: &[u8],
) -> RelayChainResult<Option<StorageValue>> {
let state = self.backend.state_at(relay_parent, TrieCacheContext::Untrusted)?;
state.storage(key).map_err(RelayChainError::GenericError)
}
async fn prove_read(
&self,
relay_parent: PHash,
relevant_keys: &Vec<Vec<u8>>,
) -> RelayChainResult<StorageProof> {
let state_backend = self.backend.state_at(relay_parent, TrieCacheContext::Untrusted)?;
pezsp_state_machine::prove_read(state_backend, relevant_keys)
.map_err(RelayChainError::StateMachineError)
}
/// Wait for a given relay chain block in an async way.
///
/// The caller needs to pass the hash of a block it waits for and the function will return when
/// the block is available or an error occurred.
///
/// The waiting for the block is implemented as follows:
///
/// 1. Get a read lock on the import lock from the backend.
///
/// 2. Check if the block is already imported. If yes, return from the function.
///
/// 3. If the block isn't imported yet, add an import notification listener.
///
/// 4. Poll the import notification listener until the block is imported or the timeout is
/// fired.
///
/// The timeout is set to 6 seconds. This should be enough time to import the block in the
/// current round and if not, the new round of the relay chain already started anyway.
async fn wait_for_block(&self, hash: PHash) -> RelayChainResult<()> {
let mut listener =
match check_block_in_chain(self.backend.clone(), self.full_client.clone(), hash)? {
BlockCheckStatus::InChain => return Ok(()),
BlockCheckStatus::Unknown(listener) => listener,
};
let mut timeout = futures_timer::Delay::new(Duration::from_secs(TIMEOUT_IN_SECONDS)).fuse();
loop {
futures::select! {
_ = timeout => return Err(RelayChainError::WaitTimeout(hash)),
evt = listener.next() => match evt {
Some(evt) if evt.hash == hash => return Ok(()),
// Not the event we waited on.
Some(_) => continue,
None => return Err(RelayChainError::ImportListenerClosed(hash)),
}
}
}
}
async fn new_best_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
let notifications_stream =
self.full_client
.import_notification_stream()
.filter_map(|notification| async move {
notification.is_new_best.then_some(notification.header)
});
Ok(Box::pin(notifications_stream))
}
async fn availability_cores(
&self,
relay_parent: PHash,
) -> RelayChainResult<Vec<CoreState<PHash, BlockNumber>>> {
Ok(self
.full_client
.runtime_api()
.availability_cores(relay_parent)?
.into_iter()
.map(|core_state| core_state.into())
.collect::<Vec<_>>())
}
async fn candidates_pending_availability(
&self,
hash: PHash,
para_id: ParaId,
) -> RelayChainResult<Vec<CommittedCandidateReceipt>> {
Ok(self
.full_client
.runtime_api()
.candidates_pending_availability(hash, para_id)?
.into_iter()
.map(|receipt| receipt.into())
.collect::<Vec<_>>())
}
async fn claim_queue(
&self,
hash: PHash,
) -> RelayChainResult<BTreeMap<CoreIndex, VecDeque<ParaId>>> {
Ok(self.full_client.runtime_api().claim_queue(hash)?)
}
async fn scheduling_lookahead(&self, hash: PHash) -> RelayChainResult<u32> {
Ok(self.full_client.runtime_api().scheduling_lookahead(hash)?)
}
async fn candidate_events(&self, hash: PHash) -> RelayChainResult<Vec<CandidateEvent>> {
Ok(self.full_client.runtime_api().candidate_events(hash)?)
}
}
pub enum BlockCheckStatus {
/// Block is in chain
InChain,
/// Block status is unknown, listener can be used to wait for notification
Unknown(ImportNotifications<PBlock>),
}
// Helper function to check if a block is in chain.
pub fn check_block_in_chain(
backend: Arc<FullBackend>,
client: Arc<FullClient>,
hash: PHash,
) -> RelayChainResult<BlockCheckStatus> {
let _lock = backend.get_import_lock().read();
if backend.blockchain().status(hash)? == BlockStatus::InChain {
return Ok(BlockCheckStatus::InChain);
}
let listener = client.import_notification_stream();
Ok(BlockCheckStatus::Unknown(listener))
}
/// Build Pezkuwi full node with teyrchain bootnode request-response protocol.
fn build_pezkuwi_with_paranode_protocol<Network>(
config: Configuration,
params: NewFullParams<CollatorOverseerGen>,
) -> Result<(NewFull, async_channel::Receiver<IncomingRequest>), pezkuwi_service::Error>
where
Network: NetworkBackend<PBlock, PHash>,
{
let fork_id = config.chain_spec.fork_id().map(ToString::to_string);
let mut pezkuwi_builder = PezkuwiServiceBuilder::<_, Network>::new(config, params)?;
let (config, request_receiver) = bootnode_request_response_config::<_, _, Network>(
pezkuwi_builder.genesis_hash(),
fork_id.as_deref(),
);
pezkuwi_builder.add_extra_request_response_protocol(config);
Ok((pezkuwi_builder.build()?, request_receiver))
}
/// Build the Pezkuwi full node using the given `config`.
#[pezsc_tracing::logging::prefix_logs_with("Relaychain")]
fn build_pezkuwi_full_node(
config: Configuration,
teyrchain_config: &Configuration,
telemetry_worker_handle: Option<TelemetryWorkerHandle>,
hwbench: Option<pezsc_sysinfo::HwBench>,
) -> Result<
(NewFull, Option<CollatorPair>, async_channel::Receiver<IncomingRequest>),
pezkuwi_service::Error,
> {
let (is_teyrchain_node, maybe_collator_key) = if teyrchain_config.role.is_authority() {
let collator_key = CollatorPair::generate().0;
(pezkuwi_service::IsTeyrchainNode::Collator(collator_key.clone()), Some(collator_key))
} else {
(pezkuwi_service::IsTeyrchainNode::FullNode, None)
};
let new_full_params = pezkuwi_service::NewFullParams {
is_teyrchain_node,
// Disable BEEFY. It should not be required by the internal relay chain node.
enable_beefy: false,
force_authoring_backoff: false,
telemetry_worker_handle,
// Pezcumulus doesn't spawn PVF workers, so we can disable version checks.
node_version: None,
secure_validator_mode: false,
workers_path: None,
workers_names: None,
overseer_gen: CollatorOverseerGen,
overseer_message_channel_capacity_override: None,
malus_finality_delay: None,
hwbench,
execute_workers_max_num: None,
prepare_workers_hard_max_num: None,
prepare_workers_soft_max_num: None,
keep_finalized_for: None,
invulnerable_ah_collators: HashSet::new(),
collator_protocol_hold_off: None,
};
let (relay_chain_full_node, paranode_req_receiver) = match config.network.network_backend {
NetworkBackendType::Libp2p => build_pezkuwi_with_paranode_protocol::<
pezsc_network::NetworkWorker<_, _>,
>(config, new_full_params)?,
NetworkBackendType::Litep2p => build_pezkuwi_with_paranode_protocol::<
pezsc_network::Litep2pNetworkBackend,
>(config, new_full_params)?,
};
Ok((relay_chain_full_node, maybe_collator_key, paranode_req_receiver))
}
/// Builds a relay chain interface by constructing a full relay chain node
pub fn build_inprocess_relay_chain(
mut pezkuwi_config: Configuration,
teyrchain_config: &Configuration,
telemetry_worker_handle: Option<TelemetryWorkerHandle>,
task_manager: &mut TaskManager,
hwbench: Option<pezsc_sysinfo::HwBench>,
) -> RelayChainResult<(
Arc<dyn RelayChainInterface + 'static>,
Option<CollatorPair>,
Arc<dyn NetworkService>,
async_channel::Receiver<IncomingRequest>,
)> {
// This is essentially a hack, but we want to ensure that we send the correct node version
// to the telemetry.
pezkuwi_config.impl_version = pezkuwi_cli::Cli::impl_version();
pezkuwi_config.impl_name = pezkuwi_cli::Cli::impl_name();
let (full_node, collator_key, paranode_req_receiver) =
build_pezkuwi_full_node(pezkuwi_config, teyrchain_config, telemetry_worker_handle, hwbench)
.map_err(|e| RelayChainError::Application(Box::new(e) as Box<_>))?;
let relay_chain_interface = Arc::new(RelayChainInProcessInterface::new(
full_node.client,
full_node.backend,
full_node.sync_service,
full_node.overseer_handle.clone().ok_or(RelayChainError::GenericError(
"Overseer not running in full node.".to_string(),
))?,
));
task_manager.add_child(full_node.task_manager);
Ok((relay_chain_interface, collator_key, full_node.network, paranode_req_receiver))
}
#[cfg(test)]
mod tests {
use super::*;
use pezkuwi_primitives::Block as PBlock;
use pezkuwi_test_client::{
construct_transfer_extrinsic, BlockBuilderExt, Client, ClientBlockImportExt,
DefaultTestClientBuilderExt, InitPezkuwiBlockBuilder, TestClientBuilder,
TestClientBuilderExt,
};
use pezsp_consensus::{BlockOrigin, SyncOracle};
use pezsp_runtime::traits::Block as BlockT;
use std::sync::Arc;
use futures::{executor::block_on, poll, task::Poll};
struct DummyNetwork {}
impl SyncOracle for DummyNetwork {
fn is_major_syncing(&self) -> bool {
unimplemented!("Not needed for test")
}
fn is_offline(&self) -> bool {
unimplemented!("Not needed for test")
}
}
fn build_client_backend_and_block() -> (Arc<Client>, PBlock, RelayChainInProcessInterface) {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let block_builder = client.init_pezkuwi_block_builder();
let block = block_builder.build().expect("Finalizes the block").block;
let dummy_network: Arc<dyn SyncOracle + Sync + Send> = Arc::new(DummyNetwork {});
let (tx, _rx) = metered::channel(30);
let mock_handle = Handle::new(tx);
(
client.clone(),
block,
RelayChainInProcessInterface::new(client, backend, dummy_network, mock_handle),
)
}
#[test]
fn returns_directly_for_available_block() {
let (client, block, relay_chain_interface) = build_client_backend_and_block();
let hash = block.hash();
block_on(client.import(BlockOrigin::Own, block)).expect("Imports the block");
block_on(async move {
// Should be ready on the first poll
assert!(matches!(
poll!(relay_chain_interface.wait_for_block(hash)),
Poll::Ready(Ok(()))
));
});
}
#[test]
fn resolve_after_block_import_notification_was_received() {
let (client, block, relay_chain_interface) = build_client_backend_and_block();
let hash = block.hash();
block_on(async move {
let mut future = relay_chain_interface.wait_for_block(hash);
// As the block is not yet imported, the first poll should return `Pending`
assert!(poll!(&mut future).is_pending());
// Import the block that should fire the notification
client.import(BlockOrigin::Own, block).await.expect("Imports the block");
// Now it should have received the notification and report that the block was imported
assert!(matches!(poll!(future), Poll::Ready(Ok(()))));
});
}
#[test]
fn wait_for_block_time_out_when_block_is_not_imported() {
let (_, block, relay_chain_interface) = build_client_backend_and_block();
let hash = block.hash();
assert!(matches!(
block_on(relay_chain_interface.wait_for_block(hash)),
Err(RelayChainError::WaitTimeout(_))
));
}
#[test]
fn do_not_resolve_after_different_block_import_notification_was_received() {
let (client, block, relay_chain_interface) = build_client_backend_and_block();
let hash = block.hash();
let ext = construct_transfer_extrinsic(
&client,
pezsp_keyring::Sr25519Keyring::Alice,
pezsp_keyring::Sr25519Keyring::Bob,
1000,
);
let mut block_builder = client.init_pezkuwi_block_builder();
// Push an extrinsic to get a different block hash.
block_builder.push_pezkuwi_extrinsic(ext).expect("Push extrinsic");
let block2 = block_builder.build().expect("Build second block").block;
let hash2 = block2.hash();
block_on(async move {
let mut future = relay_chain_interface.wait_for_block(hash);
let mut future2 = relay_chain_interface.wait_for_block(hash2);
// As the block is not yet imported, the first poll should return `Pending`
assert!(poll!(&mut future).is_pending());
assert!(poll!(&mut future2).is_pending());
// Import the block that should fire the notification
client.import(BlockOrigin::Own, block2).await.expect("Imports the second block");
// The import notification of the second block should not make this one finish
assert!(poll!(&mut future).is_pending());
// Now it should have received the notification and report that the block was imported
assert!(matches!(poll!(future2), Poll::Ready(Ok(()))));
client.import(BlockOrigin::Own, block).await.expect("Imports the first block");
// Now it should be ready
assert!(matches!(poll!(future), Poll::Ready(Ok(()))));
});
}
}
@@ -0,0 +1,42 @@
[package]
authors.workspace = true
name = "pezcumulus-relay-chain-interface"
version = "0.7.0"
edition.workspace = true
description = "Common interface for different relay chain datasources."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
pezkuwi-overseer = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-version = { workspace = true }
async-trait = { workspace = true }
codec = { workspace = true, default-features = true }
futures = { workspace = true }
jsonrpsee-core = { workspace = true }
thiserror = { workspace = true }
[features]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
"pezsp-version/runtime-benchmarks",
]
@@ -0,0 +1,431 @@
// 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 <https://www.gnu.org/licenses/>.
use std::{
collections::{BTreeMap, VecDeque},
pin::Pin,
sync::Arc,
};
use futures::Stream;
use pezkuwi_overseer::prometheus::PrometheusError;
use pezsc_client_api::StorageProof;
use pezsp_version::RuntimeVersion;
use async_trait::async_trait;
use codec::{Decode, Encode, Error as CodecError};
use jsonrpsee_core::ClientError as JsonRpcError;
use pezsp_api::ApiError;
use cumulus_primitives_core::relay_chain::{BlockId, CandidateEvent, Hash as RelayHash};
pub use cumulus_primitives_core::{
relay_chain::{
BlockNumber, CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CoreIndex,
CoreState, Hash as PHash, Header as PHeader, InboundHrmpMessage, OccupiedCoreAssumption,
SessionIndex, ValidationCodeHash, ValidatorId,
},
InboundDownwardMessage, ParaId, PersistedValidationData,
};
pub use pezkuwi_overseer::Handle as OverseerHandle;
pub use pezsp_state_machine::StorageValue;
pub type RelayChainResult<T> = Result<T, RelayChainError>;
#[derive(thiserror::Error, Debug)]
pub enum RelayChainError {
#[error("Error occurred while calling relay chain runtime: {0}")]
ApiError(#[from] ApiError),
#[error("Timeout while waiting for relay-chain block `{0}` to be imported.")]
WaitTimeout(PHash),
#[error("Import listener closed while waiting for relay-chain block `{0}` to be imported.")]
ImportListenerClosed(PHash),
#[error(
"Blockchain returned an error while waiting for relay-chain block `{0}` to be imported: {1}"
)]
WaitBlockchainError(PHash, pezsp_blockchain::Error),
#[error("Blockchain returned an error: {0}")]
BlockchainError(#[from] pezsp_blockchain::Error),
#[error("State machine error occurred: {0}")]
StateMachineError(Box<dyn pezsp_state_machine::Error>),
#[error("Unable to call RPC method '{0}'")]
RpcCallError(String),
#[error("RPC Error: '{0}'")]
JsonRpcError(#[from] JsonRpcError),
#[error("Unable to communicate with RPC worker: {0}")]
WorkerCommunicationError(String),
#[error("Scale codec deserialization error: {0}")]
DeserializationError(CodecError),
#[error(transparent)]
Application(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("Prometheus error: {0}")]
PrometheusError(#[from] PrometheusError),
#[error("Unspecified error occurred: {0}")]
GenericError(String),
}
impl From<RelayChainError> for ApiError {
fn from(r: RelayChainError) -> Self {
pezsp_api::ApiError::Application(Box::new(r))
}
}
impl From<CodecError> for RelayChainError {
fn from(e: CodecError) -> Self {
RelayChainError::DeserializationError(e)
}
}
impl From<RelayChainError> for pezsp_blockchain::Error {
fn from(r: RelayChainError) -> Self {
pezsp_blockchain::Error::Application(Box::new(r))
}
}
impl<T: std::error::Error + Send + Sync + 'static> From<Box<T>> for RelayChainError {
fn from(r: Box<T>) -> Self {
RelayChainError::Application(r)
}
}
/// Trait that provides all necessary methods for interaction between collator and relay chain.
#[async_trait]
pub trait RelayChainInterface: Send + Sync {
/// Fetch a storage item by key.
async fn get_storage_by_key(
&self,
relay_parent: PHash,
key: &[u8],
) -> RelayChainResult<Option<StorageValue>>;
/// Fetch a vector of current validators.
async fn validators(&self, block_id: PHash) -> RelayChainResult<Vec<ValidatorId>>;
/// Get the hash of the current best block.
async fn best_block_hash(&self) -> RelayChainResult<PHash>;
/// Fetch the block header of a given hash or height, if it exists.
async fn header(&self, block_id: BlockId) -> RelayChainResult<Option<PHeader>>;
/// Get the hash of the finalized block.
async fn finalized_block_hash(&self) -> RelayChainResult<PHash>;
/// Call an arbitrary runtime api. The input and output are SCALE-encoded.
async fn call_runtime_api(
&self,
method_name: &'static str,
hash: RelayHash,
payload: &[u8],
) -> RelayChainResult<Vec<u8>>;
/// Returns the whole contents of the downward message queue for the teyrchain we are collating
/// for.
///
/// Returns `None` in case of an error.
async fn retrieve_dmq_contents(
&self,
para_id: ParaId,
relay_parent: PHash,
) -> RelayChainResult<Vec<InboundDownwardMessage>>;
/// Returns channels contents for each inbound HRMP channel addressed to the teyrchain we are
/// collating for.
///
/// Empty channels are also included.
async fn retrieve_all_inbound_hrmp_channel_contents(
&self,
para_id: ParaId,
relay_parent: PHash,
) -> RelayChainResult<BTreeMap<ParaId, Vec<InboundHrmpMessage>>>;
/// Yields the persisted validation data for the given `ParaId` along with an assumption that
/// should be used if the para currently occupies a core.
///
/// Returns `None` if either the para is not registered or the assumption is `Freed`
/// and the para already occupies a core.
async fn persisted_validation_data(
&self,
block_id: PHash,
para_id: ParaId,
_: OccupiedCoreAssumption,
) -> RelayChainResult<Option<PersistedValidationData>>;
/// Get the receipt of the first candidate pending availability of this para_id. This returns
/// `Some` for any paras assigned to occupied cores in `availability_cores` and `None`
/// otherwise.
#[deprecated(
note = "`candidate_pending_availability` only returns one candidate and is deprecated. Use `candidates_pending_availability` instead."
)]
async fn candidate_pending_availability(
&self,
block_id: PHash,
para_id: ParaId,
) -> RelayChainResult<Option<CommittedCandidateReceipt>>;
/// Returns the session index expected at a child of the block.
async fn session_index_for_child(&self, block_id: PHash) -> RelayChainResult<SessionIndex>;
/// Get a stream of import block notifications.
async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>>;
/// Get a stream of new best block notifications.
async fn new_best_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>>;
/// Wait for a block with a given hash in the relay chain.
///
/// This method returns immediately on error or if the block is already
/// reported to be in chain. Otherwise, it waits for the block to arrive.
async fn wait_for_block(&self, hash: PHash) -> RelayChainResult<()>;
/// Get a stream of finality notifications.
async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>>;
/// Whether the synchronization service is undergoing major sync.
/// Returns true if so.
async fn is_major_syncing(&self) -> RelayChainResult<bool>;
/// Get a handle to the overseer.
fn overseer_handle(&self) -> RelayChainResult<OverseerHandle>;
/// Generate a storage read proof.
async fn prove_read(
&self,
relay_parent: PHash,
relevant_keys: &Vec<Vec<u8>>,
) -> RelayChainResult<StorageProof>;
/// Returns the validation code hash for the given `para_id` using the given
/// `occupied_core_assumption`.
async fn validation_code_hash(
&self,
relay_parent: PHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> RelayChainResult<Option<ValidationCodeHash>>;
/// Get the receipts of all candidates pending availability for this para_id.
async fn candidates_pending_availability(
&self,
block_id: PHash,
para_id: ParaId,
) -> RelayChainResult<Vec<CommittedCandidateReceipt>>;
/// Get the runtime version of the relay chain.
async fn version(&self, relay_parent: PHash) -> RelayChainResult<RuntimeVersion>;
/// Yields information on all availability cores as relevant to the child block.
///
/// Cores are either free, scheduled or occupied. Free cores can have paras assigned to them.
async fn availability_cores(
&self,
relay_parent: PHash,
) -> RelayChainResult<Vec<CoreState<PHash, BlockNumber>>>;
/// Fetch the claim queue.
async fn claim_queue(
&self,
relay_parent: PHash,
) -> RelayChainResult<BTreeMap<CoreIndex, VecDeque<ParaId>>>;
/// Fetch the scheduling lookahead value.
async fn scheduling_lookahead(&self, relay_parent: PHash) -> RelayChainResult<u32>;
async fn candidate_events(&self, at: RelayHash) -> RelayChainResult<Vec<CandidateEvent>>;
}
#[async_trait]
impl<T> RelayChainInterface for Arc<T>
where
T: RelayChainInterface + ?Sized,
{
async fn retrieve_dmq_contents(
&self,
para_id: ParaId,
relay_parent: PHash,
) -> RelayChainResult<Vec<InboundDownwardMessage>> {
(**self).retrieve_dmq_contents(para_id, relay_parent).await
}
async fn retrieve_all_inbound_hrmp_channel_contents(
&self,
para_id: ParaId,
relay_parent: PHash,
) -> RelayChainResult<BTreeMap<ParaId, Vec<InboundHrmpMessage>>> {
(**self).retrieve_all_inbound_hrmp_channel_contents(para_id, relay_parent).await
}
async fn persisted_validation_data(
&self,
block_id: PHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> RelayChainResult<Option<PersistedValidationData>> {
(**self)
.persisted_validation_data(block_id, para_id, occupied_core_assumption)
.await
}
#[allow(deprecated)]
async fn candidate_pending_availability(
&self,
block_id: PHash,
para_id: ParaId,
) -> RelayChainResult<Option<CommittedCandidateReceipt>> {
(**self).candidate_pending_availability(block_id, para_id).await
}
async fn session_index_for_child(&self, block_id: PHash) -> RelayChainResult<SessionIndex> {
(**self).session_index_for_child(block_id).await
}
async fn validators(&self, block_id: PHash) -> RelayChainResult<Vec<ValidatorId>> {
(**self).validators(block_id).await
}
async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
(**self).import_notification_stream().await
}
async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
(**self).finality_notification_stream().await
}
async fn best_block_hash(&self) -> RelayChainResult<PHash> {
(**self).best_block_hash().await
}
async fn finalized_block_hash(&self) -> RelayChainResult<PHash> {
(**self).finalized_block_hash().await
}
async fn call_runtime_api(
&self,
method_name: &'static str,
hash: RelayHash,
payload: &[u8],
) -> RelayChainResult<Vec<u8>> {
(**self).call_runtime_api(method_name, hash, payload).await
}
async fn is_major_syncing(&self) -> RelayChainResult<bool> {
(**self).is_major_syncing().await
}
fn overseer_handle(&self) -> RelayChainResult<OverseerHandle> {
(**self).overseer_handle()
}
async fn get_storage_by_key(
&self,
relay_parent: PHash,
key: &[u8],
) -> RelayChainResult<Option<StorageValue>> {
(**self).get_storage_by_key(relay_parent, key).await
}
async fn prove_read(
&self,
relay_parent: PHash,
relevant_keys: &Vec<Vec<u8>>,
) -> RelayChainResult<StorageProof> {
(**self).prove_read(relay_parent, relevant_keys).await
}
async fn wait_for_block(&self, hash: PHash) -> RelayChainResult<()> {
(**self).wait_for_block(hash).await
}
async fn new_best_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
(**self).new_best_notification_stream().await
}
async fn header(&self, block_id: BlockId) -> RelayChainResult<Option<PHeader>> {
(**self).header(block_id).await
}
async fn validation_code_hash(
&self,
relay_parent: PHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> RelayChainResult<Option<ValidationCodeHash>> {
(**self)
.validation_code_hash(relay_parent, para_id, occupied_core_assumption)
.await
}
async fn availability_cores(
&self,
relay_parent: PHash,
) -> RelayChainResult<Vec<CoreState<PHash, BlockNumber>>> {
(**self).availability_cores(relay_parent).await
}
async fn candidates_pending_availability(
&self,
block_id: PHash,
para_id: ParaId,
) -> RelayChainResult<Vec<CommittedCandidateReceipt>> {
(**self).candidates_pending_availability(block_id, para_id).await
}
async fn version(&self, relay_parent: PHash) -> RelayChainResult<RuntimeVersion> {
(**self).version(relay_parent).await
}
async fn claim_queue(
&self,
relay_parent: PHash,
) -> RelayChainResult<BTreeMap<CoreIndex, VecDeque<ParaId>>> {
(**self).claim_queue(relay_parent).await
}
async fn scheduling_lookahead(&self, relay_parent: PHash) -> RelayChainResult<u32> {
(**self).scheduling_lookahead(relay_parent).await
}
async fn candidate_events(&self, at: RelayHash) -> RelayChainResult<Vec<CandidateEvent>> {
(**self).candidate_events(at).await
}
}
/// Helper function to call an arbitrary runtime API using a `RelayChainInterface` client.
/// Unlike the trait method, this function can be generic, so it handles the encoding of input and
/// output params.
pub async fn call_runtime_api<R>(
client: &(impl RelayChainInterface + ?Sized),
method_name: &'static str,
hash: RelayHash,
payload: impl Encode,
) -> RelayChainResult<R>
where
R: Decode,
{
let res = client.call_runtime_api(method_name, hash, &payload.encode()).await?;
Decode::decode(&mut &*res).map_err(Into::into)
}
@@ -0,0 +1,76 @@
[package]
authors.workspace = true
name = "pezcumulus-relay-chain-minimal-node"
version = "0.7.0"
edition.workspace = true
description = "Minimal node implementation to be used in tandem with RPC or light-client mode."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
# pezkuwi deps
pezkuwi-core-primitives = { workspace = true, default-features = true }
pezkuwi-node-network-protocol = { workspace = true, default-features = true }
pezkuwi-node-subsystem-util = { workspace = true, default-features = true }
pezkuwi-overseer = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
pezkuwi-network-bridge = { workspace = true, default-features = true }
pezkuwi-service = { workspace = true, default-features = true }
# bizinikiwi deps
prometheus-endpoint = { workspace = true, default-features = true }
pezsc-authority-discovery = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsc-network-common = { workspace = true, default-features = true }
pezsc-service = { workspace = true, default-features = true }
pezsc-tracing = { workspace = true, default-features = true }
pezsc-utils = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-consensus-babe = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
# pezcumulus deps
pezcumulus-client-bootnodes = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
pezcumulus-relay-chain-rpc-interface = { workspace = true, default-features = true }
array-bytes = { workspace = true, default-features = true }
async-channel = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
tracing = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-client-bootnodes/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezcumulus-relay-chain-rpc-interface/runtime-benchmarks",
"pezkuwi-core-primitives/runtime-benchmarks",
"pezkuwi-network-bridge/runtime-benchmarks",
"pezkuwi-node-network-protocol/runtime-benchmarks",
"pezkuwi-node-subsystem-util/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezkuwi-service/runtime-benchmarks",
"pezsc-authority-discovery/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-network-common/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsc-service/runtime-benchmarks",
"pezsc-tracing/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus-babe/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
@@ -0,0 +1,540 @@
// 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 <https://www.gnu.org/licenses/>.
use std::{
collections::{BTreeMap, VecDeque},
pin::Pin,
};
use cumulus_primitives_core::{InboundDownwardMessage, ParaId, PersistedValidationData};
use cumulus_relay_chain_interface::{RelayChainError, RelayChainResult};
use cumulus_relay_chain_rpc_interface::RelayChainRpcClient;
use futures::{Stream, StreamExt};
use pezkuwi_core_primitives::{Block, BlockNumber, Hash, Header};
use pezkuwi_overseer::{ChainApiBackend, RuntimeApiSubsystemClient};
use pezkuwi_primitives::{
async_backing::{AsyncBackingParams, BackingState, Constraints},
slashing, ApprovalVotingParams, CoreIndex, NodeFeatures,
};
use pezsc_authority_discovery::{AuthorityDiscovery, Error as AuthorityDiscoveryError};
use pezsc_client_api::AuxStore;
use pezsp_api::{ApiError, RuntimeApiInfo};
use pezsp_blockchain::Info;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
#[derive(Clone)]
pub struct BlockChainRpcClient {
rpc_client: RelayChainRpcClient,
}
impl BlockChainRpcClient {
pub fn new(rpc_client: RelayChainRpcClient) -> Self {
Self { rpc_client }
}
pub async fn chain_get_header(
&self,
hash: Option<Hash>,
) -> Result<Option<Header>, RelayChainError> {
self.rpc_client.chain_get_header(hash).await
}
pub async fn block_get_hash(
&self,
number: Option<BlockNumber>,
) -> Result<Option<Hash>, RelayChainError> {
self.rpc_client.chain_get_block_hash(number).await
}
}
#[async_trait::async_trait]
impl ChainApiBackend for BlockChainRpcClient {
async fn header(
&self,
hash: <Block as BlockT>::Hash,
) -> pezsp_blockchain::Result<Option<<Block as BlockT>::Header>> {
Ok(self.rpc_client.chain_get_header(Some(hash)).await?)
}
async fn info(&self) -> pezsp_blockchain::Result<Info<Block>> {
let (best_header_opt, genesis_hash, finalized_head) = futures::try_join!(
self.rpc_client.chain_get_header(None),
self.rpc_client.chain_get_head(Some(0)),
self.rpc_client.chain_get_finalized_head()
)?;
let best_header = best_header_opt.ok_or_else(|| {
RelayChainError::GenericError(
"Unable to retrieve best header from relay chain.".to_string(),
)
})?;
let finalized_header =
self.rpc_client.chain_get_header(Some(finalized_head)).await?.ok_or_else(|| {
RelayChainError::GenericError(
"Unable to retrieve finalized header from relay chain.".to_string(),
)
})?;
Ok(Info {
best_hash: best_header.hash(),
best_number: best_header.number,
genesis_hash,
finalized_hash: finalized_head,
finalized_number: finalized_header.number,
finalized_state: Some((finalized_header.hash(), finalized_header.number)),
number_leaves: 1,
block_gap: None,
})
}
async fn number(
&self,
hash: <Block as BlockT>::Hash,
) -> pezsp_blockchain::Result<Option<<<Block as BlockT>::Header as HeaderT>::Number>> {
Ok(self
.rpc_client
.chain_get_header(Some(hash))
.await?
.map(|maybe_header| maybe_header.number))
}
async fn hash(
&self,
number: NumberFor<Block>,
) -> pezsp_blockchain::Result<Option<<Block as BlockT>::Hash>> {
Ok(self.rpc_client.chain_get_block_hash(number.into()).await?)
}
}
#[async_trait::async_trait]
impl RuntimeApiSubsystemClient for BlockChainRpcClient {
async fn validators(
&self,
at: Hash,
) -> Result<Vec<pezkuwi_primitives::ValidatorId>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_validators(at).await?)
}
async fn validator_groups(
&self,
at: Hash,
) -> Result<
(
Vec<Vec<pezkuwi_primitives::ValidatorIndex>>,
pezkuwi_primitives::GroupRotationInfo<BlockNumber>,
),
pezsp_api::ApiError,
> {
Ok(self.rpc_client.teyrchain_host_validator_groups(at).await?)
}
async fn availability_cores(
&self,
at: Hash,
) -> Result<
Vec<pezkuwi_primitives::CoreState<Hash, pezkuwi_core_primitives::BlockNumber>>,
pezsp_api::ApiError,
> {
Ok(self.rpc_client.teyrchain_host_availability_cores(at).await?)
}
async fn persisted_validation_data(
&self,
at: Hash,
para_id: ParaId,
assumption: pezkuwi_primitives::OccupiedCoreAssumption,
) -> Result<Option<PersistedValidationData<Hash, BlockNumber>>, pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_persisted_validation_data(at, para_id, assumption)
.await?)
}
async fn assumed_validation_data(
&self,
at: Hash,
para_id: ParaId,
expected_persisted_validation_data_hash: Hash,
) -> Result<
Option<(
PersistedValidationData<Hash, BlockNumber>,
pezkuwi_primitives::ValidationCodeHash,
)>,
pezsp_api::ApiError,
> {
Ok(self
.rpc_client
.teyrchain_host_assumed_validation_data(
at,
para_id,
expected_persisted_validation_data_hash,
)
.await?)
}
async fn check_validation_outputs(
&self,
at: Hash,
para_id: ParaId,
outputs: pezkuwi_primitives::CandidateCommitments,
) -> Result<bool, pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_check_validation_outputs(at, para_id, outputs)
.await?)
}
async fn session_index_for_child(
&self,
at: Hash,
) -> Result<pezkuwi_primitives::SessionIndex, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_session_index_for_child(at).await?)
}
async fn validation_code(
&self,
at: Hash,
para_id: ParaId,
assumption: pezkuwi_primitives::OccupiedCoreAssumption,
) -> Result<Option<pezkuwi_primitives::ValidationCode>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_validation_code(at, para_id, assumption).await?)
}
async fn candidate_pending_availability(
&self,
at: Hash,
para_id: cumulus_primitives_core::ParaId,
) -> Result<Option<pezkuwi_primitives::CommittedCandidateReceiptV2<Hash>>, pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_candidate_pending_availability(at, para_id)
.await?)
}
async fn candidate_events(
&self,
at: Hash,
) -> Result<Vec<pezkuwi_primitives::CandidateEvent<Hash>>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_candidate_events(at).await?)
}
async fn dmq_contents(
&self,
at: Hash,
recipient: ParaId,
) -> Result<Vec<InboundDownwardMessage<BlockNumber>>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_dmq_contents(recipient, at).await?)
}
async fn inbound_hrmp_channels_contents(
&self,
at: Hash,
recipient: ParaId,
) -> Result<
std::collections::BTreeMap<
ParaId,
Vec<pezkuwi_core_primitives::InboundHrmpMessage<BlockNumber>>,
>,
pezsp_api::ApiError,
> {
Ok(self
.rpc_client
.teyrchain_host_inbound_hrmp_channels_contents(recipient, at)
.await?)
}
async fn validation_code_by_hash(
&self,
at: Hash,
validation_code_hash: pezkuwi_primitives::ValidationCodeHash,
) -> Result<Option<pezkuwi_primitives::ValidationCode>, pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_validation_code_by_hash(at, validation_code_hash)
.await?)
}
async fn on_chain_votes(
&self,
at: Hash,
) -> Result<Option<pezkuwi_primitives::ScrapedOnChainVotes<Hash>>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_on_chain_votes(at).await?)
}
async fn session_info(
&self,
at: Hash,
index: pezkuwi_primitives::SessionIndex,
) -> Result<Option<pezkuwi_primitives::SessionInfo>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_session_info(at, index).await?)
}
async fn session_executor_params(
&self,
at: Hash,
session_index: pezkuwi_primitives::SessionIndex,
) -> Result<Option<pezkuwi_primitives::ExecutorParams>, pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_session_executor_params(at, session_index)
.await?)
}
async fn submit_pvf_check_statement(
&self,
at: Hash,
stmt: pezkuwi_primitives::PvfCheckStatement,
signature: pezkuwi_primitives::ValidatorSignature,
) -> Result<(), pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_submit_pvf_check_statement(at, stmt, signature)
.await?)
}
async fn pvfs_require_precheck(
&self,
at: Hash,
) -> Result<Vec<pezkuwi_primitives::ValidationCodeHash>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_pvfs_require_precheck(at).await?)
}
async fn validation_code_hash(
&self,
at: Hash,
para_id: ParaId,
assumption: pezkuwi_primitives::OccupiedCoreAssumption,
) -> Result<Option<pezkuwi_primitives::ValidationCodeHash>, pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_validation_code_hash(at, para_id, assumption)
.await?)
}
async fn current_epoch(&self, at: Hash) -> Result<pezsp_consensus_babe::Epoch, pezsp_api::ApiError> {
Ok(self.rpc_client.babe_api_current_epoch(at).await?)
}
async fn authorities(
&self,
at: Hash,
) -> std::result::Result<Vec<pezkuwi_primitives::AuthorityDiscoveryId>, pezsp_api::ApiError> {
Ok(self.rpc_client.authority_discovery_authorities(at).await?)
}
async fn api_version_teyrchain_host(&self, at: Hash) -> Result<Option<u32>, pezsp_api::ApiError> {
let api_id = <dyn pezkuwi_primitives::runtime_api::TeyrchainHost<Block>>::ID;
Ok(self.rpc_client.runtime_version(at).await.map(|v| v.api_version(&api_id))?)
}
async fn disputes(
&self,
at: Hash,
) -> Result<
Vec<(
pezkuwi_primitives::SessionIndex,
pezkuwi_primitives::CandidateHash,
pezkuwi_primitives::DisputeState<pezkuwi_primitives::BlockNumber>,
)>,
ApiError,
> {
Ok(self.rpc_client.teyrchain_host_disputes(at).await?)
}
async fn unapplied_slashes(
&self,
at: Hash,
) -> Result<
Vec<(
pezkuwi_primitives::SessionIndex,
pezkuwi_primitives::CandidateHash,
slashing::LegacyPendingSlashes,
)>,
ApiError,
> {
Ok(self.rpc_client.teyrchain_host_unapplied_slashes(at).await?)
}
async fn unapplied_slashes_v2(
&self,
at: Hash,
) -> Result<
Vec<(
pezkuwi_primitives::SessionIndex,
pezkuwi_primitives::CandidateHash,
slashing::PendingSlashes,
)>,
ApiError,
> {
Ok(self.rpc_client.teyrchain_host_unapplied_slashes_v2(at).await?)
}
async fn key_ownership_proof(
&self,
at: Hash,
validator_id: pezkuwi_primitives::ValidatorId,
) -> Result<Option<slashing::OpaqueKeyOwnershipProof>, ApiError> {
Ok(self.rpc_client.teyrchain_host_key_ownership_proof(at, validator_id).await?)
}
async fn submit_report_dispute_lost(
&self,
at: Hash,
dispute_proof: slashing::DisputeProof,
key_ownership_proof: slashing::OpaqueKeyOwnershipProof,
) -> Result<Option<()>, ApiError> {
Ok(self
.rpc_client
.teyrchain_host_submit_report_dispute_lost(at, dispute_proof, key_ownership_proof)
.await?)
}
async fn minimum_backing_votes(
&self,
at: Hash,
session_index: pezkuwi_primitives::SessionIndex,
) -> Result<u32, ApiError> {
Ok(self.rpc_client.teyrchain_host_minimum_backing_votes(at, session_index).await?)
}
async fn disabled_validators(
&self,
at: Hash,
) -> Result<Vec<pezkuwi_primitives::ValidatorIndex>, ApiError> {
Ok(self.rpc_client.teyrchain_host_disabled_validators(at).await?)
}
async fn async_backing_params(&self, at: Hash) -> Result<AsyncBackingParams, ApiError> {
Ok(self.rpc_client.teyrchain_host_async_backing_params(at).await?)
}
async fn para_backing_state(
&self,
at: Hash,
para_id: ParaId,
) -> Result<Option<BackingState>, ApiError> {
Ok(self.rpc_client.teyrchain_host_para_backing_state(at, para_id).await?)
}
/// Approval voting configuration parameters
async fn approval_voting_params(
&self,
at: Hash,
session_index: pezkuwi_primitives::SessionIndex,
) -> Result<ApprovalVotingParams, ApiError> {
Ok(self
.rpc_client
.teyrchain_host_staging_approval_voting_params(at, session_index)
.await?)
}
async fn node_features(&self, at: Hash) -> Result<NodeFeatures, ApiError> {
Ok(self.rpc_client.teyrchain_host_node_features(at).await?)
}
async fn claim_queue(
&self,
at: Hash,
) -> Result<BTreeMap<CoreIndex, VecDeque<ParaId>>, ApiError> {
Ok(self.rpc_client.teyrchain_host_claim_queue(at).await?)
}
async fn candidates_pending_availability(
&self,
at: Hash,
para_id: cumulus_primitives_core::ParaId,
) -> Result<Vec<pezkuwi_primitives::CommittedCandidateReceiptV2<Hash>>, pezsp_api::ApiError> {
Ok(self
.rpc_client
.teyrchain_host_candidates_pending_availability(at, para_id)
.await?)
}
async fn backing_constraints(
&self,
at: Hash,
para_id: ParaId,
) -> Result<Option<Constraints>, ApiError> {
Ok(self.rpc_client.teyrchain_host_backing_constraints(at, para_id).await?)
}
async fn scheduling_lookahead(&self, at: Hash) -> Result<u32, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_scheduling_lookahead(at).await?)
}
async fn validation_code_bomb_limit(&self, at: Hash) -> Result<u32, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_validation_code_bomb_limit(at).await?)
}
async fn para_ids(&self, at: Hash) -> Result<Vec<ParaId>, pezsp_api::ApiError> {
Ok(self.rpc_client.teyrchain_host_para_ids(at).await?)
}
}
#[async_trait::async_trait]
impl AuthorityDiscovery<Block> for BlockChainRpcClient {
async fn authorities(
&self,
at: Hash,
) -> std::result::Result<Vec<pezkuwi_primitives::AuthorityDiscoveryId>, pezsp_api::ApiError> {
let result = self.rpc_client.authority_discovery_authorities(at).await?;
Ok(result)
}
async fn best_hash(&self) -> std::result::Result<Hash, AuthorityDiscoveryError> {
self.block_get_hash(None)
.await
.ok()
.flatten()
.ok_or_else(|| AuthorityDiscoveryError::BestBlockFetchingError)
}
}
impl BlockChainRpcClient {
pub async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = Header> + Send>>> {
Ok(self.rpc_client.get_imported_heads_stream()?.boxed())
}
pub async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = Header> + Send>>> {
Ok(self.rpc_client.get_finalized_heads_stream()?.boxed())
}
}
// Implementation required by ChainApiSubsystem
// but never called in our case.
impl AuxStore for BlockChainRpcClient {
fn insert_aux<
'a,
'b: 'a,
'c: 'a,
I: IntoIterator<Item = &'a (&'c [u8], &'c [u8])>,
D: IntoIterator<Item = &'a &'b [u8]>,
>(
&self,
_insert: I,
_delete: D,
) -> pezsp_blockchain::Result<()> {
unimplemented!("Not supported on the RPC collator")
}
fn get_aux(&self, _key: &[u8]) -> pezsp_blockchain::Result<Option<Vec<u8>>> {
unimplemented!("Not supported on the RPC collator")
}
}
@@ -0,0 +1,150 @@
// 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 <https://www.gnu.org/licenses/>.
use futures::{select, StreamExt};
use std::sync::Arc;
use pezkuwi_overseer::{
BlockInfo, Handle, Overseer, OverseerConnector, OverseerHandle, SpawnGlue, UnpinHandle,
};
use pezkuwi_service::overseer::{collator_overseer_builder, OverseerGenArgs};
use pezsc_network::{request_responses::IncomingRequest, service::traits::NetworkService};
use pezsc_service::TaskManager;
use pezsc_utils::mpsc::tracing_unbounded;
use cumulus_relay_chain_interface::RelayChainError;
use crate::BlockChainRpcClient;
fn build_overseer(
connector: OverseerConnector,
args: OverseerGenArgs<pezsc_service::SpawnTaskHandle, BlockChainRpcClient>,
) -> Result<
(Overseer<SpawnGlue<pezsc_service::SpawnTaskHandle>, Arc<BlockChainRpcClient>>, OverseerHandle),
RelayChainError,
> {
let builder =
collator_overseer_builder(args).map_err(|e| RelayChainError::Application(e.into()))?;
builder
.build_with_connector(connector)
.map_err(|e| RelayChainError::Application(e.into()))
}
pub(crate) fn spawn_overseer(
overseer_args: OverseerGenArgs<pezsc_service::SpawnTaskHandle, BlockChainRpcClient>,
task_manager: &TaskManager,
relay_chain_rpc_client: Arc<BlockChainRpcClient>,
) -> Result<pezkuwi_overseer::Handle, RelayChainError> {
let (overseer, overseer_handle) = build_overseer(OverseerConnector::default(), overseer_args)
.map_err(|e| {
tracing::error!("Failed to initialize overseer: {}", e);
e
})?;
let overseer_handle = Handle::new(overseer_handle);
{
let handle = overseer_handle.clone();
task_manager.spawn_essential_handle().spawn_blocking(
"overseer",
None,
Box::pin(async move {
use futures::{pin_mut, FutureExt};
let forward = forward_collator_events(relay_chain_rpc_client, handle).fuse();
let overseer_fut = overseer.run().fuse();
pin_mut!(overseer_fut);
pin_mut!(forward);
select! {
_ = forward => (),
_ = overseer_fut => (),
}
}),
);
}
Ok(overseer_handle)
}
/// Minimal relay chain node representation
pub struct NewMinimalNode {
/// Task manager running all tasks for the minimal node
pub task_manager: TaskManager,
/// Overseer handle to interact with subsystems
pub overseer_handle: Handle,
/// Network service
pub network_service: Arc<dyn NetworkService>,
/// Teyrchain bootnode request-response protocol receiver
pub paranode_rx: async_channel::Receiver<IncomingRequest>,
}
/// Glues together the [`Overseer`] and `BlockchainEvents` by forwarding
/// import and finality notifications into the [`OverseerHandle`].
async fn forward_collator_events(
client: Arc<BlockChainRpcClient>,
mut handle: Handle,
) -> Result<(), RelayChainError> {
let mut finality = client.finality_notification_stream().await?.fuse();
let mut imports = client.import_notification_stream().await?.fuse();
// Collators do no need to pin any specific blocks
let (dummy_sink, _) = tracing_unbounded("does-not-matter", 42);
let dummy_unpin_handle = UnpinHandle::new(Default::default(), dummy_sink);
loop {
select! {
f = finality.next() => {
match f {
Some(header) => {
let hash = header.hash();
tracing::info!(
target: "minimal-pezkuwi-node",
"Received finalized block via RPC: #{} ({} -> {})",
header.number,
header.parent_hash,
hash,
);
let unpin_handle = dummy_unpin_handle.clone();
let block_info = BlockInfo { hash, parent_hash: header.parent_hash, number: header.number, unpin_handle };
handle.block_finalized(block_info).await;
}
None => return Err(RelayChainError::GenericError("Relay chain finality stream ended.".to_string())),
}
},
i = imports.next() => {
match i {
Some(header) => {
let hash = header.hash();
tracing::info!(
target: "minimal-pezkuwi-node",
"Received imported block via RPC: #{} ({} -> {})",
header.number,
header.parent_hash,
hash,
);
let unpin_handle = dummy_unpin_handle.clone();
let block_info = BlockInfo { hash, parent_hash: header.parent_hash, number: header.number, unpin_handle };
handle.block_imported(block_info).await;
}
None => return Err(RelayChainError::GenericError("Relay chain import stream ended.".to_string())),
}
}
}
}
}
@@ -0,0 +1,293 @@
// 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 <https://www.gnu.org/licenses/>.
use collator_overseer::NewMinimalNode;
use cumulus_client_bootnodes::bootnode_request_response_config;
use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface, RelayChainResult};
use cumulus_relay_chain_rpc_interface::{RelayChainRpcClient, RelayChainRpcInterface, Url};
use network::build_collator_network;
use pezkuwi_network_bridge::{peer_sets_info, IsAuthority};
use pezkuwi_node_network_protocol::{
peer_set::{PeerSet, PeerSetProtocolNames},
request_response::{
v1, v2, IncomingRequest, IncomingRequestReceiver, Protocol, ReqProtocolNames,
},
};
use pezkuwi_core_primitives::{Block as RelayBlock, Hash as RelayHash};
use pezkuwi_node_subsystem_util::metrics::prometheus::Registry;
use pezkuwi_primitives::CollatorPair;
use pezkuwi_service::{overseer::OverseerGenArgs, IsTeyrchainNode};
use pezsc_authority_discovery::Service as AuthorityDiscoveryService;
use pezsc_network::{
config::FullNetworkConfiguration, request_responses::IncomingRequest as GenericIncomingRequest,
service::traits::NetworkService, Event, NetworkBackend, NetworkEventStream,
};
use pezsc_service::{config::PrometheusConfig, Configuration, TaskManager};
use pezsp_runtime::{app_crypto::Pair, traits::Block as BlockT};
use futures::{FutureExt, StreamExt};
use std::sync::Arc;
mod blockchain_rpc_client;
mod collator_overseer;
mod network;
pub use blockchain_rpc_client::BlockChainRpcClient;
const LOG_TARGET: &str = "minimal-relaychain-node";
fn build_authority_discovery_service<Block: BlockT>(
task_manager: &TaskManager,
client: Arc<BlockChainRpcClient>,
config: &Configuration,
network: Arc<dyn NetworkService>,
prometheus_registry: Option<Registry>,
) -> AuthorityDiscoveryService {
let auth_disc_publish_non_global_ips = config.network.allow_non_globals_in_dht;
let auth_disc_public_addresses = config.network.public_addresses.clone();
let authority_discovery_role = pezsc_authority_discovery::Role::Discover;
let dht_event_stream = network.event_stream("authority-discovery").filter_map(|e| async move {
match e {
Event::Dht(e) => Some(e),
_ => None,
}
});
let net_config_path = config.network.net_config_path.clone();
let (worker, service) = pezsc_authority_discovery::new_worker_and_service_with_config(
pezsc_authority_discovery::WorkerConfig {
publish_non_global_ips: auth_disc_publish_non_global_ips,
public_addresses: auth_disc_public_addresses,
// Require that authority discovery records are signed.
strict_record_validation: true,
persisted_cache_directory: net_config_path,
..Default::default()
},
client,
Arc::new(network.clone()),
Box::pin(dht_event_stream),
authority_discovery_role,
prometheus_registry,
task_manager.spawn_handle(),
);
task_manager.spawn_handle().spawn(
"authority-discovery-worker",
Some("authority-discovery"),
worker.run(),
);
service
}
async fn build_interface(
pezkuwi_config: Configuration,
task_manager: &mut TaskManager,
client: RelayChainRpcClient,
) -> RelayChainResult<(
Arc<dyn RelayChainInterface + 'static>,
Option<CollatorPair>,
Arc<dyn NetworkService>,
async_channel::Receiver<GenericIncomingRequest>,
)> {
let collator_pair = CollatorPair::generate().0;
let blockchain_rpc_client = Arc::new(BlockChainRpcClient::new(client.clone()));
let collator_node = match pezkuwi_config.network.network_backend {
pezsc_network::config::NetworkBackendType::Libp2p =>
new_minimal_relay_chain::<RelayBlock, pezsc_network::NetworkWorker<RelayBlock, RelayHash>>(
pezkuwi_config,
collator_pair.clone(),
blockchain_rpc_client,
)
.await?,
pezsc_network::config::NetworkBackendType::Litep2p =>
new_minimal_relay_chain::<RelayBlock, pezsc_network::Litep2pNetworkBackend>(
pezkuwi_config,
collator_pair.clone(),
blockchain_rpc_client,
)
.await?,
};
task_manager.add_child(collator_node.task_manager);
Ok((
Arc::new(RelayChainRpcInterface::new(client, collator_node.overseer_handle)),
Some(collator_pair),
collator_node.network_service,
collator_node.paranode_rx,
))
}
pub async fn build_minimal_relay_chain_node_with_rpc(
relay_chain_config: Configuration,
teyrchain_prometheus_registry: Option<&Registry>,
task_manager: &mut TaskManager,
relay_chain_url: Vec<Url>,
) -> RelayChainResult<(
Arc<dyn RelayChainInterface + 'static>,
Option<CollatorPair>,
Arc<dyn NetworkService>,
async_channel::Receiver<GenericIncomingRequest>,
)> {
let client = cumulus_relay_chain_rpc_interface::create_client_and_start_worker(
relay_chain_url,
task_manager,
teyrchain_prometheus_registry,
)
.await?;
build_interface(relay_chain_config, task_manager, client).await
}
/// Builds a minimal relay chain node. Chain data is fetched
/// via [`BlockChainRpcClient`] and fed into the overseer and its subsystems.
///
/// Instead of spawning all subsystems, this minimal node will only spawn subsystems
/// required to collate:
/// - AvailabilityRecovery
/// - CollationGeneration
/// - CollatorProtocol
/// - NetworkBridgeRx
/// - NetworkBridgeTx
/// - RuntimeApi
#[pezsc_tracing::logging::prefix_logs_with("Relaychain")]
async fn new_minimal_relay_chain<Block: BlockT, Network: NetworkBackend<RelayBlock, RelayHash>>(
config: Configuration,
collator_pair: CollatorPair,
relay_chain_rpc_client: Arc<BlockChainRpcClient>,
) -> Result<NewMinimalNode, RelayChainError> {
let role = config.role;
let mut net_config = pezsc_network::config::FullNetworkConfiguration::<_, _, Network>::new(
&config.network,
config.prometheus_config.as_ref().map(|cfg| cfg.registry.clone()),
);
let metrics = Network::register_notification_metrics(
config.prometheus_config.as_ref().map(|cfg| &cfg.registry),
);
let peer_store_handle = net_config.peer_store_handle();
let prometheus_registry = config.prometheus_registry();
let task_manager = TaskManager::new(config.tokio_handle.clone(), prometheus_registry)?;
if let Some(PrometheusConfig { port, registry }) = config.prometheus_config.clone() {
task_manager.spawn_handle().spawn(
"prometheus-endpoint",
None,
prometheus_endpoint::init_prometheus(port, registry).map(drop),
);
}
let genesis_hash = relay_chain_rpc_client.block_get_hash(Some(0)).await?.unwrap_or_default();
let peerset_protocol_names =
PeerSetProtocolNames::new(genesis_hash, config.chain_spec.fork_id());
let is_authority = if role.is_authority() { IsAuthority::Yes } else { IsAuthority::No };
let notification_services = peer_sets_info::<_, Network>(
is_authority,
&peerset_protocol_names,
metrics.clone(),
Arc::clone(&peer_store_handle),
)
.into_iter()
.map(|(config, (peerset, service))| {
net_config.add_notification_protocol(config);
(peerset, service)
})
.collect::<std::collections::HashMap<PeerSet, Box<dyn pezsc_network::NotificationService>>>();
let request_protocol_names = ReqProtocolNames::new(genesis_hash, config.chain_spec.fork_id());
let (collation_req_v1_receiver, collation_req_v2_receiver, available_data_req_receiver) =
build_request_response_protocol_receivers(&request_protocol_names, &mut net_config);
let (cfg, paranode_rx) = bootnode_request_response_config::<_, _, Network>(
genesis_hash,
config.chain_spec.fork_id(),
);
net_config.add_request_response_protocol(cfg);
let best_header = relay_chain_rpc_client
.chain_get_header(None)
.await?
.ok_or_else(|| RelayChainError::RpcCallError("Unable to fetch best header".to_string()))?;
let (network, sync_service) = build_collator_network::<Network>(
&config,
net_config,
task_manager.spawn_handle(),
genesis_hash,
best_header,
metrics,
)
.map_err(|e| RelayChainError::Application(Box::new(e)))?;
let authority_discovery_service = build_authority_discovery_service::<Block>(
&task_manager,
relay_chain_rpc_client.clone(),
&config,
network.clone(),
prometheus_registry.cloned(),
);
let overseer_args = OverseerGenArgs {
runtime_client: relay_chain_rpc_client.clone(),
network_service: network.clone(),
sync_service,
authority_discovery_service,
collation_req_v1_receiver,
collation_req_v2_receiver,
available_data_req_receiver,
registry: prometheus_registry,
spawner: task_manager.spawn_handle(),
is_teyrchain_node: IsTeyrchainNode::Collator(collator_pair),
overseer_message_channel_capacity_override: None,
req_protocol_names: request_protocol_names,
peerset_protocol_names,
notification_services,
};
let overseer_handle =
collator_overseer::spawn_overseer(overseer_args, &task_manager, relay_chain_rpc_client)?;
Ok(NewMinimalNode { task_manager, overseer_handle, network_service: network, paranode_rx })
}
fn build_request_response_protocol_receivers<
Block: BlockT,
Network: NetworkBackend<Block, <Block as BlockT>::Hash>,
>(
request_protocol_names: &ReqProtocolNames,
config: &mut FullNetworkConfiguration<Block, <Block as BlockT>::Hash, Network>,
) -> (
IncomingRequestReceiver<v1::CollationFetchingRequest>,
IncomingRequestReceiver<v2::CollationFetchingRequest>,
IncomingRequestReceiver<v1::AvailableDataFetchingRequest>,
) {
let (collation_req_v1_receiver, cfg) =
IncomingRequest::get_config_receiver::<_, Network>(request_protocol_names);
config.add_request_response_protocol(cfg);
let (collation_req_v2_receiver, cfg) =
IncomingRequest::get_config_receiver::<_, Network>(request_protocol_names);
config.add_request_response_protocol(cfg);
let (available_data_req_receiver, cfg) =
IncomingRequest::get_config_receiver::<_, Network>(request_protocol_names);
config.add_request_response_protocol(cfg);
let cfg =
Protocol::ChunkFetchingV1.get_outbound_only_config::<_, Network>(request_protocol_names);
config.add_request_response_protocol(cfg);
let cfg =
Protocol::ChunkFetchingV2.get_outbound_only_config::<_, Network>(request_protocol_names);
config.add_request_response_protocol(cfg);
(collation_req_v1_receiver, collation_req_v2_receiver, available_data_req_receiver)
}
@@ -0,0 +1,176 @@
// 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 <https://www.gnu.org/licenses/>.
use pezkuwi_core_primitives::{Block, Hash, Header};
use pezsp_runtime::traits::NumberFor;
use pezsc_network::{
config::{
NetworkConfiguration, NonReservedPeerMode, NotificationHandshake, PeerStore, ProtocolId,
SetConfig,
},
peer_store::PeerStoreProvider,
service::traits::NetworkService,
NotificationMetrics,
};
use pezsc_network::{config::FullNetworkConfiguration, NetworkBackend, NotificationService};
use pezsc_network_common::{role::Roles, sync::message::BlockAnnouncesHandshake};
use pezsc_service::{error::Error, Configuration, SpawnTaskHandle};
use std::{iter, sync::Arc};
/// Build the network service, the network status sinks and an RPC sender.
pub(crate) fn build_collator_network<Network: NetworkBackend<Block, Hash>>(
config: &Configuration,
mut network_config: FullNetworkConfiguration<Block, Hash, Network>,
spawn_handle: SpawnTaskHandle,
genesis_hash: Hash,
best_header: Header,
notification_metrics: NotificationMetrics,
) -> Result<(Arc<dyn NetworkService>, Arc<dyn pezsp_consensus::SyncOracle + Send + Sync>), Error> {
let protocol_id = config.protocol_id();
let (block_announce_config, notification_service) = get_block_announce_proto_config::<Network>(
protocol_id.clone(),
&None,
Roles::from(&config.role),
best_header.number,
best_header.hash(),
genesis_hash,
notification_metrics.clone(),
network_config.peer_store_handle(),
);
// Since this node has no syncing, we do not want light-clients to connect to it.
// Here we set any potential light-client slots to 0.
adjust_network_config_light_in_peers(&mut network_config.network_config);
let peer_store = network_config.take_peer_store();
spawn_handle.spawn("peer-store", Some("networking"), peer_store.run());
let network_params = pezsc_network::config::Params::<Block, Hash, Network> {
role: config.role,
executor: {
let spawn_handle = Clone::clone(&spawn_handle);
Box::new(move |fut| {
spawn_handle.spawn("libp2p-node", Some("networking"), fut);
})
},
fork_id: None,
network_config,
genesis_hash,
protocol_id,
metrics_registry: config.prometheus_config.as_ref().map(|config| config.registry.clone()),
block_announce_config,
bitswap_config: None,
notification_metrics,
};
let network_worker = Network::new(network_params)?;
let network_service = network_worker.network_service();
// The network worker is responsible for gathering all network messages and processing
// them. This is quite a heavy task, and at the time of the writing of this comment it
// frequently happens that this future takes several seconds or in some situations
// even more than a minute until it has processed its entire queue. This is clearly an
// issue, and ideally we would like to fix the network future to take as little time as
// possible, but we also take the extra harm-prevention measure to execute the networking
// future using `spawn_blocking`.
spawn_handle.spawn_blocking("network-worker", Some("networking"), async move {
// The notification service must be kept alive to allow litep2p to handle
// requests under the hood. It has been noted that without the notification
// service of the `/block-announces/1` protocol, collators are not advertised
// and their produced blocks do not propagate:
// https://github.com/pezkuwichain/pezkuwi-sdk/issues/154
//
// This is because the full nodes on the relay chain will attempt to establish
// a connection to the minimal relay chain. By dropping the notification service,
// litep2p would terminate the background task which handles the `/block-announces/1`
// notification protocol. The downstream effect of this is that the full node
// would ban and disconnect the the minimal relay chain node.
let _notification_service = notification_service;
network_worker.run().await;
});
Ok((network_service, Arc::new(SyncOracle {})))
}
fn adjust_network_config_light_in_peers(config: &mut NetworkConfiguration) {
let light_client_in_peers = (config.default_peers_set.in_peers +
config.default_peers_set.out_peers)
.saturating_sub(config.default_peers_set_num_full);
if light_client_in_peers > 0 {
tracing::debug!(target: crate::LOG_TARGET, "Detected {light_client_in_peers} peer slots for light clients. Since this minimal node does support\
neither syncing nor light-client request/response, we are setting them to 0.");
}
config.default_peers_set.in_peers =
config.default_peers_set.in_peers.saturating_sub(light_client_in_peers);
}
struct SyncOracle;
impl pezsp_consensus::SyncOracle for SyncOracle {
fn is_major_syncing(&self) -> bool {
false
}
fn is_offline(&self) -> bool {
true
}
}
fn get_block_announce_proto_config<Network: NetworkBackend<Block, Hash>>(
protocol_id: ProtocolId,
fork_id: &Option<String>,
roles: Roles,
best_number: NumberFor<Block>,
best_hash: Hash,
genesis_hash: Hash,
metrics: NotificationMetrics,
peer_store_handle: Arc<dyn PeerStoreProvider>,
) -> (Network::NotificationProtocolConfig, Box<dyn NotificationService>) {
let block_announces_protocol = {
let genesis_hash = genesis_hash.as_ref();
if let Some(ref fork_id) = fork_id {
format!("/{}/{}/block-announces/1", array_bytes::bytes2hex("", genesis_hash), fork_id)
} else {
format!("/{}/block-announces/1", array_bytes::bytes2hex("", genesis_hash))
}
};
Network::notification_config(
block_announces_protocol.into(),
iter::once(format!("/{}/block-announces/1", protocol_id.as_ref()).into()).collect(),
1024 * 1024,
Some(NotificationHandshake::new(BlockAnnouncesHandshake::<Block>::build(
roles,
best_number,
best_hash,
genesis_hash,
))),
// NOTE: `set_config` will be ignored by `protocol.rs` as the block announcement
// protocol is still hardcoded into the peerset.
SetConfig {
in_peers: 0,
out_peers: 0,
reserved_nodes: Vec::new(),
non_reserved_mode: NonReservedPeerMode::Deny,
},
metrics,
peer_store_handle,
)
}
@@ -0,0 +1,62 @@
[package]
authors.workspace = true
name = "pezcumulus-relay-chain-rpc-interface"
version = "0.7.0"
edition.workspace = true
description = "Implementation of the RelayChainInterface trait that connects to a remote RPC-node."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
pezkuwi-overseer = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
prometheus-endpoint = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-rpc-api = { workspace = true, default-features = true }
pezsc-service = { workspace = true, default-features = true }
pezsp-authority-discovery = { workspace = true, default-features = true }
pezsp-consensus-babe = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-storage = { workspace = true, default-features = true }
pezsp-version = { workspace = true, default-features = true }
tokio = { features = ["sync"], workspace = true, default-features = true }
async-trait = { workspace = true }
codec = { workspace = true, default-features = true }
futures = { workspace = true }
futures-timer = { workspace = true }
jsonrpsee = { features = ["ws-client"], workspace = true }
prometheus = { workspace = true }
schnellru = { workspace = true }
serde = { workspace = true, default-features = true }
serde_json = { workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
url = { workspace = true }
[dev-dependencies]
portpicker = { workspace = true }
[features]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-rpc-api/runtime-benchmarks",
"pezsc-service/runtime-benchmarks",
"pezsp-authority-discovery/runtime-benchmarks",
"pezsp-consensus-babe/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
"pezsp-version/runtime-benchmarks",
]
@@ -0,0 +1,292 @@
// 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 <https://www.gnu.org/licenses/>.
use async_trait::async_trait;
use core::time::Duration;
use cumulus_primitives_core::{
relay_chain::{
CandidateEvent, CommittedCandidateReceiptV2 as CommittedCandidateReceipt,
Hash as RelayHash, Header as RelayHeader, InboundHrmpMessage, OccupiedCoreAssumption,
SessionIndex, ValidationCodeHash, ValidatorId,
},
InboundDownwardMessage, ParaId, PersistedValidationData,
};
use cumulus_relay_chain_interface::{
BlockNumber, CoreState, PHeader, RelayChainError, RelayChainInterface, RelayChainResult,
};
use futures::{FutureExt, Stream, StreamExt};
use pezkuwi_overseer::Handle;
use pezsc_client_api::StorageProof;
use pezsp_state_machine::StorageValue;
use pezsp_storage::StorageKey;
use pezsp_version::RuntimeVersion;
use std::{collections::btree_map::BTreeMap, pin::Pin};
use cumulus_primitives_core::relay_chain::BlockId;
pub use url::Url;
mod metrics;
mod reconnecting_ws_client;
mod rpc_client;
pub use rpc_client::{create_client_and_start_worker, RelayChainRpcClient};
const TIMEOUT_IN_SECONDS: u64 = 6;
/// RelayChainRpcInterface is used to interact with a full node that is running locally
/// in the same process.
#[derive(Clone)]
pub struct RelayChainRpcInterface {
rpc_client: RelayChainRpcClient,
overseer_handle: Handle,
}
impl RelayChainRpcInterface {
pub fn new(rpc_client: RelayChainRpcClient, overseer_handle: Handle) -> Self {
Self { rpc_client, overseer_handle }
}
}
#[async_trait]
impl RelayChainInterface for RelayChainRpcInterface {
async fn retrieve_dmq_contents(
&self,
para_id: ParaId,
relay_parent: RelayHash,
) -> RelayChainResult<Vec<InboundDownwardMessage>> {
self.rpc_client.teyrchain_host_dmq_contents(para_id, relay_parent).await
}
async fn retrieve_all_inbound_hrmp_channel_contents(
&self,
para_id: ParaId,
relay_parent: RelayHash,
) -> RelayChainResult<BTreeMap<ParaId, Vec<InboundHrmpMessage>>> {
self.rpc_client
.teyrchain_host_inbound_hrmp_channels_contents(para_id, relay_parent)
.await
}
async fn header(&self, block_id: BlockId) -> RelayChainResult<Option<PHeader>> {
let hash = match block_id {
BlockId::Hash(hash) => hash,
BlockId::Number(num) => {
if let Some(hash) = self.rpc_client.chain_get_block_hash(Some(num)).await? {
hash
} else {
return Ok(None);
}
},
};
let header = self.rpc_client.chain_get_header(Some(hash)).await?;
Ok(header)
}
async fn persisted_validation_data(
&self,
hash: RelayHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> RelayChainResult<Option<PersistedValidationData>> {
self.rpc_client
.teyrchain_host_persisted_validation_data(hash, para_id, occupied_core_assumption)
.await
}
async fn validation_code_hash(
&self,
hash: RelayHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> RelayChainResult<Option<ValidationCodeHash>> {
self.rpc_client
.validation_code_hash(hash, para_id, occupied_core_assumption)
.await
}
async fn candidate_pending_availability(
&self,
hash: RelayHash,
para_id: ParaId,
) -> RelayChainResult<Option<CommittedCandidateReceipt>> {
self.rpc_client
.teyrchain_host_candidate_pending_availability(hash, para_id)
.await
}
async fn session_index_for_child(&self, hash: RelayHash) -> RelayChainResult<SessionIndex> {
self.rpc_client.teyrchain_host_session_index_for_child(hash).await
}
async fn validators(&self, block_id: RelayHash) -> RelayChainResult<Vec<ValidatorId>> {
self.rpc_client.teyrchain_host_validators(block_id).await
}
async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = RelayHeader> + Send>>> {
let imported_headers_stream = self.rpc_client.get_imported_heads_stream()?;
Ok(imported_headers_stream.boxed())
}
async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = RelayHeader> + Send>>> {
let imported_headers_stream = self.rpc_client.get_finalized_heads_stream()?;
Ok(imported_headers_stream.boxed())
}
async fn best_block_hash(&self) -> RelayChainResult<RelayHash> {
self.rpc_client.chain_get_head(None).await
}
async fn finalized_block_hash(&self) -> RelayChainResult<RelayHash> {
self.rpc_client.chain_get_finalized_head().await
}
async fn call_runtime_api(
&self,
method_name: &'static str,
hash: RelayHash,
payload: &[u8],
) -> RelayChainResult<Vec<u8>> {
self.rpc_client
.call_remote_runtime_function_encoded(method_name, hash, payload)
.await
.map(|bytes| bytes.to_vec())
}
async fn is_major_syncing(&self) -> RelayChainResult<bool> {
self.rpc_client.system_health().await.map(|h| h.is_syncing)
}
fn overseer_handle(&self) -> RelayChainResult<Handle> {
Ok(self.overseer_handle.clone())
}
async fn get_storage_by_key(
&self,
relay_parent: RelayHash,
key: &[u8],
) -> RelayChainResult<Option<StorageValue>> {
let storage_key = StorageKey(key.to_vec());
self.rpc_client
.state_get_storage(storage_key, Some(relay_parent))
.await
.map(|storage_data| storage_data.map(|sv| sv.0))
}
async fn prove_read(
&self,
relay_parent: RelayHash,
relevant_keys: &Vec<Vec<u8>>,
) -> RelayChainResult<StorageProof> {
let cloned = relevant_keys.clone();
let storage_keys: Vec<StorageKey> = cloned.into_iter().map(StorageKey).collect();
self.rpc_client
.state_get_read_proof(storage_keys, Some(relay_parent))
.await
.map(|read_proof| {
StorageProof::new(read_proof.proof.into_iter().map(|bytes| bytes.to_vec()))
})
}
/// Wait for a given relay chain block
///
/// The hash of the block to wait for is passed. We wait for the block to arrive or return after
/// a timeout.
///
/// Implementation:
/// 1. Register a listener to all new blocks.
/// 2. Check if the block is already in chain. If yes, succeed early.
/// 3. Wait for the block to be imported via subscription.
/// 4. If timeout is reached, we return an error.
async fn wait_for_block(&self, wait_for_hash: RelayHash) -> RelayChainResult<()> {
let mut head_stream = self.rpc_client.get_imported_heads_stream()?;
if self.rpc_client.chain_get_header(Some(wait_for_hash)).await?.is_some() {
return Ok(());
}
let mut timeout = futures_timer::Delay::new(Duration::from_secs(TIMEOUT_IN_SECONDS)).fuse();
loop {
futures::select! {
_ = timeout => return Err(RelayChainError::WaitTimeout(wait_for_hash)),
evt = head_stream.next().fuse() => match evt {
Some(evt) if evt.hash() == wait_for_hash => return Ok(()),
// Not the event we waited on.
Some(_) => continue,
None => return Err(RelayChainError::ImportListenerClosed(wait_for_hash)),
}
}
}
}
async fn new_best_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = RelayHeader> + Send>>> {
let imported_headers_stream = self.rpc_client.get_best_heads_stream()?;
Ok(imported_headers_stream.boxed())
}
async fn candidates_pending_availability(
&self,
hash: RelayHash,
para_id: ParaId,
) -> RelayChainResult<Vec<CommittedCandidateReceipt>> {
self.rpc_client
.teyrchain_host_candidates_pending_availability(hash, para_id)
.await
}
async fn version(&self, relay_parent: RelayHash) -> RelayChainResult<RuntimeVersion> {
self.rpc_client.runtime_version(relay_parent).await
}
async fn availability_cores(
&self,
relay_parent: RelayHash,
) -> RelayChainResult<Vec<CoreState<RelayHash, BlockNumber>>> {
self.rpc_client.teyrchain_host_availability_cores(relay_parent).await
}
async fn claim_queue(
&self,
relay_parent: RelayHash,
) -> RelayChainResult<
BTreeMap<cumulus_relay_chain_interface::CoreIndex, std::collections::VecDeque<ParaId>>,
> {
self.rpc_client.teyrchain_host_claim_queue(relay_parent).await
}
async fn scheduling_lookahead(&self, relay_parent: RelayHash) -> RelayChainResult<u32> {
self.rpc_client.teyrchain_host_scheduling_lookahead(relay_parent).await
}
async fn candidate_events(
&self,
relay_parent: RelayHash,
) -> RelayChainResult<Vec<CandidateEvent>> {
self.rpc_client.teyrchain_host_candidate_events(relay_parent).await
}
}
@@ -0,0 +1,50 @@
// 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 <https://www.gnu.org/licenses/>.
use prometheus::{Error as PrometheusError, HistogramTimer, Registry};
use prometheus_endpoint::{HistogramOpts, HistogramVec, Opts};
/// Gathers metrics about the blockchain RPC client.
#[derive(Clone)]
pub(crate) struct RelaychainRpcMetrics {
rpc_request: HistogramVec,
}
impl RelaychainRpcMetrics {
pub(crate) fn register(registry: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
rpc_request: prometheus_endpoint::register(
HistogramVec::new(
HistogramOpts {
common_opts: Opts::new(
"relay_chain_rpc_interface",
"Tracks stats about pezcumulus relay chain RPC interface",
),
buckets: prometheus::exponential_buckets(0.001, 4.0, 9)
.expect("function parameters are constant and always valid; qed"),
},
&["method"],
)?,
registry,
)?,
})
}
pub(crate) fn start_request_timer(&self, method: &str) -> HistogramTimer {
self.rpc_request.with_label_values(&[method]).start_timer()
}
}
@@ -0,0 +1,528 @@
// 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 <https://www.gnu.org/licenses/>.
use cumulus_primitives_core::relay_chain::{
Block as RelayBlock, BlockNumber as RelayNumber, Hash as RelayHash, Header as RelayHeader,
};
use futures::{
channel::{mpsc::Sender, oneshot::Sender as OneshotSender},
future::BoxFuture,
stream::FuturesUnordered,
FutureExt, StreamExt,
};
use jsonrpsee::{
core::{
client::{Client as JsonRpcClient, ClientT, Subscription},
params::ArrayParams,
ClientError as JsonRpseeError, JsonValue,
},
ws_client::WsClientBuilder,
};
use pezsc_rpc_api::chain::ChainApiClient;
use schnellru::{ByLength, LruMap};
use pezsp_runtime::generic::SignedBlock;
use std::{sync::Arc, time::Duration};
use tokio::sync::mpsc::{
channel as tokio_channel, Receiver as TokioReceiver, Sender as TokioSender,
};
use url::Url;
use crate::rpc_client::{distribute_header, RpcDispatcherMessage};
const LOG_TARGET: &str = "reconnecting-websocket-client";
const DEFAULT_EXTERNAL_RPC_CONN_RETRIES: usize = 5;
const DEFAULT_SLEEP_TIME_MS_BETWEEN_RETRIES: u64 = 1000;
const DEFAULT_SLEEP_EXP_BACKOFF_BETWEEN_RETRIES: i32 = 2;
/// Worker that should be used in combination with [`RelayChainRpcClient`].
///
/// Must be polled to distribute header notifications to listeners.
pub struct ReconnectingWebsocketWorker {
ws_urls: Vec<String>,
/// Communication channel with the RPC client
client_receiver: TokioReceiver<RpcDispatcherMessage>,
/// Senders to distribute incoming header notifications to
imported_header_listeners: Vec<Sender<RelayHeader>>,
finalized_header_listeners: Vec<Sender<RelayHeader>>,
best_header_listeners: Vec<Sender<RelayHeader>>,
}
/// Format url and force addition of a port
fn url_to_string_with_port(url: Url) -> Option<String> {
// This is already validated on CLI side, just defensive here
if (url.scheme() != "ws" && url.scheme() != "wss") || url.host_str().is_none() {
tracing::warn!(target: LOG_TARGET, ?url, "Non-WebSocket URL or missing host.");
return None;
}
// Either we have a user-supplied port or use the default for 'ws' or 'wss' here
Some(format!(
"{}://{}:{}{}{}",
url.scheme(),
url.host_str()?,
url.port_or_known_default()?,
url.path(),
url.query().map(|query| format!("?{}", query)).unwrap_or_default()
))
}
/// Manages the active websocket client.
/// Responsible for creating request futures, subscription streams
/// and reconnections.
#[derive(Debug)]
struct ClientManager {
urls: Vec<String>,
active_client: Arc<JsonRpcClient>,
active_index: usize,
}
struct RelayChainSubscriptions {
import_subscription: Subscription<RelayHeader>,
finalized_subscription: Subscription<RelayHeader>,
best_subscription: Subscription<RelayHeader>,
}
/// Try to find a new RPC server to connect to. Uses a naive retry
/// logic that does an exponential backoff in between iterations
/// through all URLs from the list. It uses a constant to tell how
/// many iterations of connection attempts to all URLs we allow. We
/// return early when a connection is made.
async fn connect_next_available_rpc_server(
urls: &Vec<String>,
starting_position: usize,
) -> Result<(usize, Arc<JsonRpcClient>), ()> {
tracing::debug!(target: LOG_TARGET, starting_position, "Connecting to RPC server.");
let mut prev_iteration: u32 = 0;
for (counter, url) in urls
.iter()
.cycle()
.skip(starting_position)
.take(urls.len() * DEFAULT_EXTERNAL_RPC_CONN_RETRIES)
.enumerate()
{
// If we reached the end of the urls list, backoff before retrying
// connections to the entire list once more.
let Ok(current_iteration) = (counter / urls.len()).try_into() else {
tracing::error!(target: LOG_TARGET, "Too many connection attempts to the RPC servers, aborting...");
break;
};
if current_iteration > prev_iteration {
// Safe conversion given we convert positive i32s which are lower than u64::MAX.
tokio::time::sleep(Duration::from_millis(
DEFAULT_SLEEP_TIME_MS_BETWEEN_RETRIES *
DEFAULT_SLEEP_EXP_BACKOFF_BETWEEN_RETRIES.pow(prev_iteration) as u64,
))
.await;
prev_iteration = current_iteration;
}
let index = (starting_position + counter) % urls.len();
tracing::info!(
target: LOG_TARGET,
attempt = current_iteration,
index,
url,
"Trying to connect to next external relaychain node.",
);
match WsClientBuilder::default().build(&url).await {
Ok(ws_client) => return Ok((index, Arc::new(ws_client))),
Err(err) => tracing::debug!(target: LOG_TARGET, url, ?err, "Unable to connect."),
};
}
tracing::error!(target: LOG_TARGET, "Retrying to connect to any external relaychain node failed.");
Err(())
}
impl ClientManager {
pub async fn new(urls: Vec<String>) -> Result<Self, ()> {
if urls.is_empty() {
return Err(());
}
let active_client = connect_next_available_rpc_server(&urls, 0).await?;
Ok(Self { urls, active_client: active_client.1, active_index: active_client.0 })
}
pub async fn connect_to_new_rpc_server(&mut self) -> Result<(), ()> {
let new_active =
connect_next_available_rpc_server(&self.urls, self.active_index + 1).await?;
self.active_client = new_active.1;
self.active_index = new_active.0;
Ok(())
}
async fn get_subscriptions(&self) -> Result<RelayChainSubscriptions, JsonRpseeError> {
let import_subscription = <JsonRpcClient as ChainApiClient<
RelayNumber,
RelayHash,
RelayHeader,
SignedBlock<RelayBlock>,
>>::subscribe_all_heads(&self.active_client)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
?e,
"Unable to open `chain_subscribeAllHeads` subscription."
);
e
})?;
let best_subscription = <JsonRpcClient as ChainApiClient<
RelayNumber,
RelayHash,
RelayHeader,
SignedBlock<RelayBlock>,
>>::subscribe_new_heads(&self.active_client)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
?e,
"Unable to open `chain_subscribeNewHeads` subscription."
);
e
})?;
let finalized_subscription = <JsonRpcClient as ChainApiClient<
RelayNumber,
RelayHash,
RelayHeader,
SignedBlock<RelayBlock>,
>>::subscribe_finalized_heads(&self.active_client)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
?e,
"Unable to open `chain_subscribeFinalizedHeads` subscription."
);
e
})?;
Ok(RelayChainSubscriptions {
import_subscription,
best_subscription,
finalized_subscription,
})
}
/// Create a request future that performs an RPC request and sends the results to the caller.
/// In case of a dead websocket connection, it returns the original request parameters to
/// enable retries.
fn create_request(
&self,
method: String,
params: ArrayParams,
response_sender: OneshotSender<Result<JsonValue, JsonRpseeError>>,
) -> BoxFuture<'static, Result<(), RpcDispatcherMessage>> {
let future_client = self.active_client.clone();
async move {
let resp = future_client.request(&method, params.clone()).await;
// We should only return the original request in case
// the websocket connection is dead and requires a restart.
// Other errors should be forwarded to the request caller.
if let Err(JsonRpseeError::RestartNeeded(_)) = resp {
return Err(RpcDispatcherMessage::Request(method, params, response_sender));
}
if let Err(err) = response_sender.send(resp) {
tracing::debug!(
target: LOG_TARGET,
?err,
"Recipient no longer interested in request result"
);
}
Ok(())
}
.boxed()
}
}
enum ConnectionStatus {
Connected,
ReconnectRequired(Option<RpcDispatcherMessage>),
}
impl ReconnectingWebsocketWorker {
/// Create new worker. Returns the worker and a channel to register new listeners.
pub async fn new(
urls: Vec<Url>,
) -> (ReconnectingWebsocketWorker, TokioSender<RpcDispatcherMessage>) {
let urls = urls.into_iter().filter_map(url_to_string_with_port).collect();
let (tx, rx) = tokio_channel(100);
let worker = ReconnectingWebsocketWorker {
ws_urls: urls,
client_receiver: rx,
imported_header_listeners: Vec::new(),
finalized_header_listeners: Vec::new(),
best_header_listeners: Vec::new(),
};
(worker, tx)
}
/// Reconnect via [`ClientManager`] and provide new notification streams.
async fn handle_reconnect(
&mut self,
client_manager: &mut ClientManager,
pending_requests: &mut FuturesUnordered<
BoxFuture<'static, Result<(), RpcDispatcherMessage>>,
>,
first_failed_request: Option<RpcDispatcherMessage>,
) -> Result<RelayChainSubscriptions, String> {
let mut requests_to_retry = Vec::new();
if let Some(req @ RpcDispatcherMessage::Request(_, _, _)) = first_failed_request {
requests_to_retry.push(req);
}
// At this point, all pending requests will return an error since the
// websocket connection is dead. So draining the pending requests should be fast.
while !pending_requests.is_empty() {
if let Some(Err(req)) = pending_requests.next().await {
requests_to_retry.push(req);
}
}
if client_manager.connect_to_new_rpc_server().await.is_err() {
return Err("Unable to find valid external RPC server, shutting down.".to_string());
};
for item in requests_to_retry.into_iter() {
if let RpcDispatcherMessage::Request(method, params, response_sender) = item {
pending_requests.push(client_manager.create_request(
method,
params,
response_sender,
));
};
}
client_manager.get_subscriptions().await.map_err(|e| {
format!("Not able to create streams from newly connected RPC server, shutting down. err: {:?}", e)
})
}
/// Run this worker to drive notification streams.
/// The worker does the following:
/// - Listen for [`RpcDispatcherMessage`], perform requests and register new listeners for the
/// notification streams
/// - Distribute incoming import, best head and finalization notifications to registered
/// listeners. If an error occurs during sending, the receiver has been closed and we remove
/// the sender from the list.
/// - Find a new valid RPC server to connect to in case the websocket connection is terminated.
/// If the worker is not able to connect to an RPC server from the list, the worker shuts
/// down.
pub async fn run(mut self) {
let mut pending_requests = FuturesUnordered::new();
let urls = std::mem::take(&mut self.ws_urls);
let Ok(mut client_manager) = ClientManager::new(urls).await else {
tracing::error!(target: LOG_TARGET, "No valid RPC url found. Stopping RPC worker.");
return;
};
let Ok(mut subscriptions) = client_manager.get_subscriptions().await else {
tracing::error!(target: LOG_TARGET, "Unable to fetch subscriptions on initial connection.");
return;
};
let mut imported_blocks_cache = LruMap::new(ByLength::new(40));
let mut should_reconnect = ConnectionStatus::Connected;
let mut last_seen_finalized_num: RelayNumber = 0;
loop {
// This branch is taken if the websocket connection to the current RPC server is closed.
if let ConnectionStatus::ReconnectRequired(maybe_failed_request) = should_reconnect {
match self
.handle_reconnect(
&mut client_manager,
&mut pending_requests,
maybe_failed_request,
)
.await
{
Ok(new_subscriptions) => {
subscriptions = new_subscriptions;
},
Err(message) => {
tracing::error!(
target: LOG_TARGET,
message,
"Unable to reconnect, stopping worker."
);
return;
},
}
should_reconnect = ConnectionStatus::Connected;
}
tokio::select! {
evt = self.client_receiver.recv() => match evt {
Some(RpcDispatcherMessage::RegisterBestHeadListener(tx)) => {
self.best_header_listeners.push(tx);
},
Some(RpcDispatcherMessage::RegisterImportListener(tx)) => {
self.imported_header_listeners.push(tx)
},
Some(RpcDispatcherMessage::RegisterFinalizationListener(tx)) => {
self.finalized_header_listeners.push(tx)
},
Some(RpcDispatcherMessage::Request(method, params, response_sender)) => {
pending_requests.push(client_manager.create_request(method, params, response_sender));
},
None => {
tracing::error!(target: LOG_TARGET, "RPC client receiver closed. Stopping RPC Worker.");
return;
}
},
should_retry = pending_requests.next(), if !pending_requests.is_empty() => {
if let Some(Err(req)) = should_retry {
should_reconnect = ConnectionStatus::ReconnectRequired(Some(req));
}
},
import_event = subscriptions.import_subscription.next() => {
match import_event {
Some(Ok(header)) => {
let hash = header.hash();
if imported_blocks_cache.peek(&hash).is_some() {
tracing::debug!(
target: LOG_TARGET,
number = header.number,
?hash,
"Duplicate imported block header. This might happen after switching to a new RPC node. Skipping distribution."
);
continue;
}
imported_blocks_cache.insert(hash, ());
distribute_header(header, &mut self.imported_header_listeners);
},
None => {
tracing::error!(target: LOG_TARGET, "Subscription closed.");
should_reconnect = ConnectionStatus::ReconnectRequired(None);
},
Some(Err(error)) => {
tracing::error!(target: LOG_TARGET, ?error, "Error in RPC subscription.");
should_reconnect = ConnectionStatus::ReconnectRequired(None);
},
}
},
best_header_event = subscriptions.best_subscription.next() => {
match best_header_event {
Some(Ok(header)) => distribute_header(header, &mut self.best_header_listeners),
None => {
tracing::error!(target: LOG_TARGET, "Subscription closed.");
should_reconnect = ConnectionStatus::ReconnectRequired(None);
},
Some(Err(error)) => {
tracing::error!(target: LOG_TARGET, ?error, "Error in RPC subscription.");
should_reconnect = ConnectionStatus::ReconnectRequired(None);
},
}
}
finalized_event = subscriptions.finalized_subscription.next() => {
match finalized_event {
Some(Ok(header)) if header.number > last_seen_finalized_num => {
last_seen_finalized_num = header.number;
distribute_header(header, &mut self.finalized_header_listeners);
},
Some(Ok(header)) => {
tracing::debug!(
target: LOG_TARGET,
number = header.number,
last_seen_finalized_num,
"Duplicate finalized block header. This might happen after switching to a new RPC node. Skipping distribution."
);
},
None => {
tracing::error!(target: LOG_TARGET, "Subscription closed.");
should_reconnect = ConnectionStatus::ReconnectRequired(None);
},
Some(Err(error)) => {
tracing::error!(target: LOG_TARGET, ?error, "Error in RPC subscription.");
should_reconnect = ConnectionStatus::ReconnectRequired(None);
},
}
}
}
}
}
}
#[cfg(test)]
mod test {
use std::time::Duration;
use super::{url_to_string_with_port, ClientManager};
use jsonrpsee::Methods;
use url::Url;
const SERVER_STARTUP_DELAY_SECONDS: u64 = 10;
#[test]
fn url_to_string_works() {
let url = Url::parse("wss://something/path").unwrap();
assert_eq!(Some("wss://something:443/path".to_string()), url_to_string_with_port(url));
let url = Url::parse("ws://something/path").unwrap();
assert_eq!(Some("ws://something:80/path".to_string()), url_to_string_with_port(url));
let url = Url::parse("wss://something:100/path").unwrap();
assert_eq!(Some("wss://something:100/path".to_string()), url_to_string_with_port(url));
let url = Url::parse("wss://something:100/path").unwrap();
assert_eq!(Some("wss://something:100/path".to_string()), url_to_string_with_port(url));
let url = Url::parse("wss://something/path?query=yes").unwrap();
assert_eq!(
Some("wss://something:443/path?query=yes".to_string()),
url_to_string_with_port(url)
);
let url = Url::parse("wss://something:9090/path?query=yes").unwrap();
assert_eq!(
Some("wss://something:9090/path?query=yes".to_string()),
url_to_string_with_port(url)
);
}
#[tokio::test]
// Testing the retry logic at full means increasing CI with half a minute according
// to the current logic, so lets test it best effort.
async fn client_manager_retry_logic() {
let port = portpicker::pick_unused_port().unwrap();
let server = jsonrpsee::server::Server::builder()
.build(format!("0.0.0.0:{}", port))
.await
.unwrap();
// Start the server.
let server = tokio::spawn(async {
tokio::time::sleep(Duration::from_secs(SERVER_STARTUP_DELAY_SECONDS)).await;
server.start(Methods::default())
});
// Start the client. Not exitting right away with an error means it
// is handling gracefully received connections refused while the server
// is starting.
let res = ClientManager::new(vec![format!("ws://127.0.0.1:{}", port)]).await;
assert!(res.is_ok());
server.await.unwrap();
}
}
@@ -0,0 +1,795 @@
// 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 <https://www.gnu.org/licenses/>.
use futures::channel::{
mpsc::{Receiver, Sender},
oneshot::Sender as OneshotSender,
};
use jsonrpsee::{
core::{params::ArrayParams, ClientError as JsonRpseeError},
rpc_params,
};
use prometheus::Registry;
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value as JsonValue;
use std::collections::{btree_map::BTreeMap, VecDeque};
use tokio::sync::mpsc::Sender as TokioSender;
use codec::{Decode, Encode};
use cumulus_primitives_core::{
relay_chain::{
async_backing::{AsyncBackingParams, BackingState, Constraints},
slashing, ApprovalVotingParams, BlockNumber, CandidateCommitments, CandidateEvent,
CandidateHash, CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CoreIndex,
CoreState, DisputeState, ExecutorParams, GroupRotationInfo, Hash as RelayHash,
Header as RelayHeader, InboundHrmpMessage, NodeFeatures, OccupiedCoreAssumption,
PvfCheckStatement, ScrapedOnChainVotes, SessionIndex, SessionInfo, ValidationCode,
ValidationCodeHash, ValidatorId, ValidatorIndex, ValidatorSignature,
},
InboundDownwardMessage, ParaId, PersistedValidationData,
};
use cumulus_relay_chain_interface::{RelayChainError, RelayChainResult};
use pezsc_client_api::StorageData;
use pezsc_rpc_api::{state::ReadProof, system::Health};
use pezsc_service::TaskManager;
use pezsp_consensus_babe::Epoch;
use pezsp_storage::StorageKey;
use pezsp_version::RuntimeVersion;
use crate::{metrics::RelaychainRpcMetrics, reconnecting_ws_client::ReconnectingWebsocketWorker};
pub use url::Url;
const LOG_TARGET: &str = "relay-chain-rpc-client";
const NOTIFICATION_CHANNEL_SIZE_LIMIT: usize = 20;
/// Messages for communication between [`RelayChainRpcClient`] and the RPC workers.
#[derive(Debug)]
pub enum RpcDispatcherMessage {
/// Register new listener for the best headers stream. Contains a sender which will be used
/// to send incoming headers.
RegisterBestHeadListener(Sender<RelayHeader>),
/// Register new listener for the import headers stream. Contains a sender which will be used
/// to send incoming headers.
RegisterImportListener(Sender<RelayHeader>),
/// Register new listener for the finalized headers stream. Contains a sender which will be
/// used to send incoming headers.
RegisterFinalizationListener(Sender<RelayHeader>),
/// Register new listener for the finalized headers stream.
/// Contains the following:
/// - [`String`] representing the RPC method to be called
/// - [`ArrayParams`] for the parameters to the RPC call
/// - [`OneshotSender`] for the return value of the request
Request(String, ArrayParams, OneshotSender<Result<JsonValue, JsonRpseeError>>),
}
/// Entry point to create [`RelayChainRpcClient`] and start a worker that communicates
/// to JsonRPC servers over the network.
pub async fn create_client_and_start_worker(
urls: Vec<Url>,
task_manager: &mut TaskManager,
prometheus_registry: Option<&Registry>,
) -> RelayChainResult<RelayChainRpcClient> {
let (worker, sender) = ReconnectingWebsocketWorker::new(urls).await;
task_manager
.spawn_essential_handle()
.spawn("relay-chain-rpc-worker", None, worker.run());
let client = RelayChainRpcClient::new(sender, prometheus_registry);
Ok(client)
}
#[derive(Serialize)]
struct PayloadToHex<'a>(#[serde(with = "pezsp_core::bytes")] &'a [u8]);
/// Client that maps RPC methods and deserializes results
#[derive(Clone)]
pub struct RelayChainRpcClient {
/// Sender to send messages to the worker.
worker_channel: TokioSender<RpcDispatcherMessage>,
metrics: Option<RelaychainRpcMetrics>,
}
impl RelayChainRpcClient {
/// Initialize new RPC Client.
///
/// This client expects a channel connected to a worker that processes
/// requests sent via this channel.
pub(crate) fn new(
worker_channel: TokioSender<RpcDispatcherMessage>,
prometheus_registry: Option<&Registry>,
) -> Self {
RelayChainRpcClient {
worker_channel,
metrics: prometheus_registry
.and_then(|inner| RelaychainRpcMetrics::register(inner).map_err(|err| {
tracing::warn!(target: LOG_TARGET, error = %err, "Unable to instantiate the RPC client metrics, continuing w/o metrics setup.");
}).ok()),
}
}
/// Same as `call_remote_runtime_function` but work on encoded data
pub async fn call_remote_runtime_function_encoded(
&self,
method_name: &str,
hash: RelayHash,
payload: &[u8],
) -> RelayChainResult<pezsp_core::Bytes> {
let payload = PayloadToHex(payload);
let params = rpc_params! {
method_name,
payload,
hash
};
self.request_tracing::<pezsp_core::Bytes, _>("state_call", params, |err| {
tracing::trace!(
target: LOG_TARGET,
%method_name,
%hash,
error = %err,
"Error during call to 'state_call'.",
);
})
.await
}
/// Call a call to `state_call` rpc method.
pub async fn call_remote_runtime_function<R: Decode>(
&self,
method_name: &str,
hash: RelayHash,
payload: Option<impl Encode>,
) -> RelayChainResult<R> {
let payload_bytes =
payload.map_or(pezsp_core::Bytes(Vec::new()), |v| pezsp_core::Bytes(v.encode()));
let res = self
.call_remote_runtime_function_encoded(method_name, hash, &payload_bytes)
.await?;
Decode::decode(&mut &*res.0).map_err(Into::into)
}
/// Perform RPC request
async fn request<'a, R>(
&self,
method: &'a str,
params: ArrayParams,
) -> Result<R, RelayChainError>
where
R: DeserializeOwned + std::fmt::Debug,
{
self.request_tracing(
method,
params,
|e| tracing::trace!(target:LOG_TARGET, error = %e, %method, "Unable to complete RPC request"),
)
.await
}
/// Perform RPC request
async fn request_tracing<'a, R, OR>(
&self,
method: &'a str,
params: ArrayParams,
trace_error: OR,
) -> Result<R, RelayChainError>
where
R: DeserializeOwned + std::fmt::Debug,
OR: Fn(&RelayChainError),
{
let _timer = self.metrics.as_ref().map(|inner| inner.start_request_timer(method));
let (tx, rx) = futures::channel::oneshot::channel();
let message = RpcDispatcherMessage::Request(method.into(), params, tx);
self.worker_channel.send(message).await.map_err(|err| {
RelayChainError::WorkerCommunicationError(format!(
"Unable to send message to RPC worker: {}",
err
))
})?;
let value = rx.await.map_err(|err| {
RelayChainError::WorkerCommunicationError(format!(
"RPC worker channel closed. This can hint and connectivity issues with the supplied RPC endpoints. Message: {}",
err
))
})??;
serde_json::from_value(value).map_err(|_| {
trace_error(&RelayChainError::GenericError("Unable to deserialize value".to_string()));
RelayChainError::RpcCallError(method.to_string())
})
}
/// Returns information regarding the current epoch.
pub async fn babe_api_current_epoch(&self, at: RelayHash) -> Result<Epoch, RelayChainError> {
self.call_remote_runtime_function("BabeApi_current_epoch", at, None::<()>).await
}
/// Scrape dispute relevant from on-chain, backing votes and resolved disputes.
pub async fn teyrchain_host_on_chain_votes(
&self,
at: RelayHash,
) -> Result<Option<ScrapedOnChainVotes<RelayHash>>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_on_chain_votes", at, None::<()>)
.await
}
/// Returns code hashes of PVFs that require pre-checking by validators in the active set.
pub async fn teyrchain_host_pvfs_require_precheck(
&self,
at: RelayHash,
) -> Result<Vec<ValidationCodeHash>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_pvfs_require_precheck", at, None::<()>)
.await
}
/// Submits a PVF pre-checking statement into the transaction pool.
pub async fn teyrchain_host_submit_pvf_check_statement(
&self,
at: RelayHash,
stmt: PvfCheckStatement,
signature: ValidatorSignature,
) -> Result<(), RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_submit_pvf_check_statement",
at,
Some((stmt, signature)),
)
.await
}
/// Get system health information
pub async fn system_health(&self) -> Result<Health, RelayChainError> {
self.request("system_health", rpc_params![]).await
}
/// Get read proof for `storage_keys`
pub async fn state_get_read_proof(
&self,
storage_keys: Vec<StorageKey>,
at: Option<RelayHash>,
) -> Result<ReadProof<RelayHash>, RelayChainError> {
let params = rpc_params![storage_keys, at];
self.request("state_getReadProof", params).await
}
/// Retrieve storage item at `storage_key`
pub async fn state_get_storage(
&self,
storage_key: StorageKey,
at: Option<RelayHash>,
) -> Result<Option<StorageData>, RelayChainError> {
let params = rpc_params![storage_key, at];
self.request("state_getStorage", params).await
}
/// Get hash of the n-th block in the canon chain.
///
/// By default returns latest block hash.
pub async fn chain_get_head(&self, at: Option<u64>) -> Result<RelayHash, RelayChainError> {
let params = rpc_params![at];
self.request("chain_getHead", params).await
}
/// Returns the validator groups and rotation info localized based on the hypothetical child
/// of a block whose state this is invoked on. Note that `now` in the `GroupRotationInfo`
/// should be the successor of the number of the block.
pub async fn teyrchain_host_validator_groups(
&self,
at: RelayHash,
) -> Result<(Vec<Vec<ValidatorIndex>>, GroupRotationInfo), RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_validator_groups", at, None::<()>)
.await
}
/// Get a vector of events concerning candidates that occurred within a block.
pub async fn teyrchain_host_candidate_events(
&self,
at: RelayHash,
) -> Result<Vec<CandidateEvent>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_candidate_events", at, None::<()>)
.await
}
/// Checks if the given validation outputs pass the acceptance criteria.
pub async fn teyrchain_host_check_validation_outputs(
&self,
at: RelayHash,
para_id: ParaId,
outputs: CandidateCommitments,
) -> Result<bool, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_check_validation_outputs",
at,
Some((para_id, outputs)),
)
.await
}
/// Returns the persisted validation data for the given `ParaId` along with the corresponding
/// validation code hash. Instead of accepting assumption about the para, matches the validation
/// data hash against an expected one and yields `None` if they're not equal.
pub async fn teyrchain_host_assumed_validation_data(
&self,
at: RelayHash,
para_id: ParaId,
expected_hash: RelayHash,
) -> Result<Option<(PersistedValidationData, ValidationCodeHash)>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_persisted_assumed_validation_data",
at,
Some((para_id, expected_hash)),
)
.await
}
/// Get hash of last finalized block.
pub async fn chain_get_finalized_head(&self) -> Result<RelayHash, RelayChainError> {
self.request("chain_getFinalizedHead", rpc_params![]).await
}
/// Get hash of n-th block.
pub async fn chain_get_block_hash(
&self,
block_number: Option<BlockNumber>,
) -> Result<Option<RelayHash>, RelayChainError> {
let params = rpc_params![block_number];
self.request("chain_getBlockHash", params).await
}
/// Yields the persisted validation data for the given `ParaId` along with an assumption that
/// should be used if the para currently occupies a core.
///
/// Returns `None` if either the para is not registered or the assumption is `Freed`
/// and the para already occupies a core.
pub async fn teyrchain_host_persisted_validation_data(
&self,
at: RelayHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> Result<Option<PersistedValidationData>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_persisted_validation_data",
at,
Some((para_id, occupied_core_assumption)),
)
.await
}
/// Get the validation code from its hash.
pub async fn teyrchain_host_validation_code_by_hash(
&self,
at: RelayHash,
validation_code_hash: ValidationCodeHash,
) -> Result<Option<ValidationCode>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_validation_code_by_hash",
at,
Some(validation_code_hash),
)
.await
}
/// Yields information on all availability cores as relevant to the child block.
/// Cores are either free or occupied. Free cores can have paras assigned to them.
pub async fn teyrchain_host_availability_cores(
&self,
at: RelayHash,
) -> Result<Vec<CoreState<RelayHash, BlockNumber>>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_availability_cores", at, None::<()>)
.await
}
/// Get runtime version
pub async fn runtime_version(&self, at: RelayHash) -> Result<RuntimeVersion, RelayChainError> {
let params = rpc_params![at];
self.request("state_getRuntimeVersion", params).await
}
/// Returns all onchain disputes.
pub async fn teyrchain_host_disputes(
&self,
at: RelayHash,
) -> Result<Vec<(SessionIndex, CandidateHash, DisputeState<BlockNumber>)>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_disputes", at, None::<()>)
.await
}
/// Returns a list of validators that lost a past session dispute and need to be slashed.
///
/// This is a staging method! Do not use on production runtimes!
pub async fn teyrchain_host_unapplied_slashes(
&self,
at: RelayHash,
) -> Result<Vec<(SessionIndex, CandidateHash, slashing::LegacyPendingSlashes)>, RelayChainError>
{
self.call_remote_runtime_function("TeyrchainHost_unapplied_slashes", at, None::<()>)
.await
}
/// Returns a list of validators that lost a past session dispute and need to be slashed.
pub async fn teyrchain_host_unapplied_slashes_v2(
&self,
at: RelayHash,
) -> Result<Vec<(SessionIndex, CandidateHash, slashing::PendingSlashes)>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_unapplied_slashes_v2", at, None::<()>)
.await
}
/// Returns a merkle proof of a validator session key in a past session.
///
/// This is a staging method! Do not use on production runtimes!
pub async fn teyrchain_host_key_ownership_proof(
&self,
at: RelayHash,
validator_id: ValidatorId,
) -> Result<Option<slashing::OpaqueKeyOwnershipProof>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_key_ownership_proof",
at,
Some(validator_id),
)
.await
}
/// Submits an unsigned extrinsic to slash validators who lost a dispute about
/// a candidate of a past session.
///
/// This is a staging method! Do not use on production runtimes!
pub async fn teyrchain_host_submit_report_dispute_lost(
&self,
at: RelayHash,
dispute_proof: slashing::DisputeProof,
key_ownership_proof: slashing::OpaqueKeyOwnershipProof,
) -> Result<Option<()>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_submit_report_dispute_lost",
at,
Some((dispute_proof, key_ownership_proof)),
)
.await
}
pub async fn authority_discovery_authorities(
&self,
at: RelayHash,
) -> Result<Vec<pezsp_authority_discovery::AuthorityId>, RelayChainError> {
self.call_remote_runtime_function("AuthorityDiscoveryApi_authorities", at, None::<()>)
.await
}
/// Fetch the validation code used by a para, making the given `OccupiedCoreAssumption`.
///
/// Returns `None` if either the para is not registered or the assumption is `Freed`
/// and the para already occupies a core.
pub async fn teyrchain_host_validation_code(
&self,
at: RelayHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> Result<Option<ValidationCode>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_validation_code",
at,
Some((para_id, occupied_core_assumption)),
)
.await
}
/// Fetch the hash of the validation code used by a para, making the given
/// `OccupiedCoreAssumption`.
pub async fn teyrchain_host_validation_code_hash(
&self,
at: RelayHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> Result<Option<ValidationCodeHash>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_validation_code_hash",
at,
Some((para_id, occupied_core_assumption)),
)
.await
}
/// Get the session info for the given session, if stored.
pub async fn teyrchain_host_session_info(
&self,
at: RelayHash,
index: SessionIndex,
) -> Result<Option<SessionInfo>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_session_info", at, Some(index))
.await
}
/// Get the executor parameters for the given session, if stored
pub async fn teyrchain_host_session_executor_params(
&self,
at: RelayHash,
session_index: SessionIndex,
) -> Result<Option<ExecutorParams>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_session_executor_params",
at,
Some(session_index),
)
.await
}
/// Get header at specified hash
pub async fn chain_get_header(
&self,
hash: Option<RelayHash>,
) -> Result<Option<RelayHeader>, RelayChainError> {
let params = rpc_params![hash];
self.request("chain_getHeader", params).await
}
/// Get the receipt of a candidate pending availability. This returns `Some` for any paras
/// assigned to occupied cores in `availability_cores` and `None` otherwise.
pub async fn teyrchain_host_candidate_pending_availability(
&self,
at: RelayHash,
para_id: ParaId,
) -> Result<Option<CommittedCandidateReceipt>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_candidate_pending_availability",
at,
Some(para_id),
)
.await
}
/// Returns the session index expected at a child of the block.
///
/// This can be used to instantiate a `SigningContext`.
pub async fn teyrchain_host_session_index_for_child(
&self,
at: RelayHash,
) -> Result<SessionIndex, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_session_index_for_child", at, None::<()>)
.await
}
/// Get the current validators.
pub async fn teyrchain_host_validators(
&self,
at: RelayHash,
) -> Result<Vec<ValidatorId>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_validators", at, None::<()>)
.await
}
/// Get the contents of all channels addressed to the given recipient. Channels that have no
/// messages in them are also included.
pub async fn teyrchain_host_inbound_hrmp_channels_contents(
&self,
para_id: ParaId,
at: RelayHash,
) -> Result<BTreeMap<ParaId, Vec<InboundHrmpMessage>>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_inbound_hrmp_channels_contents",
at,
Some(para_id),
)
.await
}
/// Get all the pending inbound messages in the downward message queue for a para.
pub async fn teyrchain_host_dmq_contents(
&self,
para_id: ParaId,
at: RelayHash,
) -> Result<Vec<InboundDownwardMessage>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_dmq_contents", at, Some(para_id))
.await
}
/// Get the minimum number of backing votes for a candidate.
pub async fn teyrchain_host_minimum_backing_votes(
&self,
at: RelayHash,
_session_index: SessionIndex,
) -> Result<u32, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_minimum_backing_votes", at, None::<()>)
.await
}
pub async fn teyrchain_host_node_features(
&self,
at: RelayHash,
) -> Result<NodeFeatures, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_node_features", at, None::<()>)
.await
}
pub async fn teyrchain_host_disabled_validators(
&self,
at: RelayHash,
) -> Result<Vec<ValidatorIndex>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_disabled_validators", at, None::<()>)
.await
}
#[allow(missing_docs)]
pub async fn teyrchain_host_async_backing_params(
&self,
at: RelayHash,
) -> Result<AsyncBackingParams, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_async_backing_params", at, None::<()>)
.await
}
#[allow(missing_docs)]
pub async fn teyrchain_host_staging_approval_voting_params(
&self,
at: RelayHash,
_session_index: SessionIndex,
) -> Result<ApprovalVotingParams, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_staging_approval_voting_params",
at,
None::<()>,
)
.await
}
pub async fn teyrchain_host_para_backing_state(
&self,
at: RelayHash,
para_id: ParaId,
) -> Result<Option<BackingState>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_para_backing_state", at, Some(para_id))
.await
}
pub async fn teyrchain_host_claim_queue(
&self,
at: RelayHash,
) -> Result<BTreeMap<CoreIndex, VecDeque<ParaId>>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_claim_queue", at, None::<()>)
.await
}
/// Get the receipt of all candidates pending availability.
pub async fn teyrchain_host_candidates_pending_availability(
&self,
at: RelayHash,
para_id: ParaId,
) -> Result<Vec<CommittedCandidateReceipt>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_candidates_pending_availability",
at,
Some(para_id),
)
.await
}
pub async fn teyrchain_host_scheduling_lookahead(
&self,
at: RelayHash,
) -> Result<u32, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_scheduling_lookahead", at, None::<()>)
.await
}
pub async fn teyrchain_host_validation_code_bomb_limit(
&self,
at: RelayHash,
) -> Result<u32, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_validation_code_bomb_limit",
at,
None::<()>,
)
.await
}
pub async fn validation_code_hash(
&self,
at: RelayHash,
para_id: ParaId,
occupied_core_assumption: OccupiedCoreAssumption,
) -> Result<Option<ValidationCodeHash>, RelayChainError> {
self.call_remote_runtime_function(
"TeyrchainHost_validation_code_hash",
at,
Some((para_id, occupied_core_assumption)),
)
.await
}
pub async fn teyrchain_host_backing_constraints(
&self,
at: RelayHash,
para_id: ParaId,
) -> Result<Option<Constraints>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_backing_constraints", at, Some(para_id))
.await
}
fn send_register_message_to_worker(
&self,
message: RpcDispatcherMessage,
) -> Result<(), RelayChainError> {
self.worker_channel
.try_send(message)
.map_err(|e| RelayChainError::WorkerCommunicationError(e.to_string()))
}
/// Get a stream of all imported relay chain headers
pub fn get_imported_heads_stream(&self) -> Result<Receiver<RelayHeader>, RelayChainError> {
let (tx, rx) =
futures::channel::mpsc::channel::<RelayHeader>(NOTIFICATION_CHANNEL_SIZE_LIMIT);
self.send_register_message_to_worker(RpcDispatcherMessage::RegisterImportListener(tx))?;
Ok(rx)
}
/// Get a stream of new best relay chain headers
pub fn get_best_heads_stream(&self) -> Result<Receiver<RelayHeader>, RelayChainError> {
let (tx, rx) =
futures::channel::mpsc::channel::<RelayHeader>(NOTIFICATION_CHANNEL_SIZE_LIMIT);
self.send_register_message_to_worker(RpcDispatcherMessage::RegisterBestHeadListener(tx))?;
Ok(rx)
}
/// Get a stream of finalized relay chain headers
pub fn get_finalized_heads_stream(&self) -> Result<Receiver<RelayHeader>, RelayChainError> {
let (tx, rx) =
futures::channel::mpsc::channel::<RelayHeader>(NOTIFICATION_CHANNEL_SIZE_LIMIT);
self.send_register_message_to_worker(RpcDispatcherMessage::RegisterFinalizationListener(
tx,
))?;
Ok(rx)
}
pub async fn teyrchain_host_para_ids(
&self,
at: RelayHash,
) -> Result<Vec<ParaId>, RelayChainError> {
self.call_remote_runtime_function("TeyrchainHost_para_ids", at, None::<()>)
.await
}
}
/// Send `header` through all channels contained in `senders`.
/// If no one is listening to the sender, it is removed from the vector.
pub fn distribute_header(header: RelayHeader, senders: &mut Vec<Sender<RelayHeader>>) {
senders.retain_mut(|e| {
match e.try_send(header.clone()) {
// Receiver has been dropped, remove Sender from list.
Err(error) if error.is_disconnected() => false,
// Channel is full. This should not happen.
// TODO: Improve error handling here
// https://github.com/pezkuwichain/kurdistan-sdk/issues/90
Err(error) => {
tracing::error!(target: LOG_TARGET, ?error, "Event distribution channel has reached its limit. This can lead to missed notifications.");
true
},
_ => true,
}
});
}
@@ -0,0 +1,36 @@
[package]
name = "pezcumulus-relay-chain-streams"
version = "0.7.0"
authors.workspace = true
description = "Pezcumulus client common relay chain streams."
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
futures = { workspace = true }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
pezsp-api = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-node-subsystem = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezkuwi-node-subsystem/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
]
@@ -0,0 +1,168 @@
// 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 <https://www.gnu.org/licenses/>.
//! Common utilities for transforming relay chain streams.
use std::sync::Arc;
use cumulus_relay_chain_interface::{RelayChainInterface, RelayChainResult};
use futures::{Stream, StreamExt};
use pezkuwi_node_subsystem::messages::RuntimeApiRequest;
use pezkuwi_primitives::{
CommittedCandidateReceiptV2 as CommittedCandidateReceipt, Hash as PHash, Id as ParaId,
OccupiedCoreAssumption, SessionIndex,
};
use pezsp_api::RuntimeApiInfo;
use pezsp_consensus::SyncOracle;
const LOG_TARGET: &str = "pezcumulus-relay-chain-streams";
pub type RelayHeader = pezkuwi_primitives::Header;
/// Returns a stream over pending candidates for the teyrchain corresponding to `para_id`.
pub async fn pending_candidates(
relay_chain_client: impl RelayChainInterface + Clone,
para_id: ParaId,
sync_service: Arc<dyn SyncOracle + Sync + Send>,
) -> RelayChainResult<impl Stream<Item = (Vec<CommittedCandidateReceipt>, SessionIndex, RelayHeader)>>
{
let import_notification_stream = relay_chain_client.import_notification_stream().await?;
let filtered_stream = import_notification_stream.filter_map(move |n| {
let client = relay_chain_client.clone();
let sync_oracle = sync_service.clone();
async move {
let hash = n.hash();
if sync_oracle.is_major_syncing() {
tracing::debug!(
target: LOG_TARGET,
relay_hash = ?hash,
"Skipping candidate due to sync.",
);
return None;
}
let runtime_api_version = client
.version(hash)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Failed to fetch relay chain runtime version.",
)
})
.ok()?;
let teyrchain_host_runtime_api_version =
runtime_api_version
.api_version(
&<dyn pezkuwi_primitives::runtime_api::TeyrchainHost<
pezkuwi_primitives::Block,
>>::ID,
)
.unwrap_or_default();
// If the relay chain runtime does not support the new runtime API, fallback to the
// deprecated one.
let pending_availability_result = if teyrchain_host_runtime_api_version <
RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT
{
#[allow(deprecated)]
client
.candidate_pending_availability(hash, para_id)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Failed to fetch pending candidates.",
)
})
.map(|candidate| candidate.into_iter().collect::<Vec<_>>())
} else {
client.candidates_pending_availability(hash, para_id).await.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Failed to fetch pending candidates.",
)
})
};
let session_index_result = client.session_index_for_child(hash).await.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Failed to fetch session index.",
)
});
if let Ok(candidates) = pending_availability_result {
session_index_result.map(|session_index| (candidates, session_index, n)).ok()
} else {
None
}
}
});
Ok(filtered_stream)
}
/// Returns a stream that will yield best heads for the given `para_id`.
pub async fn new_best_heads(
relay_chain: impl RelayChainInterface + Clone,
para_id: ParaId,
) -> RelayChainResult<impl Stream<Item = Vec<u8>>> {
let new_best_notification_stream =
relay_chain.new_best_notification_stream().await?.filter_map(move |n| {
let relay_chain = relay_chain.clone();
async move { teyrchain_head_at(&relay_chain, n.hash(), para_id).await.ok().flatten() }
});
Ok(new_best_notification_stream)
}
/// Returns a stream that will yield finalized heads for the given `para_id`.
pub async fn finalized_heads(
relay_chain: impl RelayChainInterface + Clone,
para_id: ParaId,
) -> RelayChainResult<impl Stream<Item = (Vec<u8>, RelayHeader)>> {
let finality_notification_stream =
relay_chain.finality_notification_stream().await?.filter_map(move |n| {
let relay_chain = relay_chain.clone();
async move {
teyrchain_head_at(&relay_chain, n.hash(), para_id)
.await
.ok()
.flatten()
.map(|h| (h, n))
}
});
Ok(finality_notification_stream)
}
/// Returns head of the teyrchain at the given relay chain block.
async fn teyrchain_head_at(
relay_chain: &impl RelayChainInterface,
at: PHash,
para_id: ParaId,
) -> RelayChainResult<Option<Vec<u8>>> {
relay_chain
.persisted_validation_data(at, para_id, OccupiedCoreAssumption::TimedOut)
.await
.map(|s| s.map(|s| s.parent_head.0))
}
+91
View File
@@ -0,0 +1,91 @@
[package]
name = "pezcumulus-client-service"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
description = "Common functions used to assemble the components of a teyrchain node."
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-channel = { workspace = true }
futures = { workspace = true }
prometheus = { workspace = true }
# Bizinikiwi
pezsc-client-api = { workspace = true, default-features = true }
pezsc-consensus = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsc-network-sync = { workspace = true, default-features = true }
pezsc-network-transactions = { workspace = true, default-features = true }
pezsc-rpc = { workspace = true, default-features = true }
pezsc-service = { workspace = true, default-features = true }
pezsc-sysinfo = { workspace = true, default-features = true }
pezsc-telemetry = { workspace = true, default-features = true }
pezsc-tracing = { workspace = true, default-features = true }
pezsc-transaction-pool = { workspace = true, default-features = true }
pezsc-utils = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-io = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-transaction-pool = { workspace = true, default-features = true }
pezsp-trie = { workspace = true, default-features = true }
# Pezkuwi
pezkuwi-overseer = { workspace = true, default-features = true }
pezkuwi-primitives = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-client-cli = { workspace = true, default-features = true }
pezcumulus-client-collator = { workspace = true, default-features = true }
pezcumulus-client-consensus-common = { workspace = true, default-features = true }
pezcumulus-client-network = { workspace = true, default-features = true }
pezcumulus-client-pov-recovery = { workspace = true, default-features = true }
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-primitives-proof-size-hostfunction = { workspace = true, default-features = true }
pezcumulus-relay-chain-inprocess-interface = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
pezcumulus-relay-chain-minimal-node = { workspace = true, default-features = true }
pezcumulus-relay-chain-streams = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-client-cli/runtime-benchmarks",
"pezcumulus-client-collator/runtime-benchmarks",
"pezcumulus-client-consensus-common/runtime-benchmarks",
"pezcumulus-client-network/runtime-benchmarks",
"pezcumulus-client-pov-recovery/runtime-benchmarks",
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-primitives-proof-size-hostfunction/runtime-benchmarks",
"pezcumulus-relay-chain-inprocess-interface/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezcumulus-relay-chain-minimal-node/runtime-benchmarks",
"pezcumulus-relay-chain-streams/runtime-benchmarks",
"pezkuwi-overseer/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-consensus/runtime-benchmarks",
"pezsc-network-sync/runtime-benchmarks",
"pezsc-network-transactions/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsc-rpc/runtime-benchmarks",
"pezsc-service/runtime-benchmarks",
"pezsc-sysinfo/runtime-benchmarks",
"pezsc-tracing/runtime-benchmarks",
"pezsc-transaction-pool/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-transaction-pool/runtime-benchmarks",
"pezsp-trie/runtime-benchmarks",
]
+631
View File
@@ -0,0 +1,631 @@
// 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 <https://www.gnu.org/licenses/>.
//! Pezcumulus service
//!
//! Provides functions for starting a collator node or a normal full node.
use cumulus_client_cli::CollatorOptions;
use cumulus_client_network::{AssumeSybilResistance, RequireSecondedInBlockAnnounce};
use cumulus_client_pov_recovery::{PoVRecovery, RecoveryDelayRange, RecoveryHandle};
use cumulus_primitives_core::{CollectCollationInfo, ParaId};
pub use cumulus_primitives_proof_size_hostfunction::storage_proof_size;
use cumulus_relay_chain_inprocess_interface::build_inprocess_relay_chain;
use cumulus_relay_chain_interface::{RelayChainInterface, RelayChainResult};
use cumulus_relay_chain_minimal_node::build_minimal_relay_chain_node_with_rpc;
use futures::{channel::mpsc, StreamExt};
use pezkuwi_primitives::{CandidateEvent, CollatorPair, OccupiedCoreAssumption};
use prometheus::{Histogram, HistogramOpts, Registry};
use pezsc_client_api::{
Backend as BackendT, BlockBackend, BlockchainEvents, Finalizer, ProofProvider, UsageProvider,
};
use pezsc_consensus::{
import_queue::{ImportQueue, ImportQueueService},
BlockImport,
};
use pezsc_network::{
config::SyncMode, request_responses::IncomingRequest, service::traits::NetworkService,
NetworkBackend,
};
use pezsc_network_sync::SyncingService;
use pezsc_network_transactions::TransactionsHandlerController;
use pezsc_service::{Configuration, SpawnTaskHandle, TaskManager, WarpSyncConfig};
use pezsc_telemetry::{log, TelemetryWorkerHandle};
use pezsc_tracing::block::TracingExecuteBlock;
use pezsc_utils::mpsc::TracingUnboundedSender;
use pezsp_api::{ApiExt, Core, ProofRecorder, ProvideRuntimeApi};
use pezsp_blockchain::{HeaderBackend, HeaderMetadata};
use pezsp_core::Decode;
use pezsp_runtime::{
traits::{Block as BlockT, BlockIdTo, Header},
SaturatedConversion, Saturating,
};
use pezsp_trie::proof_size_extension::ProofSizeExt;
use std::{
sync::Arc,
time::{Duration, Instant},
};
/// Host functions that should be used in teyrchain nodes.
///
/// Contains the standard bizinikiwi host functions, as well as a
/// host function to enable PoV-reclaim on teyrchain nodes.
pub type TeyrchainHostFunctions = (
cumulus_primitives_proof_size_hostfunction::storage_proof_size::HostFunctions,
pezsp_io::BizinikiwiHostFunctions,
);
// Given the sporadic nature of the explicit recovery operation and the
// possibility to retry infinite times this value is more than enough.
// In practice here we expect no more than one queued messages.
const RECOVERY_CHAN_SIZE: usize = 8;
const LOG_TARGET_SYNC: &str = "sync::pezcumulus";
/// A hint about how long the node should wait before attempting to recover missing block data
/// from the data availability layer.
pub enum DARecoveryProfile {
/// Collators use an aggressive recovery profile by default.
Collator,
/// Full nodes use a passive recovery profile by default, as they are not direct
/// victims of withholding attacks.
FullNode,
/// Provide an explicit recovery profile.
Other(RecoveryDelayRange),
}
/// Parameters given to [`start_relay_chain_tasks`].
pub struct StartRelayChainTasksParams<'a, Block: BlockT, Client, RCInterface> {
pub client: Arc<Client>,
pub announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
pub para_id: ParaId,
pub relay_chain_interface: RCInterface,
pub task_manager: &'a mut TaskManager,
pub da_recovery_profile: DARecoveryProfile,
pub import_queue: Box<dyn ImportQueueService<Block>>,
pub relay_chain_slot_duration: Duration,
pub recovery_handle: Box<dyn RecoveryHandle>,
pub sync_service: Arc<SyncingService<Block>>,
pub prometheus_registry: Option<&'a Registry>,
}
/// Start necessary consensus tasks related to the relay chain.
///
/// Teyrchain nodes need to track the state of the relay chain and use the
/// relay chain's data availability service to fetch blocks if they don't
/// arrive via the normal p2p layer (i.e. when authors withhold their blocks deliberately).
///
/// This function spawns work for those side tasks.
///
/// It also spawns a teyrchain informant task that will log the relay chain state and some metrics.
pub fn start_relay_chain_tasks<Block, Client, Backend, RCInterface>(
StartRelayChainTasksParams {
client,
announce_block,
para_id,
task_manager,
da_recovery_profile,
relay_chain_interface,
import_queue,
relay_chain_slot_duration,
recovery_handle,
sync_service,
prometheus_registry,
}: StartRelayChainTasksParams<Block, Client, RCInterface>,
) -> pezsc_service::error::Result<()>
where
Block: BlockT,
Client: Finalizer<Block, Backend>
+ UsageProvider<Block>
+ HeaderBackend<Block>
+ Send
+ Sync
+ BlockBackend<Block>
+ BlockchainEvents<Block>
+ 'static,
for<'a> &'a Client: BlockImport<Block>,
Backend: BackendT<Block> + 'static,
RCInterface: RelayChainInterface + Clone + 'static,
{
let (recovery_chan_tx, recovery_chan_rx) = mpsc::channel(RECOVERY_CHAN_SIZE);
cumulus_client_consensus_common::spawn_teyrchain_consensus_tasks(
para_id,
client.clone(),
relay_chain_interface.clone(),
announce_block.clone(),
Some(recovery_chan_tx),
task_manager.spawn_essential_handle(),
);
let da_recovery_profile = match da_recovery_profile {
DARecoveryProfile::Collator => {
// We want that collators wait at maximum the relay chain slot duration before starting
// to recover blocks. Additionally, we wait at least half the slot time to give the
// relay chain the chance to increase availability.
RecoveryDelayRange {
min: relay_chain_slot_duration / 2,
max: relay_chain_slot_duration,
}
},
DARecoveryProfile::FullNode => {
// Full nodes should at least wait 2.5 minutes (assuming 6 seconds slot duration) and
// in maximum 5 minutes before starting to recover blocks. Collators should already
// start the recovery way before full nodes try to recover a certain block and then
// share the block with the network using "the normal way". Full nodes are just the
// "last resort" for block recovery.
RecoveryDelayRange {
min: relay_chain_slot_duration * 25,
max: relay_chain_slot_duration * 50,
}
},
DARecoveryProfile::Other(profile) => profile,
};
let pov_recovery = PoVRecovery::new(
recovery_handle,
da_recovery_profile,
client.clone(),
import_queue,
relay_chain_interface.clone(),
para_id,
recovery_chan_rx,
sync_service.clone(),
);
task_manager
.spawn_essential_handle()
.spawn("pezcumulus-pov-recovery", None, pov_recovery.run());
let teyrchain_informant = teyrchain_informant::<Block, _>(
para_id,
relay_chain_interface.clone(),
client.clone(),
prometheus_registry.map(TeyrchainInformantMetrics::new).transpose()?,
);
task_manager
.spawn_handle()
.spawn("teyrchain-informant", None, teyrchain_informant);
Ok(())
}
/// Prepare the teyrchain's node configuration
///
/// This function will:
/// * Disable the default announcement of Bizinikiwi for the teyrchain in favor of the one of
/// Pezcumulus.
/// * Set peers needed to start warp sync to 1.
pub fn prepare_node_config(mut teyrchain_config: Configuration) -> Configuration {
teyrchain_config.announce_block = false;
// Teyrchains only need 1 peer to start warp sync, because the target block is fetched from the
// relay chain.
teyrchain_config.network.min_peers_to_start_warp_sync = Some(1);
teyrchain_config
}
/// Build a relay chain interface.
/// Will return a minimal relay chain node with RPC
/// client or an inprocess node, based on the [`CollatorOptions`] passed in.
pub async fn build_relay_chain_interface(
relay_chain_config: Configuration,
teyrchain_config: &Configuration,
telemetry_worker_handle: Option<TelemetryWorkerHandle>,
task_manager: &mut TaskManager,
collator_options: CollatorOptions,
hwbench: Option<pezsc_sysinfo::HwBench>,
) -> RelayChainResult<(
Arc<dyn RelayChainInterface + 'static>,
Option<CollatorPair>,
Arc<dyn NetworkService>,
async_channel::Receiver<IncomingRequest>,
)> {
match collator_options.relay_chain_mode {
cumulus_client_cli::RelayChainMode::Embedded => build_inprocess_relay_chain(
relay_chain_config,
teyrchain_config,
telemetry_worker_handle,
task_manager,
hwbench,
),
cumulus_client_cli::RelayChainMode::ExternalRpc(rpc_target_urls) =>
build_minimal_relay_chain_node_with_rpc(
relay_chain_config,
teyrchain_config.prometheus_registry(),
task_manager,
rpc_target_urls,
)
.await,
}
}
/// The expected level of collator sybil-resistance on the network. This is used to
/// configure the type of metadata passed alongside block announcements on the network.
pub enum CollatorSybilResistance {
/// There is a collator-selection protocol which provides sybil-resistance,
/// such as Aura. Sybil-resistant collator-selection protocols are able to
/// operate more efficiently.
Resistant,
/// There is no collator-selection protocol providing sybil-resistance.
/// In situations such as "free-for-all" collators, the network is unresistant
/// and needs to attach more metadata to block announcements, relying on relay-chain
/// validators to avoid handling unbounded numbers of blocks.
Unresistant,
}
/// Parameters given to [`build_network`].
pub struct BuildNetworkParams<
'a,
Block: BlockT,
Client: ProvideRuntimeApi<Block>
+ BlockBackend<Block>
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ HeaderBackend<Block>
+ BlockIdTo<Block>
+ 'static,
Network: NetworkBackend<Block, <Block as BlockT>::Hash>,
RCInterface,
IQ,
> where
Client::Api: pezsp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>,
{
pub teyrchain_config: &'a Configuration,
pub net_config:
pezsc_network::config::FullNetworkConfiguration<Block, <Block as BlockT>::Hash, Network>,
pub client: Arc<Client>,
pub transaction_pool: Arc<pezsc_transaction_pool::TransactionPoolHandle<Block, Client>>,
pub para_id: ParaId,
pub relay_chain_interface: RCInterface,
pub spawn_handle: SpawnTaskHandle,
pub import_queue: IQ,
pub sybil_resistance_level: CollatorSybilResistance,
pub metrics: pezsc_network::NotificationMetrics,
}
/// Build the network service, the network status sinks and an RPC sender.
pub async fn build_network<'a, Block, Client, RCInterface, IQ, Network>(
BuildNetworkParams {
teyrchain_config,
net_config,
client,
transaction_pool,
para_id,
spawn_handle,
relay_chain_interface,
import_queue,
sybil_resistance_level,
metrics,
}: BuildNetworkParams<'a, Block, Client, Network, RCInterface, IQ>,
) -> pezsc_service::error::Result<(
Arc<dyn NetworkService>,
TracingUnboundedSender<pezsc_rpc::system::Request<Block>>,
TransactionsHandlerController<Block::Hash>,
Arc<SyncingService<Block>>,
)>
where
Block: BlockT,
Client: UsageProvider<Block>
+ HeaderBackend<Block>
+ pezsp_consensus::block_validation::Chain<Block>
+ Send
+ Sync
+ BlockBackend<Block>
+ BlockchainEvents<Block>
+ ProvideRuntimeApi<Block>
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ BlockIdTo<Block, Error = pezsp_blockchain::Error>
+ ProofProvider<Block>
+ 'static,
Client::Api: CollectCollationInfo<Block>
+ pezsp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>,
for<'b> &'b Client: BlockImport<Block>,
RCInterface: RelayChainInterface + Clone + 'static,
IQ: ImportQueue<Block> + 'static,
Network: NetworkBackend<Block, <Block as BlockT>::Hash>,
{
let warp_sync_config = match teyrchain_config.network.sync_mode {
SyncMode::Warp => {
log::debug!(target: LOG_TARGET_SYNC, "waiting for announce block...");
let target_block =
wait_for_finalized_para_head::<Block, _>(para_id, relay_chain_interface.clone())
.await
.inspect_err(|e| {
log::error!(
target: LOG_TARGET_SYNC,
"Unable to determine teyrchain target block {:?}",
e
);
})?;
Some(WarpSyncConfig::WithTarget(target_block))
},
_ => None,
};
let block_announce_validator = match sybil_resistance_level {
CollatorSybilResistance::Resistant => {
let block_announce_validator = AssumeSybilResistance::allow_seconded_messages();
Box::new(block_announce_validator) as Box<_>
},
CollatorSybilResistance::Unresistant => {
let block_announce_validator =
RequireSecondedInBlockAnnounce::new(relay_chain_interface, para_id);
Box::new(block_announce_validator) as Box<_>
},
};
pezsc_service::build_network(pezsc_service::BuildNetworkParams {
config: teyrchain_config,
net_config,
client,
transaction_pool,
spawn_handle,
import_queue,
block_announce_validator_builder: Some(Box::new(move |_| block_announce_validator)),
warp_sync_config,
block_relay: None,
metrics,
})
}
/// Waits for the relay chain to have finished syncing and then gets the teyrchain header that
/// corresponds to the last finalized relay chain block.
async fn wait_for_finalized_para_head<B, RCInterface>(
para_id: ParaId,
relay_chain_interface: RCInterface,
) -> pezsc_service::error::Result<<B as BlockT>::Header>
where
B: BlockT + 'static,
RCInterface: RelayChainInterface + Send + 'static,
{
let mut imported_blocks = relay_chain_interface
.import_notification_stream()
.await
.map_err(|error| {
pezsc_service::Error::Other(format!(
"Relay chain import notification stream error when waiting for teyrchain head: \
{error}"
))
})?
.fuse();
while imported_blocks.next().await.is_some() {
let is_syncing = relay_chain_interface
.is_major_syncing()
.await
.map_err(|e| format!("Unable to determine sync status: {e}"))?;
if !is_syncing {
let relay_chain_best_hash = relay_chain_interface
.finalized_block_hash()
.await
.map_err(|e| Box::new(e) as Box<_>)?;
let validation_data = relay_chain_interface
.persisted_validation_data(
relay_chain_best_hash,
para_id,
OccupiedCoreAssumption::TimedOut,
)
.await
.map_err(|e| format!("{e:?}"))?
.ok_or("Could not find teyrchain head in relay chain")?;
let finalized_header = B::Header::decode(&mut &validation_data.parent_head.0[..])
.map_err(|e| format!("Failed to decode teyrchain head: {e}"))?;
log::info!(
"🎉 Received target teyrchain header #{} ({}) from the relay chain.",
finalized_header.number(),
finalized_header.hash()
);
return Ok(finalized_header);
}
}
Err("Stopping following imported blocks. Could not determine teyrchain target block".into())
}
/// Task for logging candidate events and some related metrics.
async fn teyrchain_informant<Block: BlockT, Client>(
para_id: ParaId,
relay_chain_interface: impl RelayChainInterface + Clone,
client: Arc<Client>,
metrics: Option<TeyrchainInformantMetrics>,
) where
Client: HeaderBackend<Block> + Send + Sync + 'static,
{
let mut import_notifications = match relay_chain_interface.import_notification_stream().await {
Ok(import_notifications) => import_notifications,
Err(e) => {
log::error!("Failed to get import notification stream: {e:?}. Teyrchain informant will not run!");
return;
},
};
let mut last_backed_block_time: Option<Instant> = None;
while let Some(n) = import_notifications.next().await {
let candidate_events = match relay_chain_interface.candidate_events(n.hash()).await {
Ok(candidate_events) => candidate_events,
Err(e) => {
log::warn!("Failed to get candidate events for block {}: {e:?}", n.hash());
continue;
},
};
let mut backed_candidates = Vec::new();
let mut included_candidates = Vec::new();
let mut timed_out_candidates = Vec::new();
for event in candidate_events {
match event {
CandidateEvent::CandidateBacked(receipt, head, _, _) => {
if receipt.descriptor.para_id() != para_id {
continue;
}
let backed_block = match Block::Header::decode(&mut &head.0[..]) {
Ok(header) => header,
Err(e) => {
log::warn!(
"Failed to decode teyrchain header from backed block: {e:?}"
);
continue;
},
};
let backed_block_time = Instant::now();
if let Some(last_backed_block_time) = &last_backed_block_time {
let duration = backed_block_time.duration_since(*last_backed_block_time);
if let Some(metrics) = &metrics {
metrics.teyrchain_block_backed_duration.observe(duration.as_secs_f64());
}
}
last_backed_block_time = Some(backed_block_time);
backed_candidates.push(backed_block);
},
CandidateEvent::CandidateIncluded(receipt, head, _, _) => {
if receipt.descriptor.para_id() != para_id {
continue;
}
let included_block = match Block::Header::decode(&mut &head.0[..]) {
Ok(header) => header,
Err(e) => {
log::warn!(
"Failed to decode teyrchain header from included block: {e:?}"
);
continue;
},
};
let unincluded_segment_size =
client.info().best_number.saturating_sub(*included_block.number());
let unincluded_segment_size: u32 = unincluded_segment_size.saturated_into();
if let Some(metrics) = &metrics {
metrics.unincluded_segment_size.observe(unincluded_segment_size.into());
}
included_candidates.push(included_block);
},
CandidateEvent::CandidateTimedOut(receipt, head, _) => {
if receipt.descriptor.para_id() != para_id {
continue;
}
let timed_out_block = match Block::Header::decode(&mut &head.0[..]) {
Ok(header) => header,
Err(e) => {
log::warn!(
"Failed to decode teyrchain header from timed out block: {e:?}"
);
continue;
},
};
timed_out_candidates.push(timed_out_block);
},
}
}
let mut log_parts = Vec::new();
if !backed_candidates.is_empty() {
let backed_candidates = backed_candidates
.into_iter()
.map(|c| format!("#{} ({})", c.number(), c.hash()))
.collect::<Vec<_>>()
.join(", ");
log_parts.push(format!("backed: {}", backed_candidates));
};
if !included_candidates.is_empty() {
let included_candidates = included_candidates
.into_iter()
.map(|c| format!("#{} ({})", c.number(), c.hash()))
.collect::<Vec<_>>()
.join(", ");
log_parts.push(format!("included: {}", included_candidates));
};
if !timed_out_candidates.is_empty() {
let timed_out_candidates = timed_out_candidates
.into_iter()
.map(|c| format!("#{} ({})", c.number(), c.hash()))
.collect::<Vec<_>>()
.join(", ");
log_parts.push(format!("timed out: {}", timed_out_candidates));
};
if !log_parts.is_empty() {
log::info!(
"Update at relay chain block #{} ({}) - {}",
n.number(),
n.hash(),
log_parts.join(", ")
);
}
}
}
struct TeyrchainInformantMetrics {
/// Time between teyrchain blocks getting backed by the relaychain.
teyrchain_block_backed_duration: Histogram,
/// Number of blocks between best block and last included block.
unincluded_segment_size: Histogram,
}
impl TeyrchainInformantMetrics {
fn new(prometheus_registry: &Registry) -> prometheus::Result<Self> {
let teyrchain_block_authorship_duration = Histogram::with_opts(HistogramOpts::new(
"teyrchain_block_backed_duration",
"Time between teyrchain blocks getting backed by the relaychain",
))?;
prometheus_registry.register(Box::new(teyrchain_block_authorship_duration.clone()))?;
let unincluded_segment_size = Histogram::with_opts(
HistogramOpts::new(
"teyrchain_unincluded_segment_size",
"Number of blocks between best block and last included block",
)
.buckets((0..=24).into_iter().map(|i| i as f64).collect()),
)?;
prometheus_registry.register(Box::new(unincluded_segment_size.clone()))?;
Ok(Self {
teyrchain_block_backed_duration: teyrchain_block_authorship_duration,
unincluded_segment_size,
})
}
}
/// Implementation of [`TracingExecuteBlock`] for teyrchains.
///
/// Ensures that all the required extensions required by teyrchain runtimes are registered and
/// available.
pub struct TeyrchainTracingExecuteBlock<Client> {
client: Arc<Client>,
}
impl<Client> TeyrchainTracingExecuteBlock<Client> {
/// Creates a new instance of `self`.
pub fn new(client: Arc<Client>) -> Self {
Self { client }
}
}
impl<Block, Client> TracingExecuteBlock<Block> for TeyrchainTracingExecuteBlock<Client>
where
Block: BlockT,
Client: ProvideRuntimeApi<Block> + Send + Sync,
Client::Api: Core<Block>,
{
fn execute_block(&self, _: Block::Hash, block: Block) -> pezsp_blockchain::Result<()> {
let mut runtime_api = self.client.runtime_api();
let storage_proof_recorder = ProofRecorder::<Block>::default();
runtime_api.register_extension(ProofSizeExt::new(storage_proof_recorder.clone()));
runtime_api.record_proof_with_recorder(storage_proof_recorder);
runtime_api
.execute_block(*block.header().parent_hash(), block.into())
.map_err(Into::into)
}
}
@@ -0,0 +1,43 @@
[package]
name = "pezcumulus-client-teyrchain-inherent"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
description = "Inherent that needs to be present in every teyrchain block. Contains messages and a relay chain storage-proof."
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
[dependencies]
async-trait = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
# Bizinikiwi
pezsc-client-api = { workspace = true, default-features = true }
pezsc-consensus-babe = { workspace = true, default-features = true }
pezsc-network-types = { workspace = true, default-features = true }
pezsp-crypto-hashing = { workspace = true, default-features = true }
pezsp-inherents = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-storage = { workspace = true, default-features = true }
# Pezcumulus
pezcumulus-primitives-core = { workspace = true, default-features = true }
pezcumulus-primitives-teyrchain-inherent = { workspace = true, default-features = true }
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
pezcumulus-test-relay-sproof-builder = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-primitives-teyrchain-inherent/runtime-benchmarks",
"pezcumulus-relay-chain-interface/runtime-benchmarks",
"pezcumulus-test-relay-sproof-builder/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-consensus-babe/runtime-benchmarks",
"pezsp-inherents/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
]
@@ -0,0 +1,235 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Client side code for generating the teyrchain inherent.
mod mock;
use codec::Decode;
use cumulus_primitives_core::{
relay_chain::{
self, ApprovedPeerId, Block as RelayBlock, Hash as PHash, Header as RelayHeader,
HrmpChannelId,
},
ParaId, PersistedValidationData,
};
pub use cumulus_primitives_teyrchain_inherent::{TeyrchainInherentData, INHERENT_IDENTIFIER};
use cumulus_relay_chain_interface::RelayChainInterface;
pub use mock::{MockValidationDataInherentDataProvider, MockXcmConfig};
use pezsc_network_types::PeerId;
const LOG_TARGET: &str = "teyrchain-inherent";
/// Collect the relevant relay chain state in form of a proof for putting it into the validation
/// data inherent.
async fn collect_relay_storage_proof(
relay_chain_interface: &impl RelayChainInterface,
para_id: ParaId,
relay_parent: PHash,
include_authorities: bool,
include_next_authorities: bool,
additional_relay_state_keys: Vec<Vec<u8>>,
) -> Option<pezsp_state_machine::StorageProof> {
use relay_chain::well_known_keys as relay_well_known_keys;
let ingress_channels = relay_chain_interface
.get_storage_by_key(
relay_parent,
&relay_well_known_keys::hrmp_ingress_channel_index(para_id),
)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
relay_parent = ?relay_parent,
error = ?e,
"Cannot obtain the hrmp ingress channel."
)
})
.ok()?;
let ingress_channels = ingress_channels
.map(|raw| <Vec<ParaId>>::decode(&mut &raw[..]))
.transpose()
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Cannot decode the hrmp ingress channel index.",
)
})
.ok()?
.unwrap_or_default();
let egress_channels = relay_chain_interface
.get_storage_by_key(
relay_parent,
&relay_well_known_keys::hrmp_egress_channel_index(para_id),
)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Cannot obtain the hrmp egress channel.",
)
})
.ok()?;
let egress_channels = egress_channels
.map(|raw| <Vec<ParaId>>::decode(&mut &raw[..]))
.transpose()
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Cannot decode the hrmp egress channel index.",
)
})
.ok()?
.unwrap_or_default();
let mut relevant_keys = vec![
relay_well_known_keys::CURRENT_BLOCK_RANDOMNESS.to_vec(),
relay_well_known_keys::ONE_EPOCH_AGO_RANDOMNESS.to_vec(),
relay_well_known_keys::TWO_EPOCHS_AGO_RANDOMNESS.to_vec(),
relay_well_known_keys::CURRENT_SLOT.to_vec(),
relay_well_known_keys::ACTIVE_CONFIG.to_vec(),
relay_well_known_keys::dmq_mqc_head(para_id),
// TODO paritytech/pezkuwi#6283: Remove all usages of `relay_dispatch_queue_size`
// We need to keep this here until all teyrchains have migrated to
// `relay_dispatch_queue_remaining_capacity`.
#[allow(deprecated)]
relay_well_known_keys::relay_dispatch_queue_size(para_id),
relay_well_known_keys::relay_dispatch_queue_remaining_capacity(para_id).key,
relay_well_known_keys::hrmp_ingress_channel_index(para_id),
relay_well_known_keys::hrmp_egress_channel_index(para_id),
relay_well_known_keys::upgrade_go_ahead_signal(para_id),
relay_well_known_keys::upgrade_restriction_signal(para_id),
relay_well_known_keys::para_head(para_id),
];
relevant_keys.extend(ingress_channels.into_iter().map(|sender| {
relay_well_known_keys::hrmp_channels(HrmpChannelId { sender, recipient: para_id })
}));
relevant_keys.extend(egress_channels.into_iter().map(|recipient| {
relay_well_known_keys::hrmp_channels(HrmpChannelId { sender: para_id, recipient })
}));
if include_authorities {
relevant_keys.push(relay_well_known_keys::AUTHORITIES.to_vec());
}
if include_next_authorities {
relevant_keys.push(relay_well_known_keys::NEXT_AUTHORITIES.to_vec());
}
// Add additional relay state keys
let unique_keys: Vec<Vec<u8>> = additional_relay_state_keys
.into_iter()
.filter(|key| !relevant_keys.contains(key))
.collect();
relevant_keys.extend(unique_keys);
relay_chain_interface
.prove_read(relay_parent, &relevant_keys)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
relay_parent = ?relay_parent,
error = ?e,
"Cannot obtain read proof from relay chain.",
);
})
.ok()
}
pub struct TeyrchainInherentDataProvider;
impl TeyrchainInherentDataProvider {
/// Create the [`TeyrchainInherentData`] at the given `relay_parent`.
///
/// Returns `None` if the creation failed.
pub async fn create_at(
relay_parent: PHash,
relay_chain_interface: &impl RelayChainInterface,
validation_data: &PersistedValidationData,
para_id: ParaId,
relay_parent_descendants: Vec<RelayHeader>,
additional_relay_state_keys: Vec<Vec<u8>>,
collator_peer_id: PeerId,
) -> Option<TeyrchainInherentData> {
let collator_peer_id = ApprovedPeerId::try_from(collator_peer_id.to_bytes())
.inspect_err(|_e| {
tracing::warn!(
target: LOG_TARGET,
"Could not convert collator_peer_id into ApprovedPeerId. The collator_peer_id \
should contain a sequence of at most 64 bytes",
);
})
.ok();
// Only include next epoch authorities when the descendants include an epoch digest.
// Skip the first entry because this is the relay parent itself.
let include_next_authorities = relay_parent_descendants
.iter()
.skip(1)
.any(pezsc_consensus_babe::contains_epoch_change::<RelayBlock>);
let relay_chain_state = collect_relay_storage_proof(
relay_chain_interface,
para_id,
relay_parent,
!relay_parent_descendants.is_empty(),
include_next_authorities,
additional_relay_state_keys,
)
.await?;
let downward_messages = relay_chain_interface
.retrieve_dmq_contents(para_id, relay_parent)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
relay_parent = ?relay_parent,
error = ?e,
"An error occurred during requesting the downward messages.",
);
})
.ok()?;
let horizontal_messages = relay_chain_interface
.retrieve_all_inbound_hrmp_channel_contents(para_id, relay_parent)
.await
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
relay_parent = ?relay_parent,
error = ?e,
"An error occurred during requesting the inbound HRMP messages.",
);
})
.ok()?;
Some(TeyrchainInherentData {
downward_messages,
horizontal_messages,
validation_data: validation_data.clone(),
relay_chain_state,
relay_parent_descendants,
collator_peer_id,
})
}
}
@@ -0,0 +1,258 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::TeyrchainInherentData;
use codec::Decode;
use cumulus_primitives_core::{
relay_chain,
relay_chain::{Slot, UpgradeGoAhead},
InboundDownwardMessage, InboundHrmpMessage, ParaId, PersistedValidationData,
};
use cumulus_primitives_teyrchain_inherent::MessageQueueChain;
use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder;
use pezsc_client_api::{Backend, StorageProvider};
use pezsp_crypto_hashing::twox_128;
use pezsp_inherents::{InherentData, InherentDataProvider};
use pezsp_runtime::traits::Block;
use std::collections::BTreeMap;
/// Inherent data provider that supplies mocked validation data.
///
/// This is useful when running a node that is not actually backed by any relay chain.
/// For example when running a local node, or running integration tests.
///
/// We mock a relay chain block number as follows:
/// relay_block_number = offset + relay_blocks_per_para_block * current_para_block
/// To simulate a teyrchain that starts in relay block 1000 and gets a block in every other relay
/// block, use 1000 and 2
///
/// Optionally, mock XCM messages can be injected into the runtime. When mocking XCM,
/// in addition to the messages themselves, you must provide some information about
/// your teyrchain's configuration in order to mock the MQC heads properly.
/// See [`MockXcmConfig`] for more information
#[derive(Default)]
pub struct MockValidationDataInherentDataProvider<R = ()> {
/// The current block number of the local block chain (the teyrchain).
pub current_para_block: u32,
/// The teyrchain ID of the teyrchain for that the inherent data is created.
pub para_id: ParaId,
/// The current block head data of the local block chain (the teyrchain).
pub current_para_block_head: Option<cumulus_primitives_core::relay_chain::HeadData>,
/// The relay block in which this teyrchain appeared to start. This will be the relay block
/// number in para block #P1.
pub relay_offset: u32,
/// The number of relay blocks that elapses between each parablock. Probably set this to 1 or 2
/// to simulate optimistic or realistic relay chain behavior.
pub relay_blocks_per_para_block: u32,
/// Number of teyrchain blocks per relay chain epoch
/// Mock epoch is computed by dividing `current_para_block` by this value.
pub para_blocks_per_relay_epoch: u32,
/// Function to mock BABE one epoch ago randomness.
pub relay_randomness_config: R,
/// XCM messages and associated configuration information.
pub xcm_config: MockXcmConfig,
/// Inbound downward XCM messages to be injected into the block.
pub raw_downward_messages: Vec<Vec<u8>>,
/// Inbound Horizontal messages sorted by channel.
pub raw_horizontal_messages: Vec<(ParaId, Vec<u8>)>,
/// Additional key-value pairs that should be injected.
pub additional_key_values: Option<Vec<(Vec<u8>, Vec<u8>)>>,
/// Whether upgrade go ahead should be set.
pub upgrade_go_ahead: Option<UpgradeGoAhead>,
}
/// Something that can generate randomness.
pub trait GenerateRandomness<I> {
/// Generate the randomness using the given `input`.
fn generate_randomness(&self, input: I) -> relay_chain::Hash;
}
impl GenerateRandomness<u64> for () {
/// Default implementation uses relay epoch as randomness value
/// A more seemingly random implementation may hash the relay epoch instead
fn generate_randomness(&self, input: u64) -> relay_chain::Hash {
let mut mock_randomness: [u8; 32] = [0u8; 32];
mock_randomness[..8].copy_from_slice(&input.to_be_bytes());
mock_randomness.into()
}
}
/// Parameters for how the Mock inherent data provider should inject XCM messages.
/// In addition to the messages themselves, some information about the teyrchain's
/// configuration is also required so that the MQC heads can be read out of the
/// teyrchain's storage, and the corresponding relay data mocked.
#[derive(Default)]
pub struct MockXcmConfig {
/// The starting state of the dmq_mqc_head.
pub starting_dmq_mqc_head: relay_chain::Hash,
/// The starting state of each teyrchain's mqc head
pub starting_hrmp_mqc_heads: BTreeMap<ParaId, relay_chain::Hash>,
}
/// The name of the teyrchain system in the runtime.
///
/// This name is used by frame to prefix storage items and will be required to read data from the
/// storage.
///
/// The `Default` implementation sets the name to `TeyrchainSystem`.
pub struct TeyrchainSystemName(pub Vec<u8>);
impl Default for TeyrchainSystemName {
fn default() -> Self {
Self(b"TeyrchainSystem".to_vec())
}
}
impl MockXcmConfig {
/// Create a MockXcmConfig by reading the mqc_heads directly
/// from the storage of a previous block.
pub fn new<B: Block, BE: Backend<B>, C: StorageProvider<B, BE>>(
client: &C,
parent_block: B::Hash,
teyrchain_system_name: TeyrchainSystemName,
) -> Self {
let starting_dmq_mqc_head = client
.storage(
parent_block,
&pezsp_storage::StorageKey(
[twox_128(&teyrchain_system_name.0), twox_128(b"LastDmqMqcHead")]
.concat()
.to_vec(),
),
)
.expect("We should be able to read storage from the parent block.")
.map(|ref mut raw_data| {
Decode::decode(&mut &raw_data.0[..]).expect("Stored data should decode correctly")
})
.unwrap_or_default();
let starting_hrmp_mqc_heads = client
.storage(
parent_block,
&pezsp_storage::StorageKey(
[twox_128(&teyrchain_system_name.0), twox_128(b"LastHrmpMqcHeads")]
.concat()
.to_vec(),
),
)
.expect("We should be able to read storage from the parent block.")
.map(|ref mut raw_data| {
Decode::decode(&mut &raw_data.0[..]).expect("Stored data should decode correctly")
})
.unwrap_or_default();
Self { starting_dmq_mqc_head, starting_hrmp_mqc_heads }
}
}
#[async_trait::async_trait]
impl<R: Send + Sync + GenerateRandomness<u64>> InherentDataProvider
for MockValidationDataInherentDataProvider<R>
{
async fn provide_inherent_data(
&self,
inherent_data: &mut InherentData,
) -> Result<(), pezsp_inherents::Error> {
// Use the "sproof" (spoof proof) builder to build valid mock state root and proof.
let mut sproof_builder =
RelayStateSproofBuilder { para_id: self.para_id, ..Default::default() };
// Calculate the mocked relay block based on the current para block
let relay_parent_number =
self.relay_offset + self.relay_blocks_per_para_block * self.current_para_block;
sproof_builder.current_slot = Slot::from(relay_parent_number as u64);
sproof_builder.upgrade_go_ahead = self.upgrade_go_ahead;
// Process the downward messages and set up the correct head
let mut downward_messages = Vec::new();
let mut dmq_mqc = MessageQueueChain::new(self.xcm_config.starting_dmq_mqc_head);
for msg in &self.raw_downward_messages {
let wrapped = InboundDownwardMessage { sent_at: relay_parent_number, msg: msg.clone() };
dmq_mqc.extend_downward(&wrapped);
downward_messages.push(wrapped);
}
sproof_builder.dmq_mqc_head = Some(dmq_mqc.head());
// Process the hrmp messages and set up the correct heads
// Begin by collecting them into a Map
let mut horizontal_messages = BTreeMap::<ParaId, Vec<InboundHrmpMessage>>::new();
for (para_id, msg) in &self.raw_horizontal_messages {
let wrapped = InboundHrmpMessage { sent_at: relay_parent_number, data: msg.clone() };
horizontal_messages.entry(*para_id).or_default().push(wrapped);
}
// Now iterate again, updating the heads as we go
for (para_id, messages) in &horizontal_messages {
let mut channel_mqc = MessageQueueChain::new(
*self
.xcm_config
.starting_hrmp_mqc_heads
.get(para_id)
.unwrap_or(&relay_chain::Hash::default()),
);
for message in messages {
channel_mqc.extend_hrmp(message);
}
sproof_builder.upsert_inbound_channel(*para_id).mqc_head = Some(channel_mqc.head());
}
// Epoch is set equal to current para block / blocks per epoch
sproof_builder.current_epoch = if self.para_blocks_per_relay_epoch == 0 {
// do not divide by 0 => set epoch to para block number
self.current_para_block.into()
} else {
(self.current_para_block / self.para_blocks_per_relay_epoch).into()
};
// Randomness is set by randomness generator
sproof_builder.randomness =
self.relay_randomness_config.generate_randomness(self.current_para_block.into());
if let Some(key_values) = &self.additional_key_values {
sproof_builder.additional_key_values = key_values.clone()
}
// Inject current para block head, if any
sproof_builder.included_para_head = self.current_para_block_head.clone();
let (relay_parent_storage_root, proof) = sproof_builder.into_state_root_and_proof();
let teyrchain_inherent_data = TeyrchainInherentData {
validation_data: PersistedValidationData {
parent_head: Default::default(),
relay_parent_storage_root,
relay_parent_number,
max_pov_size: Default::default(),
},
downward_messages,
horizontal_messages,
relay_chain_state: proof,
relay_parent_descendants: Default::default(),
collator_peer_id: None,
};
teyrchain_inherent_data.provide_inherent_data(inherent_data).await
}
// Copied from the real implementation
async fn try_handle_error(
&self,
_: &pezsp_inherents::InherentIdentifier,
_: &[u8],
) -> Option<Result<(), pezsp_inherents::Error>> {
None
}
}