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, SchedulingSignals,
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		// Capture the signed scheduling info before `scheduling_proof` is moved; the tail below is
295		// rebuilt from it.
296		let signed_scheduling_info =
297			scheduling_proof.as_ref().and_then(|p| p.signed_scheduling_info.clone());
298
299		let block_data = ParachainBlockData::<Block>::new(blocks, compact_proof, scheduling_proof);
300
301		let pov = polkadot_node_primitives::maybe_compress_pov(PoV {
302			block_data: BlockData(if api_version >= 3 {
303				block_data.encode()
304			} else {
305				let block_data = block_data.as_v0();
306
307				if block_data.is_none() {
308					tracing::error!(
309						target: LOG_TARGET,
310						"Trying to submit a collation with multiple blocks is not supported by the current runtime."
311					);
312				}
313
314				block_data?.encode()
315			}),
316		});
317
318		// Emit the scheduling-signal tail. A signed scheduling info (resubmission) replaces the
319		// block's own signals wholesale, via the same `SchedulingSignals::from_scheduling_info` the
320		// PVF applies, so the two can't drift; otherwise the block's signals pass through
321		// unchanged.
322		match signed_scheduling_info {
323			Some(signed_info) => upward_messages
324				.extend(SchedulingSignals::from_scheduling_info(&signed_info).into_ump_messages()),
325			None => {
326				if !upward_message_signals.is_empty() {
327					upward_messages.push(UMP_SEPARATOR);
328					upward_messages.extend(upward_message_signals.into_iter());
329				}
330			},
331		}
332
333		let upward_messages = upward_messages
334			.try_into()
335			.map_err(|e| {
336				tracing::error!(
337					target: LOG_TARGET,
338					error = ?e,
339					"Number of upward messages should not be greater than `MAX_UPWARD_MESSAGE_NUM`",
340				)
341			})
342			.ok()?;
343		let horizontal_messages = horizontal_messages
344			.try_into()
345			.map_err(|e| {
346				tracing::error!(
347					target: LOG_TARGET,
348					error = ?e,
349					"Number of horizontal messages should not be greater than `MAX_HORIZONTAL_MESSAGE_NUM`",
350				)
351			})
352			.ok()?;
353
354		let collation = Collation {
355			upward_messages,
356			new_validation_code,
357			processed_downward_messages,
358			horizontal_messages,
359			// If these are `None`, there was no block.
360			hrmp_watermark: hrmp_watermark?,
361			head_data: head_data?,
362			proof_of_validity: MaybeCompressedPoV::Compressed(pov),
363		};
364
365		Some((collation, block_data))
366	}
367}
368
369impl<Block, BS, RA> ServiceInterface<Block> for CollatorService<Block, BS, RA>
370where
371	Block: BlockT,
372	BS: BlockBackend<Block>,
373	RA: ProvideRuntimeApi<Block>,
374	RA::Api: CollectCollationInfo<Block>,
375{
376	fn check_block_status(&self, hash: Block::Hash, header: &Block::Header) -> bool {
377		CollatorService::check_block_status(self, hash, header)
378	}
379
380	fn build_collation(
381		&self,
382		parent_header: &Block::Header,
383		_: Block::Hash,
384		candidate: ParachainCandidate<Block>,
385		scheduling_proof: Option<SchedulingProof>,
386	) -> Option<(Collation, ParachainBlockData<Block>)> {
387		CollatorService::build_multi_block_collation(
388			self,
389			parent_header,
390			vec![candidate.block],
391			candidate.proof,
392			scheduling_proof,
393		)
394	}
395
396	fn announce_block(&self, block_hash: Block::Hash, data: Option<Vec<u8>>) {
397		(self.announce_block)(block_hash, data)
398	}
399
400	fn build_multi_block_collation(
401		&self,
402		parent_header: &<Block as BlockT>::Header,
403		blocks: Vec<Block>,
404		proof: StorageProof,
405		scheduling_proof: Option<SchedulingProof>,
406	) -> Option<(Collation, ParachainBlockData<Block>)> {
407		CollatorService::build_multi_block_collation(
408			self,
409			parent_header,
410			blocks,
411			proof,
412			scheduling_proof,
413		)
414	}
415}