referrerpolicy=no-referrer-when-downgrade

cumulus_client_consensus_aura/
collator.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 core collator logic for Aura - slot claiming, block proposing, and collation
19//! packaging.
20//!
21//! The [`Collator`] struct exposed here is meant to be a component of higher-level logic
22//! which actually manages the control flow of the collator - which slots to claim, how
23//! many collations to build, when to work, etc.
24//!
25//! This module also exposes some standalone functions for common operations when building
26//! aura-based collators.
27
28use crate::collators::RelayParentData;
29use codec::Codec;
30use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
31use cumulus_client_consensus_common::{
32	self as consensus_common, ParachainBlockImportMarker, ParachainCandidate,
33};
34use cumulus_client_parachain_inherent::{ParachainInherentData, ParachainInherentDataProvider};
35use cumulus_primitives_core::{
36	relay_chain::Hash as PHash, DigestItem, ParachainBlockData, PersistedValidationData,
37	RelayProofRequest,
38};
39use cumulus_relay_chain_interface::RelayChainInterface;
40use futures::prelude::*;
41use polkadot_node_primitives::{Collation, MaybeCompressedPoV};
42use polkadot_primitives::{Header as PHeader, Id as ParaId};
43use sc_client_api::BackendTransaction;
44use sc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy, StateAction};
45use sc_consensus_aura::standalone as aura_internal;
46use sc_network_types::PeerId;
47use sp_api::{ApiExt, ProofRecorder, ProvideRuntimeApi, StorageProof};
48use sp_application_crypto::AppPublic;
49use sp_consensus::{BlockOrigin, Environment, ProposeArgs, Proposer};
50use sp_consensus_aura::{AuraApi, Slot, SlotDuration};
51use sp_core::crypto::Pair;
52use sp_externalities::Extensions;
53use sp_inherents::{CreateInherentDataProviders, InherentData, InherentDataProvider};
54use sp_keystore::KeystorePtr;
55use sp_runtime::{
56	generic::Digest,
57	traits::{Block as BlockT, HashingFor, Header as HeaderT, Member},
58};
59use sp_state_machine::StorageChanges;
60use sp_timestamp::Timestamp;
61use sp_trie::proof_size_extension::ProofSizeExt;
62use std::{error::Error, time::Duration};
63
64/// Parameters for instantiating a [`Collator`].
65pub struct Params<BI, CIDP, RClient, PF, CS> {
66	/// A builder for inherent data builders.
67	pub create_inherent_data_providers: CIDP,
68	/// The block import handle.
69	pub block_import: BI,
70	/// An interface to the relay-chain client.
71	pub relay_client: RClient,
72	/// The keystore handle used for accessing parachain key material.
73	pub keystore: KeystorePtr,
74	/// The collator network peer id.
75	pub collator_peer_id: PeerId,
76	/// The identifier of the parachain within the relay-chain.
77	pub para_id: ParaId,
78	/// The proposer used for building blocks.
79	pub proposer: PF,
80	/// The collator service used for bundling proposals into collations and announcing
81	/// to the network.
82	pub collator_service: CS,
83}
84
85/// Parameters for [`Collator::build_block_and_import`].
86pub struct BuildBlockAndImportParams<'a, Block: BlockT, P: Pair> {
87	/// The parent header to build on top of.
88	pub parent_header: &'a Block::Header,
89	/// The slot claim for this block.
90	pub slot_claim: &'a SlotClaim<P::Public>,
91	/// Additional pre-digest items to include.
92	pub additional_pre_digest: Vec<DigestItem>,
93	/// Parachain-specific inherent data.
94	pub parachain_inherent_data: ParachainInherentData,
95	/// Other inherent data (timestamp, etc.).
96	pub extra_inherent_data: InherentData,
97	/// Maximum duration to spend on block proposal.
98	pub proposal_duration: Duration,
99	/// Maximum PoV size in bytes.
100	pub max_pov_size: usize,
101	/// Optional [`ProofRecorder`] to use.
102	///
103	/// If not set, one will be initialized internally and [`ProofSizeExt`] will be
104	/// registered.
105	pub storage_proof_recorder: Option<ProofRecorder<Block>>,
106	/// Extra extensions to forward to the block production.
107	pub extra_extensions: Extensions,
108}
109
110/// Result of [`Collator::build_block_and_import`].
111pub struct BuiltBlock<Block: BlockT> {
112	/// The block that was built.
113	pub block: Block,
114	/// The proof that was recorded while building the block.
115	pub proof: StorageProof,
116	/// The transaction resulting from building the block.
117	///
118	/// This contains all the state changes.
119	pub backend_transaction: BackendTransaction<HashingFor<Block>>,
120}
121
122impl<Block: BlockT> From<BuiltBlock<Block>> for ParachainCandidate<Block> {
123	fn from(built: BuiltBlock<Block>) -> Self {
124		Self { block: built.block, proof: built.proof }
125	}
126}
127
128/// A utility struct for writing collation logic that makes use of Aura entirely
129/// or in part. See module docs for more details.
130pub struct Collator<Block, P, BI, CIDP, RClient, PF, CS> {
131	create_inherent_data_providers: CIDP,
132	block_import: BI,
133	relay_client: RClient,
134	keystore: KeystorePtr,
135	para_id: ParaId,
136	proposer: PF,
137	collator_service: CS,
138	_marker: std::marker::PhantomData<(Block, Box<dyn Fn(P) + Send + Sync + 'static>)>,
139}
140
141impl<Block, P, BI, CIDP, RClient, PF, CS> Collator<Block, P, BI, CIDP, RClient, PF, CS>
142where
143	Block: BlockT,
144	RClient: RelayChainInterface,
145	CIDP: CreateInherentDataProviders<Block, ()> + 'static,
146	BI: BlockImport<Block> + ParachainBlockImportMarker + Send + Sync + 'static,
147	PF: Environment<Block>,
148	CS: CollatorServiceInterface<Block>,
149	P: Pair,
150	P::Public: AppPublic + Member,
151	P::Signature: TryFrom<Vec<u8>> + Member + Codec,
152{
153	/// Instantiate a new instance of the `Aura` manager.
154	pub fn new(params: Params<BI, CIDP, RClient, PF, CS>) -> Self {
155		Collator {
156			create_inherent_data_providers: params.create_inherent_data_providers,
157			block_import: params.block_import,
158			relay_client: params.relay_client,
159			keystore: params.keystore,
160			para_id: params.para_id,
161			proposer: params.proposer,
162			collator_service: params.collator_service,
163			_marker: std::marker::PhantomData,
164		}
165	}
166
167	/// Explicitly creates the inherent data for parachain block authoring and overrides
168	/// the timestamp inherent data with the one provided, if any. Additionally, allows to specify
169	/// relay parent descendants that can be used to prevent authoring at the tip of the relay
170	/// chain.
171	pub async fn create_inherent_data_with_rp_offset(
172		&self,
173		relay_parent: PHash,
174		validation_data: &PersistedValidationData,
175		parent_hash: Block::Hash,
176		timestamp: impl Into<Option<Timestamp>>,
177		relay_parent_descendants: Option<RelayParentData>,
178		relay_proof_request: RelayProofRequest,
179		collator_peer_id: PeerId,
180	) -> Result<(ParachainInherentData, InherentData), Box<dyn Error + Send + Sync + 'static>> {
181		let paras_inherent_data = ParachainInherentDataProvider::create_at(
182			relay_parent,
183			&self.relay_client,
184			validation_data,
185			self.para_id,
186			relay_parent_descendants
187				.map(RelayParentData::into_inherent_descendant_list)
188				.unwrap_or_default(),
189			relay_proof_request,
190			collator_peer_id,
191		)
192		.await;
193
194		let paras_inherent_data = match paras_inherent_data {
195			Some(p) => p,
196			None => {
197				return Err(
198					format!("Could not create paras inherent data at {:?}", relay_parent).into()
199				)
200			},
201		};
202
203		let mut other_inherent_data = self
204			.create_inherent_data_providers
205			.create_inherent_data_providers(parent_hash, ())
206			.map_err(|e| e as Box<dyn Error + Send + Sync + 'static>)
207			.await?
208			.create_inherent_data()
209			.await
210			.map_err(Box::new)?;
211
212		if let Some(timestamp) = timestamp.into() {
213			other_inherent_data.replace_data(sp_timestamp::INHERENT_IDENTIFIER, &timestamp);
214		}
215
216		Ok((paras_inherent_data, other_inherent_data))
217	}
218
219	/// Explicitly creates the inherent data for parachain block authoring and overrides
220	/// the timestamp inherent data with the one provided, if any.
221	pub async fn create_inherent_data(
222		&self,
223		relay_parent: PHash,
224		validation_data: &PersistedValidationData,
225		parent_hash: Block::Hash,
226		timestamp: impl Into<Option<Timestamp>>,
227		relay_proof_request: RelayProofRequest,
228		collator_peer_id: PeerId,
229	) -> Result<(ParachainInherentData, InherentData), Box<dyn Error + Send + Sync + 'static>> {
230		self.create_inherent_data_with_rp_offset(
231			relay_parent,
232			validation_data,
233			parent_hash,
234			timestamp,
235			None,
236			relay_proof_request,
237			collator_peer_id,
238		)
239		.await
240	}
241
242	/// Build and import a parachain block using the given parameters.
243	pub async fn build_block_and_import(
244		&mut self,
245		params: BuildBlockAndImportParams<'_, Block, P>,
246	) -> Result<Option<BuiltBlock<Block>>, Box<dyn Error + Send + 'static>> {
247		let Some((built_block, import_block)) = self.build_block(params).await? else {
248			return Ok(None);
249		};
250
251		self.import_block(import_block).await?;
252
253		Ok(Some(built_block))
254	}
255
256	/// Build a parachain block using the given parameters.
257	pub async fn build_block(
258		&mut self,
259		mut params: BuildBlockAndImportParams<'_, Block, P>,
260	) -> Result<
261		Option<(BuiltBlock<Block>, BlockImportParams<Block>)>,
262		Box<dyn Error + Send + 'static>,
263	> {
264		let mut digest = params.additional_pre_digest;
265		digest.push(params.slot_claim.pre_digest.clone());
266
267		// Create the proposer using the factory
268		let proposer = self
269			.proposer
270			.init(&params.parent_header)
271			.await
272			.map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
273
274		// Prepare inherent data - merge parachain inherent data with other inherent data
275		let mut inherent_data_combined = params.extra_inherent_data;
276		params
277			.parachain_inherent_data
278			.provide_inherent_data(&mut inherent_data_combined)
279			.await
280			.map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
281
282		let recorder_passed = params.storage_proof_recorder.is_some();
283		let storage_proof_recorder = params.storage_proof_recorder.unwrap_or_default();
284		let proof_size_ext_registered =
285			params.extra_extensions.is_registered(ProofSizeExt::type_id());
286
287		if !proof_size_ext_registered {
288			params
289				.extra_extensions
290				.register(ProofSizeExt::new(storage_proof_recorder.clone()));
291		} else if !recorder_passed {
292			return Err(
293				Box::from("`ProofSizeExt` registered, but no `storage_proof_recorder` provided. This is a bug.")
294					as Box<dyn Error + Send + Sync>
295			);
296		}
297
298		// Create proposal arguments
299		let propose_args = ProposeArgs {
300			inherent_data: inherent_data_combined,
301			inherent_digests: Digest { logs: digest },
302			max_duration: params.proposal_duration,
303			block_size_limit: Some(params.max_pov_size),
304			extra_extensions: params.extra_extensions,
305			storage_proof_recorder: Some(storage_proof_recorder.clone()),
306		};
307
308		// Propose the block
309		let proposal = proposer
310			.propose(propose_args)
311			.await
312			.map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
313
314		let sealed_importable = seal::<_, P>(
315			proposal.block,
316			proposal.storage_changes,
317			&params.slot_claim.author_pub,
318			&self.keystore,
319		)
320		.map_err(|e| e as Box<dyn Error + Send>)?;
321
322		let block = Block::new(
323			sealed_importable.post_header(),
324			sealed_importable
325				.body
326				.as_ref()
327				.expect("body always created with this `propose` fn; qed")
328				.clone(),
329		);
330
331		let Some(backend_transaction) = sealed_importable
332			.state_action
333			.as_storage_changes()
334			.map(|c| c.transaction.clone())
335		else {
336			tracing::error!(target: crate::LOG_TARGET, "Building a block should return storage changes!");
337
338			return Ok(None);
339		};
340
341		let proof = storage_proof_recorder.drain_storage_proof();
342
343		Ok(Some((BuiltBlock { block, proof, backend_transaction }, sealed_importable)))
344	}
345
346	/// Import the given `import_block`.
347	pub async fn import_block(
348		&mut self,
349		import_block: BlockImportParams<Block>,
350	) -> Result<(), Box<dyn Error + Send + 'static>> {
351		self.block_import
352			.import_block(import_block)
353			.map_err(|e| Box::new(e) as Box<dyn Error + Send>)
354			.await
355			.map(drop)
356	}
357
358	/// Propose, seal, import a block and packaging it into a collation.
359	///
360	/// Provide the slot to build at as well as any other necessary pre-digest logs,
361	/// the inherent data, and the proposal duration and PoV size limits.
362	///
363	/// The Aura pre-digest should not be explicitly provided and is set internally.
364	///
365	/// This does not announce the collation to the parachain network or the relay chain.
366	pub async fn collate(
367		&mut self,
368		parent_header: &Block::Header,
369		slot_claim: &SlotClaim<P::Public>,
370		additional_pre_digest: impl Into<Option<Vec<DigestItem>>>,
371		inherent_data: (ParachainInherentData, InherentData),
372		proposal_duration: Duration,
373		max_pov_size: usize,
374		scheduling_proof: Option<cumulus_primitives_core::SchedulingProof>,
375	) -> Result<Option<(Collation, ParachainBlockData<Block>)>, Box<dyn Error + Send + 'static>> {
376		let maybe_candidate = self
377			.build_block_and_import(BuildBlockAndImportParams {
378				parent_header,
379				slot_claim,
380				additional_pre_digest: additional_pre_digest.into().unwrap_or_default(),
381				parachain_inherent_data: inherent_data.0,
382				extra_inherent_data: inherent_data.1,
383				proposal_duration,
384				max_pov_size,
385				storage_proof_recorder: None,
386				extra_extensions: Default::default(),
387			})
388			.await?;
389
390		let Some(candidate) = maybe_candidate else { return Ok(None) };
391
392		let hash = candidate.block.header().hash();
393		if let Some((collation, block_data)) = self.collator_service.build_collation(
394			parent_header,
395			hash,
396			candidate.into(),
397			scheduling_proof,
398		) {
399			block_data.log_size_info();
400
401			if let MaybeCompressedPoV::Compressed(ref pov) = collation.proof_of_validity {
402				tracing::info!(
403					target: crate::LOG_TARGET,
404					"Compressed PoV size: {}kb",
405					pov.block_data.0.len() as f64 / 1024f64,
406				);
407			}
408
409			Ok(Some((collation, block_data)))
410		} else {
411			Err(Box::<dyn Error + Send + Sync>::from("Unable to produce collation"))
412		}
413	}
414
415	/// Get the underlying collator service.
416	pub fn collator_service(&self) -> &CS {
417		&self.collator_service
418	}
419}
420
421/// A claim on an Aura slot.
422pub struct SlotClaim<Pub> {
423	author_pub: Pub,
424	pre_digest: DigestItem,
425	slot: Slot,
426	timestamp: Timestamp,
427}
428
429impl<Pub> SlotClaim<Pub> {
430	/// Create a slot-claim from the given author public key, slot, and timestamp.
431	///
432	/// This does not check whether the author actually owns the slot or the timestamp
433	/// falls within the slot.
434	pub fn unchecked<P>(author_pub: Pub, slot: Slot, timestamp: Timestamp) -> Self
435	where
436		P: Pair<Public = Pub>,
437		P::Public: Codec,
438		P::Signature: Codec,
439	{
440		SlotClaim { author_pub, timestamp, pre_digest: aura_internal::pre_digest::<P>(slot), slot }
441	}
442
443	/// Get the author's public key.
444	pub fn author_pub(&self) -> &Pub {
445		&self.author_pub
446	}
447
448	/// Get the Aura pre-digest for this slot.
449	pub fn pre_digest(&self) -> &DigestItem {
450		&self.pre_digest
451	}
452
453	/// Get the slot assigned to this claim.
454	pub fn slot(&self) -> Slot {
455		self.slot
456	}
457
458	/// Get the timestamp corresponding to the relay-chain slot this claim was
459	/// generated against.
460	pub fn timestamp(&self) -> Timestamp {
461		self.timestamp
462	}
463}
464
465/// Attempt to claim a slot derived from the given relay-parent header's slot.
466pub async fn claim_slot<B, C, P>(
467	client: &C,
468	parent_hash: B::Hash,
469	relay_parent_header: &PHeader,
470	slot_duration: SlotDuration,
471	relay_chain_slot_duration: Duration,
472	keystore: &KeystorePtr,
473) -> Result<Option<SlotClaim<P::Public>>, Box<dyn Error>>
474where
475	B: BlockT,
476	C: ProvideRuntimeApi<B> + Send + Sync + 'static,
477	C::Api: AuraApi<B, P::Public>,
478	P: Pair,
479	P::Public: Codec,
480	P::Signature: Codec,
481{
482	let mut runtime_api = client.runtime_api();
483	runtime_api.set_call_context(sp_core::traits::CallContext::Onchain { import: false });
484	let authorities = runtime_api.authorities(parent_hash).map_err(Box::new)?;
485
486	// Determine the current slot and timestamp based on the relay-parent's.
487	let (slot_now, timestamp) = match consensus_common::get_relay_slot_and_timestamp(
488		relay_parent_header,
489		relay_chain_slot_duration,
490	) {
491		Some((r_s, t)) => {
492			let our_slot = Slot::from_timestamp(t, slot_duration);
493			tracing::debug!(
494				target: crate::LOG_TARGET,
495				relay_slot = ?r_s,
496				para_slot = ?our_slot,
497				timestamp = ?t,
498				?slot_duration,
499				?relay_chain_slot_duration,
500				"Adjusted relay-chain slot to parachain slot"
501			);
502			(our_slot, t)
503		},
504		None => return Ok(None),
505	};
506
507	// Try to claim the slot locally.
508	let author_pub = {
509		let res = aura_internal::claim_slot::<P>(slot_now, &authorities, keystore).await;
510		match res {
511			Some(p) => p,
512			None => return Ok(None),
513		}
514	};
515
516	Ok(Some(SlotClaim::unchecked::<P>(author_pub, slot_now, timestamp)))
517}
518
519/// Seal a block with a signature in the header.
520pub fn seal<B: BlockT, P>(
521	pre_sealed: B,
522	storage_changes: StorageChanges<HashingFor<B>>,
523	author_pub: &P::Public,
524	keystore: &KeystorePtr,
525) -> Result<BlockImportParams<B>, Box<dyn Error + Send + Sync + 'static>>
526where
527	P: Pair,
528	P::Signature: Codec + TryFrom<Vec<u8>>,
529	P::Public: AppPublic,
530{
531	let (pre_header, body) = pre_sealed.deconstruct();
532	let pre_hash = pre_header.hash();
533	let block_number = *pre_header.number();
534
535	// seal the block.
536	let block_import_params = {
537		let seal_digest =
538			aura_internal::seal::<_, P>(&pre_hash, &author_pub, keystore).map_err(Box::new)?;
539		let mut block_import_params = BlockImportParams::new(BlockOrigin::Own, pre_header);
540		block_import_params.post_digests.push(seal_digest);
541		block_import_params.body = Some(body);
542		block_import_params.state_action =
543			StateAction::ApplyChanges(sc_consensus::StorageChanges::Changes(storage_changes));
544		block_import_params.fork_choice = Some(ForkChoiceStrategy::LongestChain);
545		block_import_params
546	};
547	let post_hash = block_import_params.post_hash();
548
549	tracing::info!(
550		target: crate::LOG_TARGET,
551		"๐Ÿ”– Pre-sealed block for proposal at {}. Hash now {:?}, previously {:?}.",
552		block_number,
553		post_hash,
554		pre_hash,
555	);
556
557	Ok(block_import_params)
558}