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;
use sp_runtime::traits::{Block as BlockT, Header as HeaderT};
const LOG_TARGET: &str = "consensus::common::parent_search";
#[derive(Debug)]
pub struct ParentSearchParams {
pub relay_parent: RelayHash,
pub para_id: ParaId,
pub ancestry_lookback: usize,
}
#[derive(PartialEq, Clone)]
pub struct ParentSearchResult<B: BlockT> {
pub included_header: B::Header,
pub best_parent_header: B::Header,
}
impl<B: BlockT> std::fmt::Debug for ParentSearchResult<B> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ParentSearchResult")
.field("included_number", &self.included_header.number())
.field("best_parent_hash", &self.best_parent_header.hash())
.field("best_parent_number", &self.best_parent_header.number())
.finish()
}
}
pub async fn find_parent_for_building<B: BlockT>(
params: ParentSearchParams,
backend: &impl Backend<B>,
relay_client: &impl RelayChainInterface,
) -> Result<Option<ParentSearchResult<B>>, RelayChainError> {
tracing::trace!(target: LOG_TARGET, "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(None);
};
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();
let Ok(Some(header)) = backend.blockchain().header(pending_hash) else {
tracing::warn!(
target: LOG_TARGET,
%pending_hash,
"Failed to get header for pending block.",
);
return Ok(None);
};
Some((header, pending_hash))
} else {
None
}
};
let (start_hash, start_header) = match &maybe_pending {
Some((pending_header, pending_hash)) => {
let route =
sp_blockchain::tree_route(backend.blockchain(), included_hash, *pending_hash)?;
if !route.retracted().is_empty() {
tracing::warn!(
target: LOG_TARGET,
"Included block not an ancestor of pending block. This should not happen."
);
return Ok(None);
}
(*pending_hash, pending_header.clone())
},
None => (included_hash, included_header.clone()),
};
let rp_ancestry =
build_relay_parent_ancestry(params.ancestry_lookback, params.relay_parent, relay_client)
.await?;
let best_parent_header =
find_deepest_valid_parent(start_hash, start_header, backend, &rp_ancestry);
Ok(Some(ParentSearchResult { included_header, best_parent_header }))
}
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) | Err(_) => {
tracing::warn!(
target: LOG_TARGET,
%included_hash,
"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)
}
fn find_deepest_valid_parent<Block: BlockT>(
start_hash: Block::Hash,
start_header: Block::Header,
backend: &impl Backend<Block>,
rp_ancestry: &[(RelayHash, RelayHash)],
) -> Block::Header {
let mut best = start_header;
let mut frontier: Vec<Block::Hash> =
backend.blockchain().children(start_hash).ok().into_iter().flatten().collect();
tracing::trace!(
target: LOG_TARGET,
?start_hash,
num_children = frontier.len(),
"Searching for deepest valid parent."
);
while let Some(hash) = frontier.pop() {
let Ok(Some(header)) = backend.blockchain().header(hash) else { continue };
if !is_relay_parent_in_ancestry::<Block>(&header, rp_ancestry) {
continue;
}
if header.number() > best.number() {
best = header.clone();
}
frontier.extend(backend.blockchain().children(hash).ok().into_iter().flatten());
}
best
}
fn is_relay_parent_in_ancestry<Block: BlockT>(
header: &Block::Header,
rp_ancestry: &[(RelayHash, RelayHash)],
) -> bool {
let digest = header.digest();
let relay_parent = cumulus_primitives_core::extract_relay_parent(digest);
let storage_root =
cumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(digest)
.map(|(r, _)| r);
rp_ancestry.iter().any(|(rp_hash, rp_storage_root)| {
relay_parent.map_or(false, |rp| *rp_hash == rp) ||
storage_root.map_or(false, |sr| *rp_storage_root == sr)
})
}