// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute
// 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 .
use codec::Decode;
use pezkuwi_primitives::Hash as RelayHash;
use pezcumulus_primitives_core::{
relay_chain::{BlockId as RBlockId, OccupiedCoreAssumption},
ParaId,
};
use pezcumulus_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 {
/// 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 std::fmt::Debug for PotentialParent {
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(
params: ParentSearchParams,
backend: &impl Backend,
relay_client: &impl RelayChainInterface,
) -> Result>, 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(
relay_client: &impl RelayChainInterface,
backend: &impl Backend,
para_id: ParaId,
relay_parent: RelayHash,
) -> Result