// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute // This file is part of Pezcumulus. // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 // Pezcumulus is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Pezcumulus is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Pezcumulus. If not, see . //! A collator for Aura that looks ahead of the most recently included teyrchain block //! when determining what to build upon. //! //! This collator also builds additional blocks when the maximum backlog is not saturated. //! The size of the backlog is determined by invoking a runtime API. If that runtime API //! is not supported, this assumes a maximum backlog size of 1. //! //! This takes more advantage of asynchronous backing, though not complete advantage. //! When the backlog is not saturated, this approach lets the backlog temporarily 'catch up' //! with periods of higher throughput. When the backlog is saturated, we typically //! fall back to the limited cadence of a single teyrchain block per relay-chain block. //! //! Despite this, the fact that there is a backlog at all allows us to spend more time //! building the block, as there is some buffer before it can get posted to the relay-chain. //! The main limitation is block propagation time - i.e. the new blocks created by an author //! must be propagated to the next author before their turn. use codec::{Codec, Encode}; use pezcumulus_client_collator::service::ServiceInterface as CollatorServiceInterface; use pezcumulus_client_consensus_common::{self as consensus_common, TeyrchainBlockImportMarker}; use pezcumulus_client_consensus_proposer::ProposerInterface; use pezcumulus_primitives_aura::AuraUnincludedSegmentApi; use pezcumulus_primitives_core::{CollectCollationInfo, PersistedValidationData}; use pezcumulus_relay_chain_interface::RelayChainInterface; use pezkuwi_node_subsystem::messages::CollationGenerationMessage; use pezkuwi_overseer::Handle as OverseerHandle; use pezkuwi_pez_node_primitives::SubmitCollationParams; use pezkuwi_primitives::{CollatorPair, Id as ParaId, OccupiedCoreAssumption}; use crate::{ collator as collator_util, collators::{claim_queue_at, BackingGroupConnectionHelper}, export_pov_to_path, }; use futures::prelude::*; use pezsc_client_api::{backend::AuxStore, BlockBackend, BlockOf}; use pezsc_consensus::BlockImport; use pezsc_network_types::PeerId; use pezsp_api::ProvideRuntimeApi; use pezsp_application_crypto::AppPublic; use pezsp_blockchain::HeaderBackend; use pezsp_consensus_aura::{AuraApi, Slot}; use pezsp_core::crypto::Pair; use pezsp_inherents::CreateInherentDataProviders; use pezsp_keystore::KeystorePtr; use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, Member}; use pezsp_timestamp::Timestamp; use std::{path::PathBuf, sync::Arc, time::Duration}; /// Parameters for [`run`]. pub struct Params { /// Inherent data providers. Only non-consensus inherent data should be provided, i.e. /// the timestamp, slot, and paras inherents should be omitted, as they are set by this /// collator. pub create_inherent_data_providers: CIDP, /// Used to actually import blocks. pub block_import: BI, /// The underlying para client. pub para_client: Arc, /// The para client's backend, used to access the database. pub para_backend: Arc, /// A handle to the relay-chain client. pub relay_client: RClient, /// A validation code hash provider, used to get the current validation code hash. pub code_hash_provider: CHP, /// The underlying keystore, which should contain Aura consensus keys. pub keystore: KeystorePtr, /// The collator key used to sign collations before submitting to validators. pub collator_key: CollatorPair, /// The collator network peer id. pub collator_peer_id: PeerId, /// The para's ID. pub para_id: ParaId, /// A handle to the relay-chain client's "Overseer" or task orchestrator. pub overseer_handle: OverseerHandle, /// The length of slots in the relay chain. pub relay_chain_slot_duration: Duration, /// The underlying block proposer this should call into. pub proposer: Proposer, /// The generic collator service used to plug into this consensus engine. pub collator_service: CS, /// The amount of time to spend authoring each block. pub authoring_duration: Duration, /// Whether we should reinitialize the collator config (i.e. we are transitioning to aura). pub reinitialize: bool, /// The maximum percentage of the maximum PoV size that the collator can use. /// It will be removed once is fixed. pub max_pov_percentage: Option, } /// Get the current teyrchain slot from a given block hash. /// /// Returns the teyrchain slot, relay chain slot, and timestamp. fn get_teyrchain_slot( para_client: &Client, block_hash: Block::Hash, relay_parent_header: &pezkuwi_primitives::Header, relay_chain_slot_duration: Duration, ) -> Option<(Slot, Slot, Timestamp)> where Block: BlockT, Client: ProvideRuntimeApi, Client::Api: AuraApi, P: Codec, { let slot_duration = match pezsc_consensus_aura::standalone::slot_duration_at(para_client, block_hash) { Ok(sd) => sd, Err(err) => { tracing::error!(target: crate::LOG_TARGET, ?err, "Failed to acquire teyrchain slot duration"); return None; }, }; tracing::debug!(target: crate::LOG_TARGET, ?slot_duration, ?block_hash, "Teyrchain slot duration acquired"); let (relay_slot, timestamp) = consensus_common::relay_slot_and_timestamp(relay_parent_header, relay_chain_slot_duration)?; let slot_now = Slot::from_timestamp(timestamp, slot_duration); tracing::debug!( target: crate::LOG_TARGET, ?relay_slot, para_slot = ?slot_now, ?timestamp, ?slot_duration, ?relay_chain_slot_duration, "Adjusted relay-chain slot to teyrchain slot" ); Some((slot_now, relay_slot, timestamp)) } /// Run async-backing-friendly Aura. pub fn run( params: Params, ) -> impl Future + Send + 'static where Block: BlockT, Client: ProvideRuntimeApi + BlockOf + AuxStore + HeaderBackend + BlockBackend + Send + Sync + 'static, Client::Api: AuraApi + CollectCollationInfo + AuraUnincludedSegmentApi, Backend: pezsc_client_api::Backend + 'static, RClient: RelayChainInterface + Clone + 'static, CIDP: CreateInherentDataProviders + 'static, CIDP::InherentDataProviders: Send, BI: BlockImport + TeyrchainBlockImportMarker + Send + Sync + 'static, Proposer: ProposerInterface + Send + Sync + 'static, CS: CollatorServiceInterface + Send + Sync + 'static, CHP: consensus_common::ValidationCodeHashProvider + Send + 'static, P: Pair + Send + Sync + 'static, P::Public: AppPublic + Member + Codec, P::Signature: TryFrom> + Member + Codec, { run_with_export::<_, P, _, _, _, _, _, _, _, _>(ParamsWithExport { params, export_pov: None }) } /// Parameters for [`run_with_export`]. pub struct ParamsWithExport { /// The parameters. pub params: Params, /// When set, the collator will export every produced `POV` to this folder. pub export_pov: Option, } /// Run async-backing-friendly Aura. /// /// This is exactly the same as [`run`], but it supports the optional export of each produced `POV` /// to the file system. pub fn run_with_export( ParamsWithExport { mut params, export_pov }: ParamsWithExport< BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS, >, ) -> impl Future + Send + 'static where Block: BlockT, Client: ProvideRuntimeApi + BlockOf + AuxStore + HeaderBackend + BlockBackend + Send + Sync + 'static, Client::Api: AuraApi + CollectCollationInfo + AuraUnincludedSegmentApi, Backend: pezsc_client_api::Backend + 'static, RClient: RelayChainInterface + Clone + 'static, CIDP: CreateInherentDataProviders + 'static, CIDP::InherentDataProviders: Send, BI: BlockImport + TeyrchainBlockImportMarker + Send + Sync + 'static, Proposer: ProposerInterface + Send + Sync + 'static, CS: CollatorServiceInterface + Send + Sync + 'static, CHP: consensus_common::ValidationCodeHashProvider + Send + 'static, P: Pair + Send + Sync + 'static, P::Public: AppPublic + Member + Codec, P::Signature: TryFrom> + Member + Codec, { async move { pezcumulus_client_collator::initialize_collator_subsystems( &mut params.overseer_handle, params.collator_key, params.para_id, params.reinitialize, ) .await; let mut import_notifications = match params.relay_client.import_notification_stream().await { Ok(s) => s, Err(err) => { tracing::error!( target: crate::LOG_TARGET, ?err, "Failed to initialize consensus: no relay chain import notification stream" ); return; }, }; let mut collator = { let params = collator_util::Params { create_inherent_data_providers: params.create_inherent_data_providers, block_import: params.block_import, relay_client: params.relay_client.clone(), keystore: params.keystore.clone(), collator_peer_id: params.collator_peer_id, para_id: params.para_id, proposer: params.proposer, collator_service: params.collator_service, }; collator_util::Collator::::new(params) }; let mut connection_helper = BackingGroupConnectionHelper::new( params.keystore.clone(), params.overseer_handle.clone(), ); while let Some(relay_parent_header) = import_notifications.next().await { let relay_parent = relay_parent_header.hash(); let Some(core_index) = claim_queue_at(relay_parent, &mut params.relay_client) .await .iter_claims_at_depth_for_para(0, params.para_id) .next() else { tracing::trace!( target: crate::LOG_TARGET, ?relay_parent, ?params.para_id, "Para is not scheduled on any core, skipping import notification", ); continue; }; let max_pov_size = match params .relay_client .persisted_validation_data( relay_parent, params.para_id, OccupiedCoreAssumption::Included, ) .await { Ok(None) => continue, Ok(Some(pvd)) => pvd.max_pov_size, Err(err) => { tracing::error!(target: crate::LOG_TARGET, ?err, "Failed to gather information from relay-client"); continue; }, }; let (included_block, initial_parent) = match crate::collators::find_parent( relay_parent, params.para_id, &*params.para_backend, ¶ms.relay_client, ) .await { Some(value) => value, None => continue, }; let para_client = &*params.para_client; let keystore = ¶ms.keystore; let can_build_upon = |block_hash| { let (slot_now, relay_slot, timestamp) = get_teyrchain_slot::<_, _, P::Public>( para_client, block_hash, &relay_parent_header, params.relay_chain_slot_duration, )?; Some(super::can_build_upon::<_, _, P>( slot_now, relay_slot, timestamp, block_hash, included_block.hash(), para_client, &keystore, )) }; // Build in a loop until not allowed. Note that the authorities can change // at any block, so we need to re-claim our slot every time. let mut parent_hash = initial_parent.hash; let mut parent_header = initial_parent.header; let overseer_handle = &mut params.overseer_handle; // Do not try to build upon an unknown, pruned or bad block if !collator.collator_service().check_block_status(parent_hash, &parent_header) { continue; } // Trigger pre-conect to backing groups if necessary. if let (Some((slot_now, _relay_slot, _timestamp)), Ok(authorities)) = ( get_teyrchain_slot::<_, _, P::Public>( para_client, parent_hash, &relay_parent_header, params.relay_chain_slot_duration, ), para_client.runtime_api().authorities(parent_hash), ) { connection_helper.update::

