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 polkadot_primitives::Hash as RelayHash;
20
21use cumulus_primitives_core::{
22	relay_chain::{BlockId as RBlockId, OccupiedCoreAssumption},
23	ParaId,
24};
25use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface};
26
27use sc_client_api::{Backend, HeaderBackend};
28
29use sp_blockchain::{Backend as BlockchainBackend, TreeRoute};
30
31use sp_runtime::traits::{Block as BlockT, Header as HeaderT};
32
33const PARENT_SEARCH_LOG_TARGET: &str = "consensus::common::find_potential_parents";
34
35/// Parameters when searching for suitable parents to build on top of.
36#[derive(Debug)]
37pub struct ParentSearchParams {
38	/// The relay-parent that is intended to be used.
39	pub relay_parent: RelayHash,
40	/// The ID of the parachain.
41	pub para_id: ParaId,
42	/// A limitation on the age of relay parents for parachain blocks that are being
43	/// considered. This is relative to the `relay_parent` number.
44	pub ancestry_lookback: usize,
45	/// How "deep" parents can be relative to the included parachain block at the relay-parent.
46	/// The included block has depth 0.
47	pub max_depth: usize,
48	/// Whether to only ignore "alternative" branches, i.e. branches of the chain
49	/// which do not contain the block pending availability.
50	pub ignore_alternative_branches: bool,
51}
52
53/// A potential parent block returned from [`find_potential_parents`]
54#[derive(PartialEq)]
55pub struct PotentialParent<B: BlockT> {
56	/// The hash of the block.
57	pub hash: B::Hash,
58	/// The header of the block.
59	pub header: B::Header,
60	/// The depth of the block with respect to the included block.
61	pub depth: usize,
62	/// Whether the block is the included block, is itself pending on-chain, or descends
63	/// from the block pending availability.
64	pub aligned_with_pending: bool,
65}
66
67impl<B: BlockT> std::fmt::Debug for PotentialParent<B> {
68	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69		f.debug_struct("PotentialParent")
70			.field("hash", &self.hash)
71			.field("depth", &self.depth)
72			.field("aligned_with_pending", &self.aligned_with_pending)
73			.field("number", &self.header.number())
74			.finish()
75	}
76}
77
78/// Perform a recursive search through blocks to find potential
79/// parent blocks for a new block.
80///
81/// This accepts a relay-chain block to be used as an anchor and a maximum search depth,
82/// along with some arguments for filtering parachain blocks and performs a recursive search
83/// for parachain blocks. The search begins at the last included parachain block and returns
84/// a set of [`PotentialParent`]s which could be potential parents of a new block with this
85/// relay-parent according to the search parameters.
86///
87/// A parachain block is a potential parent if it is either the last included parachain block, the
88/// pending parachain block (when `max_depth` >= 1), or all of the following hold:
89///   * its parent is a potential parent
90///   * its relay-parent is within `ancestry_lookback` of the targeted relay-parent.
91///   * its relay-parent is within the same session as the targeted relay-parent.
92///   * the block number is within `max_depth` blocks of the included block
93pub async fn find_potential_parents<B: BlockT>(
94	params: ParentSearchParams,
95	backend: &impl Backend<B>,
96	relay_client: &impl RelayChainInterface,
97) -> Result<Vec<PotentialParent<B>>, RelayChainError> {
98	tracing::trace!("Parent search parameters: {params:?}");
99	// Get the included block.
100	let Some((included_header, included_hash)) =
101		fetch_included_from_relay_chain(relay_client, backend, params.para_id, params.relay_parent)
102			.await?
103	else {
104		return Ok(Default::default())
105	};
106
107	let only_included = vec![PotentialParent {
108		hash: included_hash,
109		header: included_header.clone(),
110		depth: 0,
111		aligned_with_pending: true,
112	}];
113
114	if params.max_depth == 0 {
115		return Ok(only_included)
116	};
117
118	// Pending header and hash.
119	let maybe_pending = {
120		// Fetch the most recent pending header from the relay chain. We use
121		// `OccupiedCoreAssumption::Included` so the candidate pending availability gets enacted
122		// before being returned to us.
123		let pending_header = relay_client
124			.persisted_validation_data(
125				params.relay_parent,
126				params.para_id,
127				OccupiedCoreAssumption::Included,
128			)
129			.await?
130			.and_then(|p| B::Header::decode(&mut &p.parent_head.0[..]).ok())
131			.filter(|x| x.hash() != included_hash);
132
133		// If the pending block is not locally known, we can't do anything.
134		if let Some(header) = pending_header {
135			let pending_hash = header.hash();
136			match backend.blockchain().header(pending_hash) {
137				// We are supposed to ignore branches that don't contain the pending block, but we
138				// do not know the pending block locally.
139				Ok(None) | Err(_) if params.ignore_alternative_branches => {
140					tracing::warn!(
141						target: PARENT_SEARCH_LOG_TARGET,
142						%pending_hash,
143						"Failed to get header for pending block.",
144					);
145					return Ok(Default::default())
146				},
147				Ok(Some(_)) => Some((header, pending_hash)),
148				_ => None,
149			}
150		} else {
151			None
152		}
153	};
154
155	let maybe_route_to_last_pending = maybe_pending
156		.as_ref()
157		.map(|(_, pending)| {
158			sp_blockchain::tree_route(backend.blockchain(), included_hash, *pending)
159		})
160		.transpose()?;
161
162	// If we want to ignore alternative branches there is no reason to start
163	// the parent search at the included block. We can add the included block and
164	// the path to the pending block to the potential parents directly (limited by max_depth).
165	let (frontier, potential_parents) = match (
166		&maybe_pending,
167		params.ignore_alternative_branches,
168		&maybe_route_to_last_pending,
169	) {
170		(Some((pending_header, pending_hash)), true, Some(ref route_to_pending)) => {
171			let mut potential_parents = only_included;
172
173			// This is a defensive check, should never happen.
174			if !route_to_pending.retracted().is_empty() {
175				tracing::warn!(target: PARENT_SEARCH_LOG_TARGET, "Included block not an ancestor of pending block. This should not happen.");
176				return Ok(Default::default())
177			}
178
179			// Add all items on the path included -> pending - 1 to the potential parents, but
180			// not more than `max_depth`.
181			let num_parents_on_path =
182				route_to_pending.enacted().len().saturating_sub(1).min(params.max_depth);
183			for (num, block) in
184				route_to_pending.enacted().iter().take(num_parents_on_path).enumerate()
185			{
186				let Ok(Some(header)) = backend.blockchain().header(block.hash) else { continue };
187
188				potential_parents.push(PotentialParent {
189					hash: block.hash,
190					header,
191					depth: 1 + num,
192					aligned_with_pending: true,
193				});
194			}
195
196			// The search for additional potential parents should now start at the children of
197			// the pending block.
198			(
199				vec![PotentialParent {
200					hash: *pending_hash,
201					header: pending_header.clone(),
202					depth: route_to_pending.enacted().len(),
203					aligned_with_pending: true,
204				}],
205				potential_parents,
206			)
207		},
208		_ => (only_included, Default::default()),
209	};
210
211	if potential_parents.len() > params.max_depth {
212		return Ok(potential_parents);
213	}
214
215	// Build up the ancestry record of the relay chain to compare against.
216	let rp_ancestry =
217		build_relay_parent_ancestry(params.ancestry_lookback, params.relay_parent, relay_client)
218			.await?;
219
220	Ok(search_child_branches_for_parents(
221		frontier,
222		maybe_route_to_last_pending,
223		included_header,
224		maybe_pending.map(|(_, hash)| hash),
225		backend,
226		params.max_depth,
227		params.ignore_alternative_branches,
228		rp_ancestry,
229		potential_parents,
230	))
231}
232
233/// Fetch the included block from the relay chain.
234async fn fetch_included_from_relay_chain<B: BlockT>(
235	relay_client: &impl RelayChainInterface,
236	backend: &impl Backend<B>,
237	para_id: ParaId,
238	relay_parent: RelayHash,
239) -> Result<Option<(B::Header, B::Hash)>, RelayChainError> {
240	// Fetch the pending header from the relay chain. We use `OccupiedCoreAssumption::TimedOut`
241	// so that even if there is a pending candidate, we assume it is timed out and we get the
242	// included head.
243	let included_header = relay_client
244		.persisted_validation_data(relay_parent, para_id, OccupiedCoreAssumption::TimedOut)
245		.await?;
246	let included_header = match included_header {
247		Some(pvd) => pvd.parent_head,
248		None => return Ok(None), // this implies the para doesn't exist.
249	};
250
251	let included_header = match B::Header::decode(&mut &included_header.0[..]).ok() {
252		None => return Ok(None),
253		Some(x) => x,
254	};
255
256	let included_hash = included_header.hash();
257	// If the included block is not locally known, we can't do anything.
258	match backend.blockchain().header(included_hash) {
259		Ok(None) => {
260			tracing::warn!(
261				target: PARENT_SEARCH_LOG_TARGET,
262				%included_hash,
263				"Failed to get header for included block.",
264			);
265			return Ok(None)
266		},
267		Err(e) => {
268			tracing::warn!(
269				target: PARENT_SEARCH_LOG_TARGET,
270				%included_hash,
271				%e,
272				"Failed to get header for included block.",
273			);
274			return Ok(None)
275		},
276		_ => {},
277	};
278
279	Ok(Some((included_header, included_hash)))
280}
281
282/// Build an ancestry of relay parents that are acceptable.
283///
284/// An acceptable relay parent is one that is no more than `ancestry_lookback` + 1 blocks below the
285/// relay parent we want to build on. Parachain blocks anchored on relay parents older than that can
286/// not be considered potential parents for block building. They have no chance of still getting
287/// included, so our newly build parachain block would also not get included.
288///
289/// On success, returns a vector of `(header_hash, state_root)` of the relevant relay chain
290/// ancestry blocks.
291async fn build_relay_parent_ancestry(
292	ancestry_lookback: usize,
293	relay_parent: RelayHash,
294	relay_client: &impl RelayChainInterface,
295) -> Result<Vec<(RelayHash, RelayHash)>, RelayChainError> {
296	let mut ancestry = Vec::with_capacity(ancestry_lookback + 1);
297	let mut current_rp = relay_parent;
298	let mut required_session = None;
299	while ancestry.len() <= ancestry_lookback {
300		let Some(header) = relay_client.header(RBlockId::hash(current_rp)).await? else { break };
301
302		let session = relay_client.session_index_for_child(current_rp).await?;
303		if required_session.get_or_insert(session) != &session {
304			// Respect the relay-chain rule not to cross session boundaries.
305			break;
306		}
307
308		ancestry.push((current_rp, *header.state_root()));
309		current_rp = *header.parent_hash();
310
311		// don't iterate back into the genesis block.
312		if header.number == 1 {
313			break
314		}
315	}
316	Ok(ancestry)
317}
318
319/// Start search for child blocks that can be used as parents.
320pub fn search_child_branches_for_parents<Block: BlockT>(
321	mut frontier: Vec<PotentialParent<Block>>,
322	maybe_route_to_last_pending: Option<TreeRoute<Block>>,
323	included_header: Block::Header,
324	pending_hash: Option<Block::Hash>,
325	backend: &impl Backend<Block>,
326	max_depth: usize,
327	ignore_alternative_branches: bool,
328	rp_ancestry: Vec<(RelayHash, RelayHash)>,
329	mut potential_parents: Vec<PotentialParent<Block>>,
330) -> Vec<PotentialParent<Block>> {
331	let included_hash = included_header.hash();
332	let is_hash_in_ancestry = |hash| rp_ancestry.iter().any(|x| x.0 == hash);
333	let is_root_in_ancestry = |root| rp_ancestry.iter().any(|x| x.1 == root);
334
335	// The distance between pending and included block. Is later used to check if a child
336	// is aligned with pending when it is between pending and included block.
337	let pending_distance = maybe_route_to_last_pending.as_ref().map(|route| route.enacted().len());
338
339	// If a block is on the path included -> pending, we consider it `aligned_with_pending`.
340	let is_child_pending = |hash| {
341		maybe_route_to_last_pending
342			.as_ref()
343			.map_or(true, |route| route.enacted().iter().any(|x| x.hash == hash))
344	};
345
346	tracing::trace!(
347		target: PARENT_SEARCH_LOG_TARGET,
348		?included_hash,
349		included_num = ?included_header.number(),
350		?pending_hash ,
351		?rp_ancestry,
352		"Searching relay chain ancestry."
353	);
354	while let Some(entry) = frontier.pop() {
355		let is_pending = pending_hash.as_ref().map_or(false, |h| &entry.hash == h);
356		let is_included = included_hash == entry.hash;
357
358		// note: even if the pending block or included block have a relay parent
359		// outside of the expected part of the relay chain, they are always allowed
360		// because they have already been posted on chain.
361		let is_potential = is_pending || is_included || {
362			let digest = entry.header.digest();
363			let is_hash_in_ancestry_check = cumulus_primitives_core::extract_relay_parent(digest)
364				.map_or(false, is_hash_in_ancestry);
365			let is_root_in_ancestry_check =
366				cumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(digest)
367					.map(|(r, _n)| r)
368					.map_or(false, is_root_in_ancestry);
369
370			is_hash_in_ancestry_check || is_root_in_ancestry_check
371		};
372
373		let parent_aligned_with_pending = entry.aligned_with_pending;
374		let child_depth = entry.depth + 1;
375		let hash = entry.hash;
376
377		tracing::trace!(
378			target: PARENT_SEARCH_LOG_TARGET,
379			?hash,
380			is_potential,
381			is_pending,
382			is_included,
383			"Checking potential parent."
384		);
385
386		if is_potential {
387			potential_parents.push(entry);
388		}
389
390		if !is_potential || child_depth > max_depth {
391			continue
392		}
393
394		// push children onto search frontier.
395		for child in backend.blockchain().children(hash).ok().into_iter().flatten() {
396			tracing::trace!(target: PARENT_SEARCH_LOG_TARGET, ?child, child_depth, ?pending_distance, "Looking at child.");
397
398			let aligned_with_pending = parent_aligned_with_pending &&
399				(pending_distance.map_or(true, |dist| child_depth > dist) ||
400					is_child_pending(child));
401
402			if ignore_alternative_branches && !aligned_with_pending {
403				tracing::trace!(target: PARENT_SEARCH_LOG_TARGET, ?child, "Child is not aligned with pending block.");
404				continue
405			}
406
407			let Ok(Some(header)) = backend.blockchain().header(child) else { continue };
408
409			frontier.push(PotentialParent {
410				hash: child,
411				header,
412				depth: child_depth,
413				aligned_with_pending,
414			});
415		}
416	}
417
418	potential_parents
419}