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:
@@ -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",
|
||||
]
|
||||
@@ -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(¶_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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(¶_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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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, ×tamp);
|
||||
}
|
||||
|
||||
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,
|
||||
¶ms.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,
|
||||
¶ms.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,
|
||||
¶ms.relay_client,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(value) => value,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let para_client = &*params.para_client;
|
||||
let keystore = ¶ms.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,
|
||||
¶ms.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(), ¶_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(), ¶_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(), ¶_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(), ¶_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(), ¶_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(), ¶_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(), ¶_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;
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user