(slot_now, &authorities).await; } // This needs to change to support elastic scaling, but for continuously // scheduled chains this ensures that the backlog will grow steadily. for n_built in 0..2 { let slot_claim = match can_build_upon(parent_hash) { Some(fut) => match fut.await { None => break, Some(c) => c, }, None => break, }; tracing::debug!( target: crate::LOG_TARGET, ?relay_parent, unincluded_segment_len = initial_parent.depth + n_built, "Slot claimed. Building" ); let validation_data = PersistedValidationData { parent_head: parent_header.encode().into(), relay_parent_number: *relay_parent_header.number(), relay_parent_storage_root: *relay_parent_header.state_root(), max_pov_size, }; // Build and announce collations recursively until // `can_build_upon` fails or building a collation fails. let (teyrchain_inherent_data, other_inherent_data) = match collator .create_inherent_data( relay_parent, &validation_data, parent_hash, slot_claim.timestamp(), params.collator_peer_id, ) .await { Err(err) => { tracing::error!(target: crate::LOG_TARGET, ?err); break; }, Ok(x) => x, }; let Some(validation_code_hash) = params.code_hash_provider.code_hash_at(parent_hash) else { tracing::error!(target: crate::LOG_TARGET, ?parent_hash, "Could not fetch validation code hash"); break; }; super::check_validation_code_or_log( &validation_code_hash, params.para_id, ¶ms.relay_client, relay_parent, ) .await; let allowed_pov_size = if let Some(max_pov_percentage) = params.max_pov_percentage { validation_data.max_pov_size * max_pov_percentage / 100 } else { // Set the block limit to 85% of the maximum PoV size. // // Once https://github.com/pezkuwichain/pezkuwi-sdk/issues/193 issue is // fixed, the reservation should be removed. validation_data.max_pov_size * 85 / 100 } as usize; match collator .collate( &parent_header, &slot_claim, None, (teyrchain_inherent_data, other_inherent_data), params.authoring_duration, allowed_pov_size, ) .await { Ok(Some((collation, block_data))) => { let Some(new_block_header) = block_data.blocks().first().map(|b| b.header().clone()) else { tracing::error!(target: crate::LOG_TARGET, "Produced PoV doesn't contain any blocks"); break; }; let new_block_hash = new_block_header.hash(); // Here we are assuming that the import logic protects against equivocations // and provides sybil-resistance, as it should. collator.collator_service().announce_block(new_block_hash, None); if let Some(ref export_pov) = export_pov { export_pov_to_path::( export_pov.clone(), collation.proof_of_validity.clone().into_compressed(), new_block_hash, *new_block_header.number(), parent_header.clone(), *relay_parent_header.state_root(), *relay_parent_header.number(), validation_data.max_pov_size, ); } // Send a submit-collation message to the collation generation subsystem, // which then distributes this to validators. // // Here we are assuming that the leaf is imported, as we've gotten an // import notification. overseer_handle .send_msg( CollationGenerationMessage::SubmitCollation( SubmitCollationParams { relay_parent, collation, parent_head: parent_header.encode().into(), validation_code_hash, result_sender: None, core_index, }, ), "SubmitCollation", ) .await; parent_hash = new_block_hash; parent_header = new_block_header; }, Ok(None) => { tracing::debug!(target: crate::LOG_TARGET, "No block proposal"); break; }, Err(err) => { tracing::error!(target: crate::LOG_TARGET, ?err); break; }, } } } } }