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}
75
76impl<B: BlockT> std::fmt::Debug for ParentSearchResult<B> {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 f.debug_struct("ParentSearchResult")
79 .field("included_at_scheduling_number", &self.included_at_scheduling.number())
80 .field("best_parent_hash", &self.best_parent_header.hash())
81 .field("best_parent_number", &self.best_parent_header.number())
82 .finish()
83 }
84}
85
86fn get_para_header<Block: BlockT>(
87 backend: &impl Backend<Block>,
88 hash: Block::Hash,
89) -> Option<Block::Header> {
90 let Ok(Some(header)) = backend.blockchain().header(hash) else {
91 tracing::warn!(
92 target: LOG_TARGET,
93 %hash,
94 "Failed to get header for para block.",
95 );
96 return None;
97 };
98
99 Some(header)
100}
101
102async fn fetch_pvd_header<Block: BlockT>(
103 relay_client: &impl RelayChainInterface,
104 at: RelayHash,
105 para_id: ParaId,
106 occupied_core_assumption: OccupiedCoreAssumption,
107) -> RelayChainResult<Option<Block::Header>> {
108 let maybe_header = relay_client
109 .persisted_validation_data(at, para_id, occupied_core_assumption)
110 .await?
111 .and_then(|pvd| Block::Header::decode(&mut &pvd.parent_head.0[..]).ok());
112
113 Ok(maybe_header)
114}
115
116pub async fn fetch_included_from_relay_chain<B: BlockT>(
118 relay_client: &impl RelayChainInterface,
119 backend: &impl Backend<B>,
120 at: RelayHash,
121 para_id: ParaId,
122) -> Result<Option<(B::Header, B::Hash)>, RelayChainError> {
123 let Some(included_header) =
127 fetch_pvd_header::<B>(relay_client, at, para_id, OccupiedCoreAssumption::TimedOut).await?
128 else {
129 return Ok(None);
130 };
131
132 let included_hash = included_header.hash();
133 let Some(included_header) = get_para_header(backend, included_hash) else {
135 return Ok(None);
136 };
137 Ok(Some((included_header, included_hash)))
138}
139
140async fn build_relay_parent_ancestry(
150 relay_client: &impl RelayChainInterface,
151 relay_parent: RelayHash,
152 ancestry_lookback: usize,
153) -> Result<Vec<(RelayHash, RelayHash)>, RelayChainError> {
154 let mut ancestry = Vec::with_capacity(ancestry_lookback + 1);
155 let mut current_rp = relay_parent;
156 while ancestry.len() <= ancestry_lookback {
157 let Some(header) = relay_client.header(RelayBlockId::hash(current_rp)).await? else {
158 tracing::warn!(
159 target: LOG_TARGET,
160 ?current_rp,
161 "Relay chain header missing while walking the allowed ancestry.",
162 );
163 break;
164 };
165
166 ancestry.push((current_rp, *header.state_root()));
167 current_rp = *header.parent_hash();
168
169 if contains_epoch_change::<RelayBlock>(&header) {
171 break;
172 }
173
174 if header.number == 1 {
176 break;
177 }
178 }
179 Ok(ancestry)
180}
181
182fn is_relay_parent_in_ancestry<Block: BlockT>(
184 header: &Block::Header,
185 rp_ancestry: &[(RelayHash, RelayHash)],
186) -> bool {
187 let digest = header.digest();
188 let relay_parent = cumulus_primitives_core::extract_relay_parent(digest);
189 let storage_root =
190 cumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(digest)
191 .map(|(storage_root, _)| storage_root);
192 if relay_parent.is_none() && storage_root.is_none() {
193 return false;
194 }
195
196 rp_ancestry.iter().any(|(rp_hash, rp_storage_root)| {
197 Some(*rp_hash) == relay_parent || Some(*rp_storage_root) == storage_root
198 })
199}
200
201async fn find_deepest_valid_parent<Block: BlockT, Fut: Future<Output = bool>>(
207 backend: &impl Backend<Block>,
208 start_header: Block::Header,
209 start_hash: Block::Hash,
210 is_valid: impl Fn(&Block::Header) -> Fut,
211) -> Block::Header {
212 let mut best = start_header;
213
214 let mut frontier: Vec<Block::Hash> =
215 backend.blockchain().children(start_hash).ok().into_iter().flatten().collect();
216
217 tracing::trace!(
218 target: LOG_TARGET,
219 ?start_hash,
220 num_children = frontier.len(),
221 "Searching for deepest valid parent."
222 );
223
224 while let Some(hash) = frontier.pop() {
225 let Ok(Some(header)) = backend.blockchain().header(hash) else { continue };
226
227 if !is_valid(&header).await {
228 continue;
229 }
230
231 if header.number() > best.number() {
233 best = header;
234 }
235
236 frontier.extend(backend.blockchain().children(hash).ok().into_iter().flatten());
237 }
238
239 best
240}
241
242async fn get_relay_parent<Block: BlockT>(
243 relay_client: &impl RelayChainInterface,
244 header: &Block::Header,
245) -> RelayChainResult<Option<RelayHash>> {
246 let digest = header.digest();
247
248 if let Some(relay_parent) = cumulus_primitives_core::extract_relay_parent(digest) {
249 return Ok(Some(relay_parent));
250 }
251
252 if let Some((storage_root, number)) =
253 cumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(digest)
254 {
255 let Some(relay_parent_header) = relay_client.header(RelayBlockId::Number(number)).await?
256 else {
257 return Ok(None);
258 };
259 if relay_parent_header.state_root != storage_root {
260 return Ok(None);
261 }
262 return Ok(Some(relay_parent_header.hash()));
263 }
264
265 Ok(None)
266}
267
268async fn has_ancestor_relay_parent_info<Block: BlockT>(
271 relay_client: &impl RelayChainInterface,
272 scheduling_parent: RelayHash,
273 header: &Block::Header,
274) -> RelayChainResult<bool> {
275 let Some(relay_parent) = get_relay_parent::<Block>(relay_client, header).await? else {
276 return Ok(false);
277 };
278
279 if relay_parent == scheduling_parent {
280 return Ok(true);
281 }
282
283 let relay_parent_session = relay_client.session_index_for_child(relay_parent).await?;
284 let maybe_info = relay_client
285 .ancestor_relay_parent_info(scheduling_parent, relay_parent_session, relay_parent)
286 .await?;
287 Ok(maybe_info.is_some())
288}
289
290pub async fn find_parent_for_building<Block: BlockT>(
300 relay_client: &impl RelayChainInterface,
301 backend: &impl Backend<Block>,
302 para_id: ParaId,
303 params: ParentSearchParams,
304) -> RelayChainResult<Option<ParentSearchResult<Block>>> {
305 tracing::trace!(
306 target: LOG_TARGET,
307 ?para_id,
308 ?params,
309 "Parent search"
310 );
311
312 let scheduling_parent = *params.scheduling_parent();
313 let Some((included_header, included_hash)) =
314 fetch_included_from_relay_chain(relay_client, backend, scheduling_parent, para_id).await?
315 else {
316 return Ok(None);
317 };
318
319 let maybe_pending = {
321 let maybe_header = fetch_pvd_header::<Block>(
325 relay_client,
326 scheduling_parent,
327 para_id,
328 OccupiedCoreAssumption::Included,
329 )
330 .await?
331 .filter(|header| header.hash() != included_hash);
332
333 if let Some(header) = maybe_header {
335 let hash = header.hash();
336 let Some(header) = get_para_header(backend, hash) else {
337 return Ok(None);
338 };
339 Some((header, hash))
340 } else {
341 None
342 }
343 };
344 let (start_header, start_hash) =
346 maybe_pending.unwrap_or((included_header.clone(), included_hash));
347
348 let best_parent_header = match params {
349 ParentSearchParams::V2 { scheduling_parent: relay_parent } => {
350 let ancestry_lookback = relay_client
351 .scheduling_lookahead(relay_parent)
352 .await
353 .unwrap_or(DEFAULT_SCHEDULING_LOOKAHEAD)
354 .saturating_sub(1) as usize;
355 let rp_ancestry =
357 build_relay_parent_ancestry(relay_client, relay_parent, ancestry_lookback).await?;
358
359 find_deepest_valid_parent(backend, start_header, start_hash, |header| {
361 let is_valid = is_relay_parent_in_ancestry::<Block>(header, &rp_ancestry);
362 async move { is_valid }
363 })
364 .await
365 },
366 ParentSearchParams::V3 { scheduling_parent } => {
367 find_deepest_valid_parent(backend, start_header, start_hash, |header| {
368 let header = header.clone();
369 async move {
370 has_ancestor_relay_parent_info::<Block>(
371 relay_client,
372 scheduling_parent,
373 &header,
374 )
375 .await
376 .unwrap_or(false)
377 }
378 })
379 .await
380 },
381 };
382
383 Ok(Some(ParentSearchResult { included_at_scheduling: included_header, best_parent_header }))
384}