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 fn scheduling_parent(&self) -> &RelayHash {
50 match self {
51 ParentSearchParams::V2 { scheduling_parent } => scheduling_parent,
52 ParentSearchParams::V3 { scheduling_parent } => scheduling_parent,
53 }
54 }
55}
56
57#[derive(PartialEq, Clone)]
59pub struct ParentSearchResult<Block: BlockT> {
60 pub included_at_scheduling: Block::Header,
62 pub best_parent_header: Block::Header,
64}
65
66impl<B: BlockT> std::fmt::Debug for ParentSearchResult<B> {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 f.debug_struct("ParentSearchResult")
69 .field("included_at_scheduling_number", &self.included_at_scheduling.number())
70 .field("best_parent_hash", &self.best_parent_header.hash())
71 .field("best_parent_number", &self.best_parent_header.number())
72 .finish()
73 }
74}
75
76fn get_para_header<Block: BlockT>(
77 backend: &impl Backend<Block>,
78 hash: Block::Hash,
79) -> Option<Block::Header> {
80 let Ok(Some(header)) = backend.blockchain().header(hash) else {
81 tracing::warn!(
82 target: LOG_TARGET,
83 %hash,
84 "Failed to get header for para block.",
85 );
86 return None;
87 };
88
89 Some(header)
90}
91
92async fn fetch_pvd_header<Block: BlockT>(
93 relay_client: &impl RelayChainInterface,
94 at: RelayHash,
95 para_id: ParaId,
96 occupied_core_assumption: OccupiedCoreAssumption,
97) -> RelayChainResult<Option<Block::Header>> {
98 let maybe_header = relay_client
99 .persisted_validation_data(at, para_id, occupied_core_assumption)
100 .await?
101 .and_then(|pvd| Block::Header::decode(&mut &pvd.parent_head.0[..]).ok());
102
103 Ok(maybe_header)
104}
105
106pub async fn fetch_included_from_relay_chain<B: BlockT>(
108 relay_client: &impl RelayChainInterface,
109 backend: &impl Backend<B>,
110 at: RelayHash,
111 para_id: ParaId,
112) -> Result<Option<(B::Header, B::Hash)>, RelayChainError> {
113 let Some(included_header) =
117 fetch_pvd_header::<B>(relay_client, at, para_id, OccupiedCoreAssumption::TimedOut).await?
118 else {
119 return Ok(None);
120 };
121
122 let included_hash = included_header.hash();
123 let Some(included_header) = get_para_header(backend, included_hash) else {
125 return Ok(None);
126 };
127 Ok(Some((included_header, included_hash)))
128}
129
130async fn build_relay_parent_ancestry(
140 relay_client: &impl RelayChainInterface,
141 relay_parent: RelayHash,
142 ancestry_lookback: usize,
143) -> Result<Vec<(RelayHash, RelayHash)>, RelayChainError> {
144 let mut ancestry = Vec::with_capacity(ancestry_lookback + 1);
145 let mut current_rp = relay_parent;
146 while ancestry.len() <= ancestry_lookback {
147 let Some(header) = relay_client.header(RelayBlockId::hash(current_rp)).await? else {
148 break;
149 };
150
151 ancestry.push((current_rp, *header.state_root()));
152 current_rp = *header.parent_hash();
153
154 if contains_epoch_change::<RelayBlock>(&header) {
156 break;
157 }
158
159 if header.number == 1 {
161 break;
162 }
163 }
164 Ok(ancestry)
165}
166
167fn is_relay_parent_in_ancestry<Block: BlockT>(
169 header: &Block::Header,
170 rp_ancestry: &[(RelayHash, RelayHash)],
171) -> bool {
172 let digest = header.digest();
173 let relay_parent = cumulus_primitives_core::extract_relay_parent(digest);
174 let storage_root =
175 cumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(digest)
176 .map(|(storage_root, _)| storage_root);
177 if relay_parent.is_none() && storage_root.is_none() {
178 return false;
179 }
180
181 rp_ancestry.iter().any(|(rp_hash, rp_storage_root)| {
182 Some(*rp_hash) == relay_parent || Some(*rp_storage_root) == storage_root
183 })
184}
185
186async fn find_deepest_valid_parent<Block: BlockT, Fut: Future<Output = bool>>(
192 backend: &impl Backend<Block>,
193 start_header: Block::Header,
194 start_hash: Block::Hash,
195 is_valid: impl Fn(&Block::Header) -> Fut,
196) -> Block::Header {
197 let mut best = start_header;
198
199 let mut frontier: Vec<Block::Hash> =
200 backend.blockchain().children(start_hash).ok().into_iter().flatten().collect();
201
202 tracing::trace!(
203 target: LOG_TARGET,
204 ?start_hash,
205 num_children = frontier.len(),
206 "Searching for deepest valid parent."
207 );
208
209 while let Some(hash) = frontier.pop() {
210 let Ok(Some(header)) = backend.blockchain().header(hash) else { continue };
211
212 if !is_valid(&header).await {
213 continue;
214 }
215
216 if header.number() > best.number() {
218 best = header;
219 }
220
221 frontier.extend(backend.blockchain().children(hash).ok().into_iter().flatten());
222 }
223
224 best
225}
226
227async fn get_relay_parent<Block: BlockT>(
228 relay_client: &impl RelayChainInterface,
229 header: &Block::Header,
230) -> RelayChainResult<Option<RelayHash>> {
231 let digest = header.digest();
232
233 if let Some(relay_parent) = cumulus_primitives_core::extract_relay_parent(digest) {
234 return Ok(Some(relay_parent));
235 }
236
237 if let Some((storage_root, number)) =
238 cumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(digest)
239 {
240 let Some(relay_parent_header) = relay_client.header(RelayBlockId::Number(number)).await?
241 else {
242 return Ok(None);
243 };
244 if relay_parent_header.state_root != storage_root {
245 return Ok(None);
246 }
247 return Ok(Some(relay_parent_header.hash()));
248 }
249
250 Ok(None)
251}
252
253async fn has_ancestor_relay_parent_info<Block: BlockT>(
254 relay_client: &impl RelayChainInterface,
255 scheduling_parent: RelayHash,
256 header: &Block::Header,
257) -> RelayChainResult<bool> {
258 let Some(relay_parent) = get_relay_parent::<Block>(relay_client, header).await? else {
259 return Ok(false);
260 };
261
262 if relay_parent == scheduling_parent {
263 return Ok(true);
264 }
265
266 let relay_parent_session = relay_client.session_index_for_child(relay_parent).await?;
267 let maybe_info = relay_client
268 .ancestor_relay_parent_info(scheduling_parent, relay_parent_session, relay_parent)
269 .await?;
270 Ok(maybe_info.is_some())
271}
272
273pub async fn find_parent_for_building<Block: BlockT>(
283 relay_client: &impl RelayChainInterface,
284 backend: &impl Backend<Block>,
285 para_id: ParaId,
286 params: ParentSearchParams,
287) -> RelayChainResult<Option<ParentSearchResult<Block>>> {
288 tracing::trace!(
289 target: LOG_TARGET,
290 ?para_id,
291 ?params,
292 "Parent search"
293 );
294
295 let scheduling_parent = *params.scheduling_parent();
296 let Some((included_header, included_hash)) =
297 fetch_included_from_relay_chain(relay_client, backend, scheduling_parent, para_id).await?
298 else {
299 return Ok(None);
300 };
301
302 let maybe_pending = {
304 let maybe_header = fetch_pvd_header::<Block>(
308 relay_client,
309 scheduling_parent,
310 para_id,
311 OccupiedCoreAssumption::Included,
312 )
313 .await?
314 .filter(|header| header.hash() != included_hash);
315
316 if let Some(header) = maybe_header {
318 let hash = header.hash();
319 let Some(header) = get_para_header(backend, hash) else {
320 return Ok(None);
321 };
322 Some((header, hash))
323 } else {
324 None
325 }
326 };
327 let (start_header, start_hash) =
329 maybe_pending.unwrap_or((included_header.clone(), included_hash));
330
331 let best_parent_header = match params {
332 ParentSearchParams::V2 { scheduling_parent: relay_parent } => {
333 let ancestry_lookback = relay_client
334 .scheduling_lookahead(relay_parent)
335 .await
336 .unwrap_or(DEFAULT_SCHEDULING_LOOKAHEAD)
337 .saturating_sub(1) as usize;
338 let rp_ancestry =
340 build_relay_parent_ancestry(relay_client, relay_parent, ancestry_lookback).await?;
341
342 find_deepest_valid_parent(backend, start_header, start_hash, |header| {
344 let is_valid = is_relay_parent_in_ancestry::<Block>(header, &rp_ancestry);
345 async move { is_valid }
346 })
347 .await
348 },
349 ParentSearchParams::V3 { scheduling_parent } => {
350 find_deepest_valid_parent(backend, start_header, start_hash, |header| {
351 let header = header.clone();
352 async move {
353 has_ancestor_relay_parent_info::<Block>(
354 relay_client,
355 scheduling_parent,
356 &header,
357 )
358 .await
359 .unwrap_or(false)
360 }
361 })
362 .await
363 },
364 };
365
366 Ok(Some(ParentSearchResult { included_at_scheduling: included_header, best_parent_header }))
367}