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	/// The header of the included block (confirmed on relay chain) at the scheduling parent.
71	pub included_at_scheduling: Block::Header,
72	/// The header of the best parent block to build on.
73	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
116/// Fetch the included block from the relay chain.
117pub 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	// Fetch the pending header from the relay chain. We use `OccupiedCoreAssumption::TimedOut`
124	// so that even if there is a pending candidate, we assume it is timed out, and we get the
125	// included head.
126	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	// If the included block is not locally known, we can't do anything.
134	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
140/// Build an ancestry of relay parents that are acceptable.
141///
142/// An acceptable relay parent is one that is no more than `ancestry_lookback` + 1 blocks below the
143/// relay parent we want to build on. Parachain blocks anchored on relay parents older than that can
144/// not be considered potential parents for block building. They have no chance of still getting
145/// included, so our newly build parachain block would also not get included.
146///
147/// On success, returns a vector of `(header_hash, state_root)` of the relevant relay chain
148/// ancestry blocks.
149async 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		// Respect the relay-chain rule not to cross session boundaries.
170		if contains_epoch_change::<RelayBlock>(&header) {
171			break;
172		}
173
174		// don't iterate back into the genesis block.
175		if header.number == 1 {
176			break;
177		}
178	}
179	Ok(ancestry)
180}
181
182/// Check if a block's relay parent is within the allowed ancestry.
183fn 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
201/// Find the deepest valid parent block starting from `start`.
202///
203/// The `start` block (pending or included) is always valid by construction.
204/// This function explores its descendants via DFS, returning the deepest block
205/// whose relay-parent is within the allowed ancestry.
206async 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		// This block is valid - update best if it's deeper.
232		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
268/// True if `header`'s relay parent is a known ancestor of `scheduling_parent` on the relay chain,
269/// i.e. one the relay chain still accepts candidates on.
270async 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
290/// Find the best parent block to build on.
291///
292/// This accepts a relay-chain block to be used as an anchor and searches for the best
293/// parachain block to use as a parent for a new block.
294///
295/// The search starts from either the pending block (if one exists) or the included block,
296/// and finds the deepest descendant whose relay-parent is within the allowed ancestry.
297///
298/// Returns `None` if no suitable parent can be found (e.g., included block unknown locally).
299pub 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	// Fetch the pending block if one exists.
320	let maybe_pending = {
321		// Fetch the most recent pending header from the relay chain. We use
322		// `OccupiedCoreAssumption::Included` so the candidate pending availability gets enacted
323		// before being returned to us.
324		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 the pending block is not locally known, we can't proceed.
334		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	// Determine the starting point for the search.
345	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			// Build up the ancestry record of the relay chain to compare against.
356			let rp_ancestry =
357				build_relay_parent_ancestry(relay_client, relay_parent, ancestry_lookback).await?;
358
359			// Search for the deepest valid parent starting from the pending/included block.
360			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}