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	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/// A potential parent block returned from [`find_parent_for_building`]
58#[derive(PartialEq, Clone)]
59pub struct ParentSearchResult<Block: BlockT> {
60	/// The header of the included block (confirmed on relay chain) at the scheduling parent.
61	pub included_at_scheduling: Block::Header,
62	/// The header of the best parent block to build on.
63	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
106/// Fetch the included block from the relay chain.
107pub 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	// Fetch the pending header from the relay chain. We use `OccupiedCoreAssumption::TimedOut`
114	// so that even if there is a pending candidate, we assume it is timed out, and we get the
115	// included head.
116	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	// If the included block is not locally known, we can't do anything.
124	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
130/// Build an ancestry of relay parents that are acceptable.
131///
132/// An acceptable relay parent is one that is no more than `ancestry_lookback` + 1 blocks below the
133/// relay parent we want to build on. Parachain blocks anchored on relay parents older than that can
134/// not be considered potential parents for block building. They have no chance of still getting
135/// included, so our newly build parachain block would also not get included.
136///
137/// On success, returns a vector of `(header_hash, state_root)` of the relevant relay chain
138/// ancestry blocks.
139async 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		// Respect the relay-chain rule not to cross session boundaries.
155		if contains_epoch_change::<RelayBlock>(&header) {
156			break;
157		}
158
159		// don't iterate back into the genesis block.
160		if header.number == 1 {
161			break;
162		}
163	}
164	Ok(ancestry)
165}
166
167/// Check if a block's relay parent is within the allowed ancestry.
168fn 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
186/// Find the deepest valid parent block starting from `start`.
187///
188/// The `start` block (pending or included) is always valid by construction.
189/// This function explores its descendants via DFS, returning the deepest block
190/// whose relay-parent is within the allowed ancestry.
191async 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		// This block is valid - update best if it's deeper.
217		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
273/// Find the best parent block to build on.
274///
275/// This accepts a relay-chain block to be used as an anchor and searches for the best
276/// parachain block to use as a parent for a new block.
277///
278/// The search starts from either the pending block (if one exists) or the included block,
279/// and finds the deepest descendant whose relay-parent is within the allowed ancestry.
280///
281/// Returns `None` if no suitable parent can be found (e.g., included block unknown locally).
282pub 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	// Fetch the pending block if one exists.
303	let maybe_pending = {
304		// Fetch the most recent pending header from the relay chain. We use
305		// `OccupiedCoreAssumption::Included` so the candidate pending availability gets enacted
306		// before being returned to us.
307		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 the pending block is not locally known, we can't proceed.
317		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	// Determine the starting point for the search.
328	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			// Build up the ancestry record of the relay chain to compare against.
339			let rp_ancestry =
340				build_relay_parent_ancestry(relay_client, relay_parent, ancestry_lookback).await?;
341
342			// Search for the deepest valid parent starting from the pending/included block.
343			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}