cumulus_client_consensus_common/
parent_search.rs1use codec::Decode;
19use cumulus_primitives_core::{
20 relay_chain::{BlockId as RelayBlockId, OccupiedCoreAssumption},
21 ParaId,
22};
23use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface, RelayChainResult};
24use polkadot_primitives::{Block as RelayBlock, Hash as RelayHash, DEFAULT_SCHEDULING_LOOKAHEAD};
25use sc_client_api::{Backend, HeaderBackend};
26use sc_consensus_babe::contains_epoch_change;
27use sp_blockchain::Backend as BlockchainBackend;
28use sp_runtime::traits::{Block as BlockT, Header as HeaderT};
29use std::future::Future;
30
31const LOG_TARGET: &str = "consensus::common::parent_search";
32
33#[derive(Clone, Debug)]
34pub enum ParentSearchParams {
35 V2 {
37 scheduling_parent: RelayHash,
40 },
41 V3 {
43 scheduling_parent: RelayHash,
45 },
46}
47
48impl ParentSearchParams {
49 pub fn new(v3_enabled: bool, scheduling_parent: RelayHash, relay_parent: RelayHash) -> Self {
52 if v3_enabled {
53 Self::V3 { scheduling_parent }
54 } else {
55 Self::V2 { scheduling_parent: relay_parent }
56 }
57 }
58
59 fn scheduling_parent(&self) -> &RelayHash {
60 match self {
61 ParentSearchParams::V2 { scheduling_parent } => scheduling_parent,
62 ParentSearchParams::V3 { scheduling_parent } => scheduling_parent,
63 }
64 }
65}
66
67#[derive(PartialEq, Clone)]
69pub struct ParentSearchResult<Block: BlockT> {
70 pub included_at_scheduling: Block::Header,
72 pub best_parent_header: Block::Header,
74 pub resubmittable_segment: Vec<Block::Header>,
76}
77
78pub(crate) fn trim_best_parent_to_filter<Block: BlockT>(
82 mut result: ParentSearchResult<Block>,
83 get_header: impl Fn(Block::Hash) -> Option<Block::Header>,
84 filter_parent: impl Fn(&Block::Header) -> bool,
85) -> ParentSearchResult<Block> {
86 while !filter_parent(&result.best_parent_header) {
87 let parent_hash = *result.best_parent_header.parent_hash();
88 result.resubmittable_segment.pop();
89 let parent =
92 result.resubmittable_segment.last().cloned().or_else(|| get_header(parent_hash));
93 match parent {
94 Some(header) => {
95 result.best_parent_header = header;
96 if parent_hash == result.included_at_scheduling.hash() {
97 break;
98 }
99 },
100 None => {
101 result.best_parent_header = result.included_at_scheduling.clone();
102 result.resubmittable_segment.clear();
103 break;
104 },
105 }
106 }
107 result
108}
109
110impl<B: BlockT> std::fmt::Debug for ParentSearchResult<B> {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 f.debug_struct("ParentSearchResult")
113 .field("included_at_scheduling_number", &self.included_at_scheduling.number())
114 .field("best_parent_hash", &self.best_parent_header.hash())
115 .field("best_parent_number", &self.best_parent_header.number())
116 .field("resubmittable_segment_len", &self.resubmittable_segment.len())
117 .finish()
118 }
119}
120
121fn get_para_header<Block: BlockT>(
122 backend: &impl Backend<Block>,
123 hash: Block::Hash,
124) -> Option<Block::Header> {
125 let Ok(Some(header)) = backend.blockchain().header(hash) else {
126 tracing::warn!(
127 target: LOG_TARGET,
128 %hash,
129 "Failed to get header for para block.",
130 );
131 return None;
132 };
133
134 Some(header)
135}
136
137async fn fetch_pvd_header<Block: BlockT>(
138 relay_client: &impl RelayChainInterface,
139 at: RelayHash,
140 para_id: ParaId,
141 occupied_core_assumption: OccupiedCoreAssumption,
142) -> RelayChainResult<Option<Block::Header>> {
143 let maybe_header = relay_client
144 .persisted_validation_data(at, para_id, occupied_core_assumption)
145 .await?
146 .and_then(|pvd| Block::Header::decode(&mut &pvd.parent_head.0[..]).ok());
147
148 Ok(maybe_header)
149}
150
151pub async fn fetch_included_from_relay_chain<B: BlockT>(
153 relay_client: &impl RelayChainInterface,
154 backend: &impl Backend<B>,
155 at: RelayHash,
156 para_id: ParaId,
157) -> Result<Option<(B::Header, B::Hash)>, RelayChainError> {
158 let Some(included_header) =
162 fetch_pvd_header::<B>(relay_client, at, para_id, OccupiedCoreAssumption::TimedOut).await?
163 else {
164 return Ok(None);
165 };
166
167 let included_hash = included_header.hash();
168 let Some(included_header) = get_para_header(backend, included_hash) else {
170 return Ok(None);
171 };
172 Ok(Some((included_header, included_hash)))
173}
174
175async fn build_relay_parent_ancestry(
185 relay_client: &impl RelayChainInterface,
186 relay_parent: RelayHash,
187 ancestry_lookback: usize,
188) -> Result<Vec<(RelayHash, RelayHash)>, RelayChainError> {
189 let mut ancestry = Vec::with_capacity(ancestry_lookback + 1);
190 let mut current_rp = relay_parent;
191 while ancestry.len() <= ancestry_lookback {
192 let Some(header) = relay_client.header(RelayBlockId::hash(current_rp)).await? else {
193 tracing::warn!(
194 target: LOG_TARGET,
195 ?current_rp,
196 "Relay chain header missing while walking the allowed ancestry.",
197 );
198 break;
199 };
200
201 ancestry.push((current_rp, *header.state_root()));
202 current_rp = *header.parent_hash();
203
204 if contains_epoch_change::<RelayBlock>(&header) {
206 break;
207 }
208
209 if header.number == 1 {
211 break;
212 }
213 }
214 Ok(ancestry)
215}
216
217fn is_relay_parent_in_ancestry<Block: BlockT>(
219 header: &Block::Header,
220 rp_ancestry: &[(RelayHash, RelayHash)],
221) -> bool {
222 let digest = header.digest();
223 let relay_parent = cumulus_primitives_core::extract_relay_parent(digest);
224 let storage_root =
225 cumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(digest)
226 .map(|(storage_root, _)| storage_root);
227 if relay_parent.is_none() && storage_root.is_none() {
228 return false;
229 }
230
231 rp_ancestry.iter().any(|(rp_hash, rp_storage_root)| {
232 Some(*rp_hash) == relay_parent || Some(*rp_storage_root) == storage_root
233 })
234}
235
236async fn find_deepest_valid_parent<Block: BlockT, Fut: Future<Output = bool>>(
242 backend: &impl Backend<Block>,
243 start_header: Block::Header,
244 start_hash: Block::Hash,
245 is_valid: impl Fn(&Block::Header) -> Fut,
246) -> Block::Header {
247 let mut best = start_header;
248
249 let mut frontier: Vec<Block::Hash> =
250 backend.blockchain().children(start_hash).ok().into_iter().flatten().collect();
251
252 tracing::trace!(
253 target: LOG_TARGET,
254 ?start_hash,
255 num_children = frontier.len(),
256 "Searching for deepest valid parent."
257 );
258
259 while let Some(hash) = frontier.pop() {
260 let Ok(Some(header)) = backend.blockchain().header(hash) else { continue };
261
262 if !is_valid(&header).await {
263 continue;
264 }
265
266 if header.number() > best.number() {
268 best = header;
269 }
270
271 frontier.extend(backend.blockchain().children(hash).ok().into_iter().flatten());
272 }
273
274 best
275}
276
277fn reconstruct_resubmittable_segment<Block: BlockT>(
279 backend: &impl Backend<Block>,
280 best: &Block::Header,
281 start_hash: Block::Hash,
282) -> Vec<Block::Header> {
283 let mut ancestry = Vec::new();
284 let mut current = best.clone();
285 while current.hash() != start_hash {
286 let parent_hash = *current.parent_hash();
287 ancestry.push(current);
288 match backend.blockchain().header(parent_hash) {
289 Ok(Some(parent)) => current = parent,
290 _ => break,
291 }
292 }
293 ancestry.reverse();
294 ancestry
295}
296
297async fn get_relay_parent<Block: BlockT>(
298 relay_client: &impl RelayChainInterface,
299 header: &Block::Header,
300) -> RelayChainResult<Option<RelayHash>> {
301 let digest = header.digest();
302
303 if let Some(relay_parent) = cumulus_primitives_core::extract_relay_parent(digest) {
304 return Ok(Some(relay_parent));
305 }
306
307 if let Some((storage_root, number)) =
308 cumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(digest)
309 {
310 let Some(relay_parent_header) = relay_client.header(RelayBlockId::Number(number)).await?
311 else {
312 return Ok(None);
313 };
314 if relay_parent_header.state_root != storage_root {
315 return Ok(None);
316 }
317 return Ok(Some(relay_parent_header.hash()));
318 }
319
320 Ok(None)
321}
322
323async fn has_ancestor_relay_parent_info<Block: BlockT>(
326 relay_client: &impl RelayChainInterface,
327 scheduling_parent: RelayHash,
328 header: &Block::Header,
329) -> RelayChainResult<bool> {
330 let Some(relay_parent) = get_relay_parent::<Block>(relay_client, header).await? else {
331 return Ok(false);
332 };
333
334 if relay_parent == scheduling_parent {
335 return Ok(true);
336 }
337
338 let relay_parent_session = relay_client.session_index_for_child(relay_parent).await?;
339 let maybe_info = relay_client
340 .ancestor_relay_parent_info(scheduling_parent, relay_parent_session, relay_parent)
341 .await?;
342 Ok(maybe_info.is_some())
343}
344
345pub async fn find_parent_for_building<Block: BlockT>(
355 relay_client: &impl RelayChainInterface,
356 backend: &impl Backend<Block>,
357 para_id: ParaId,
358 params: ParentSearchParams,
359 filter_parent: impl Fn(&Block::Header) -> bool,
360) -> RelayChainResult<Option<ParentSearchResult<Block>>> {
361 tracing::trace!(
362 target: LOG_TARGET,
363 ?para_id,
364 ?params,
365 "Parent search"
366 );
367
368 let scheduling_parent = *params.scheduling_parent();
369 let Some((included_header, included_hash)) =
370 fetch_included_from_relay_chain(relay_client, backend, scheduling_parent, para_id).await?
371 else {
372 return Ok(None);
373 };
374
375 let maybe_pending = {
377 let maybe_header = fetch_pvd_header::<Block>(
381 relay_client,
382 scheduling_parent,
383 para_id,
384 OccupiedCoreAssumption::Included,
385 )
386 .await?
387 .filter(|header| header.hash() != included_hash);
388
389 if let Some(header) = maybe_header {
391 let hash = header.hash();
392 let Some(header) = get_para_header(backend, hash) else {
393 return Ok(None);
394 };
395 Some((header, hash))
396 } else {
397 None
398 }
399 };
400 let (start_header, start_hash) =
402 maybe_pending.unwrap_or((included_header.clone(), included_hash));
403
404 let result = match params {
405 ParentSearchParams::V2 { scheduling_parent: relay_parent } => {
406 let ancestry_lookback = relay_client
407 .scheduling_lookahead(relay_parent)
408 .await
409 .unwrap_or(DEFAULT_SCHEDULING_LOOKAHEAD)
410 .saturating_sub(1) as usize;
411 let rp_ancestry =
413 build_relay_parent_ancestry(relay_client, relay_parent, ancestry_lookback).await?;
414
415 let best_parent_header =
417 find_deepest_valid_parent(backend, start_header, start_hash, |header| {
418 let is_valid = is_relay_parent_in_ancestry::<Block>(header, &rp_ancestry);
419 async move { is_valid }
420 })
421 .await;
422
423 ParentSearchResult {
424 included_at_scheduling: included_header,
425 best_parent_header,
426 resubmittable_segment: Vec::new(),
427 }
428 },
429 ParentSearchParams::V3 { scheduling_parent } => {
430 let best_parent_header =
431 find_deepest_valid_parent(backend, start_header, start_hash, |header| {
432 let header = header.clone();
433 async move {
434 has_ancestor_relay_parent_info::<Block>(
435 relay_client,
436 scheduling_parent,
437 &header,
438 )
439 .await
440 .unwrap_or(false)
441 }
442 })
443 .await;
444
445 let resubmittable_segment =
447 reconstruct_resubmittable_segment(backend, &best_parent_header, start_hash);
448
449 ParentSearchResult {
450 included_at_scheduling: included_header,
451 best_parent_header,
452 resubmittable_segment,
453 }
454 },
455 };
456
457 Ok(Some(trim_best_parent_to_filter(
460 result,
461 |hash| backend.blockchain().header(hash).ok().flatten(),
462 filter_parent,
463 )))
464}