use codec::Decode;
use polkadot_primitives::Hash as RelayHash;
use cumulus_primitives_core::{
relay_chain::{BlockId as RBlockId, OccupiedCoreAssumption},
ParaId,
};
use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface};
use sc_client_api::{Backend, HeaderBackend};
use sp_blockchain::{Backend as BlockchainBackend, TreeRoute};
use sp_runtime::traits::{Block as BlockT, Header as HeaderT};
const PARENT_SEARCH_LOG_TARGET: &str = "consensus::common::find_potential_parents";
#[derive(Debug)]
pub struct ParentSearchParams {
pub relay_parent: RelayHash,
pub para_id: ParaId,
pub ancestry_lookback: usize,
pub max_depth: usize,
pub ignore_alternative_branches: bool,
}
#[derive(PartialEq)]
pub struct PotentialParent<B: BlockT> {
pub hash: B::Hash,
pub header: B::Header,
pub depth: usize,
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()
}
}
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:?}");
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)
};
let maybe_pending = {
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 let Some(header) = pending_header {
let pending_hash = header.hash();
match backend.blockchain().header(pending_hash) {
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)| {
sp_blockchain::tree_route(backend.blockchain(), included_hash, *pending)
})
.transpose()?;
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;
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())
}
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,
});
}
(
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);
}
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,
))
}
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> {
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), };
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();
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)))
}
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 {
break;
}
ancestry.push((current_rp, *header.state_root()));
current_rp = *header.parent_hash();
if header.number == 1 {
break
}
}
Ok(ancestry)
}
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);
let pending_distance = maybe_route_to_last_pending.as_ref().map(|route| route.enacted().len());
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;
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
}
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
}