feat: Rebrand Polkadot/Substrate references to PezkuwiChain

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

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

This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
@@ -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.",
);
}
}