referrerpolicy=no-referrer-when-downgrade

cumulus_client_collator/
service.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
18//! The Cumulus [`CollatorService`] is a utility struct for performing common
19//! operations used in parachain consensus/authoring.
20
21use cumulus_primitives_core::{
22	CollationInfo, CollectCollationInfo, ParachainBlockData, SchedulingProof,
23};
24
25use polkadot_primitives::UMP_SEPARATOR;
26use sc_client_api::BlockBackend;
27use sp_api::{ApiExt, ProvideRuntimeApi, StorageProof};
28use sp_consensus::BlockStatus;
29use sp_runtime::traits::{Block as BlockT, HashingFor, Header as HeaderT, Zero};
30
31use cumulus_client_consensus_common::ParachainCandidate;
32use polkadot_node_primitives::{BlockData, Collation, MaybeCompressedPoV, PoV};
33
34use codec::Encode;
35use std::sync::Arc;
36/// The logging target.
37const LOG_TARGET: &str = "cumulus-collator";
38
39/// Utility functions generally applicable to writing collators for Cumulus.
40pub trait ServiceInterface<Block: BlockT> {
41	/// Checks the status of the given block hash in the Parachain.
42	///
43	/// Returns `true` if the block could be found and is good to be build on.
44	fn check_block_status(&self, hash: Block::Hash, header: &Block::Header) -> bool;
45
46	/// Build a full [`Collation`] from a given [`ParachainCandidate`]. This requires
47	/// that the underlying block has been fully imported into the underlying client,
48	/// as implementations will fetch underlying runtime API data.
49	///
50	/// `scheduling_proof` is `Some` for V3 candidates (produces [`ParachainBlockData::V2`])
51	/// and `None` for legacy candidates (produces [`ParachainBlockData::V1`]).
52	///
53	/// This also returns the unencoded parachain block data, in case that is desired.
54	fn build_collation(
55		&self,
56		parent_header: &Block::Header,
57		block_hash: Block::Hash,
58		candidate: ParachainCandidate<Block>,
59		scheduling_proof: Option<SchedulingProof>,
60	) -> Option<(Collation, ParachainBlockData<Block>)>;
61
62	/// Build a multi-block collation.
63	///
64	/// Does the same as [`Self::build_collation`], but includes multiple blocks into one collation.
65	/// The given `parent_header` should be the header from the parent of the first block.
66	///
67	/// `scheduling_proof` is `Some` for V3 candidates (produces [`ParachainBlockData::V2`])
68	/// and `None` for legacy candidates (produces [`ParachainBlockData::V1`]).
69	fn build_multi_block_collation(
70		&self,
71		parent_header: &Block::Header,
72		blocks: Vec<Block>,
73		proof: StorageProof,
74		scheduling_proof: Option<SchedulingProof>,
75	) -> Option<(Collation, ParachainBlockData<Block>)>;
76
77	/// Directly announce a block on the network.
78	fn announce_block(&self, block_hash: Block::Hash, data: Option<Vec<u8>>);
79}
80
81/// The [`CollatorService`] provides common utilities for parachain consensus and authoring.
82///
83/// This includes logic for checking the block status of arbitrary parachain headers
84/// gathered from the relay chain state, creating full [`Collation`]s to be shared with validators,
85/// and distributing new parachain blocks along the network.
86pub struct CollatorService<Block: BlockT, BS, RA> {
87	block_status: Arc<BS>,
88	announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
89	runtime_api: Arc<RA>,
90}
91
92impl<Block: BlockT, BS, RA> Clone for CollatorService<Block, BS, RA> {
93	fn clone(&self) -> Self {
94		Self {
95			block_status: self.block_status.clone(),
96			announce_block: self.announce_block.clone(),
97			runtime_api: self.runtime_api.clone(),
98		}
99	}
100}
101
102impl<Block, BS, RA> CollatorService<Block, BS, RA>
103where
104	Block: BlockT,
105	BS: BlockBackend<Block>,
106	RA: ProvideRuntimeApi<Block>,
107	RA::Api: CollectCollationInfo<Block>,
108{
109	fn split_at_separator(messages: Vec<Vec<u8>>) -> (Vec<Vec<u8>>, Vec<Vec<u8>>) {
110		let mut parts = messages.splitn(2, |m: &Vec<u8>| m.is_empty());
111		(parts.next().unwrap_or(&[]).to_vec(), parts.next().unwrap_or(&[]).to_vec())
112	}
113
114	/// Create a new instance.
115	pub fn new(
116		block_status: Arc<BS>,
117		announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
118		runtime_api: Arc<RA>,
119	) -> Self {
120		Self { block_status, announce_block, runtime_api }
121	}
122
123	/// Checks the status of the given block hash in the Parachain.
124	///
125	/// Returns `true` if the block could be found and is good to be build on.
126	pub fn check_block_status(&self, hash: Block::Hash, header: &Block::Header) -> bool {
127		match self.block_status.block_status(hash) {
128			Ok(BlockStatus::Queued) => {
129				tracing::debug!(
130					target: LOG_TARGET,
131					block_hash = ?hash,
132					"Skipping candidate production, because block is still queued for import.",
133				);
134				false
135			},
136			Ok(BlockStatus::InChainWithState) => true,
137			Ok(BlockStatus::InChainPruned) => {
138				tracing::error!(
139					target: LOG_TARGET,
140					"Skipping candidate production, because block `{:?}` is already pruned!",
141					hash,
142				);
143				false
144			},
145			Ok(BlockStatus::KnownBad) => {
146				tracing::error!(
147					target: LOG_TARGET,
148					block_hash = ?hash,
149					"Block is tagged as known bad and is included in the relay chain! Skipping candidate production!",
150				);
151				false
152			},
153			Ok(BlockStatus::Unknown) => {
154				if header.number().is_zero() {
155					tracing::error!(
156						target: LOG_TARGET,
157						block_hash = ?hash,
158						"Could not find the header of the genesis block in the database!",
159					);
160				} else {
161					tracing::debug!(
162						target: LOG_TARGET,
163						block_hash = ?hash,
164						"Skipping candidate production, because block is unknown.",
165					);
166				}
167				false
168			},
169			Err(e) => {
170				tracing::error!(
171					target: LOG_TARGET,
172					block_hash = ?hash,
173					error = ?e,
174					"Failed to get block status.",
175				);
176				false
177			},
178		}
179	}
180
181	/// Fetch the collation info from the runtime.
182	///
183	/// Returns `Ok(Some((CollationInfo, ApiVersion)))` on success, `Err(_)` on error or `Ok(None)`
184	/// if the runtime api isn't implemented by the runtime. `ApiVersion` being the version of the
185	/// [`CollectCollationInfo`] runtime api.
186	pub fn fetch_collation_info(
187		&self,
188		block_hash: Block::Hash,
189		header: &Block::Header,
190	) -> Result<Option<(CollationInfo, u32)>, sp_api::ApiError> {
191		let runtime_api = self.runtime_api.runtime_api();
192
193		let api_version =
194			match runtime_api.api_version::<dyn CollectCollationInfo<Block>>(block_hash)? {
195				Some(version) => version,
196				None => {
197					tracing::error!(
198						target: LOG_TARGET,
199						"Could not fetch `CollectCollationInfo` runtime api version."
200					);
201					return Ok(None);
202				},
203			};
204
205		let collation_info = if api_version < 2 {
206			#[allow(deprecated)]
207			runtime_api
208				.collect_collation_info_before_version_2(block_hash)?
209				.into_latest(header.encode().into())
210		} else {
211			runtime_api.collect_collation_info(block_hash, header)?
212		};
213
214		Ok(Some((collation_info, api_version)))
215	}
216
217	/// Build a full [`Collation`] from a given [`ParachainCandidate`]. This requires
218	/// that the underlying block has been fully imported into the underlying client,
219	/// as it fetches underlying runtime API data.
220	///
221	/// This also returns the unencoded parachain block data, in case that is desired.
222	fn build_multi_block_collation(
223		&self,
224		parent_header: &Block::Header,
225		blocks: Vec<Block>,
226		proof: StorageProof,
227		scheduling_proof: Option<SchedulingProof>,
228	) -> Option<(Collation, ParachainBlockData<Block>)> {
229		let compact_proof =
230			match proof.into_compact_proof::<HashingFor<Block>>(*parent_header.state_root()) {
231				Ok(proof) => proof,
232				Err(e) => {
233					tracing::error!(target: "cumulus-collator", "Failed to compact proof: {:?}", e);
234					return None;
235				},
236			};
237
238		// We are always using the `api_version` of the parent block. The `api_version` can only
239		// change with a runtime upgrade and this is when we want to observe the old
240		// `api_version`. Because this old `api_version` is the one used to validate this
241		// block. Otherwise, we already assume the `api_version` is higher than what the relay
242		// chain will use and this will lead to validation errors.
243		let api_version = self
244			.runtime_api
245			.runtime_api()
246			.api_version::<dyn CollectCollationInfo<Block>>(parent_header.hash())
247			.ok()
248			.flatten()?;
249		let mut upward_messages = Vec::new();
250		let mut upward_message_signals = Vec::<Vec<u8>>::with_capacity(4);
251		let mut horizontal_messages = Vec::new();
252		let mut new_validation_code = None;
253		let mut processed_downward_messages = 0;
254		let mut hrmp_watermark = None;
255		let mut head_data = None;
256
257		for block in &blocks {
258			// Create the parachain block data for the validators.
259			let (collation_info, _api_version) = self
260				.fetch_collation_info(block.hash(), block.header())
261				.map_err(|e| {
262					tracing::error!(
263						target: LOG_TARGET,
264						error = ?e,
265						"Failed to collect collation info.",
266					)
267				})
268				.ok()
269				.flatten()?;
270
271			let (messages, signals) = Self::split_at_separator(collation_info.upward_messages);
272
273			upward_messages.extend(messages);
274			upward_message_signals.extend(signals);
275			horizontal_messages.extend(collation_info.horizontal_messages);
276
277			if let Some(new_code) = collation_info.new_validation_code {
278				if new_validation_code.replace(new_code).is_some() {
279					tracing::warn!(
280						target: LOG_TARGET,
281						block = ?block.hash(),
282						"Overwriting validation code from an earlier block in the bundle.",
283					);
284				}
285			}
286			processed_downward_messages += collation_info.processed_downward_messages;
287			hrmp_watermark = Some(collation_info.hrmp_watermark);
288			head_data = Some(collation_info.head_data);
289		}
290
291		// Sort by recipient as required by the relay chain rules.
292		horizontal_messages.sort_by(|a, b| a.recipient.cmp(&b.recipient));
293
294		let block_data = ParachainBlockData::<Block>::new(blocks, compact_proof, scheduling_proof);
295
296		let pov = polkadot_node_primitives::maybe_compress_pov(PoV {
297			block_data: BlockData(if api_version >= 3 {
298				block_data.encode()
299			} else {
300				let block_data = block_data.as_v0();
301
302				if block_data.is_none() {
303					tracing::error!(
304						target: LOG_TARGET,
305						"Trying to submit a collation with multiple blocks is not supported by the current runtime."
306					);
307				}
308
309				block_data?.encode()
310			}),
311		});
312
313		// If we got some signals, push them now.
314		if !upward_message_signals.is_empty() {
315			upward_messages.push(UMP_SEPARATOR);
316			upward_messages.extend(upward_message_signals.into_iter());
317		}
318
319		let upward_messages = upward_messages
320			.try_into()
321			.map_err(|e| {
322				tracing::error!(
323					target: LOG_TARGET,
324					error = ?e,
325					"Number of upward messages should not be greater than `MAX_UPWARD_MESSAGE_NUM`",
326				)
327			})
328			.ok()?;
329		let horizontal_messages = horizontal_messages
330			.try_into()
331			.map_err(|e| {
332				tracing::error!(
333					target: LOG_TARGET,
334					error = ?e,
335					"Number of horizontal messages should not be greater than `MAX_HORIZONTAL_MESSAGE_NUM`",
336				)
337			})
338			.ok()?;
339
340		let collation = Collation {
341			upward_messages,
342			new_validation_code,
343			processed_downward_messages,
344			horizontal_messages,
345			// If these are `None`, there was no block.
346			hrmp_watermark: hrmp_watermark?,
347			head_data: head_data?,
348			proof_of_validity: MaybeCompressedPoV::Compressed(pov),
349		};
350
351		Some((collation, block_data))
352	}
353}
354
355impl<Block, BS, RA> ServiceInterface<Block> for CollatorService<Block, BS, RA>
356where
357	Block: BlockT,
358	BS: BlockBackend<Block>,
359	RA: ProvideRuntimeApi<Block>,
360	RA::Api: CollectCollationInfo<Block>,
361{
362	fn check_block_status(&self, hash: Block::Hash, header: &Block::Header) -> bool {
363		CollatorService::check_block_status(self, hash, header)
364	}
365
366	fn build_collation(
367		&self,
368		parent_header: &Block::Header,
369		_: Block::Hash,
370		candidate: ParachainCandidate<Block>,
371		scheduling_proof: Option<SchedulingProof>,
372	) -> Option<(Collation, ParachainBlockData<Block>)> {
373		CollatorService::build_multi_block_collation(
374			self,
375			parent_header,
376			vec![candidate.block],
377			candidate.proof,
378			scheduling_proof,
379		)
380	}
381
382	fn announce_block(&self, block_hash: Block::Hash, data: Option<Vec<u8>>) {
383		(self.announce_block)(block_hash, data)
384	}
385
386	fn build_multi_block_collation(
387		&self,
388		parent_header: &<Block as BlockT>::Header,
389		blocks: Vec<Block>,
390		proof: StorageProof,
391		scheduling_proof: Option<SchedulingProof>,
392	) -> Option<(Collation, ParachainBlockData<Block>)> {
393		CollatorService::build_multi_block_collation(
394			self,
395			parent_header,
396			blocks,
397			proof,
398			scheduling_proof,
399		)
400	}
401}