cumulus_client_consensus_common/
parent_search.rs1use codec::Decode;
19use polkadot_primitives::Hash as RelayHash;
20
21use cumulus_primitives_core::{
22 relay_chain::{BlockId as RBlockId, OccupiedCoreAssumption},
23 ParaId,
24};
25use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface};
26
27use sc_client_api::{Backend, HeaderBackend};
28
29use sp_blockchain::{Backend as BlockchainBackend, TreeRoute};
30
31use sp_runtime::traits::{Block as BlockT, Header as HeaderT};
32
33const PARENT_SEARCH_LOG_TARGET: &str = "consensus::common::find_potential_parents";
34
35#[derive(Debug)]
37pub struct ParentSearchParams {
38 pub relay_parent: RelayHash,
40 pub para_id: ParaId,
42 pub ancestry_lookback: usize,
45 pub max_depth: usize,
48 pub ignore_alternative_branches: bool,
51}
52
53#[derive(PartialEq)]
55pub struct PotentialParent<B: BlockT> {
56 pub hash: B::Hash,
58 pub header: B::Header,
60 pub depth: usize,
62 pub aligned_with_pending: bool,
65}
66
67impl<B: BlockT> std::fmt::Debug for PotentialParent<B> {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.debug_struct("PotentialParent")
70 .field("hash", &self.hash)
71 .field("depth", &self.depth)
72 .field("aligned_with_pending", &self.aligned_with_pending)
73 .field("number", &self.header.number())
74 .finish()
75 }
76}
77
78pub async fn find_potential_parents<B: BlockT>(
94 params: ParentSearchParams,
95 backend: &impl Backend<B>,
96 relay_client: &impl RelayChainInterface,
97) -> Result<Vec<PotentialParent<B>>, RelayChainError> {
98 tracing::trace!("Parent search parameters: {params:?}");
99 let Some((included_header, included_hash)) =
101 fetch_included_from_relay_chain(relay_client, backend, params.para_id, params.relay_parent)
102 .await?
103 else {
104 return Ok(Default::default())
105 };
106
107 let only_included = vec![PotentialParent {
108 hash: included_hash,
109 header: included_header.clone(),
110 depth: 0,
111 aligned_with_pending: true,
112 }];
113
114 if params.max_depth == 0 {
115 return Ok(only_included)
116 };
117
118 let maybe_pending = {
120 let pending_header = relay_client
124 .persisted_validation_data(
125 params.relay_parent,
126 params.para_id,
127 OccupiedCoreAssumption::Included,
128 )
129 .await?
130 .and_then(|p| B::Header::decode(&mut &p.parent_head.0[..]).ok())
131 .filter(|x| x.hash() != included_hash);
132
133 if let Some(header) = pending_header {
135 let pending_hash = header.hash();
136 match backend.blockchain().header(pending_hash) {
137 Ok(None) | Err(_) if params.ignore_alternative_branches => {
140 tracing::warn!(
141 target: PARENT_SEARCH_LOG_TARGET,
142 %pending_hash,
143 "Failed to get header for pending block.",
144 );
145 return Ok(Default::default())
146 },
147 Ok(Some(_)) => Some((header, pending_hash)),
148 _ => None,
149 }
150 } else {
151 None
152 }
153 };
154
155 let maybe_route_to_last_pending = maybe_pending
156 .as_ref()
157 .map(|(_, pending)| {
158 sp_blockchain::tree_route(backend.blockchain(), included_hash, *pending)
159 })
160 .transpose()?;
161
162 let (frontier, potential_parents) = match (
166 &maybe_pending,
167 params.ignore_alternative_branches,
168 &maybe_route_to_last_pending,
169 ) {
170 (Some((pending_header, pending_hash)), true, Some(ref route_to_pending)) => {
171 let mut potential_parents = only_included;
172
173 if !route_to_pending.retracted().is_empty() {
175 tracing::warn!(target: PARENT_SEARCH_LOG_TARGET, "Included block not an ancestor of pending block. This should not happen.");
176 return Ok(Default::default())
177 }
178
179 let num_parents_on_path =
182 route_to_pending.enacted().len().saturating_sub(1).min(params.max_depth);
183 for (num, block) in
184 route_to_pending.enacted().iter().take(num_parents_on_path).enumerate()
185 {
186 let Ok(Some(header)) = backend.blockchain().header(block.hash) else { continue };
187
188 potential_parents.push(PotentialParent {
189 hash: block.hash,
190 header,
191 depth: 1 + num,
192 aligned_with_pending: true,
193 });
194 }
195
196 (
199 vec![PotentialParent {
200 hash: *pending_hash,
201 header: pending_header.clone(),
202 depth: route_to_pending.enacted().len(),
203 aligned_with_pending: true,
204 }],
205 potential_parents,
206 )
207 },
208 _ => (only_included, Default::default()),
209 };
210
211 if potential_parents.len() > params.max_depth {
212 return Ok(potential_parents);
213 }
214
215 let rp_ancestry =
217 build_relay_parent_ancestry(params.ancestry_lookback, params.relay_parent, relay_client)
218 .await?;
219
220 Ok(search_child_branches_for_parents(
221 frontier,
222 maybe_route_to_last_pending,
223 included_header,
224 maybe_pending.map(|(_, hash)| hash),
225 backend,
226 params.max_depth,
227 params.ignore_alternative_branches,
228 rp_ancestry,
229 potential_parents,
230 ))
231}
232
233async fn fetch_included_from_relay_chain<B: BlockT>(
235 relay_client: &impl RelayChainInterface,
236 backend: &impl Backend<B>,
237 para_id: ParaId,
238 relay_parent: RelayHash,
239) -> Result<Option<(B::Header, B::Hash)>, RelayChainError> {
240 let included_header = relay_client
244 .persisted_validation_data(relay_parent, para_id, OccupiedCoreAssumption::TimedOut)
245 .await?;
246 let included_header = match included_header {
247 Some(pvd) => pvd.parent_head,
248 None => return Ok(None), };
250
251 let included_header = match B::Header::decode(&mut &included_header.0[..]).ok() {
252 None => return Ok(None),
253 Some(x) => x,
254 };
255
256 let included_hash = included_header.hash();
257 match backend.blockchain().header(included_hash) {
259 Ok(None) => {
260 tracing::warn!(
261 target: PARENT_SEARCH_LOG_TARGET,
262 %included_hash,
263 "Failed to get header for included block.",
264 );
265 return Ok(None)
266 },
267 Err(e) => {
268 tracing::warn!(
269 target: PARENT_SEARCH_LOG_TARGET,
270 %included_hash,
271 %e,
272 "Failed to get header for included block.",
273 );
274 return Ok(None)
275 },
276 _ => {},
277 };
278
279 Ok(Some((included_header, included_hash)))
280}
281
282async fn build_relay_parent_ancestry(
292 ancestry_lookback: usize,
293 relay_parent: RelayHash,
294 relay_client: &impl RelayChainInterface,
295) -> Result<Vec<(RelayHash, RelayHash)>, RelayChainError> {
296 let mut ancestry = Vec::with_capacity(ancestry_lookback + 1);
297 let mut current_rp = relay_parent;
298 let mut required_session = None;
299 while ancestry.len() <= ancestry_lookback {
300 let Some(header) = relay_client.header(RBlockId::hash(current_rp)).await? else { break };
301
302 let session = relay_client.session_index_for_child(current_rp).await?;
303 if required_session.get_or_insert(session) != &session {
304 break;
306 }
307
308 ancestry.push((current_rp, *header.state_root()));
309 current_rp = *header.parent_hash();
310
311 if header.number == 1 {
313 break
314 }
315 }
316 Ok(ancestry)
317}
318
319pub fn search_child_branches_for_parents<Block: BlockT>(
321 mut frontier: Vec<PotentialParent<Block>>,
322 maybe_route_to_last_pending: Option<TreeRoute<Block>>,
323 included_header: Block::Header,
324 pending_hash: Option<Block::Hash>,
325 backend: &impl Backend<Block>,
326 max_depth: usize,
327 ignore_alternative_branches: bool,
328 rp_ancestry: Vec<(RelayHash, RelayHash)>,
329 mut potential_parents: Vec<PotentialParent<Block>>,
330) -> Vec<PotentialParent<Block>> {
331 let included_hash = included_header.hash();
332 let is_hash_in_ancestry = |hash| rp_ancestry.iter().any(|x| x.0 == hash);
333 let is_root_in_ancestry = |root| rp_ancestry.iter().any(|x| x.1 == root);
334
335 let pending_distance = maybe_route_to_last_pending.as_ref().map(|route| route.enacted().len());
338
339 let is_child_pending = |hash| {
341 maybe_route_to_last_pending
342 .as_ref()
343 .map_or(true, |route| route.enacted().iter().any(|x| x.hash == hash))
344 };
345
346 tracing::trace!(
347 target: PARENT_SEARCH_LOG_TARGET,
348 ?included_hash,
349 included_num = ?included_header.number(),
350 ?pending_hash ,
351 ?rp_ancestry,
352 "Searching relay chain ancestry."
353 );
354 while let Some(entry) = frontier.pop() {
355 let is_pending = pending_hash.as_ref().map_or(false, |h| &entry.hash == h);
356 let is_included = included_hash == entry.hash;
357
358 let is_potential = is_pending || is_included || {
362 let digest = entry.header.digest();
363 let is_hash_in_ancestry_check = cumulus_primitives_core::extract_relay_parent(digest)
364 .map_or(false, is_hash_in_ancestry);
365 let is_root_in_ancestry_check =
366 cumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(digest)
367 .map(|(r, _n)| r)
368 .map_or(false, is_root_in_ancestry);
369
370 is_hash_in_ancestry_check || is_root_in_ancestry_check
371 };
372
373 let parent_aligned_with_pending = entry.aligned_with_pending;
374 let child_depth = entry.depth + 1;
375 let hash = entry.hash;
376
377 tracing::trace!(
378 target: PARENT_SEARCH_LOG_TARGET,
379 ?hash,
380 is_potential,
381 is_pending,
382 is_included,
383 "Checking potential parent."
384 );
385
386 if is_potential {
387 potential_parents.push(entry);
388 }
389
390 if !is_potential || child_depth > max_depth {
391 continue
392 }
393
394 for child in backend.blockchain().children(hash).ok().into_iter().flatten() {
396 tracing::trace!(target: PARENT_SEARCH_LOG_TARGET, ?child, child_depth, ?pending_distance, "Looking at child.");
397
398 let aligned_with_pending = parent_aligned_with_pending &&
399 (pending_distance.map_or(true, |dist| child_depth > dist) ||
400 is_child_pending(child));
401
402 if ignore_alternative_branches && !aligned_with_pending {
403 tracing::trace!(target: PARENT_SEARCH_LOG_TARGET, ?child, "Child is not aligned with pending block.");
404 continue
405 }
406
407 let Ok(Some(header)) = backend.blockchain().header(child) else { continue };
408
409 frontier.push(PotentialParent {
410 hash: child,
411 header,
412 depth: child_depth,
413 aligned_with_pending,
414 });
415 }
416 }
417
418 potential_parents
419}