referrerpolicy=no-referrer-when-downgrade

cumulus_client_consensus_common/
parent_search.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Cumulus.
3// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
4
5// Cumulus is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9
10// Cumulus is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14
15// You should have received a copy of the GNU General Public License
16// along with Cumulus. If not, see <https://www.gnu.org/licenses/>.
17
18use 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	/// Candidate version V2
36	V2 {
37		/// The scheduling-parent that is intended to be used.
38		/// For V2, the scheduling parent is equal to the relay parent.
39		scheduling_parent: RelayHash,
40	},
41	/// Candidate version V3
42	V3 {
43		/// The scheduling-parent that is intended to be used.
44		scheduling_parent: RelayHash,
45	},
46}
47
48impl ParentSearchParams {
49	/// The params for the scheduling version: V3 anchors at the scheduling parent, V2 at the
50	/// relay parent.
51	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/// A potential parent block returned from [`find_parent_for_building`].
68#[derive(PartialEq, Clone)]
69pub struct ParentSearchResult<Block: BlockT> {
70	/// Included head at the scheduling parent.
71	pub included_at_scheduling: Block::Header,
72	/// Block to build on.
73	pub best_parent_header: Block::Header,
74	/// V3 resubmittable segment, oldest first; empty for V2.
75	pub resubmittable_segment: Vec<Block::Header>,
76}
77
78/// Trim the best parent (and segment) back to the first ancestor passing `filter_parent`, or to
79/// the included head. Walks the in-memory segment first, then follows storage via `get_header` past
80/// the segment base to an older bundle-ender when the segment itself holds none.
81pub(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		// Inside the segment the popped tail is the parent, read from memory; once it empties, the
90		// parent sits below the segment base and is read from storage.
91		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
151/// Fetch the included block from the relay chain.
152pub 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	// Fetch the pending header from the relay chain. We use `OccupiedCoreAssumption::TimedOut`
159	// so that even if there is a pending candidate, we assume it is timed out, and we get the
160	// included head.
161	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	// If the included block is not locally known, we can't do anything.
169	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
175/// Build an ancestry of relay parents that are acceptable.
176///
177/// An acceptable relay parent is one that is no more than `ancestry_lookback` + 1 blocks below the
178/// relay parent we want to build on. Parachain blocks anchored on relay parents older than that can
179/// not be considered potential parents for block building. They have no chance of still getting
180/// included, so our newly build parachain block would also not get included.
181///
182/// On success, returns a vector of `(header_hash, state_root)` of the relevant relay chain
183/// ancestry blocks.
184async 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		// Respect the relay-chain rule not to cross session boundaries.
205		if contains_epoch_change::<RelayBlock>(&header) {
206			break;
207		}
208
209		// don't iterate back into the genesis block.
210		if header.number == 1 {
211			break;
212		}
213	}
214	Ok(ancestry)
215}
216
217/// Check if a block's relay parent is within the allowed ancestry.
218fn 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
236/// Find the deepest valid parent block starting from `start`.
237///
238/// The `start` block (pending or included) is always valid by construction.
239/// This function explores its descendants via DFS, returning the deepest block
240/// whose relay-parent is within the allowed ancestry.
241async 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		// This block is valid - update best if it's deeper.
267		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
277/// The chain from `best` back to `start_hash` (exclusive), oldest first.
278fn 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
323/// True if `header`'s relay parent is a known ancestor of `scheduling_parent` on the relay chain,
324/// i.e. one the relay chain still accepts candidates on.
325async 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
345/// Find the best parent block to build on.
346///
347/// This accepts a relay-chain block to be used as an anchor and searches for the best
348/// parachain block to use as a parent for a new block.
349///
350/// The search starts from either the pending block (if one exists) or the included block,
351/// and finds the deepest descendant whose relay-parent is within the allowed ancestry.
352///
353/// Returns `None` if no suitable parent can be found (e.g., included block unknown locally).
354pub 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	// Fetch the pending block if one exists.
376	let maybe_pending = {
377		// Fetch the most recent pending header from the relay chain. We use
378		// `OccupiedCoreAssumption::Included` so the candidate pending availability gets enacted
379		// before being returned to us.
380		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 the pending block is not locally known, we can't proceed.
390		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	// Determine the starting point for the search.
401	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			// Build up the ancestry record of the relay chain to compare against.
412			let rp_ancestry =
413				build_relay_parent_ancestry(relay_client, relay_parent, ancestry_lookback).await?;
414
415			// V2 has no resubmittable segment.
416			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			// V3 also carries the resubmittable segment.
446			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	// Trim the best parent (and its segment) back to the first ancestor passing the filter, so the
458	// returned result is the final parent to build on.
459	Ok(Some(trim_best_parent_to_filter(
460		result,
461		|hash| backend.blockchain().header(hash).ok().flatten(),
462		filter_parent,
463	)))
464}