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;
30
31use sp_runtime::traits::{Block as BlockT, Header as HeaderT};
32
33const LOG_TARGET: &str = "consensus::common::parent_search";
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}
46
47/// A potential parent block returned from [`find_parent_for_building`]
48#[derive(PartialEq, Clone)]
49pub struct ParentSearchResult<B: BlockT> {
50	/// The header of the included block (confirmed on relay chain).
51	pub included_header: B::Header,
52	/// The header of the best parent block to build on.
53	pub best_parent_header: B::Header,
54}
55
56impl<B: BlockT> std::fmt::Debug for ParentSearchResult<B> {
57	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58		f.debug_struct("ParentSearchResult")
59			.field("included_number", &self.included_header.number())
60			.field("best_parent_hash", &self.best_parent_header.hash())
61			.field("best_parent_number", &self.best_parent_header.number())
62			.finish()
63	}
64}
65
66/// Find the best parent block to build on.
67///
68/// This accepts a relay-chain block to be used as an anchor and searches for the best
69/// parachain block to use as a parent for a new block.
70///
71/// The search starts from either the pending block (if one exists) or the included block,
72/// and finds the deepest descendant whose relay-parent is within the allowed ancestry.
73///
74/// Returns `None` if no suitable parent can be found (e.g., included block unknown locally).
75pub async fn find_parent_for_building<B: BlockT>(
76	params: ParentSearchParams,
77	backend: &impl Backend<B>,
78	relay_client: &impl RelayChainInterface,
79) -> Result<Option<ParentSearchResult<B>>, RelayChainError> {
80	tracing::trace!(target: LOG_TARGET, "Parent search parameters: {params:?}");
81
82	// Get the included block.
83	let Some((included_header, included_hash)) =
84		fetch_included_from_relay_chain(relay_client, backend, params.para_id, params.relay_parent)
85			.await?
86	else {
87		return Ok(None);
88	};
89
90	// Fetch the pending block if one exists.
91	let maybe_pending = {
92		// Fetch the most recent pending header from the relay chain. We use
93		// `OccupiedCoreAssumption::Included` so the candidate pending availability gets enacted
94		// before being returned to us.
95		let pending_header = relay_client
96			.persisted_validation_data(
97				params.relay_parent,
98				params.para_id,
99				OccupiedCoreAssumption::Included,
100			)
101			.await?
102			.and_then(|p| B::Header::decode(&mut &p.parent_head.0[..]).ok())
103			.filter(|x| x.hash() != included_hash);
104
105		// If the pending block is not locally known, we can't proceed.
106		if let Some(header) = pending_header {
107			let pending_hash = header.hash();
108			let Ok(Some(header)) = backend.blockchain().header(pending_hash) else {
109				tracing::warn!(
110					target: LOG_TARGET,
111					%pending_hash,
112					"Failed to get header for pending block.",
113				);
114				return Ok(None);
115			};
116			Some((header, pending_hash))
117		} else {
118			None
119		}
120	};
121
122	// Determine the starting point for the search.
123	let (start_hash, start_header) = match &maybe_pending {
124		Some((pending_header, pending_hash)) => {
125			// Verify pending is a descendant of included.
126			let route =
127				sp_blockchain::tree_route(backend.blockchain(), included_hash, *pending_hash)?;
128			if !route.retracted().is_empty() {
129				tracing::warn!(
130					target: LOG_TARGET,
131					"Included block not an ancestor of pending block. This should not happen."
132				);
133				return Ok(None);
134			}
135			(*pending_hash, pending_header.clone())
136		},
137		None => (included_hash, included_header.clone()),
138	};
139
140	// Build up the ancestry record of the relay chain to compare against.
141	let rp_ancestry =
142		build_relay_parent_ancestry(params.ancestry_lookback, params.relay_parent, relay_client)
143			.await?;
144
145	// Search for the deepest valid parent starting from the pending/included block.
146	let best_parent_header =
147		find_deepest_valid_parent(start_hash, start_header, backend, &rp_ancestry);
148
149	Ok(Some(ParentSearchResult { included_header, best_parent_header }))
150}
151
152/// Fetch the included block from the relay chain.
153async fn fetch_included_from_relay_chain<B: BlockT>(
154	relay_client: &impl RelayChainInterface,
155	backend: &impl Backend<B>,
156	para_id: ParaId,
157	relay_parent: RelayHash,
158) -> Result<Option<(B::Header, B::Hash)>, RelayChainError> {
159	// Fetch the pending header from the relay chain. We use `OccupiedCoreAssumption::TimedOut`
160	// so that even if there is a pending candidate, we assume it is timed out and we get the
161	// included head.
162	let included_header = relay_client
163		.persisted_validation_data(relay_parent, para_id, OccupiedCoreAssumption::TimedOut)
164		.await?;
165	let included_header = match included_header {
166		Some(pvd) => pvd.parent_head,
167		None => return Ok(None), // this implies the para doesn't exist.
168	};
169
170	let included_header = match B::Header::decode(&mut &included_header.0[..]).ok() {
171		None => return Ok(None),
172		Some(x) => x,
173	};
174
175	let included_hash = included_header.hash();
176	// If the included block is not locally known, we can't do anything.
177	match backend.blockchain().header(included_hash) {
178		Ok(None) | Err(_) => {
179			tracing::warn!(
180				target: LOG_TARGET,
181				%included_hash,
182				"Failed to get header for included block.",
183			);
184			return Ok(None);
185		},
186		_ => {},
187	};
188
189	Ok(Some((included_header, included_hash)))
190}
191
192/// Build an ancestry of relay parents that are acceptable.
193///
194/// An acceptable relay parent is one that is no more than `ancestry_lookback` + 1 blocks below the
195/// relay parent we want to build on. Parachain blocks anchored on relay parents older than that can
196/// not be considered potential parents for block building. They have no chance of still getting
197/// included, so our newly build parachain block would also not get included.
198///
199/// On success, returns a vector of `(header_hash, state_root)` of the relevant relay chain
200/// ancestry blocks.
201async fn build_relay_parent_ancestry(
202	ancestry_lookback: usize,
203	relay_parent: RelayHash,
204	relay_client: &impl RelayChainInterface,
205) -> Result<Vec<(RelayHash, RelayHash)>, RelayChainError> {
206	let mut ancestry = Vec::with_capacity(ancestry_lookback + 1);
207	let mut current_rp = relay_parent;
208	let mut required_session = None;
209	while ancestry.len() <= ancestry_lookback {
210		let Some(header) = relay_client.header(RBlockId::hash(current_rp)).await? else { break };
211
212		let session = relay_client.session_index_for_child(current_rp).await?;
213		if required_session.get_or_insert(session) != &session {
214			// Respect the relay-chain rule not to cross session boundaries.
215			break;
216		}
217
218		ancestry.push((current_rp, *header.state_root()));
219		current_rp = *header.parent_hash();
220
221		// don't iterate back into the genesis block.
222		if header.number == 1 {
223			break;
224		}
225	}
226	Ok(ancestry)
227}
228
229/// Find the deepest valid parent block starting from `start`.
230///
231/// The `start` block (pending or included) is always valid by construction.
232/// This function explores its descendants via DFS, returning the deepest block
233/// whose relay-parent is within the allowed ancestry.
234fn find_deepest_valid_parent<Block: BlockT>(
235	start_hash: Block::Hash,
236	start_header: Block::Header,
237	backend: &impl Backend<Block>,
238	rp_ancestry: &[(RelayHash, RelayHash)],
239) -> Block::Header {
240	let mut best = start_header;
241
242	let mut frontier: Vec<Block::Hash> =
243		backend.blockchain().children(start_hash).ok().into_iter().flatten().collect();
244
245	tracing::trace!(
246		target: LOG_TARGET,
247		?start_hash,
248		num_children = frontier.len(),
249		"Searching for deepest valid parent."
250	);
251
252	while let Some(hash) = frontier.pop() {
253		let Ok(Some(header)) = backend.blockchain().header(hash) else { continue };
254
255		if !is_relay_parent_in_ancestry::<Block>(&header, rp_ancestry) {
256			continue;
257		}
258
259		// This block is valid - update best if it's deeper.
260		if header.number() > best.number() {
261			best = header.clone();
262		}
263
264		frontier.extend(backend.blockchain().children(hash).ok().into_iter().flatten());
265	}
266
267	best
268}
269
270/// Check if a block's relay parent is within the allowed ancestry.
271fn is_relay_parent_in_ancestry<Block: BlockT>(
272	header: &Block::Header,
273	rp_ancestry: &[(RelayHash, RelayHash)],
274) -> bool {
275	let digest = header.digest();
276	let relay_parent = cumulus_primitives_core::extract_relay_parent(digest);
277	let storage_root =
278		cumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(digest)
279			.map(|(r, _)| r);
280
281	rp_ancestry.iter().any(|(rp_hash, rp_storage_root)| {
282		relay_parent.map_or(false, |rp| *rp_hash == rp) ||
283			storage_root.map_or(false, |sr| *rp_storage_root == sr)
284	})
285}