feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
[package]
|
||||
name = "pezcumulus-client-consensus-aura"
|
||||
description = "AURA consensus algorithm for teyrchains"
|
||||
version = "0.7.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
codec = { features = ["derive"], workspace = true, default-features = true }
|
||||
futures = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
schnellru = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros"] }
|
||||
tracing = { workspace = true, default-features = true }
|
||||
|
||||
# Bizinikiwi
|
||||
prometheus-endpoint = { workspace = true, default-features = true }
|
||||
pezsc-client-api = { workspace = true, default-features = true }
|
||||
pezsc-consensus = { workspace = true, default-features = true }
|
||||
pezsc-consensus-aura = { workspace = true, default-features = true }
|
||||
pezsc-consensus-babe = { workspace = true, default-features = true }
|
||||
pezsc-consensus-slots = { workspace = true, default-features = true }
|
||||
pezsc-network-types = { workspace = true, default-features = true }
|
||||
pezsc-telemetry = { workspace = true, default-features = true }
|
||||
pezsc-utils = { workspace = true, default-features = true }
|
||||
pezsp-api = { workspace = true, default-features = true }
|
||||
pezsp-application-crypto = { workspace = true, default-features = true }
|
||||
pezsp-block-builder = { workspace = true, default-features = true }
|
||||
pezsp-blockchain = { workspace = true, default-features = true }
|
||||
pezsp-consensus = { workspace = true, default-features = true }
|
||||
pezsp-consensus-aura = { workspace = true, default-features = true }
|
||||
pezsp-core = { workspace = true, default-features = true }
|
||||
pezsp-inherents = { workspace = true, default-features = true }
|
||||
pezsp-keystore = { workspace = true, default-features = true }
|
||||
pezsp-runtime = { workspace = true, default-features = true }
|
||||
pezsp-state-machine = { workspace = true, default-features = true }
|
||||
pezsp-timestamp = { workspace = true, default-features = true }
|
||||
pezsp-trie = { workspace = true, default-features = true }
|
||||
|
||||
# Pezcumulus
|
||||
pezcumulus-client-collator = { workspace = true, default-features = true }
|
||||
pezcumulus-client-consensus-common = { workspace = true, default-features = true }
|
||||
pezcumulus-client-consensus-proposer = { workspace = true, default-features = true }
|
||||
pezcumulus-client-teyrchain-inherent = { workspace = true, default-features = true }
|
||||
pezcumulus-primitives-aura = { workspace = true, default-features = true }
|
||||
pezcumulus-primitives-core = { workspace = true, default-features = true }
|
||||
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
|
||||
|
||||
# Pezkuwi
|
||||
pezkuwi-node-primitives = { workspace = true, default-features = true }
|
||||
pezkuwi-node-subsystem = { workspace = true, default-features = true }
|
||||
pezkuwi-node-subsystem-util = { workspace = true, default-features = true }
|
||||
pezkuwi-overseer = { workspace = true, default-features = true }
|
||||
pezkuwi-primitives = { workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezcumulus-test-client = { workspace = true }
|
||||
pezcumulus-test-relay-sproof-builder = { workspace = true }
|
||||
rstest = { workspace = true }
|
||||
pezsp-keyring = { workspace = true }
|
||||
pezsp-tracing = { workspace = true }
|
||||
pezsp-version = { workspace = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezcumulus-client-collator/runtime-benchmarks",
|
||||
"pezcumulus-client-consensus-common/runtime-benchmarks",
|
||||
"pezcumulus-client-consensus-proposer/runtime-benchmarks",
|
||||
"pezcumulus-client-teyrchain-inherent/runtime-benchmarks",
|
||||
"pezcumulus-primitives-aura/runtime-benchmarks",
|
||||
"pezcumulus-primitives-core/runtime-benchmarks",
|
||||
"pezcumulus-relay-chain-interface/runtime-benchmarks",
|
||||
"pezcumulus-test-client/runtime-benchmarks",
|
||||
"pezcumulus-test-relay-sproof-builder/runtime-benchmarks",
|
||||
"pezkuwi-node-primitives/runtime-benchmarks",
|
||||
"pezkuwi-node-subsystem-util/runtime-benchmarks",
|
||||
"pezkuwi-node-subsystem/runtime-benchmarks",
|
||||
"pezkuwi-overseer/runtime-benchmarks",
|
||||
"pezkuwi-primitives/runtime-benchmarks",
|
||||
"pezsc-client-api/runtime-benchmarks",
|
||||
"pezsc-consensus-aura/runtime-benchmarks",
|
||||
"pezsc-consensus-babe/runtime-benchmarks",
|
||||
"pezsc-consensus-slots/runtime-benchmarks",
|
||||
"pezsc-consensus/runtime-benchmarks",
|
||||
"pezsp-api/runtime-benchmarks",
|
||||
"pezsp-block-builder/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus-aura/runtime-benchmarks",
|
||||
"pezsp-consensus/runtime-benchmarks",
|
||||
"pezsp-inherents/runtime-benchmarks",
|
||||
"pezsp-keyring/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"pezsp-state-machine/runtime-benchmarks",
|
||||
"pezsp-timestamp/runtime-benchmarks",
|
||||
"pezsp-trie/runtime-benchmarks",
|
||||
"pezsp-version/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,442 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! The core collator logic for Aura - slot claiming, block proposing, and collation
|
||||
//! packaging.
|
||||
//!
|
||||
//! The [`Collator`] struct exposed here is meant to be a component of higher-level logic
|
||||
//! which actually manages the control flow of the collator - which slots to claim, how
|
||||
//! many collations to build, when to work, etc.
|
||||
//!
|
||||
//! This module also exposes some standalone functions for common operations when building
|
||||
//! aura-based collators.
|
||||
|
||||
use codec::Codec;
|
||||
use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
|
||||
use cumulus_client_consensus_common::{
|
||||
self as consensus_common, TeyrchainBlockImportMarker, TeyrchainCandidate,
|
||||
};
|
||||
use cumulus_client_consensus_proposer::ProposerInterface;
|
||||
use cumulus_client_teyrchain_inherent::{TeyrchainInherentData, TeyrchainInherentDataProvider};
|
||||
use cumulus_primitives_core::{
|
||||
relay_chain::Hash as PHash, DigestItem, PersistedValidationData, TeyrchainBlockData,
|
||||
};
|
||||
use cumulus_relay_chain_interface::RelayChainInterface;
|
||||
|
||||
use pezkuwi_node_primitives::{Collation, MaybeCompressedPoV};
|
||||
use pezkuwi_primitives::{Header as PHeader, Id as ParaId};
|
||||
|
||||
use crate::collators::RelayParentData;
|
||||
use futures::prelude::*;
|
||||
use pezsc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy, StateAction};
|
||||
use pezsc_consensus_aura::standalone as aura_internal;
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_application_crypto::AppPublic;
|
||||
use pezsp_consensus::BlockOrigin;
|
||||
use pezsp_consensus_aura::{AuraApi, Slot, SlotDuration};
|
||||
use pezsp_core::crypto::Pair;
|
||||
use pezsp_inherents::{CreateInherentDataProviders, InherentData, InherentDataProvider};
|
||||
use pezsp_keystore::KeystorePtr;
|
||||
use pezsp_runtime::{
|
||||
generic::Digest,
|
||||
traits::{Block as BlockT, HashingFor, Header as HeaderT, Member},
|
||||
};
|
||||
use pezsp_state_machine::StorageChanges;
|
||||
use pezsp_timestamp::Timestamp;
|
||||
use std::{error::Error, time::Duration};
|
||||
|
||||
/// Parameters for instantiating a [`Collator`].
|
||||
pub struct Params<BI, CIDP, RClient, Proposer, CS> {
|
||||
/// A builder for inherent data builders.
|
||||
pub create_inherent_data_providers: CIDP,
|
||||
/// The block import handle.
|
||||
pub block_import: BI,
|
||||
/// An interface to the relay-chain client.
|
||||
pub relay_client: RClient,
|
||||
/// The keystore handle used for accessing teyrchain key material.
|
||||
pub keystore: KeystorePtr,
|
||||
/// The collator network peer id.
|
||||
pub collator_peer_id: PeerId,
|
||||
/// The identifier of the teyrchain within the relay-chain.
|
||||
pub para_id: ParaId,
|
||||
/// The block proposer used for building blocks.
|
||||
pub proposer: Proposer,
|
||||
/// The collator service used for bundling proposals into collations and announcing
|
||||
/// to the network.
|
||||
pub collator_service: CS,
|
||||
}
|
||||
|
||||
/// A utility struct for writing collation logic that makes use of Aura entirely
|
||||
/// or in part. See module docs for more details.
|
||||
pub struct Collator<Block, P, BI, CIDP, RClient, Proposer, CS> {
|
||||
create_inherent_data_providers: CIDP,
|
||||
block_import: BI,
|
||||
relay_client: RClient,
|
||||
keystore: KeystorePtr,
|
||||
para_id: ParaId,
|
||||
proposer: Proposer,
|
||||
collator_service: CS,
|
||||
_marker: std::marker::PhantomData<(Block, Box<dyn Fn(P) + Send + Sync + 'static>)>,
|
||||
}
|
||||
|
||||
impl<Block, P, BI, CIDP, RClient, Proposer, CS> Collator<Block, P, BI, CIDP, RClient, Proposer, CS>
|
||||
where
|
||||
Block: BlockT,
|
||||
RClient: RelayChainInterface,
|
||||
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
|
||||
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
|
||||
Proposer: ProposerInterface<Block>,
|
||||
CS: CollatorServiceInterface<Block>,
|
||||
P: Pair,
|
||||
P::Public: AppPublic + Member,
|
||||
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
|
||||
{
|
||||
/// Instantiate a new instance of the `Aura` manager.
|
||||
pub fn new(params: Params<BI, CIDP, RClient, Proposer, CS>) -> Self {
|
||||
Collator {
|
||||
create_inherent_data_providers: params.create_inherent_data_providers,
|
||||
block_import: params.block_import,
|
||||
relay_client: params.relay_client,
|
||||
keystore: params.keystore,
|
||||
para_id: params.para_id,
|
||||
proposer: params.proposer,
|
||||
collator_service: params.collator_service,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Explicitly creates the inherent data for teyrchain block authoring and overrides
|
||||
/// the timestamp inherent data with the one provided, if any. Additionally allows to specify
|
||||
/// relay parent descendants that can be used to prevent authoring at the tip of the relay
|
||||
/// chain.
|
||||
pub async fn create_inherent_data_with_rp_offset(
|
||||
&self,
|
||||
relay_parent: PHash,
|
||||
validation_data: &PersistedValidationData,
|
||||
parent_hash: Block::Hash,
|
||||
timestamp: impl Into<Option<Timestamp>>,
|
||||
relay_parent_descendants: Option<RelayParentData>,
|
||||
collator_peer_id: PeerId,
|
||||
) -> Result<(TeyrchainInherentData, InherentData), Box<dyn Error + Send + Sync + 'static>> {
|
||||
let paras_inherent_data = TeyrchainInherentDataProvider::create_at(
|
||||
relay_parent,
|
||||
&self.relay_client,
|
||||
validation_data,
|
||||
self.para_id,
|
||||
relay_parent_descendants
|
||||
.map(RelayParentData::into_inherent_descendant_list)
|
||||
.unwrap_or_default(),
|
||||
Vec::new(),
|
||||
collator_peer_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
let paras_inherent_data = match paras_inherent_data {
|
||||
Some(p) => p,
|
||||
None =>
|
||||
return Err(
|
||||
format!("Could not create paras inherent data at {:?}", relay_parent).into()
|
||||
),
|
||||
};
|
||||
|
||||
let mut other_inherent_data = self
|
||||
.create_inherent_data_providers
|
||||
.create_inherent_data_providers(parent_hash, ())
|
||||
.map_err(|e| e as Box<dyn Error + Send + Sync + 'static>)
|
||||
.await?
|
||||
.create_inherent_data()
|
||||
.await
|
||||
.map_err(Box::new)?;
|
||||
|
||||
if let Some(timestamp) = timestamp.into() {
|
||||
other_inherent_data.replace_data(pezsp_timestamp::INHERENT_IDENTIFIER, ×tamp);
|
||||
}
|
||||
|
||||
Ok((paras_inherent_data, other_inherent_data))
|
||||
}
|
||||
|
||||
/// Explicitly creates the inherent data for teyrchain block authoring and overrides
|
||||
/// the timestamp inherent data with the one provided, if any.
|
||||
pub async fn create_inherent_data(
|
||||
&self,
|
||||
relay_parent: PHash,
|
||||
validation_data: &PersistedValidationData,
|
||||
parent_hash: Block::Hash,
|
||||
timestamp: impl Into<Option<Timestamp>>,
|
||||
collator_peer_id: PeerId,
|
||||
) -> Result<(TeyrchainInherentData, InherentData), Box<dyn Error + Send + Sync + 'static>> {
|
||||
self.create_inherent_data_with_rp_offset(
|
||||
relay_parent,
|
||||
validation_data,
|
||||
parent_hash,
|
||||
timestamp,
|
||||
None,
|
||||
collator_peer_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Build and import a teyrchain block on the given parent header, using the given slot claim.
|
||||
pub async fn build_block_and_import(
|
||||
&mut self,
|
||||
parent_header: &Block::Header,
|
||||
slot_claim: &SlotClaim<P::Public>,
|
||||
additional_pre_digest: impl Into<Option<Vec<DigestItem>>>,
|
||||
inherent_data: (TeyrchainInherentData, InherentData),
|
||||
proposal_duration: Duration,
|
||||
max_pov_size: usize,
|
||||
) -> Result<Option<TeyrchainCandidate<Block>>, Box<dyn Error + Send + 'static>> {
|
||||
let mut digest = additional_pre_digest.into().unwrap_or_default();
|
||||
digest.push(slot_claim.pre_digest.clone());
|
||||
|
||||
let maybe_proposal = self
|
||||
.proposer
|
||||
.propose(
|
||||
&parent_header,
|
||||
&inherent_data.0,
|
||||
inherent_data.1,
|
||||
Digest { logs: digest },
|
||||
proposal_duration,
|
||||
Some(max_pov_size),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
|
||||
|
||||
let proposal = match maybe_proposal {
|
||||
None => return Ok(None),
|
||||
Some(p) => p,
|
||||
};
|
||||
|
||||
let sealed_importable = seal::<_, P>(
|
||||
proposal.block,
|
||||
proposal.storage_changes,
|
||||
&slot_claim.author_pub,
|
||||
&self.keystore,
|
||||
)
|
||||
.map_err(|e| e as Box<dyn Error + Send>)?;
|
||||
|
||||
let block = Block::new(
|
||||
sealed_importable.post_header(),
|
||||
sealed_importable
|
||||
.body
|
||||
.as_ref()
|
||||
.expect("body always created with this `propose` fn; qed")
|
||||
.clone(),
|
||||
);
|
||||
|
||||
self.block_import
|
||||
.import_block(sealed_importable)
|
||||
.map_err(|e| Box::new(e) as Box<dyn Error + Send>)
|
||||
.await?;
|
||||
|
||||
Ok(Some(TeyrchainCandidate { block, proof: proposal.proof }))
|
||||
}
|
||||
|
||||
/// Propose, seal, import a block and packaging it into a collation.
|
||||
///
|
||||
/// Provide the slot to build at as well as any other necessary pre-digest logs,
|
||||
/// the inherent data, and the proposal duration and PoV size limits.
|
||||
///
|
||||
/// The Aura pre-digest should not be explicitly provided and is set internally.
|
||||
///
|
||||
/// This does not announce the collation to the teyrchain network or the relay chain.
|
||||
pub async fn collate(
|
||||
&mut self,
|
||||
parent_header: &Block::Header,
|
||||
slot_claim: &SlotClaim<P::Public>,
|
||||
additional_pre_digest: impl Into<Option<Vec<DigestItem>>>,
|
||||
inherent_data: (TeyrchainInherentData, InherentData),
|
||||
proposal_duration: Duration,
|
||||
max_pov_size: usize,
|
||||
) -> Result<Option<(Collation, TeyrchainBlockData<Block>)>, Box<dyn Error + Send + 'static>> {
|
||||
let maybe_candidate = self
|
||||
.build_block_and_import(
|
||||
parent_header,
|
||||
slot_claim,
|
||||
additional_pre_digest,
|
||||
inherent_data,
|
||||
proposal_duration,
|
||||
max_pov_size,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Some(candidate) = maybe_candidate else { return Ok(None) };
|
||||
|
||||
let hash = candidate.block.header().hash();
|
||||
if let Some((collation, block_data)) =
|
||||
self.collator_service.build_collation(parent_header, hash, candidate)
|
||||
{
|
||||
block_data.log_size_info();
|
||||
|
||||
if let MaybeCompressedPoV::Compressed(ref pov) = collation.proof_of_validity {
|
||||
tracing::info!(
|
||||
target: crate::LOG_TARGET,
|
||||
"Compressed PoV size: {}kb",
|
||||
pov.block_data.0.len() as f64 / 1024f64,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Some((collation, block_data)))
|
||||
} else {
|
||||
Err(Box::<dyn Error + Send + Sync>::from("Unable to produce collation"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the underlying collator service.
|
||||
pub fn collator_service(&self) -> &CS {
|
||||
&self.collator_service
|
||||
}
|
||||
}
|
||||
|
||||
/// A claim on an Aura slot.
|
||||
pub struct SlotClaim<Pub> {
|
||||
author_pub: Pub,
|
||||
pre_digest: DigestItem,
|
||||
slot: Slot,
|
||||
timestamp: Timestamp,
|
||||
}
|
||||
|
||||
impl<Pub> SlotClaim<Pub> {
|
||||
/// Create a slot-claim from the given author public key, slot, and timestamp.
|
||||
///
|
||||
/// This does not check whether the author actually owns the slot or the timestamp
|
||||
/// falls within the slot.
|
||||
pub fn unchecked<P>(author_pub: Pub, slot: Slot, timestamp: Timestamp) -> Self
|
||||
where
|
||||
P: Pair<Public = Pub>,
|
||||
P::Public: Codec,
|
||||
P::Signature: Codec,
|
||||
{
|
||||
SlotClaim { author_pub, timestamp, pre_digest: aura_internal::pre_digest::<P>(slot), slot }
|
||||
}
|
||||
|
||||
/// Get the author's public key.
|
||||
pub fn author_pub(&self) -> &Pub {
|
||||
&self.author_pub
|
||||
}
|
||||
|
||||
/// Get the Aura pre-digest for this slot.
|
||||
pub fn pre_digest(&self) -> &DigestItem {
|
||||
&self.pre_digest
|
||||
}
|
||||
|
||||
/// Get the slot assigned to this claim.
|
||||
pub fn slot(&self) -> Slot {
|
||||
self.slot
|
||||
}
|
||||
|
||||
/// Get the timestamp corresponding to the relay-chain slot this claim was
|
||||
/// generated against.
|
||||
pub fn timestamp(&self) -> Timestamp {
|
||||
self.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to claim a slot derived from the given relay-parent header's slot.
|
||||
pub async fn claim_slot<B, C, P>(
|
||||
client: &C,
|
||||
parent_hash: B::Hash,
|
||||
relay_parent_header: &PHeader,
|
||||
slot_duration: SlotDuration,
|
||||
relay_chain_slot_duration: Duration,
|
||||
keystore: &KeystorePtr,
|
||||
) -> Result<Option<SlotClaim<P::Public>>, Box<dyn Error>>
|
||||
where
|
||||
B: BlockT,
|
||||
C: ProvideRuntimeApi<B> + Send + Sync + 'static,
|
||||
C::Api: AuraApi<B, P::Public>,
|
||||
P: Pair,
|
||||
P::Public: Codec,
|
||||
P::Signature: Codec,
|
||||
{
|
||||
// load authorities
|
||||
let authorities = client.runtime_api().authorities(parent_hash).map_err(Box::new)?;
|
||||
|
||||
// Determine the current slot and timestamp based on the relay-parent's.
|
||||
let (slot_now, timestamp) = match consensus_common::relay_slot_and_timestamp(
|
||||
relay_parent_header,
|
||||
relay_chain_slot_duration,
|
||||
) {
|
||||
Some((r_s, t)) => {
|
||||
let our_slot = Slot::from_timestamp(t, slot_duration);
|
||||
tracing::debug!(
|
||||
target: crate::LOG_TARGET,
|
||||
relay_slot = ?r_s,
|
||||
para_slot = ?our_slot,
|
||||
timestamp = ?t,
|
||||
?slot_duration,
|
||||
?relay_chain_slot_duration,
|
||||
"Adjusted relay-chain slot to teyrchain slot"
|
||||
);
|
||||
(our_slot, t)
|
||||
},
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// Try to claim the slot locally.
|
||||
let author_pub = {
|
||||
let res = aura_internal::claim_slot::<P>(slot_now, &authorities, keystore).await;
|
||||
match res {
|
||||
Some(p) => p,
|
||||
None => return Ok(None),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(SlotClaim::unchecked::<P>(author_pub, slot_now, timestamp)))
|
||||
}
|
||||
|
||||
/// Seal a block with a signature in the header.
|
||||
pub fn seal<B: BlockT, P>(
|
||||
pre_sealed: B,
|
||||
storage_changes: StorageChanges<HashingFor<B>>,
|
||||
author_pub: &P::Public,
|
||||
keystore: &KeystorePtr,
|
||||
) -> Result<BlockImportParams<B>, Box<dyn Error + Send + Sync + 'static>>
|
||||
where
|
||||
P: Pair,
|
||||
P::Signature: Codec + TryFrom<Vec<u8>>,
|
||||
P::Public: AppPublic,
|
||||
{
|
||||
let (pre_header, body) = pre_sealed.deconstruct();
|
||||
let pre_hash = pre_header.hash();
|
||||
let block_number = *pre_header.number();
|
||||
|
||||
// seal the block.
|
||||
let block_import_params = {
|
||||
let seal_digest =
|
||||
aura_internal::seal::<_, P>(&pre_hash, &author_pub, keystore).map_err(Box::new)?;
|
||||
let mut block_import_params = BlockImportParams::new(BlockOrigin::Own, pre_header);
|
||||
block_import_params.post_digests.push(seal_digest);
|
||||
block_import_params.body = Some(body);
|
||||
block_import_params.state_action =
|
||||
StateAction::ApplyChanges(pezsc_consensus::StorageChanges::Changes(storage_changes));
|
||||
block_import_params.fork_choice = Some(ForkChoiceStrategy::LongestChain);
|
||||
block_import_params
|
||||
};
|
||||
let post_hash = block_import_params.post_hash();
|
||||
|
||||
tracing::info!(
|
||||
target: crate::LOG_TARGET,
|
||||
"🔖 Pre-sealed block for proposal at {}. Hash now {:?}, previously {:?}.",
|
||||
block_number,
|
||||
post_hash,
|
||||
pre_hash,
|
||||
);
|
||||
|
||||
Ok(block_import_params)
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! This provides the option to run a basic relay-chain driven Aura implementation.
|
||||
//!
|
||||
//! This collator only builds on top of the most recently included block, limiting the
|
||||
//! block time to a maximum of two times the relay-chain block time, and requiring the
|
||||
//! block to be built and distributed to validators between two relay-chain blocks.
|
||||
//!
|
||||
//! For more information about AuRa, the Bizinikiwi crate should be checked.
|
||||
|
||||
use codec::{Codec, Decode};
|
||||
use cumulus_client_collator::{
|
||||
relay_chain_driven::CollationRequest, service::ServiceInterface as CollatorServiceInterface,
|
||||
};
|
||||
use cumulus_client_consensus_common::TeyrchainBlockImportMarker;
|
||||
use cumulus_client_consensus_proposer::ProposerInterface;
|
||||
use cumulus_primitives_core::{relay_chain::BlockId as RBlockId, CollectCollationInfo};
|
||||
use cumulus_relay_chain_interface::RelayChainInterface;
|
||||
|
||||
use pezkuwi_node_primitives::CollationResult;
|
||||
use pezkuwi_overseer::Handle as OverseerHandle;
|
||||
use pezkuwi_primitives::{CollatorPair, Id as ParaId, ValidationCode};
|
||||
|
||||
use futures::{channel::mpsc::Receiver, prelude::*};
|
||||
use pezsc_client_api::{backend::AuxStore, BlockBackend, BlockOf};
|
||||
use pezsc_consensus::BlockImport;
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_api::{CallApiAt, ProvideRuntimeApi};
|
||||
use pezsp_application_crypto::AppPublic;
|
||||
use pezsp_blockchain::HeaderBackend;
|
||||
use pezsp_consensus_aura::AuraApi;
|
||||
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_state_machine::Backend as _;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use crate::collator as collator_util;
|
||||
|
||||
/// Parameters for [`run`].
|
||||
pub struct Params<BI, CIDP, Client, RClient, Proposer, CS> {
|
||||
/// 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<Client>,
|
||||
/// A handle to the relay-chain client.
|
||||
pub relay_client: RClient,
|
||||
/// 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,
|
||||
/// Receiver for collation requests. If `None`, Aura consensus will establish a new receiver.
|
||||
/// Should be used when a chain migrates from a different consensus algorithm and was already
|
||||
/// processing collation requests before initializing Aura.
|
||||
pub collation_request_receiver: Option<Receiver<CollationRequest>>,
|
||||
}
|
||||
|
||||
/// Run bare Aura consensus as a relay-chain-driven collator.
|
||||
pub fn run<Block, P, BI, CIDP, Client, RClient, Proposer, CS>(
|
||||
params: Params<BI, CIDP, Client, RClient, Proposer, CS>,
|
||||
) -> impl Future<Output = ()> + Send + 'static
|
||||
where
|
||||
Block: BlockT + Send,
|
||||
Client: ProvideRuntimeApi<Block>
|
||||
+ BlockOf
|
||||
+ AuxStore
|
||||
+ HeaderBackend<Block>
|
||||
+ BlockBackend<Block>
|
||||
+ CallApiAt<Block>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
Client::Api: AuraApi<Block, P::Public> + CollectCollationInfo<Block>,
|
||||
RClient: RelayChainInterface + Send + Clone + 'static,
|
||||
CIDP: CreateInherentDataProviders<Block, ()> + Send + 'static,
|
||||
CIDP::InherentDataProviders: Send,
|
||||
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
|
||||
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
|
||||
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
|
||||
P: Pair,
|
||||
P::Public: AppPublic + Member + Codec,
|
||||
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
|
||||
{
|
||||
async move {
|
||||
let mut collation_requests = match params.collation_request_receiver {
|
||||
Some(receiver) => receiver,
|
||||
None =>
|
||||
cumulus_client_collator::relay_chain_driven::init(
|
||||
params.collator_key,
|
||||
params.para_id,
|
||||
params.overseer_handle,
|
||||
)
|
||||
.await,
|
||||
};
|
||||
|
||||
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::<Block, P, _, _, _, _, _>::new(params)
|
||||
};
|
||||
|
||||
let mut last_processed_slot = 0;
|
||||
let mut last_relay_chain_block = Default::default();
|
||||
|
||||
while let Some(request) = collation_requests.next().await {
|
||||
macro_rules! reject_with_error {
|
||||
($err:expr) => {{
|
||||
request.complete(None);
|
||||
tracing::error!(target: crate::LOG_TARGET, err = ?{ $err });
|
||||
continue;
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! try_request {
|
||||
($x:expr) => {{
|
||||
match $x {
|
||||
Ok(x) => x,
|
||||
Err(e) => reject_with_error!(e),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
let validation_data = request.persisted_validation_data();
|
||||
|
||||
let parent_header =
|
||||
try_request!(Block::Header::decode(&mut &validation_data.parent_head.0[..]));
|
||||
|
||||
let parent_hash = parent_header.hash();
|
||||
|
||||
if !collator.collator_service().check_block_status(parent_hash, &parent_header) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(Some(code)) =
|
||||
params.para_client.state_at(parent_hash).map_err(drop).and_then(|s| {
|
||||
s.storage(&pezsp_core::storage::well_known_keys::CODE).map_err(drop)
|
||||
})
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
super::check_validation_code_or_log(
|
||||
&ValidationCode::from(code).hash(),
|
||||
params.para_id,
|
||||
¶ms.relay_client,
|
||||
*request.relay_parent(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let relay_parent_header =
|
||||
match params.relay_client.header(RBlockId::hash(*request.relay_parent())).await {
|
||||
Err(e) => reject_with_error!(e),
|
||||
Ok(None) => continue, // sanity: would be inconsistent to get `None` here
|
||||
Ok(Some(h)) => h,
|
||||
};
|
||||
|
||||
let slot_duration = match params.para_client.runtime_api().slot_duration(parent_hash) {
|
||||
Ok(d) => d,
|
||||
Err(e) => reject_with_error!(e),
|
||||
};
|
||||
|
||||
let claim = match collator_util::claim_slot::<_, _, P>(
|
||||
&*params.para_client,
|
||||
parent_hash,
|
||||
&relay_parent_header,
|
||||
slot_duration,
|
||||
params.relay_chain_slot_duration,
|
||||
¶ms.keystore,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(None) => continue,
|
||||
Ok(Some(c)) => c,
|
||||
Err(e) => reject_with_error!(e),
|
||||
};
|
||||
|
||||
// With async backing this function will be called every relay chain block.
|
||||
//
|
||||
// Most teyrchains currently run with 12 seconds slots and thus, they would try to
|
||||
// produce multiple blocks per slot which very likely would fail on chain. Thus, we have
|
||||
// this "hack" to only produce one block per slot per relay chain fork.
|
||||
//
|
||||
// With https://github.com/pezkuwichain/pezkuwi-sdk/issues/127 this implementation will be
|
||||
// obsolete and also the underlying issue will be fixed.
|
||||
if last_processed_slot >= *claim.slot() &&
|
||||
last_relay_chain_block < *relay_parent_header.number()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let (teyrchain_inherent_data, other_inherent_data) = try_request!(
|
||||
collator
|
||||
.create_inherent_data(
|
||||
*request.relay_parent(),
|
||||
&validation_data,
|
||||
parent_hash,
|
||||
claim.timestamp(),
|
||||
params.collator_peer_id,
|
||||
)
|
||||
.await
|
||||
);
|
||||
|
||||
let allowed_pov_size = (validation_data.max_pov_size / 2) as usize;
|
||||
|
||||
let maybe_collation = try_request!(
|
||||
collator
|
||||
.collate(
|
||||
&parent_header,
|
||||
&claim,
|
||||
None,
|
||||
(teyrchain_inherent_data, other_inherent_data),
|
||||
params.authoring_duration,
|
||||
allowed_pov_size,
|
||||
)
|
||||
.await
|
||||
);
|
||||
|
||||
if let Some((collation, block_data)) = maybe_collation {
|
||||
let Some(block_hash) = block_data.blocks().first().map(|b| b.hash()) else {
|
||||
continue;
|
||||
};
|
||||
let result_sender =
|
||||
Some(collator.collator_service().announce_with_barrier(block_hash));
|
||||
request.complete(Some(CollationResult { collation, result_sender }));
|
||||
} else {
|
||||
request.complete(None);
|
||||
tracing::debug!(target: crate::LOG_TARGET, "No block proposal");
|
||||
}
|
||||
|
||||
last_processed_slot = *claim.slot();
|
||||
last_relay_chain_block = *relay_parent_header.number();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! 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 cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
|
||||
use cumulus_client_consensus_common::{self as consensus_common, TeyrchainBlockImportMarker};
|
||||
use cumulus_client_consensus_proposer::ProposerInterface;
|
||||
use cumulus_primitives_aura::AuraUnincludedSegmentApi;
|
||||
use cumulus_primitives_core::{CollectCollationInfo, PersistedValidationData};
|
||||
use cumulus_relay_chain_interface::RelayChainInterface;
|
||||
|
||||
use pezkuwi_node_primitives::SubmitCollationParams;
|
||||
use pezkuwi_node_subsystem::messages::CollationGenerationMessage;
|
||||
use pezkuwi_overseer::Handle as OverseerHandle;
|
||||
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<BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS> {
|
||||
/// 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<Client>,
|
||||
/// The para client's backend, used to access the database.
|
||||
pub para_backend: Arc<Backend>,
|
||||
/// 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 <https://github.com/pezkuwichain/pezkuwi-sdk/issues/23> is fixed.
|
||||
pub max_pov_percentage: Option<u32>,
|
||||
}
|
||||
|
||||
/// Get the current teyrchain slot from a given block hash.
|
||||
///
|
||||
/// Returns the teyrchain slot, relay chain slot, and timestamp.
|
||||
fn get_teyrchain_slot<Block, Client, P>(
|
||||
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<Block>,
|
||||
Client::Api: AuraApi<Block, P>,
|
||||
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<Block, P, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS>(
|
||||
params: Params<BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS>,
|
||||
) -> impl Future<Output = ()> + Send + 'static
|
||||
where
|
||||
Block: BlockT,
|
||||
Client: ProvideRuntimeApi<Block>
|
||||
+ BlockOf
|
||||
+ AuxStore
|
||||
+ HeaderBackend<Block>
|
||||
+ BlockBackend<Block>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
Client::Api:
|
||||
AuraApi<Block, P::Public> + CollectCollationInfo<Block> + AuraUnincludedSegmentApi<Block>,
|
||||
Backend: pezsc_client_api::Backend<Block> + 'static,
|
||||
RClient: RelayChainInterface + Clone + 'static,
|
||||
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
|
||||
CIDP::InherentDataProviders: Send,
|
||||
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
|
||||
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
|
||||
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
|
||||
CHP: consensus_common::ValidationCodeHashProvider<Block::Hash> + Send + 'static,
|
||||
P: Pair + Send + Sync + 'static,
|
||||
P::Public: AppPublic + Member + Codec,
|
||||
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
|
||||
{
|
||||
run_with_export::<_, P, _, _, _, _, _, _, _, _>(ParamsWithExport { params, export_pov: None })
|
||||
}
|
||||
|
||||
/// Parameters for [`run_with_export`].
|
||||
pub struct ParamsWithExport<BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS> {
|
||||
/// The parameters.
|
||||
pub params: Params<BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS>,
|
||||
|
||||
/// When set, the collator will export every produced `POV` to this folder.
|
||||
pub export_pov: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// 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<Block, P, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS>(
|
||||
ParamsWithExport { mut params, export_pov }: ParamsWithExport<
|
||||
BI,
|
||||
CIDP,
|
||||
Client,
|
||||
Backend,
|
||||
RClient,
|
||||
CHP,
|
||||
Proposer,
|
||||
CS,
|
||||
>,
|
||||
) -> impl Future<Output = ()> + Send + 'static
|
||||
where
|
||||
Block: BlockT,
|
||||
Client: ProvideRuntimeApi<Block>
|
||||
+ BlockOf
|
||||
+ AuxStore
|
||||
+ HeaderBackend<Block>
|
||||
+ BlockBackend<Block>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
Client::Api:
|
||||
AuraApi<Block, P::Public> + CollectCollationInfo<Block> + AuraUnincludedSegmentApi<Block>,
|
||||
Backend: pezsc_client_api::Backend<Block> + 'static,
|
||||
RClient: RelayChainInterface + Clone + 'static,
|
||||
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
|
||||
CIDP::InherentDataProviders: Send,
|
||||
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
|
||||
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
|
||||
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
|
||||
CHP: consensus_common::ValidationCodeHashProvider<Block::Hash> + Send + 'static,
|
||||
P: Pair + Send + Sync + 'static,
|
||||
P::Public: AppPublic + Member + Codec,
|
||||
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
|
||||
{
|
||||
async move {
|
||||
cumulus_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::<Block, P, _, _, _, _, _>::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::<P>(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/23 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::<Block>(
|
||||
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;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,709 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Stock, pure Aura collators.
|
||||
//!
|
||||
//! This includes the [`basic`] collator, which only builds on top of the most recently
|
||||
//! included teyrchain block, as well as the [`lookahead`] collator, which prospectively
|
||||
//! builds on teyrchain blocks which have not yet been included in the relay chain.
|
||||
|
||||
use crate::collator::SlotClaim;
|
||||
use codec::Codec;
|
||||
use cumulus_client_consensus_common::{self as consensus_common, ParentSearchParams};
|
||||
use cumulus_primitives_aura::{AuraUnincludedSegmentApi, Slot};
|
||||
use cumulus_primitives_core::{relay_chain::Header as RelayHeader, BlockT};
|
||||
use cumulus_relay_chain_interface::{OverseerHandle, RelayChainInterface};
|
||||
use pezkuwi_node_subsystem::messages::{CollatorProtocolMessage, RuntimeApiRequest};
|
||||
use pezkuwi_node_subsystem_util::runtime::ClaimQueueSnapshot;
|
||||
use pezkuwi_primitives::{
|
||||
Hash as RelayHash, Id as ParaId, OccupiedCoreAssumption, ValidationCodeHash,
|
||||
DEFAULT_SCHEDULING_LOOKAHEAD,
|
||||
};
|
||||
use pezsc_consensus_aura::{standalone as aura_internal, AuraApi};
|
||||
use pezsp_api::{ApiExt, ProvideRuntimeApi, RuntimeApiInfo};
|
||||
use pezsp_core::Pair;
|
||||
use pezsp_keystore::KeystorePtr;
|
||||
use pezsp_timestamp::Timestamp;
|
||||
|
||||
pub mod basic;
|
||||
pub mod lookahead;
|
||||
pub mod slot_based;
|
||||
|
||||
// This is an arbitrary value which is guaranteed to exceed the required depth for 500ms blocks
|
||||
// built with a relay parent offset of 1. It must be larger than the unincluded segment capacity.
|
||||
//
|
||||
// The formula we use to compute the capacity of the unincluded segment in the teyrchain runtime
|
||||
// is:
|
||||
// UNINCLUDED_SEGMENT_CAPACITY = (2 + RELAY_PARENT_OFFSET) * BLOCK_PROCESSING_VELOCITY + 1.
|
||||
//
|
||||
// Since we only search for parent blocks which have already been imported,
|
||||
// we can guarantee that all imported blocks respect the unincluded segment
|
||||
// rules specified by the teyrchain's runtime and thus will never be too deep. This is just an extra
|
||||
// sanity check.
|
||||
const PARENT_SEARCH_DEPTH: usize = 40;
|
||||
|
||||
// Helper to pre-connect to the backing group we got assigned to and keep the connection
|
||||
// open until backing group changes or own slot ends.
|
||||
struct BackingGroupConnectionHelper {
|
||||
keystore: pezsp_keystore::KeystorePtr,
|
||||
overseer_handle: OverseerHandle,
|
||||
our_slot: Option<Slot>,
|
||||
}
|
||||
|
||||
impl BackingGroupConnectionHelper {
|
||||
pub fn new(keystore: pezsp_keystore::KeystorePtr, overseer_handle: OverseerHandle) -> Self {
|
||||
Self { keystore, overseer_handle, our_slot: None }
|
||||
}
|
||||
|
||||
async fn send_subsystem_message(&mut self, message: CollatorProtocolMessage) {
|
||||
self.overseer_handle.send_msg(message, "BackingGroupConnectionHelper").await;
|
||||
}
|
||||
|
||||
/// Update the current slot and initiate connections to backing groups if needed.
|
||||
pub async fn update<P>(&mut self, current_slot: Slot, authorities: &[P::Public])
|
||||
where
|
||||
P: pezsp_core::Pair + Send + Sync,
|
||||
P::Public: Codec,
|
||||
{
|
||||
if Some(current_slot) <= self.our_slot {
|
||||
// Current slot or next slot is ours.
|
||||
// We already sent pre-connect message, no need to proceed further.
|
||||
return;
|
||||
}
|
||||
|
||||
let next_slot = current_slot + 1;
|
||||
let next_slot_is_ours =
|
||||
aura_internal::claim_slot::<P>(next_slot, authorities, &self.keystore)
|
||||
.await
|
||||
.is_some();
|
||||
|
||||
if next_slot_is_ours {
|
||||
// Only send message if we were not connected. This avoids sending duplicate messages
|
||||
// when running with a single collator.
|
||||
if self.our_slot.is_none() {
|
||||
// Next slot is ours, send connect message.
|
||||
tracing::debug!(target: crate::LOG_TARGET, "Our slot {} is next, connecting to backing groups", next_slot);
|
||||
self.send_subsystem_message(CollatorProtocolMessage::ConnectToBackingGroups)
|
||||
.await;
|
||||
}
|
||||
self.our_slot = Some(next_slot);
|
||||
} else if self.our_slot.take().is_some() {
|
||||
// Next slot is not ours, send disconnect only if we had a slot before.
|
||||
tracing::debug!(target: crate::LOG_TARGET, "Current slot = {}, disconnecting from backing groups", current_slot);
|
||||
self.send_subsystem_message(CollatorProtocolMessage::DisconnectFromBackingGroups)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check the `local_validation_code_hash` against the validation code hash in the relay chain
|
||||
/// state.
|
||||
///
|
||||
/// If the code hashes do not match, it prints a warning.
|
||||
async fn check_validation_code_or_log(
|
||||
local_validation_code_hash: &ValidationCodeHash,
|
||||
para_id: ParaId,
|
||||
relay_client: &impl RelayChainInterface,
|
||||
relay_parent: RelayHash,
|
||||
) {
|
||||
let state_validation_code_hash = match relay_client
|
||||
.validation_code_hash(relay_parent, para_id, OccupiedCoreAssumption::Included)
|
||||
.await
|
||||
{
|
||||
Ok(hash) => hash,
|
||||
Err(error) => {
|
||||
tracing::debug!(
|
||||
target: super::LOG_TARGET,
|
||||
%error,
|
||||
?relay_parent,
|
||||
%para_id,
|
||||
"Failed to fetch validation code hash",
|
||||
);
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
match state_validation_code_hash {
|
||||
Some(state) =>
|
||||
if state != *local_validation_code_hash {
|
||||
tracing::warn!(
|
||||
target: super::LOG_TARGET,
|
||||
%para_id,
|
||||
?relay_parent,
|
||||
?local_validation_code_hash,
|
||||
relay_validation_code_hash = ?state,
|
||||
"Teyrchain code doesn't match validation code stored in the relay chain state.",
|
||||
);
|
||||
},
|
||||
None => {
|
||||
tracing::warn!(
|
||||
target: super::LOG_TARGET,
|
||||
%para_id,
|
||||
?relay_parent,
|
||||
"Could not find validation code for teyrchain in the relay chain state.",
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch scheduling lookahead at given relay parent.
|
||||
async fn scheduling_lookahead(
|
||||
relay_parent: RelayHash,
|
||||
relay_client: &impl RelayChainInterface,
|
||||
) -> Option<u32> {
|
||||
let runtime_api_version = relay_client
|
||||
.version(relay_parent)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(
|
||||
target: super::LOG_TARGET,
|
||||
error = ?e,
|
||||
"Failed to fetch relay chain runtime version.",
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let teyrchain_host_runtime_api_version = runtime_api_version
|
||||
.api_version(
|
||||
&<dyn pezkuwi_primitives::runtime_api::TeyrchainHost<pezkuwi_primitives::Block>>::ID,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
if teyrchain_host_runtime_api_version <
|
||||
RuntimeApiRequest::SCHEDULING_LOOKAHEAD_RUNTIME_REQUIREMENT
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
match relay_client.scheduling_lookahead(relay_parent).await {
|
||||
Ok(scheduling_lookahead) => Some(scheduling_lookahead),
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
target: crate::LOG_TARGET,
|
||||
?err,
|
||||
?relay_parent,
|
||||
"Failed to fetch scheduling lookahead from relay chain",
|
||||
);
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the claim queue at the given relay parent.
|
||||
async fn claim_queue_at(
|
||||
relay_parent: RelayHash,
|
||||
relay_client: &impl RelayChainInterface,
|
||||
) -> ClaimQueueSnapshot {
|
||||
// Get `ClaimQueue` from runtime
|
||||
match relay_client.claim_queue(relay_parent).await {
|
||||
Ok(claim_queue) => claim_queue.into(),
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
target: crate::LOG_TARGET,
|
||||
?error,
|
||||
?relay_parent,
|
||||
"Failed to query claim queue runtime API",
|
||||
);
|
||||
Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if we own the slot at the given block and whether there
|
||||
// is space in the unincluded segment.
|
||||
async fn can_build_upon<Block: BlockT, Client, P>(
|
||||
para_slot: Slot,
|
||||
relay_slot: Slot,
|
||||
timestamp: Timestamp,
|
||||
parent_hash: Block::Hash,
|
||||
included_block: Block::Hash,
|
||||
client: &Client,
|
||||
keystore: &KeystorePtr,
|
||||
) -> Option<SlotClaim<P::Public>>
|
||||
where
|
||||
Client: ProvideRuntimeApi<Block>,
|
||||
Client::Api: AuraApi<Block, P::Public> + AuraUnincludedSegmentApi<Block> + ApiExt<Block>,
|
||||
P: Pair,
|
||||
P::Public: Codec,
|
||||
P::Signature: Codec,
|
||||
{
|
||||
let runtime_api = client.runtime_api();
|
||||
let authorities = runtime_api.authorities(parent_hash).ok()?;
|
||||
let author_pub = aura_internal::claim_slot::<P>(para_slot, &authorities, keystore).await?;
|
||||
|
||||
// This function is typically called when we want to build block N. At that point, the
|
||||
// unincluded segment in the runtime is unaware of the hash of block N-1. If the unincluded
|
||||
// segment in the runtime is full, but block N-1 is the included block, the unincluded segment
|
||||
// should have length 0 and we can build. Since the hash is not available to the runtime
|
||||
// however, we need this extra check here.
|
||||
if parent_hash == included_block {
|
||||
return Some(SlotClaim::unchecked::<P>(author_pub, para_slot, timestamp));
|
||||
}
|
||||
|
||||
let api_version = runtime_api
|
||||
.api_version::<dyn AuraUnincludedSegmentApi<Block>>(parent_hash)
|
||||
.ok()
|
||||
.flatten()?;
|
||||
|
||||
let slot = if api_version > 1 { relay_slot } else { para_slot };
|
||||
|
||||
runtime_api
|
||||
.can_build_upon(parent_hash, included_block, slot)
|
||||
.ok()?
|
||||
.then(|| SlotClaim::unchecked::<P>(author_pub, para_slot, timestamp))
|
||||
}
|
||||
|
||||
/// Use [`cumulus_client_consensus_common::find_potential_parents`] to find teyrchain blocks that
|
||||
/// we can build on. Once a list of potential parents is retrieved, return the last one of the
|
||||
/// longest chain.
|
||||
async fn find_parent<Block>(
|
||||
relay_parent: RelayHash,
|
||||
para_id: ParaId,
|
||||
para_backend: &impl pezsc_client_api::Backend<Block>,
|
||||
relay_client: &impl RelayChainInterface,
|
||||
) -> Option<(<Block as BlockT>::Header, consensus_common::PotentialParent<Block>)>
|
||||
where
|
||||
Block: BlockT,
|
||||
{
|
||||
let parent_search_params = ParentSearchParams {
|
||||
relay_parent,
|
||||
para_id,
|
||||
ancestry_lookback: scheduling_lookahead(relay_parent, relay_client)
|
||||
.await
|
||||
.unwrap_or(DEFAULT_SCHEDULING_LOOKAHEAD)
|
||||
.saturating_sub(1) as usize,
|
||||
max_depth: PARENT_SEARCH_DEPTH,
|
||||
ignore_alternative_branches: true,
|
||||
};
|
||||
|
||||
let potential_parents = cumulus_client_consensus_common::find_potential_parents::<Block>(
|
||||
parent_search_params,
|
||||
para_backend,
|
||||
relay_client,
|
||||
)
|
||||
.await;
|
||||
|
||||
let potential_parents = match potential_parents {
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
target: crate::LOG_TARGET,
|
||||
?relay_parent,
|
||||
err = ?e,
|
||||
"Could not fetch potential parents to build upon"
|
||||
);
|
||||
|
||||
return None;
|
||||
},
|
||||
Ok(x) => x,
|
||||
};
|
||||
|
||||
let included_block = potential_parents.iter().find(|x| x.depth == 0)?.header.clone();
|
||||
potential_parents
|
||||
.into_iter()
|
||||
.max_by_key(|a| a.depth)
|
||||
.map(|parent| (included_block, parent))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::collators::{can_build_upon, BackingGroupConnectionHelper};
|
||||
use codec::Encode;
|
||||
use cumulus_primitives_aura::Slot;
|
||||
use cumulus_primitives_core::BlockT;
|
||||
use cumulus_relay_chain_interface::PHash;
|
||||
use cumulus_test_client::{
|
||||
runtime::{Block, Hash},
|
||||
Client, DefaultTestClientBuilderExt, InitBlockBuilder, TestClientBuilder,
|
||||
TestClientBuilderExt,
|
||||
};
|
||||
use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder;
|
||||
use futures::StreamExt;
|
||||
use pezkuwi_overseer::{Event, Handle};
|
||||
use pezkuwi_primitives::HeadData;
|
||||
use pezsc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy};
|
||||
use pezsp_consensus::BlockOrigin;
|
||||
use pezsp_keystore::{Keystore, KeystorePtr};
|
||||
use pezsp_timestamp::Timestamp;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
async fn import_block<I: BlockImport<Block>>(
|
||||
importer: &I,
|
||||
block: Block,
|
||||
origin: BlockOrigin,
|
||||
import_as_best: bool,
|
||||
) {
|
||||
let (header, body) = block.deconstruct();
|
||||
|
||||
let mut block_import_params = BlockImportParams::new(origin, header);
|
||||
block_import_params.fork_choice = Some(ForkChoiceStrategy::Custom(import_as_best));
|
||||
block_import_params.body = Some(body);
|
||||
importer.import_block(block_import_params).await.unwrap();
|
||||
}
|
||||
|
||||
fn sproof_with_parent_by_hash(client: &Client, hash: PHash) -> RelayStateSproofBuilder {
|
||||
let header = client.header(hash).ok().flatten().expect("No header for parent block");
|
||||
let included = HeadData(header.encode());
|
||||
let mut builder = RelayStateSproofBuilder::default();
|
||||
builder.para_id = cumulus_test_client::runtime::TEYRCHAIN_ID.into();
|
||||
builder.included_para_head = Some(included);
|
||||
|
||||
builder
|
||||
}
|
||||
async fn build_and_import_block(client: &Client, included: Hash) -> Block {
|
||||
let sproof = sproof_with_parent_by_hash(client, included);
|
||||
|
||||
let block_builder = client.init_block_builder(None, sproof).block_builder;
|
||||
|
||||
let block = block_builder.build().unwrap().block;
|
||||
|
||||
let origin = BlockOrigin::NetworkInitialSync;
|
||||
import_block(client, block.clone(), origin, true).await;
|
||||
block
|
||||
}
|
||||
|
||||
fn set_up_components(num_authorities: usize) -> (Arc<Client>, KeystorePtr) {
|
||||
let keystore = Arc::new(pezsp_keystore::testing::MemoryKeystore::new()) as Arc<_>;
|
||||
for key in pezsp_keyring::Sr25519Keyring::iter().take(num_authorities) {
|
||||
Keystore::sr25519_generate_new(
|
||||
&*keystore,
|
||||
pezsp_application_crypto::key_types::AURA,
|
||||
Some(&key.to_seed()),
|
||||
)
|
||||
.expect("Can insert key into MemoryKeyStore");
|
||||
}
|
||||
(Arc::new(TestClientBuilder::new().build()), keystore)
|
||||
}
|
||||
|
||||
/// This tests a special scenario where the unincluded segment in the runtime
|
||||
/// is full. We are calling `can_build_upon`, passing the last built block as the
|
||||
/// included one. In the runtime we will not find the hash of the included block in the
|
||||
/// unincluded segment. The `can_build_upon` runtime API would therefore return `false`, but
|
||||
/// we are ensuring on the node side that we are are always able to build on the included block.
|
||||
#[tokio::test]
|
||||
async fn test_can_build_upon() {
|
||||
let (client, keystore) = set_up_components(6);
|
||||
|
||||
let genesis_hash = client.chain_info().genesis_hash;
|
||||
let mut last_hash = genesis_hash;
|
||||
|
||||
// Fill up the unincluded segment tracker in the runtime.
|
||||
while can_build_upon::<_, _, pezsp_consensus_aura::sr25519::AuthorityPair>(
|
||||
Slot::from(u64::MAX),
|
||||
Slot::from(u64::MAX),
|
||||
Timestamp::default(),
|
||||
last_hash,
|
||||
genesis_hash,
|
||||
&*client,
|
||||
&keystore,
|
||||
)
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
let block = build_and_import_block(&client, genesis_hash).await;
|
||||
last_hash = block.header().hash();
|
||||
}
|
||||
|
||||
// Blocks were built with the genesis hash set as included block.
|
||||
// We call `can_build_upon` with the last built block as the included block.
|
||||
let result = can_build_upon::<_, _, pezsp_consensus_aura::sr25519::AuthorityPair>(
|
||||
Slot::from(u64::MAX),
|
||||
Slot::from(u64::MAX),
|
||||
Timestamp::default(),
|
||||
last_hash,
|
||||
last_hash,
|
||||
&*client,
|
||||
&keystore,
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_some());
|
||||
}
|
||||
|
||||
/// Helper to create a mock overseer handle and message recorder
|
||||
fn create_overseer_handle() -> (OverseerHandle, Arc<Mutex<Vec<CollatorProtocolMessage>>>) {
|
||||
let messages = Arc::new(Mutex::new(Vec::new()));
|
||||
let messages_clone = messages.clone();
|
||||
|
||||
let (tx, mut rx) = pezkuwi_node_subsystem_util::metered::channel(100);
|
||||
|
||||
// Spawn a task to receive and record overseer messages
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = rx.next().await {
|
||||
if let Event::MsgToSubsystem { msg, .. } = event {
|
||||
if let pezkuwi_node_subsystem::AllMessages::CollatorProtocol(cp_msg) = msg {
|
||||
messages_clone.lock().unwrap().push(cp_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(Handle::new(tx), messages)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preconnect_when_next_slot_is_ours() {
|
||||
let (client, keystore) = set_up_components(1);
|
||||
let genesis_hash = client.chain_info().genesis_hash;
|
||||
let (overseer_handle, messages_recorder) = create_overseer_handle();
|
||||
|
||||
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
|
||||
|
||||
// Fetch authorities for the update call
|
||||
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
|
||||
|
||||
// Update with slot 5, next slot (6) should be ours
|
||||
helper
|
||||
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
|
||||
.await;
|
||||
|
||||
// Give time for message to be processed
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
|
||||
let messages = messages_recorder.lock().unwrap();
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert!(matches!(messages[0], CollatorProtocolMessage::ConnectToBackingGroups));
|
||||
assert_eq!(helper.our_slot, Some(Slot::from(6)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preconnect_no_duplicate_connect_message() {
|
||||
let (client, keystore) = set_up_components(1);
|
||||
let genesis_hash = client.chain_info().genesis_hash;
|
||||
let (overseer_handle, messages_recorder) = create_overseer_handle();
|
||||
|
||||
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
|
||||
|
||||
// Fetch authorities for the update calls
|
||||
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
|
||||
|
||||
// Update with slot 5, next slot (6) is ours
|
||||
helper
|
||||
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
|
||||
.await;
|
||||
|
||||
// Give time for message to be processed
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
assert_eq!(messages_recorder.lock().unwrap().len(), 1);
|
||||
messages_recorder.lock().unwrap().clear();
|
||||
|
||||
// Update with slot 5 again - should not send another message
|
||||
helper
|
||||
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
|
||||
.await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
assert_eq!(messages_recorder.lock().unwrap().len(), 0);
|
||||
|
||||
// Update with slot 1 (our slot) - should not send another message
|
||||
helper
|
||||
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(6), &authorities)
|
||||
.await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
assert_eq!(messages_recorder.lock().unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preconnect_disconnect_when_slot_passes() {
|
||||
let (client, keystore) = set_up_components(1);
|
||||
let genesis_hash = client.chain_info().genesis_hash;
|
||||
let (overseer_handle, messages_recorder) = create_overseer_handle();
|
||||
|
||||
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
|
||||
|
||||
// Fetch authorities for the update calls
|
||||
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
|
||||
|
||||
// Slot 0 -> Alice, Slot 1 -> Bob, Slot 2 -> Charlie, Slot 3 -> Dave, Slot 4 -> Eve,
|
||||
// Slot 5 -> Ferdie, Slot 6 -> Alice
|
||||
|
||||
// Update with slot 5, next slot (6) is ours -> should connect
|
||||
helper
|
||||
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
|
||||
.await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
assert_eq!(helper.our_slot, Some(Slot::from(6)));
|
||||
messages_recorder.lock().unwrap().clear();
|
||||
|
||||
// Update with slot 8, next slot (9) is Charlie's -> should disconnect
|
||||
helper
|
||||
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(8), &authorities)
|
||||
.await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
|
||||
{
|
||||
let messages = messages_recorder.lock().unwrap();
|
||||
assert_eq!(messages.len(), 1, "Expected exactly one disconnect message");
|
||||
assert!(matches!(messages[0], CollatorProtocolMessage::DisconnectFromBackingGroups));
|
||||
assert_eq!(helper.our_slot, None);
|
||||
}
|
||||
|
||||
messages_recorder.lock().unwrap().clear();
|
||||
|
||||
// Update again with slot 8, next slot (9) is Charlie's -> should not send another
|
||||
// disconnect message
|
||||
helper
|
||||
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(8), &authorities)
|
||||
.await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
|
||||
let messages = messages_recorder.lock().unwrap();
|
||||
assert_eq!(messages.len(), 0, "Expected no messages");
|
||||
assert_eq!(helper.our_slot, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preconnect_no_disconnect_without_previous_connection() {
|
||||
let (client, keystore) = set_up_components(1);
|
||||
let genesis_hash = client.chain_info().genesis_hash;
|
||||
let (overseer_handle, messages_recorder) = create_overseer_handle();
|
||||
|
||||
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
|
||||
|
||||
// Fetch authorities for the update call
|
||||
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
|
||||
|
||||
// Slot 0 -> Alice, Slot 1 -> Bob, Slot 2 -> Charlie, Slot 3 -> Dave, Slot 4 -> Eve,
|
||||
// Slot 5 -> Ferdie
|
||||
|
||||
// Update with slot 1 (Bob's slot), next slot (2) is Charlie's
|
||||
// Since we never connected before (our_slot is None), we should not send disconnect
|
||||
helper
|
||||
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(1), &authorities)
|
||||
.await;
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
// Should not send any message since we never connected
|
||||
assert_eq!(messages_recorder.lock().unwrap().len(), 0);
|
||||
assert_eq!(helper.our_slot, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preconnect_multiple_cycles() {
|
||||
let (client, keystore) = set_up_components(1);
|
||||
let genesis_hash = client.chain_info().genesis_hash;
|
||||
let (overseer_handle, messages_recorder) = create_overseer_handle();
|
||||
|
||||
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
|
||||
|
||||
// Fetch authorities for the update calls
|
||||
let authorities = client.runtime_api().authorities(genesis_hash).unwrap();
|
||||
|
||||
// Slot 0 -> Alice, Slot 1 -> Bob, Slot 2 -> Charlie, Slot 3 -> Dave, Slot 4 -> Eve,
|
||||
// Slot 5 -> Ferdie, Slot 6 -> Alice, Slot 7 -> Bob, ...
|
||||
|
||||
// Cycle 1: Connect at slot 5, next slot (6) is ours
|
||||
helper
|
||||
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(5), &authorities)
|
||||
.await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
{
|
||||
let messages = messages_recorder.lock().unwrap();
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert!(matches!(messages[0], CollatorProtocolMessage::ConnectToBackingGroups));
|
||||
}
|
||||
assert_eq!(helper.our_slot, Some(Slot::from(6)));
|
||||
messages_recorder.lock().unwrap().clear();
|
||||
|
||||
// Cycle 1: Disconnect at slot 7, next slot (8) is Charlie's
|
||||
helper
|
||||
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(7), &authorities)
|
||||
.await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
{
|
||||
let messages = messages_recorder.lock().unwrap();
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert!(matches!(messages[0], CollatorProtocolMessage::DisconnectFromBackingGroups));
|
||||
}
|
||||
assert_eq!(helper.our_slot, None);
|
||||
messages_recorder.lock().unwrap().clear();
|
||||
|
||||
// Cycle 2: Connect again at slot 11, next slot (12) is ours
|
||||
helper
|
||||
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(11), &authorities)
|
||||
.await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
{
|
||||
let messages = messages_recorder.lock().unwrap();
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert!(matches!(messages[0], CollatorProtocolMessage::ConnectToBackingGroups));
|
||||
}
|
||||
assert_eq!(helper.our_slot, Some(Slot::from(12)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preconnect_handles_empty_authorities() {
|
||||
let keystore = Arc::new(pezsp_keystore::testing::MemoryKeystore::new()) as Arc<_>;
|
||||
let (overseer_handle, messages_recorder) = create_overseer_handle();
|
||||
|
||||
let mut helper = BackingGroupConnectionHelper::new(keystore, overseer_handle);
|
||||
|
||||
// Pass empty authorities list
|
||||
let authorities = vec![];
|
||||
helper
|
||||
.update::<pezsp_consensus_aura::sr25519::AuthorityPair>(Slot::from(0), &authorities)
|
||||
.await;
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
// Should not send any message if authorities list is empty
|
||||
assert_eq!(messages_recorder.lock().unwrap().len(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds a relay parent and its descendants.
|
||||
pub struct RelayParentData {
|
||||
/// The relay parent block header
|
||||
relay_parent: RelayHeader,
|
||||
/// Ordered collection of descendant block headers, from oldest to newest
|
||||
descendants: Vec<RelayHeader>,
|
||||
}
|
||||
|
||||
impl RelayParentData {
|
||||
/// Creates a new instance with the given relay parent and no descendants.
|
||||
pub fn new(relay_parent: RelayHeader) -> Self {
|
||||
Self { relay_parent, descendants: Default::default() }
|
||||
}
|
||||
|
||||
/// Creates a new instance with the given relay parent and descendants.
|
||||
pub fn new_with_descendants(relay_parent: RelayHeader, descendants: Vec<RelayHeader>) -> Self {
|
||||
Self { relay_parent, descendants }
|
||||
}
|
||||
|
||||
/// Returns a reference to the relay parent header.
|
||||
pub fn relay_parent(&self) -> &RelayHeader {
|
||||
&self.relay_parent
|
||||
}
|
||||
|
||||
/// Returns the number of descendants.
|
||||
#[cfg(test)]
|
||||
pub fn descendants_len(&self) -> usize {
|
||||
self.descendants.len()
|
||||
}
|
||||
|
||||
/// Consumes the structure and returns a vector containing the relay parent followed by its
|
||||
/// descendants in chronological order. The resulting list should be provided to the teyrchain
|
||||
/// inherent data.
|
||||
pub fn into_inherent_descendant_list(self) -> Vec<RelayHeader> {
|
||||
let Self { relay_parent, mut descendants } = self;
|
||||
|
||||
if descendants.is_empty() {
|
||||
return Default::default();
|
||||
}
|
||||
|
||||
let mut result = vec![relay_parent];
|
||||
result.append(&mut descendants);
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use codec::{Codec, Encode};
|
||||
|
||||
use super::CollatorMessage;
|
||||
use crate::{
|
||||
collator as collator_util,
|
||||
collators::{
|
||||
check_validation_code_or_log,
|
||||
slot_based::{
|
||||
relay_chain_data_cache::{RelayChainData, RelayChainDataCache},
|
||||
slot_timer::{SlotInfo, SlotTimer},
|
||||
},
|
||||
BackingGroupConnectionHelper, RelayParentData,
|
||||
},
|
||||
LOG_TARGET,
|
||||
};
|
||||
use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
|
||||
use cumulus_client_consensus_common::{self as consensus_common, TeyrchainBlockImportMarker};
|
||||
use cumulus_client_consensus_proposer::ProposerInterface;
|
||||
use cumulus_primitives_aura::{AuraUnincludedSegmentApi, Slot};
|
||||
use cumulus_primitives_core::{
|
||||
extract_relay_parent, rpsr_digest, ClaimQueueOffset, CoreInfo, CoreSelector, CumulusDigestItem,
|
||||
PersistedValidationData, RelayParentOffsetApi,
|
||||
};
|
||||
use cumulus_relay_chain_interface::RelayChainInterface;
|
||||
use futures::prelude::*;
|
||||
use pezkuwi_primitives::{
|
||||
Block as RelayBlock, CoreIndex, Hash as RelayHash, Header as RelayHeader, Id as ParaId,
|
||||
};
|
||||
use pezsc_client_api::{backend::AuxStore, BlockBackend, BlockOf, UsageProvider};
|
||||
use pezsc_consensus::BlockImport;
|
||||
use pezsc_consensus_aura::SlotDuration;
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_application_crypto::AppPublic;
|
||||
use pezsp_blockchain::HeaderBackend;
|
||||
use pezsp_consensus_aura::AuraApi;
|
||||
use pezsp_core::crypto::Pair;
|
||||
use pezsp_inherents::CreateInherentDataProviders;
|
||||
use pezsp_keystore::KeystorePtr;
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, Member, Zero};
|
||||
use std::{collections::VecDeque, sync::Arc, time::Duration};
|
||||
|
||||
/// Parameters for [`run_block_builder`].
|
||||
pub struct BuilderTaskParams<
|
||||
Block: BlockT,
|
||||
BI,
|
||||
CIDP,
|
||||
Client,
|
||||
Backend,
|
||||
RelayClient,
|
||||
CHP,
|
||||
Proposer,
|
||||
CS,
|
||||
> {
|
||||
/// 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<Client>,
|
||||
/// The para client's backend, used to access the database.
|
||||
pub para_backend: Arc<Backend>,
|
||||
/// A handle to the relay-chain client.
|
||||
pub relay_client: RelayClient,
|
||||
/// 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 network peer id.
|
||||
pub collator_peer_id: PeerId,
|
||||
/// The para's ID.
|
||||
pub para_id: ParaId,
|
||||
/// 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,
|
||||
/// Channel to send built blocks to the collation task.
|
||||
pub collator_sender: pezsc_utils::mpsc::TracingUnboundedSender<CollatorMessage<Block>>,
|
||||
/// Slot duration of the relay chain.
|
||||
pub relay_chain_slot_duration: Duration,
|
||||
/// Offset all time operations by this duration.
|
||||
///
|
||||
/// This is a time quantity that is subtracted from the actual timestamp when computing
|
||||
/// the time left to enter a new slot. In practice, this *left-shifts* the clock time with the
|
||||
/// intent to keep our "clock" slightly behind the relay chain one and thus reducing the
|
||||
/// likelihood of encountering unfavorable notification arrival timings (i.e. we don't want to
|
||||
/// wait for relay chain notifications because we woke up too early).
|
||||
pub slot_offset: Duration,
|
||||
/// The maximum percentage of the maximum PoV size that the collator can use.
|
||||
/// It will be removed once https://github.com/pezkuwichain/pezkuwi-sdk/issues/23 is fixed.
|
||||
pub max_pov_percentage: Option<u32>,
|
||||
}
|
||||
|
||||
/// Run block-builder.
|
||||
pub fn run_block_builder<Block, P, BI, CIDP, Client, Backend, RelayClient, CHP, Proposer, CS>(
|
||||
params: BuilderTaskParams<Block, BI, CIDP, Client, Backend, RelayClient, CHP, Proposer, CS>,
|
||||
) -> impl Future<Output = ()> + Send + 'static
|
||||
where
|
||||
Block: BlockT,
|
||||
Client: ProvideRuntimeApi<Block>
|
||||
+ UsageProvider<Block>
|
||||
+ BlockOf
|
||||
+ AuxStore
|
||||
+ HeaderBackend<Block>
|
||||
+ BlockBackend<Block>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
Client::Api:
|
||||
AuraApi<Block, P::Public> + RelayParentOffsetApi<Block> + AuraUnincludedSegmentApi<Block>,
|
||||
Backend: pezsc_client_api::Backend<Block> + 'static,
|
||||
RelayClient: RelayChainInterface + Clone + 'static,
|
||||
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
|
||||
CIDP::InherentDataProviders: Send,
|
||||
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
|
||||
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
|
||||
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
|
||||
CHP: consensus_common::ValidationCodeHashProvider<Block::Hash> + Send + 'static,
|
||||
P: Pair + Send + Sync + 'static,
|
||||
P::Public: AppPublic + Member + Codec,
|
||||
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
|
||||
{
|
||||
async move {
|
||||
tracing::info!(target: LOG_TARGET, "Starting slot-based block-builder task.");
|
||||
let BuilderTaskParams {
|
||||
relay_client,
|
||||
create_inherent_data_providers,
|
||||
para_client,
|
||||
keystore,
|
||||
block_import,
|
||||
collator_peer_id,
|
||||
para_id,
|
||||
proposer,
|
||||
collator_service,
|
||||
collator_sender,
|
||||
code_hash_provider,
|
||||
authoring_duration,
|
||||
relay_chain_slot_duration,
|
||||
para_backend,
|
||||
slot_offset,
|
||||
max_pov_percentage,
|
||||
} = params;
|
||||
|
||||
let mut slot_timer = SlotTimer::<_, _, P>::new_with_offset(
|
||||
para_client.clone(),
|
||||
slot_offset,
|
||||
relay_chain_slot_duration,
|
||||
);
|
||||
|
||||
let mut collator = {
|
||||
let params = collator_util::Params {
|
||||
create_inherent_data_providers,
|
||||
block_import,
|
||||
relay_client: relay_client.clone(),
|
||||
keystore: keystore.clone(),
|
||||
collator_peer_id,
|
||||
para_id,
|
||||
proposer,
|
||||
collator_service,
|
||||
};
|
||||
|
||||
collator_util::Collator::<Block, P, _, _, _, _, _>::new(params)
|
||||
};
|
||||
|
||||
let mut relay_chain_data_cache = RelayChainDataCache::new(relay_client.clone(), para_id);
|
||||
let mut connection_helper = BackingGroupConnectionHelper::new(
|
||||
keystore.clone(),
|
||||
relay_client
|
||||
.overseer_handle()
|
||||
// Should never fail. If it fails, then providing collations to relay chain
|
||||
// doesn't work either. So it is fine to panic here.
|
||||
.expect("Relay chain interface must provide overseer handle."),
|
||||
);
|
||||
|
||||
loop {
|
||||
// We wait here until the next slot arrives.
|
||||
if slot_timer.wait_until_next_slot().await.is_err() {
|
||||
tracing::error!(target: LOG_TARGET, "Unable to wait for next slot.");
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(relay_best_hash) = relay_client.best_block_hash().await else {
|
||||
tracing::warn!(target: crate::LOG_TARGET, "Unable to fetch latest relay chain block hash.");
|
||||
continue;
|
||||
};
|
||||
|
||||
let best_hash = para_client.info().best_hash;
|
||||
let relay_parent_offset =
|
||||
para_client.runtime_api().relay_parent_offset(best_hash).unwrap_or_default();
|
||||
|
||||
let Ok(para_slot_duration) = crate::slot_duration(&*para_client) else {
|
||||
tracing::error!(target: LOG_TARGET, "Failed to fetch slot duration from runtime.");
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(Some(rp_data)) = offset_relay_parent_find_descendants(
|
||||
&mut relay_chain_data_cache,
|
||||
relay_best_hash,
|
||||
relay_parent_offset,
|
||||
)
|
||||
.await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(para_slot) = adjust_para_to_relay_parent_slot(
|
||||
rp_data.relay_parent(),
|
||||
relay_chain_slot_duration,
|
||||
para_slot_duration,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let relay_parent = rp_data.relay_parent().hash();
|
||||
let relay_parent_header = rp_data.relay_parent().clone();
|
||||
|
||||
let Some((included_header, parent)) =
|
||||
crate::collators::find_parent(relay_parent, para_id, &*para_backend, &relay_client)
|
||||
.await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let parent_hash = parent.hash;
|
||||
let parent_header = &parent.header;
|
||||
|
||||
// Retrieve the core.
|
||||
let core = match determine_core(
|
||||
&mut relay_chain_data_cache,
|
||||
&relay_parent_header,
|
||||
para_id,
|
||||
parent_header,
|
||||
relay_parent_offset,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(()) => {
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
?relay_parent,
|
||||
"Failed to determine core"
|
||||
);
|
||||
|
||||
continue;
|
||||
},
|
||||
Ok(Some(cores)) => {
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
?relay_parent,
|
||||
core_selector = ?cores.selector,
|
||||
claim_queue_offset = ?cores.claim_queue_offset,
|
||||
"Going to claim core",
|
||||
);
|
||||
|
||||
cores
|
||||
},
|
||||
Ok(None) => {
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
?relay_parent,
|
||||
"No core scheduled"
|
||||
);
|
||||
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
let Ok(RelayChainData { max_pov_size, last_claimed_core_selector, .. }) =
|
||||
relay_chain_data_cache.get_mut_relay_chain_data(relay_parent).await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
slot_timer.update_scheduling(core.total_cores().into());
|
||||
|
||||
// We mainly call this to inform users at genesis if there is a mismatch with the
|
||||
// on-chain data.
|
||||
collator.collator_service().check_block_status(parent_hash, parent_header);
|
||||
|
||||
let Ok(relay_slot) =
|
||||
pezsc_consensus_babe::find_pre_digest::<RelayBlock>(&relay_parent_header)
|
||||
.map(|babe_pre_digest| babe_pre_digest.slot())
|
||||
else {
|
||||
tracing::error!(target: crate::LOG_TARGET, "Relay chain does not contain babe slot. This should never happen.");
|
||||
continue;
|
||||
};
|
||||
|
||||
let included_header_hash = included_header.hash();
|
||||
|
||||
if let Ok(authorities) = para_client.runtime_api().authorities(parent_hash) {
|
||||
connection_helper.update::<P>(para_slot.slot, &authorities).await;
|
||||
}
|
||||
|
||||
let slot_claim = match crate::collators::can_build_upon::<_, _, P>(
|
||||
para_slot.slot,
|
||||
relay_slot,
|
||||
para_slot.timestamp,
|
||||
parent_hash,
|
||||
included_header_hash,
|
||||
&*para_client,
|
||||
&keystore,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(slot) => slot,
|
||||
None => {
|
||||
tracing::debug!(
|
||||
target: crate::LOG_TARGET,
|
||||
unincluded_segment_len = parent.depth,
|
||||
relay_parent = ?relay_parent,
|
||||
relay_parent_num = %relay_parent_header.number(),
|
||||
included_hash = ?included_header_hash,
|
||||
included_num = %included_header.number(),
|
||||
parent = ?parent_hash,
|
||||
slot = ?para_slot.slot,
|
||||
"Not building block."
|
||||
);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
tracing::debug!(
|
||||
target: crate::LOG_TARGET,
|
||||
unincluded_segment_len = parent.depth,
|
||||
relay_parent = %relay_parent,
|
||||
relay_parent_num = %relay_parent_header.number(),
|
||||
relay_parent_offset,
|
||||
included_hash = %included_header_hash,
|
||||
included_num = %included_header.number(),
|
||||
parent = %parent_hash,
|
||||
slot = ?para_slot.slot,
|
||||
"Building block."
|
||||
);
|
||||
|
||||
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: *max_pov_size,
|
||||
};
|
||||
|
||||
let (teyrchain_inherent_data, other_inherent_data) = match collator
|
||||
.create_inherent_data_with_rp_offset(
|
||||
relay_parent,
|
||||
&validation_data,
|
||||
parent_hash,
|
||||
slot_claim.timestamp(),
|
||||
Some(rp_data),
|
||||
collator_peer_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
tracing::error!(target: crate::LOG_TARGET, ?err);
|
||||
break;
|
||||
},
|
||||
Ok(x) => x,
|
||||
};
|
||||
|
||||
let validation_code_hash = match code_hash_provider.code_hash_at(parent_hash) {
|
||||
None => {
|
||||
tracing::error!(target: crate::LOG_TARGET, ?parent_hash, "Could not fetch validation code hash");
|
||||
break;
|
||||
},
|
||||
Some(v) => v,
|
||||
};
|
||||
|
||||
check_validation_code_or_log(
|
||||
&validation_code_hash,
|
||||
para_id,
|
||||
&relay_client,
|
||||
relay_parent,
|
||||
)
|
||||
.await;
|
||||
|
||||
let allowed_pov_size = if let Some(max_pov_percentage) = 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/23 issue is
|
||||
// fixed, this should be removed.
|
||||
validation_data.max_pov_size * 85 / 100
|
||||
} as usize;
|
||||
|
||||
let adjusted_authoring_duration =
|
||||
slot_timer.adjust_authoring_duration(authoring_duration);
|
||||
tracing::debug!(target: crate::LOG_TARGET, duration = ?adjusted_authoring_duration, "Adjusted proposal duration.");
|
||||
|
||||
let Some(adjusted_authoring_duration) = adjusted_authoring_duration else {
|
||||
tracing::debug!(
|
||||
target: crate::LOG_TARGET,
|
||||
unincluded_segment_len = parent.depth,
|
||||
relay_parent = ?relay_parent,
|
||||
relay_parent_num = %relay_parent_header.number(),
|
||||
included_hash = ?included_header_hash,
|
||||
included_num = %included_header.number(),
|
||||
parent = ?parent_hash,
|
||||
slot = ?para_slot.slot,
|
||||
"Not building block due to insufficient authoring duration."
|
||||
);
|
||||
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(Some(candidate)) = collator
|
||||
.build_block_and_import(
|
||||
&parent_header,
|
||||
&slot_claim,
|
||||
Some(vec![CumulusDigestItem::CoreInfo(core.core_info()).to_digest_item()]),
|
||||
(teyrchain_inherent_data, other_inherent_data),
|
||||
adjusted_authoring_duration,
|
||||
allowed_pov_size,
|
||||
)
|
||||
.await
|
||||
else {
|
||||
tracing::error!(target: crate::LOG_TARGET, "Unable to build block at slot.");
|
||||
continue;
|
||||
};
|
||||
|
||||
let new_block_hash = candidate.block.header().hash();
|
||||
|
||||
// Announce the newly built block to our peers.
|
||||
collator.collator_service().announce_block(new_block_hash, None);
|
||||
|
||||
*last_claimed_core_selector = Some(core.core_selector());
|
||||
|
||||
if let Err(err) = collator_sender.unbounded_send(CollatorMessage {
|
||||
relay_parent,
|
||||
parent_header: parent_header.clone(),
|
||||
teyrchain_candidate: candidate,
|
||||
validation_code_hash,
|
||||
core_index: core.core_index(),
|
||||
max_pov_size: validation_data.max_pov_size,
|
||||
}) {
|
||||
tracing::error!(target: crate::LOG_TARGET, ?err, "Unable to send block to collation task.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Translate the slot of the relay parent to the slot of the teyrchain.
|
||||
fn adjust_para_to_relay_parent_slot(
|
||||
relay_header: &RelayHeader,
|
||||
relay_chain_slot_duration: Duration,
|
||||
para_slot_duration: SlotDuration,
|
||||
) -> Option<SlotInfo> {
|
||||
let relay_slot = pezsc_consensus_babe::find_pre_digest::<RelayBlock>(&relay_header)
|
||||
.map(|babe_pre_digest| babe_pre_digest.slot())
|
||||
.ok()?;
|
||||
let new_slot = Slot::from_timestamp(
|
||||
relay_slot
|
||||
.timestamp(SlotDuration::from_millis(relay_chain_slot_duration.as_millis() as u64))?,
|
||||
para_slot_duration,
|
||||
);
|
||||
let para_slot = SlotInfo { slot: new_slot, timestamp: new_slot.timestamp(para_slot_duration)? };
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
timestamp = ?para_slot.timestamp,
|
||||
slot = ?para_slot.slot,
|
||||
"Teyrchain slot adjusted to relay chain.",
|
||||
);
|
||||
Some(para_slot)
|
||||
}
|
||||
|
||||
/// Finds a relay chain parent block at a specified offset from the best block, collecting its
|
||||
/// descendants.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(RelayParentData)` - Contains the target relay parent and its ordered list of descendants
|
||||
/// * `Err(())` - If any relay chain block header cannot be retrieved
|
||||
///
|
||||
/// The function traverses backwards from the best block until it finds the block at the specified
|
||||
/// offset, collecting all blocks in between to maintain the chain of ancestry.
|
||||
pub(crate) async fn offset_relay_parent_find_descendants<RelayClient>(
|
||||
relay_chain_data_cache: &mut RelayChainDataCache<RelayClient>,
|
||||
relay_best_block: RelayHash,
|
||||
relay_parent_offset: u32,
|
||||
) -> Result<Option<RelayParentData>, ()>
|
||||
where
|
||||
RelayClient: RelayChainInterface + Clone + 'static,
|
||||
{
|
||||
let Ok(mut relay_header) = relay_chain_data_cache
|
||||
.get_mut_relay_chain_data(relay_best_block)
|
||||
.await
|
||||
.map(|d| d.relay_parent_header.clone())
|
||||
else {
|
||||
tracing::error!(target: LOG_TARGET, ?relay_best_block, "Unable to fetch best relay chain block header.");
|
||||
return Err(());
|
||||
};
|
||||
|
||||
if relay_parent_offset == 0 {
|
||||
return Ok(Some(RelayParentData::new(relay_header)));
|
||||
}
|
||||
|
||||
if pezsc_consensus_babe::contains_epoch_change::<RelayBlock>(&relay_header) {
|
||||
tracing::debug!(target: LOG_TARGET, ?relay_best_block, relay_best_block_number = relay_header.number(), "Relay parent is in previous session.");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut required_ancestors: VecDeque<RelayHeader> = Default::default();
|
||||
required_ancestors.push_front(relay_header.clone());
|
||||
while required_ancestors.len() < relay_parent_offset as usize {
|
||||
let next_header = relay_chain_data_cache
|
||||
.get_mut_relay_chain_data(*relay_header.parent_hash())
|
||||
.await?
|
||||
.relay_parent_header
|
||||
.clone();
|
||||
if pezsc_consensus_babe::contains_epoch_change::<RelayBlock>(&next_header) {
|
||||
tracing::debug!(target: LOG_TARGET, ?relay_best_block, ancestor = %next_header.hash(), ancestor_block_number = next_header.number(), "Ancestor of best block is in previous session.");
|
||||
return Ok(None);
|
||||
}
|
||||
required_ancestors.push_front(next_header.clone());
|
||||
relay_header = next_header;
|
||||
}
|
||||
|
||||
let relay_parent = relay_chain_data_cache
|
||||
.get_mut_relay_chain_data(*relay_header.parent_hash())
|
||||
.await?
|
||||
.relay_parent_header
|
||||
.clone();
|
||||
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
relay_parent_hash = %relay_parent.hash(),
|
||||
relay_parent_num = relay_parent.number(),
|
||||
num_descendants = required_ancestors.len(),
|
||||
"Relay parent descendants."
|
||||
);
|
||||
|
||||
Ok(Some(RelayParentData::new_with_descendants(relay_parent, required_ancestors.into())))
|
||||
}
|
||||
|
||||
/// Return value of [`determine_core`].
|
||||
pub(crate) struct Core {
|
||||
selector: CoreSelector,
|
||||
claim_queue_offset: ClaimQueueOffset,
|
||||
core_index: CoreIndex,
|
||||
number_of_cores: u16,
|
||||
}
|
||||
|
||||
impl Core {
|
||||
/// Returns the current [`CoreInfo`].
|
||||
fn core_info(&self) -> CoreInfo {
|
||||
CoreInfo {
|
||||
selector: self.selector,
|
||||
claim_queue_offset: self.claim_queue_offset,
|
||||
number_of_cores: self.number_of_cores.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current [`CoreSelector`].
|
||||
pub(crate) fn core_selector(&self) -> CoreSelector {
|
||||
self.selector
|
||||
}
|
||||
|
||||
/// Returns the current [`CoreIndex`].
|
||||
pub(crate) fn core_index(&self) -> CoreIndex {
|
||||
self.core_index
|
||||
}
|
||||
|
||||
/// Returns the total number of cores.
|
||||
pub(crate) fn total_cores(&self) -> u16 {
|
||||
self.number_of_cores
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine the core for the given `para_id`.
|
||||
pub(crate) async fn determine_core<H: HeaderT, RI: RelayChainInterface + 'static>(
|
||||
relay_chain_data_cache: &mut RelayChainDataCache<RI>,
|
||||
relay_parent: &RelayHeader,
|
||||
para_id: ParaId,
|
||||
para_parent: &H,
|
||||
relay_parent_offset: u32,
|
||||
) -> Result<Option<Core>, ()> {
|
||||
let cores_at_offset = &relay_chain_data_cache
|
||||
.get_mut_relay_chain_data(relay_parent.hash())
|
||||
.await?
|
||||
.claim_queue
|
||||
.iter_claims_at_depth_for_para(relay_parent_offset as usize, para_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let is_new_relay_parent = if para_parent.number().is_zero() {
|
||||
true
|
||||
} else {
|
||||
match extract_relay_parent(para_parent.digest()) {
|
||||
Some(last_relay_parent) => last_relay_parent != relay_parent.hash(),
|
||||
None =>
|
||||
rpsr_digest::extract_relay_parent_storage_root(para_parent.digest())
|
||||
.ok_or(())?
|
||||
.0 != *relay_parent.state_root(),
|
||||
}
|
||||
};
|
||||
|
||||
let core_info = CumulusDigestItem::find_core_info(para_parent.digest());
|
||||
|
||||
// If we are using a new relay parent, we can start over from the start.
|
||||
let (selector, core_index) = if is_new_relay_parent {
|
||||
let Some(core_index) = cores_at_offset.get(0) else { return Ok(None) };
|
||||
|
||||
(0, *core_index)
|
||||
} else if let Some(core_info) = core_info {
|
||||
let selector = core_info.selector.0 as usize + 1;
|
||||
let Some(core_index) = cores_at_offset.get(selector) else { return Ok(None) };
|
||||
|
||||
(selector, *core_index)
|
||||
} else {
|
||||
let last_claimed_core_selector = relay_chain_data_cache
|
||||
.get_mut_relay_chain_data(relay_parent.hash())
|
||||
.await?
|
||||
.last_claimed_core_selector;
|
||||
|
||||
let selector = last_claimed_core_selector.map_or(0, |cs| cs.0 as usize) + 1;
|
||||
let Some(core_index) = cores_at_offset.get(selector) else { return Ok(None) };
|
||||
|
||||
(selector, *core_index)
|
||||
};
|
||||
|
||||
Ok(Some(Core {
|
||||
selector: CoreSelector(selector as u8),
|
||||
core_index,
|
||||
claim_queue_offset: ClaimQueueOffset(relay_parent_offset as u8),
|
||||
number_of_cores: cores_at_offset.len() as u16,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use futures::{stream::FusedStream, StreamExt};
|
||||
use pezsc_consensus::{BlockImport, StateAction};
|
||||
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
|
||||
use pezsp_api::{ApiExt, CallApiAt, CallContext, Core, ProvideRuntimeApi, StorageProof};
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header as _};
|
||||
use pezsp_trie::proof_size_extension::ProofSizeExt;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Handle for receiving the block and the storage proof from the [`SlotBasedBlockImport`].
|
||||
///
|
||||
/// This handle should be passed to [`Params`](super::Params) or can also be dropped if the node is
|
||||
/// not running as collator.
|
||||
pub struct SlotBasedBlockImportHandle<Block> {
|
||||
receiver: TracingUnboundedReceiver<(Block, StorageProof)>,
|
||||
}
|
||||
|
||||
impl<Block> SlotBasedBlockImportHandle<Block> {
|
||||
/// Returns the next item.
|
||||
///
|
||||
/// The future will never return when the internal channel is closed.
|
||||
pub async fn next(&mut self) -> (Block, StorageProof) {
|
||||
loop {
|
||||
if self.receiver.is_terminated() {
|
||||
futures::pending!()
|
||||
} else if let Some(res) = self.receiver.next().await {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Special block import for the slot based collator.
|
||||
pub struct SlotBasedBlockImport<Block, BI, Client> {
|
||||
inner: BI,
|
||||
client: Arc<Client>,
|
||||
sender: TracingUnboundedSender<(Block, StorageProof)>,
|
||||
}
|
||||
|
||||
impl<Block, BI, Client> SlotBasedBlockImport<Block, BI, Client> {
|
||||
/// Create a new instance.
|
||||
///
|
||||
/// The returned [`SlotBasedBlockImportHandle`] needs to be passed to the
|
||||
/// [`Params`](super::Params), so that this block import instance can communicate with the
|
||||
/// collation task. If the node is not running as a collator, just dropping the handle is fine.
|
||||
pub fn new(inner: BI, client: Arc<Client>) -> (Self, SlotBasedBlockImportHandle<Block>) {
|
||||
let (sender, receiver) = tracing_unbounded("SlotBasedBlockImportChannel", 1000);
|
||||
|
||||
(Self { sender, client, inner }, SlotBasedBlockImportHandle { receiver })
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block, BI: Clone, Client> Clone for SlotBasedBlockImport<Block, BI, Client> {
|
||||
fn clone(&self) -> Self {
|
||||
Self { inner: self.inner.clone(), client: self.client.clone(), sender: self.sender.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<Block, BI, Client> BlockImport<Block> for SlotBasedBlockImport<Block, BI, Client>
|
||||
where
|
||||
Block: BlockT,
|
||||
BI: BlockImport<Block> + Send + Sync,
|
||||
BI::Error: Into<pezsp_consensus::Error>,
|
||||
Client: ProvideRuntimeApi<Block> + CallApiAt<Block> + Send + Sync,
|
||||
Client::StateBackend: Send,
|
||||
Client::Api: Core<Block>,
|
||||
{
|
||||
type Error = pezsp_consensus::Error;
|
||||
|
||||
async fn check_block(
|
||||
&self,
|
||||
block: pezsc_consensus::BlockCheckParams<Block>,
|
||||
) -> Result<pezsc_consensus::ImportResult, Self::Error> {
|
||||
self.inner.check_block(block).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn import_block(
|
||||
&self,
|
||||
mut params: pezsc_consensus::BlockImportParams<Block>,
|
||||
) -> Result<pezsc_consensus::ImportResult, Self::Error> {
|
||||
// If the channel exists and it is required to execute the block, we will execute the block
|
||||
// here. This is done to collect the storage proof and to prevent re-execution, we push
|
||||
// downwards the state changes. `StateAction::ApplyChanges` is ignored, because it either
|
||||
// means that the node produced the block itself or the block was imported via state sync.
|
||||
if !self.sender.is_closed() && !matches!(params.state_action, StateAction::ApplyChanges(_))
|
||||
{
|
||||
let mut runtime_api = self.client.runtime_api();
|
||||
|
||||
runtime_api.set_call_context(CallContext::Onchain);
|
||||
|
||||
runtime_api.record_proof();
|
||||
let recorder = runtime_api
|
||||
.proof_recorder()
|
||||
.expect("Proof recording is enabled in the line above; qed.");
|
||||
runtime_api.register_extension(ProofSizeExt::new(recorder));
|
||||
|
||||
let parent_hash = *params.header.parent_hash();
|
||||
|
||||
let block = Block::new(params.header.clone(), params.body.clone().unwrap_or_default());
|
||||
|
||||
runtime_api
|
||||
.execute_block(parent_hash, block.clone().into())
|
||||
.map_err(|e| Box::new(e) as Box<_>)?;
|
||||
|
||||
let storage_proof =
|
||||
runtime_api.extract_proof().expect("Proof recording was enabled above; qed");
|
||||
|
||||
let state = self.client.state_at(parent_hash).map_err(|e| Box::new(e) as Box<_>)?;
|
||||
let gen_storage_changes = runtime_api
|
||||
.into_storage_changes(&state, parent_hash)
|
||||
.map_err(pezsp_consensus::Error::ChainLookup)?;
|
||||
|
||||
if params.header.state_root() != &gen_storage_changes.transaction_storage_root {
|
||||
return Err(pezsp_consensus::Error::Other(Box::new(
|
||||
pezsp_blockchain::Error::InvalidStateRoot,
|
||||
)));
|
||||
}
|
||||
|
||||
params.state_action = StateAction::ApplyChanges(pezsc_consensus::StorageChanges::Changes(
|
||||
gen_storage_changes,
|
||||
));
|
||||
|
||||
let _ = self.sender.unbounded_send((block, storage_proof));
|
||||
}
|
||||
|
||||
self.inner.import_block(params).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use codec::Encode;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
|
||||
use cumulus_relay_chain_interface::RelayChainInterface;
|
||||
|
||||
use pezkuwi_node_primitives::{MaybeCompressedPoV, SubmitCollationParams};
|
||||
use pezkuwi_node_subsystem::messages::CollationGenerationMessage;
|
||||
use pezkuwi_overseer::Handle as OverseerHandle;
|
||||
use pezkuwi_primitives::{CollatorPair, Id as ParaId};
|
||||
|
||||
use cumulus_primitives_core::relay_chain::BlockId;
|
||||
use futures::prelude::*;
|
||||
|
||||
use crate::export_pov_to_path;
|
||||
use pezsc_utils::mpsc::TracingUnboundedReceiver;
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header};
|
||||
|
||||
use super::CollatorMessage;
|
||||
|
||||
const LOG_TARGET: &str = "aura::pezcumulus::collation_task";
|
||||
|
||||
/// Parameters for the collation task.
|
||||
pub struct Params<Block: BlockT, RClient, CS> {
|
||||
/// A handle to the relay-chain client.
|
||||
pub relay_client: RClient,
|
||||
/// The collator key used to sign collations before submitting to validators.
|
||||
pub collator_key: CollatorPair,
|
||||
/// The para's ID.
|
||||
pub para_id: ParaId,
|
||||
/// Whether we should reinitialize the collator config (i.e. we are transitioning to aura).
|
||||
pub reinitialize: bool,
|
||||
/// Collator service interface
|
||||
pub collator_service: CS,
|
||||
/// Receiver channel for communication with the block builder task.
|
||||
pub collator_receiver: TracingUnboundedReceiver<CollatorMessage<Block>>,
|
||||
/// The handle from the special slot based block import.
|
||||
pub block_import_handle: super::SlotBasedBlockImportHandle<Block>,
|
||||
/// When set, the collator will export every produced `POV` to this folder.
|
||||
pub export_pov: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Asynchronously executes the collation task for a teyrchain.
|
||||
///
|
||||
/// This function initializes the collator subsystems necessary for producing and submitting
|
||||
/// collations to the relay chain. It listens for new best relay chain block notifications and
|
||||
/// handles collator messages. If our teyrchain is scheduled on a core and we have a candidate,
|
||||
/// the task will build a collation and send it to the relay chain.
|
||||
pub async fn run_collation_task<Block, RClient, CS>(
|
||||
Params {
|
||||
relay_client,
|
||||
collator_key,
|
||||
para_id,
|
||||
reinitialize,
|
||||
collator_service,
|
||||
mut collator_receiver,
|
||||
mut block_import_handle,
|
||||
export_pov,
|
||||
}: Params<Block, RClient, CS>,
|
||||
) where
|
||||
Block: BlockT,
|
||||
CS: CollatorServiceInterface<Block> + Send + Sync + 'static,
|
||||
RClient: RelayChainInterface + Clone + 'static,
|
||||
{
|
||||
let Ok(mut overseer_handle) = relay_client.overseer_handle() else {
|
||||
tracing::error!(target: LOG_TARGET, "Failed to get overseer handle.");
|
||||
return;
|
||||
};
|
||||
|
||||
cumulus_client_collator::initialize_collator_subsystems(
|
||||
&mut overseer_handle,
|
||||
collator_key,
|
||||
para_id,
|
||||
reinitialize,
|
||||
)
|
||||
.await;
|
||||
|
||||
loop {
|
||||
futures::select! {
|
||||
collator_message = collator_receiver.next() => {
|
||||
let Some(message) = collator_message else {
|
||||
return;
|
||||
};
|
||||
|
||||
handle_collation_message(message, &collator_service, &mut overseer_handle,relay_client.clone(),export_pov.clone()).await;
|
||||
},
|
||||
block_import_msg = block_import_handle.next().fuse() => {
|
||||
// TODO: Implement me.
|
||||
// Issue: https://github.com/pezkuwichain/pezkuwi-sdk/issues/24
|
||||
let _ = block_import_msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an incoming collation message from the block builder task.
|
||||
/// This builds the collation from the [`CollatorMessage`] and submits it to
|
||||
/// the collation-generation subsystem of the relay chain.
|
||||
async fn handle_collation_message<Block: BlockT, RClient: RelayChainInterface + Clone + 'static>(
|
||||
message: CollatorMessage<Block>,
|
||||
collator_service: &impl CollatorServiceInterface<Block>,
|
||||
overseer_handle: &mut OverseerHandle,
|
||||
relay_client: RClient,
|
||||
export_pov: Option<PathBuf>,
|
||||
) {
|
||||
let CollatorMessage {
|
||||
parent_header,
|
||||
teyrchain_candidate,
|
||||
validation_code_hash,
|
||||
relay_parent,
|
||||
core_index,
|
||||
max_pov_size,
|
||||
} = message;
|
||||
|
||||
let hash = teyrchain_candidate.block.header().hash();
|
||||
let number = *teyrchain_candidate.block.header().number();
|
||||
let (collation, block_data) =
|
||||
match collator_service.build_collation(&parent_header, hash, teyrchain_candidate) {
|
||||
Some(collation) => collation,
|
||||
None => {
|
||||
tracing::warn!(target: LOG_TARGET, %hash, ?number, ?core_index, "Unable to build collation.");
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
block_data.log_size_info();
|
||||
|
||||
if let MaybeCompressedPoV::Compressed(ref pov) = collation.proof_of_validity {
|
||||
if let Some(pov_path) = export_pov {
|
||||
if let Ok(Some(relay_parent_header)) =
|
||||
relay_client.header(BlockId::Hash(relay_parent)).await
|
||||
{
|
||||
if let Some(header) = block_data.blocks().first().map(|b| b.header()) {
|
||||
export_pov_to_path::<Block>(
|
||||
pov_path.clone(),
|
||||
pov.clone(),
|
||||
header.hash(),
|
||||
*header.number(),
|
||||
parent_header.clone(),
|
||||
relay_parent_header.state_root,
|
||||
relay_parent_header.number,
|
||||
max_pov_size,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tracing::error!(target: LOG_TARGET, "Failed to get relay parent header from hash: {relay_parent:?}");
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
target: LOG_TARGET,
|
||||
"Compressed PoV size: {}kb",
|
||||
pov.block_data.0.len() as f64 / 1024f64,
|
||||
);
|
||||
}
|
||||
|
||||
tracing::debug!(target: LOG_TARGET, ?core_index, ?hash, %number, "Submitting collation for core.");
|
||||
|
||||
overseer_handle
|
||||
.send_msg(
|
||||
CollationGenerationMessage::SubmitCollation(SubmitCollationParams {
|
||||
relay_parent,
|
||||
collation,
|
||||
parent_head: parent_header.encode().into(),
|
||||
validation_code_hash,
|
||||
core_index,
|
||||
result_sender: None,
|
||||
}),
|
||||
"SubmitCollation",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! # Architecture Overview
|
||||
//!
|
||||
//! The block building mechanism operates through two coordinated tasks:
|
||||
//!
|
||||
//! 1. **Block Builder Task**: Orchestrates the timing and execution of teyrchain block production
|
||||
//! 2. **Collator Task**: Processes built blocks into collations for relay chain submission
|
||||
//!
|
||||
//! # Block Builder Task Details
|
||||
//!
|
||||
//! The block builder task manages block production timing and execution through an iterative
|
||||
//! process:
|
||||
//!
|
||||
//! 1. Awaits the next production signal from the internal timer
|
||||
//! 2. Retrieves the current best relay chain block and identifies a valid parent block (see
|
||||
//! [find_potential_parents][cumulus_client_consensus_common::find_potential_parents] for parent
|
||||
//! selection criteria)
|
||||
//! 3. Validates that:
|
||||
//! - The teyrchain has an assigned core on the relay chain
|
||||
//! - No block has been previously built on the target core
|
||||
//! 4. Executes block building and import operations
|
||||
//! 5. Transmits the completed block to the collator task
|
||||
//!
|
||||
//! # Block Production Timing
|
||||
//!
|
||||
//! When a block is produced is determined by the following parameters:
|
||||
//!
|
||||
//! - Teyrchain slot duration
|
||||
//! - Number of assigned teyrchain cores
|
||||
//! - Teyrchain runtime configuration
|
||||
//!
|
||||
//! ## Timing Examples
|
||||
//!
|
||||
//! The following table demonstrates various timing configurations and their effects. The "AURA
|
||||
//! Slot" column shows which author is responsible for the block.
|
||||
//!
|
||||
//! | Slot Duration (ms) | Cores | Production Attempts (ms) | AURA Slot |
|
||||
//! |-------------------|--------|-------------------------|------------|
|
||||
//! | 2000 | 3 | 0, 2000, 4000, 6000 | 0, 1, 2, 3 |
|
||||
//! | 6000 | 1 | 0, 6000, 12000, 18000 | 0, 1, 2, 3 |
|
||||
//! | 6000 | 3 | 0, 2000, 4000, 6000 | 0, 0, 0, 1 |
|
||||
//! | 12000 | 1 | 0, 6000, 12000, 18000 | 0, 0, 1, 1 |
|
||||
//! | 12000 | 3 | 0, 2000, 4000, 6000 | 0, 0, 0, 0 |
|
||||
//!
|
||||
//! # Collator Task Details
|
||||
//!
|
||||
//! The collator task receives built blocks from the block builder task and performs two primary
|
||||
//! functions:
|
||||
//!
|
||||
//! 1. Block compression
|
||||
//! 2. Submission to the collation-generation subsystem
|
||||
|
||||
use self::{block_builder_task::run_block_builder, collation_task::run_collation_task};
|
||||
pub use block_import::{SlotBasedBlockImport, SlotBasedBlockImportHandle};
|
||||
use codec::Codec;
|
||||
use consensus_common::TeyrchainCandidate;
|
||||
use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface;
|
||||
use cumulus_client_consensus_common::{self as consensus_common, TeyrchainBlockImportMarker};
|
||||
use cumulus_client_consensus_proposer::ProposerInterface;
|
||||
use cumulus_primitives_aura::AuraUnincludedSegmentApi;
|
||||
use cumulus_primitives_core::RelayParentOffsetApi;
|
||||
use cumulus_relay_chain_interface::RelayChainInterface;
|
||||
use futures::FutureExt;
|
||||
use pezkuwi_primitives::{
|
||||
CollatorPair, CoreIndex, Hash as RelayHash, Id as ParaId, ValidationCodeHash,
|
||||
};
|
||||
use pezsc_client_api::{backend::AuxStore, BlockBackend, BlockOf, UsageProvider};
|
||||
use pezsc_consensus::BlockImport;
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsc_utils::mpsc::tracing_unbounded;
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_application_crypto::AppPublic;
|
||||
use pezsp_blockchain::HeaderBackend;
|
||||
use pezsp_consensus_aura::AuraApi;
|
||||
use pezsp_core::{crypto::Pair, traits::SpawnEssentialNamed};
|
||||
use pezsp_inherents::CreateInherentDataProviders;
|
||||
use pezsp_keystore::KeystorePtr;
|
||||
use pezsp_runtime::traits::{Block as BlockT, Member};
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
mod block_builder_task;
|
||||
mod block_import;
|
||||
mod collation_task;
|
||||
mod relay_chain_data_cache;
|
||||
mod slot_timer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Parameters for [`run`].
|
||||
pub struct Params<Block, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS, Spawner> {
|
||||
/// 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<Client>,
|
||||
/// The para client's backend, used to access the database.
|
||||
pub para_backend: Arc<Backend>,
|
||||
/// 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,
|
||||
/// 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,
|
||||
/// Offset slots by a fixed duration. This can be used to create more preferrable authoring
|
||||
/// timings.
|
||||
pub slot_offset: Duration,
|
||||
/// The handle returned by [`SlotBasedBlockImport`].
|
||||
pub block_import_handle: SlotBasedBlockImportHandle<Block>,
|
||||
/// Spawner for spawning futures.
|
||||
pub spawner: Spawner,
|
||||
/// Slot duration of the relay chain
|
||||
pub relay_chain_slot_duration: Duration,
|
||||
/// When set, the collator will export every produced `POV` to this folder.
|
||||
pub export_pov: Option<PathBuf>,
|
||||
/// The maximum percentage of the maximum PoV size that the collator can use.
|
||||
/// It will be removed once <https://github.com/pezkuwichain/pezkuwi-sdk/issues/23> is fixed.
|
||||
pub max_pov_percentage: Option<u32>,
|
||||
}
|
||||
|
||||
/// Run aura-based block building and collation task.
|
||||
pub fn run<Block, P, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS, Spawner>(
|
||||
params: Params<Block, BI, CIDP, Client, Backend, RClient, CHP, Proposer, CS, Spawner>,
|
||||
) where
|
||||
Block: BlockT,
|
||||
Client: ProvideRuntimeApi<Block>
|
||||
+ BlockOf
|
||||
+ AuxStore
|
||||
+ HeaderBackend<Block>
|
||||
+ BlockBackend<Block>
|
||||
+ UsageProvider<Block>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
Client::Api:
|
||||
AuraApi<Block, P::Public> + AuraUnincludedSegmentApi<Block> + RelayParentOffsetApi<Block>,
|
||||
Backend: pezsc_client_api::Backend<Block> + 'static,
|
||||
RClient: RelayChainInterface + Clone + 'static,
|
||||
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
|
||||
CIDP::InherentDataProviders: Send,
|
||||
BI: BlockImport<Block> + TeyrchainBlockImportMarker + Send + Sync + 'static,
|
||||
Proposer: ProposerInterface<Block> + Send + Sync + 'static,
|
||||
CS: CollatorServiceInterface<Block> + Send + Sync + Clone + 'static,
|
||||
CHP: consensus_common::ValidationCodeHashProvider<Block::Hash> + Send + 'static,
|
||||
P: Pair + Send + Sync + 'static,
|
||||
P::Public: AppPublic + Member + Codec,
|
||||
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
|
||||
Spawner: SpawnEssentialNamed + Clone + 'static,
|
||||
{
|
||||
let Params {
|
||||
create_inherent_data_providers,
|
||||
block_import,
|
||||
para_client,
|
||||
para_backend,
|
||||
relay_client,
|
||||
code_hash_provider,
|
||||
keystore,
|
||||
collator_key,
|
||||
collator_peer_id,
|
||||
para_id,
|
||||
proposer,
|
||||
collator_service,
|
||||
authoring_duration,
|
||||
reinitialize,
|
||||
slot_offset,
|
||||
block_import_handle,
|
||||
spawner,
|
||||
export_pov,
|
||||
relay_chain_slot_duration,
|
||||
max_pov_percentage,
|
||||
} = params;
|
||||
|
||||
let (tx, rx) = tracing_unbounded("mpsc_builder_to_collator", 100);
|
||||
let collator_task_params = collation_task::Params {
|
||||
relay_client: relay_client.clone(),
|
||||
collator_key,
|
||||
para_id,
|
||||
reinitialize,
|
||||
collator_service: collator_service.clone(),
|
||||
collator_receiver: rx,
|
||||
block_import_handle,
|
||||
export_pov,
|
||||
};
|
||||
|
||||
let collation_task_fut = run_collation_task::<Block, _, _>(collator_task_params);
|
||||
|
||||
let block_builder_params = block_builder_task::BuilderTaskParams {
|
||||
create_inherent_data_providers,
|
||||
block_import,
|
||||
para_client,
|
||||
para_backend,
|
||||
relay_client,
|
||||
code_hash_provider,
|
||||
keystore,
|
||||
collator_peer_id,
|
||||
para_id,
|
||||
proposer,
|
||||
collator_service,
|
||||
authoring_duration,
|
||||
collator_sender: tx,
|
||||
relay_chain_slot_duration,
|
||||
slot_offset,
|
||||
max_pov_percentage,
|
||||
};
|
||||
|
||||
let block_builder_fut =
|
||||
run_block_builder::<Block, P, _, _, _, _, _, _, _, _>(block_builder_params);
|
||||
|
||||
spawner.spawn_essential_blocking(
|
||||
"slot-based-block-builder",
|
||||
Some("slot-based-collator"),
|
||||
block_builder_fut.boxed(),
|
||||
);
|
||||
spawner.spawn_essential_blocking(
|
||||
"slot-based-collation",
|
||||
Some("slot-based-collator"),
|
||||
collation_task_fut.boxed(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Message to be sent from the block builder to the collation task.
|
||||
///
|
||||
/// Contains all data necessary to submit a collation to the relay chain.
|
||||
struct CollatorMessage<Block: BlockT> {
|
||||
/// The hash of the relay chain block that provides the context for the teyrchain block.
|
||||
pub relay_parent: RelayHash,
|
||||
/// The header of the parent block.
|
||||
pub parent_header: Block::Header,
|
||||
/// The teyrchain block candidate.
|
||||
pub teyrchain_candidate: TeyrchainCandidate<Block>,
|
||||
/// The validation code hash at the parent block.
|
||||
pub validation_code_hash: ValidationCodeHash,
|
||||
/// Core index that this block should be submitted on
|
||||
pub core_index: CoreIndex,
|
||||
/// Maximum pov size. Currently needed only for exporting PoV.
|
||||
pub max_pov_size: u32,
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Utility for caching [`RelayChainData`] for different relay blocks.
|
||||
|
||||
use crate::collators::claim_queue_at;
|
||||
use cumulus_primitives_core::CoreSelector;
|
||||
use cumulus_relay_chain_interface::RelayChainInterface;
|
||||
use pezkuwi_node_subsystem_util::runtime::ClaimQueueSnapshot;
|
||||
use pezkuwi_primitives::{
|
||||
Hash as RelayHash, Header as RelayHeader, Id as ParaId, OccupiedCoreAssumption,
|
||||
};
|
||||
use pezsp_runtime::generic::BlockId;
|
||||
|
||||
/// Contains relay chain data necessary for teyrchain block building.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RelayChainData {
|
||||
/// Current relay chain parent header.
|
||||
pub relay_parent_header: RelayHeader,
|
||||
/// The claim queue at the relay parent.
|
||||
pub claim_queue: ClaimQueueSnapshot,
|
||||
/// Maximum configured PoV size on the relay chain.
|
||||
pub max_pov_size: u32,
|
||||
/// The last [`CoreSelector`] we used.
|
||||
pub last_claimed_core_selector: Option<CoreSelector>,
|
||||
}
|
||||
|
||||
/// Simple helper to fetch relay chain data and cache it based on the current relay chain best block
|
||||
/// hash.
|
||||
pub struct RelayChainDataCache<RI> {
|
||||
relay_client: RI,
|
||||
para_id: ParaId,
|
||||
cached_data: schnellru::LruMap<RelayHash, RelayChainData>,
|
||||
}
|
||||
|
||||
impl<RI> RelayChainDataCache<RI>
|
||||
where
|
||||
RI: RelayChainInterface + 'static,
|
||||
{
|
||||
pub fn new(relay_client: RI, para_id: ParaId) -> Self {
|
||||
Self {
|
||||
relay_client,
|
||||
para_id,
|
||||
// 50 cached relay chain blocks should be more than enough.
|
||||
cached_data: schnellru::LruMap::new(schnellru::ByLength::new(50)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch required [`RelayChainData`] from the relay chain.
|
||||
/// If this data has been fetched in the past for the incoming hash, it will reuse
|
||||
/// cached data.
|
||||
pub async fn get_mut_relay_chain_data(
|
||||
&mut self,
|
||||
relay_parent: RelayHash,
|
||||
) -> Result<&mut RelayChainData, ()> {
|
||||
let insert_data = if self.cached_data.peek(&relay_parent).is_some() {
|
||||
tracing::trace!(target: crate::LOG_TARGET, %relay_parent, "Using cached data for relay parent.");
|
||||
None
|
||||
} else {
|
||||
tracing::trace!(target: crate::LOG_TARGET, %relay_parent, "Relay chain best block changed, fetching new data from relay chain.");
|
||||
Some(self.update_for_relay_parent(relay_parent).await?)
|
||||
};
|
||||
|
||||
Ok(self
|
||||
.cached_data
|
||||
.get_or_insert(relay_parent, || {
|
||||
insert_data.expect("`insert_data` exists if not cached yet; qed")
|
||||
})
|
||||
.expect("There is space for at least one element; qed"))
|
||||
}
|
||||
|
||||
/// Fetch fresh data from the relay chain for the given relay parent hash.
|
||||
async fn update_for_relay_parent(&self, relay_parent: RelayHash) -> Result<RelayChainData, ()> {
|
||||
let claim_queue = claim_queue_at(relay_parent, &self.relay_client).await;
|
||||
|
||||
let Ok(Some(relay_parent_header)) =
|
||||
self.relay_client.header(BlockId::Hash(relay_parent)).await
|
||||
else {
|
||||
tracing::warn!(target: crate::LOG_TARGET, "Unable to fetch latest relay chain block header.");
|
||||
return Err(());
|
||||
};
|
||||
|
||||
let max_pov_size = match self
|
||||
.relay_client
|
||||
.persisted_validation_data(relay_parent, self.para_id, OccupiedCoreAssumption::Included)
|
||||
.await
|
||||
{
|
||||
Ok(None) => return Err(()),
|
||||
Ok(Some(pvd)) => pvd.max_pov_size,
|
||||
Err(err) => {
|
||||
tracing::error!(target: crate::LOG_TARGET, ?err, "Failed to gather information from relay-client");
|
||||
return Err(());
|
||||
},
|
||||
};
|
||||
|
||||
Ok(RelayChainData {
|
||||
relay_parent_header,
|
||||
claim_queue,
|
||||
max_pov_size,
|
||||
last_claimed_core_selector: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn insert_test_data(&mut self, relay_parent: RelayHash, data: RelayChainData) {
|
||||
self.cached_data.insert(relay_parent, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::LOG_TARGET;
|
||||
use codec::Codec;
|
||||
use cumulus_primitives_aura::Slot;
|
||||
use cumulus_primitives_core::BlockT;
|
||||
use pezsc_client_api::UsageProvider;
|
||||
use pezsc_consensus_aura::SlotDuration;
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_application_crypto::AppPublic;
|
||||
use pezsp_consensus_aura::AuraApi;
|
||||
use pezsp_core::Pair;
|
||||
use pezsp_runtime::traits::Member;
|
||||
use pezsp_timestamp::Timestamp;
|
||||
use std::{
|
||||
cmp::{max, min},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// Lower limits of allowed block production interval.
|
||||
/// Defensive mechanism, corresponds to 12 cores at 6 second block time.
|
||||
const BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS: Duration = Duration::from_millis(500);
|
||||
|
||||
/// Theoretically, the block production is capped at `BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS`.
|
||||
/// In practice, there might be slight deviations due to timing inaccuracies and delays.
|
||||
///
|
||||
/// This constant is taken into account while adjusting the authoring duration to fit into the slot.
|
||||
/// Therefore, it will only reduce the authoring duration if we are within the
|
||||
/// `BLOCK_PRODUCTION_ADJUSTMENT_MS` threshold of the next slot.
|
||||
///
|
||||
/// ### 12 cores 500ms blocks
|
||||
///
|
||||
/// For example, for 12 cores 500ms blocks: the next slot is scheduled in 490ms due to delays.
|
||||
/// In that case, we still want to attempt producing the block, as missing the slot would be worse
|
||||
/// than producing slightly too fast.
|
||||
const BLOCK_PRODUCTION_THRESHOLD_MS: Duration = Duration::from_millis(100);
|
||||
|
||||
/// The amount of time the authoring duration of the last block production attempt
|
||||
/// should be reduced by to fit into the slot timing.
|
||||
const BLOCK_PRODUCTION_ADJUSTMENT_MS: Duration = Duration::from_millis(1000);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SlotInfo {
|
||||
pub timestamp: Timestamp,
|
||||
pub slot: Slot,
|
||||
}
|
||||
|
||||
/// Manages block-production timings based on chain parameters and assigned cores.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SlotTimer<Block, Client, P> {
|
||||
/// Teyrchain client that is used for runtime calls
|
||||
client: Arc<Client>,
|
||||
/// Offset the current time by this duration.
|
||||
time_offset: Duration,
|
||||
/// Last reported core count.
|
||||
last_reported_core_num: Option<u32>,
|
||||
/// Slot duration of the relay chain. This is used to compute how man block-production
|
||||
/// attempts we should trigger per relay chain block.
|
||||
relay_slot_duration: Duration,
|
||||
/// Stores the latest slot that was reported by [`Self::wait_until_next_slot`].
|
||||
last_reported_slot: Option<Slot>,
|
||||
_marker: std::marker::PhantomData<(Block, Box<dyn Fn(P) + Send + Sync + 'static>)>,
|
||||
}
|
||||
|
||||
/// Compute when to try block-authoring next.
|
||||
/// The exact time point is determined by the slot duration of relay- and teyrchain as
|
||||
/// well as the last observed core count. If more cores are available, we attempt to author blocks
|
||||
/// for them.
|
||||
///
|
||||
/// Returns a tuple with:
|
||||
/// - `Duration`: How long to wait until the next slot.
|
||||
/// - `Slot`: The AURA slot used for authoring
|
||||
fn compute_next_wake_up_time(
|
||||
para_slot_duration: SlotDuration,
|
||||
relay_slot_duration: Duration,
|
||||
core_count: Option<u32>,
|
||||
time_now: Duration,
|
||||
time_offset: Duration,
|
||||
) -> (Duration, Slot) {
|
||||
let para_slots_per_relay_block =
|
||||
(relay_slot_duration.as_millis() / para_slot_duration.as_millis() as u128) as u32;
|
||||
let assigned_core_num = core_count.unwrap_or(1);
|
||||
|
||||
// Trigger at least once per relay block, if we have for example 12 second slot duration,
|
||||
// we should still produce two blocks if we are scheduled on every relay block.
|
||||
let mut block_production_interval = min(para_slot_duration.as_duration(), relay_slot_duration);
|
||||
|
||||
if assigned_core_num > para_slots_per_relay_block &&
|
||||
para_slot_duration.as_duration() >= relay_slot_duration
|
||||
{
|
||||
block_production_interval =
|
||||
max(relay_slot_duration / assigned_core_num, BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS);
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
?block_production_interval,
|
||||
"Expected to produce for {assigned_core_num} cores but only have {para_slots_per_relay_block} slots. Attempting to produce multiple blocks per slot."
|
||||
);
|
||||
}
|
||||
|
||||
let (duration, timestamp) =
|
||||
time_until_next_attempt(time_now, block_production_interval, time_offset);
|
||||
let aura_slot = Slot::from_timestamp(timestamp, para_slot_duration);
|
||||
(duration, aura_slot)
|
||||
}
|
||||
|
||||
/// Compute the time until the next slot changes.
|
||||
///
|
||||
/// Returns None if the next slot cannot be computed.
|
||||
fn compute_time_until_next_slot_change(
|
||||
para_slot_duration: SlotDuration,
|
||||
time_now: Duration,
|
||||
time_offset: Duration,
|
||||
last_reported_slot: Slot,
|
||||
) -> Option<(Duration, Slot)> {
|
||||
let now = time_now.saturating_sub(time_offset);
|
||||
let next_slot = last_reported_slot + Slot::from(1);
|
||||
|
||||
let Some(next_slot_timestamp) = next_slot.timestamp(para_slot_duration) else {
|
||||
return None;
|
||||
};
|
||||
let remaining_time = next_slot_timestamp.as_duration().saturating_sub(now);
|
||||
|
||||
Some((remaining_time, next_slot))
|
||||
}
|
||||
|
||||
/// Returns current duration since Unix epoch.
|
||||
fn duration_now() -> Duration {
|
||||
use std::time::SystemTime;
|
||||
let now = SystemTime::now();
|
||||
now.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_else(|e| {
|
||||
panic!("Current time {:?} is before Unix epoch. Something is wrong: {:?}", now, e)
|
||||
})
|
||||
}
|
||||
|
||||
/// Adjust the authoring duration.
|
||||
fn adjust_authoring_duration(
|
||||
mut authoring_duration: Duration,
|
||||
next_block: (Duration, Slot),
|
||||
next_slot_change: (Duration, Slot),
|
||||
different_authors: bool,
|
||||
) -> Option<Duration> {
|
||||
let (duration, next_block_slot) = next_block;
|
||||
let (duration_until_next_slot, next_slot) = next_slot_change;
|
||||
|
||||
// The authoring of blocks must stop 1 second before the slot ends.
|
||||
let duration_until_deadline =
|
||||
duration_until_next_slot.saturating_sub(BLOCK_PRODUCTION_ADJUSTMENT_MS);
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
?authoring_duration,
|
||||
?duration,
|
||||
?next_block_slot,
|
||||
?duration_until_next_slot,
|
||||
?next_slot,
|
||||
?duration_until_deadline,
|
||||
?different_authors,
|
||||
"Adjusting authoring duration for slot.",
|
||||
);
|
||||
|
||||
// Ensure no blocks are produced in the last second of the slot,
|
||||
// regardless of authoring duration.
|
||||
if duration_until_deadline == Duration::ZERO {
|
||||
if different_authors {
|
||||
tracing::warn!(
|
||||
target: LOG_TARGET,
|
||||
?duration_until_next_slot,
|
||||
?next_slot,
|
||||
"Not enough time left in the slot to adjust authoring duration. Skipping block production for the slot."
|
||||
);
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
// If authors are the same, we can still attempt producing the block
|
||||
// considering the next block duration.
|
||||
return Some(authoring_duration.min(duration));
|
||||
}
|
||||
|
||||
// Clamp the authoring duration to fit into the slot deadline only if authors are different.
|
||||
// For most cases, the deadline is farther in the future than the authoring duration.
|
||||
if different_authors && authoring_duration >= duration_until_deadline {
|
||||
authoring_duration = duration_until_deadline;
|
||||
|
||||
// Ensure we are not going below the minimum interval within a reasonable threshold.
|
||||
// For 12 cores, we might have a scenario where the last 3 blocks are skipped:
|
||||
// - Block 10: next slot change in 1.493s:
|
||||
// - After adjusting the deadline: 1.493s - 1s = 0.493s the block could be produced
|
||||
// without issues.
|
||||
// - Block 11: next slot change in 0.993s - skipped by the deadline
|
||||
// - Block 12: next slot change in 0.493s - skipped by the deadline
|
||||
if authoring_duration <
|
||||
BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS.saturating_sub(BLOCK_PRODUCTION_THRESHOLD_MS)
|
||||
{
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
?authoring_duration,
|
||||
?next_slot,
|
||||
"Authoring duration is below minimum. Skipping block production for the slot."
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// The `duration` intends to slightly adjust when then block production
|
||||
// attempt happens. This goes slightly below the `BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS`
|
||||
// threshold.
|
||||
Some(authoring_duration.min(duration))
|
||||
}
|
||||
|
||||
/// Returns the duration until the next block production should be attempted.
|
||||
/// Returns:
|
||||
/// - Duration: The duration until the next attempt.
|
||||
fn time_until_next_attempt(
|
||||
now: Duration,
|
||||
block_production_interval: Duration,
|
||||
offset: Duration,
|
||||
) -> (Duration, Timestamp) {
|
||||
let now = now.as_millis().saturating_sub(offset.as_millis());
|
||||
|
||||
let next_slot_time = ((now + block_production_interval.as_millis()) /
|
||||
block_production_interval.as_millis()) *
|
||||
block_production_interval.as_millis();
|
||||
let remaining_millis = next_slot_time - now;
|
||||
(Duration::from_millis(remaining_millis as u64), Timestamp::from(next_slot_time as u64))
|
||||
}
|
||||
|
||||
impl<Block, Client, P> SlotTimer<Block, Client, P>
|
||||
where
|
||||
Block: BlockT,
|
||||
Client: ProvideRuntimeApi<Block> + UsageProvider<Block> + Send + Sync + 'static,
|
||||
Client::Api: AuraApi<Block, P::Public>,
|
||||
P: Pair,
|
||||
P::Public: AppPublic + Member + Codec,
|
||||
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
|
||||
{
|
||||
/// Create a new slot timer.
|
||||
pub fn new_with_offset(
|
||||
client: Arc<Client>,
|
||||
time_offset: Duration,
|
||||
relay_slot_duration: Duration,
|
||||
) -> Self {
|
||||
Self {
|
||||
client,
|
||||
time_offset,
|
||||
last_reported_core_num: None,
|
||||
relay_slot_duration,
|
||||
last_reported_slot: Default::default(),
|
||||
_marker: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inform the slot timer about the last seen number of cores.
|
||||
pub fn update_scheduling(&mut self, num_cores_next_block: u32) {
|
||||
self.last_reported_core_num = Some(num_cores_next_block);
|
||||
}
|
||||
|
||||
/// Returns the slot and how much time left until the next block production attempt.
|
||||
pub fn time_until_next_block(&mut self, slot_duration: SlotDuration) -> (Duration, Slot) {
|
||||
compute_next_wake_up_time(
|
||||
slot_duration,
|
||||
self.relay_slot_duration,
|
||||
self.last_reported_core_num,
|
||||
duration_now(),
|
||||
self.time_offset,
|
||||
)
|
||||
}
|
||||
|
||||
/// Compute the time until the next slot changes.
|
||||
fn time_until_next_slot_change(
|
||||
&mut self,
|
||||
slot_duration: SlotDuration,
|
||||
) -> Option<(Duration, Slot)> {
|
||||
compute_time_until_next_slot_change(
|
||||
slot_duration,
|
||||
duration_now(),
|
||||
self.time_offset,
|
||||
self.last_reported_slot.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if two slots have different authors based on AURA round-robin algorithm.
|
||||
///
|
||||
/// Returns true if the authors for the two slots are different.
|
||||
fn check_different_slot_authors(&self, slot: Slot, next_slot: Slot) -> bool {
|
||||
let best_hash = self.client.usage_info().chain.best_hash;
|
||||
|
||||
let Ok(authorities) = self.client.runtime_api().authorities(best_hash) else {
|
||||
tracing::warn!(target: LOG_TARGET, "Failed to fetch authorities for slot author comparison");
|
||||
// Presume they are different, this will adjust the slot authoring duration more
|
||||
// conservatively.
|
||||
return true;
|
||||
};
|
||||
|
||||
let authorities_len = authorities.len() as u64;
|
||||
if authorities_len <= 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let author1_idx = *slot % authorities_len;
|
||||
let author2_idx = *next_slot % authorities_len;
|
||||
|
||||
author1_idx != author2_idx
|
||||
}
|
||||
|
||||
/// Adjust the authoring duration to fit into the slot timing.
|
||||
///
|
||||
/// Returns the adjusted authoring duration and the slot that it corresponds to.
|
||||
pub fn adjust_authoring_duration(&mut self, authoring_duration: Duration) -> Option<Duration> {
|
||||
let Ok(slot_duration) = crate::slot_duration(&*self.client) else {
|
||||
tracing::error!(target: LOG_TARGET, "Failed to fetch slot duration from runtime.");
|
||||
return None;
|
||||
};
|
||||
|
||||
let next_block = self.time_until_next_block(slot_duration);
|
||||
let Some(next_slot_change) = self.time_until_next_slot_change(slot_duration) else {
|
||||
tracing::error!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to compute time until next slot change. Using unadjusted authoring duration."
|
||||
);
|
||||
return Some(authoring_duration);
|
||||
};
|
||||
|
||||
// Check if authors at current and next slots are different
|
||||
let current_slot = self.last_reported_slot.unwrap_or(next_block.1);
|
||||
let different_authors = self.check_different_slot_authors(current_slot, next_slot_change.1);
|
||||
|
||||
adjust_authoring_duration(
|
||||
authoring_duration,
|
||||
next_block,
|
||||
next_slot_change,
|
||||
different_authors,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a future that resolves when the next block production should be attempted.
|
||||
pub async fn wait_until_next_slot(&mut self) -> Result<(), ()> {
|
||||
let slot_duration = match crate::slot_duration(&*self.client) {
|
||||
Ok(d) => d,
|
||||
Err(error) => {
|
||||
tracing::error!(target: LOG_TARGET, %error, "Failed to fetch slot duration from runtime.");
|
||||
return Err(());
|
||||
},
|
||||
};
|
||||
|
||||
let (time_until_next_attempt, mut next_aura_slot) =
|
||||
self.time_until_next_block(slot_duration);
|
||||
|
||||
tracing::trace!(
|
||||
target: LOG_TARGET,
|
||||
?time_until_next_attempt,
|
||||
aura_slot = ?next_aura_slot,
|
||||
last_reported = ?self.last_reported_slot,
|
||||
"Determined next block production opportunity."
|
||||
);
|
||||
|
||||
match self.last_reported_slot {
|
||||
// If we already reported a slot, we don't want to skip a slot. But we also don't want
|
||||
// to go through all the slots if a node was halted for some reason.
|
||||
Some(ls) if ls + 1 < next_aura_slot && next_aura_slot <= ls + 3 => {
|
||||
next_aura_slot = ls + 1u64;
|
||||
},
|
||||
None | Some(_) => {
|
||||
tracing::trace!(target: LOG_TARGET, ?time_until_next_attempt, "Sleeping until the next slot.");
|
||||
tokio::time::sleep(time_until_next_attempt).await;
|
||||
},
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
?slot_duration,
|
||||
aura_slot = ?next_aura_slot,
|
||||
"New block production opportunity."
|
||||
);
|
||||
|
||||
self.last_reported_slot = Some(next_aura_slot);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rstest::rstest;
|
||||
use pezsc_consensus_aura::SlotDuration;
|
||||
const RELAY_CHAIN_SLOT_DURATION: u64 = 6000;
|
||||
|
||||
#[rstest]
|
||||
// Test that different now timestamps have correct impact
|
||||
// ||||
|
||||
#[case(6000, Some(1), 1000, 0, 5000)]
|
||||
#[case(6000, Some(1), 0, 0, 6000)]
|
||||
#[case(6000, Some(1), 6000, 0, 6000)]
|
||||
#[case(6000, Some(0), 6000, 0, 6000)]
|
||||
// Test that `None` core defaults to 1
|
||||
// ||||
|
||||
#[case(6000, None, 1000, 0, 5000)]
|
||||
#[case(6000, None, 0, 0, 6000)]
|
||||
#[case(6000, None, 6000, 0, 6000)]
|
||||
// Test that offset affects the current time correctly
|
||||
// ||||
|
||||
#[case(6000, Some(1), 1000, 1000, 6000)]
|
||||
#[case(6000, Some(1), 12000, 2000, 2000)]
|
||||
#[case(6000, Some(1), 12000, 6000, 6000)]
|
||||
#[case(6000, Some(1), 12000, 7000, 1000)]
|
||||
// Test that number of cores affects the block production interval
|
||||
// |||||||
|
||||
#[case(6000, Some(3), 12000, 0, 2000)]
|
||||
#[case(6000, Some(2), 12000, 0, 3000)]
|
||||
#[case(6000, Some(3), 11999, 0, 1)]
|
||||
// High core count
|
||||
// ||||||||
|
||||
#[case(6000, Some(12), 0, 0, 500)]
|
||||
/// Test that the minimum block interval is respected
|
||||
/// at high core counts.
|
||||
/// |||||||||
|
||||
#[case(6000, Some(100), 0, 0, 500)]
|
||||
// Test that slot_duration works correctly
|
||||
// ||||
|
||||
#[case(2000, Some(1), 1000, 0, 1000)]
|
||||
#[case(2000, Some(1), 3000, 0, 1000)]
|
||||
#[case(2000, Some(1), 10000, 0, 2000)]
|
||||
#[case(2000, Some(2), 1000, 0, 1000)]
|
||||
// Cores are ignored if relay_slot_duration != para_slot_duration
|
||||
// |||||||
|
||||
#[case(2000, Some(3), 3000, 0, 1000)]
|
||||
// For long slot durations, we should still check
|
||||
// every relay chain block for the slot.
|
||||
// |||||
|
||||
#[case(12000, None, 0, 0, 6000)]
|
||||
#[case(12000, None, 6100, 0, 5900)]
|
||||
#[case(12000, None, 6000, 2000, 2000)]
|
||||
#[case(12000, Some(2), 6000, 0, 3000)]
|
||||
#[case(12000, Some(3), 6000, 0, 2000)]
|
||||
#[case(12000, Some(3), 8100, 0, 1900)]
|
||||
fn test_get_next_slot(
|
||||
#[case] para_slot_millis: u64,
|
||||
#[case] core_count: Option<u32>,
|
||||
#[case] time_now: u64,
|
||||
#[case] offset_millis: u64,
|
||||
#[case] expected_wait_duration: u128,
|
||||
) {
|
||||
let para_slot_duration = SlotDuration::from_millis(para_slot_millis); // 6 second slots
|
||||
let relay_slot_duration = Duration::from_millis(RELAY_CHAIN_SLOT_DURATION);
|
||||
let time_now = Duration::from_millis(time_now); // 1 second passed
|
||||
let offset = Duration::from_millis(offset_millis);
|
||||
|
||||
let (wait_duration, _) = compute_next_wake_up_time(
|
||||
para_slot_duration,
|
||||
relay_slot_duration,
|
||||
core_count,
|
||||
time_now,
|
||||
offset,
|
||||
);
|
||||
|
||||
assert_eq!(wait_duration.as_millis(), expected_wait_duration, "Wait time mismatch.");
|
||||
// Should wait 5 seconds
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
// Basic slot change scenarios
|
||||
#[case(6000, 0, 0, Slot::from(0), 6000, Slot::from(1))]
|
||||
#[case(6000, 1000, 0, Slot::from(0), 5000, Slot::from(1))]
|
||||
#[case(6000, 6000, 0, Slot::from(1), 6000, Slot::from(2))]
|
||||
#[case(6000, 12000, 0, Slot::from(2), 6000, Slot::from(3))]
|
||||
// Test with offset
|
||||
#[case(6000, 1000, 1000, Slot::from(0), 6000, Slot::from(1))]
|
||||
#[case(6000, 2000, 1000, Slot::from(0), 5000, Slot::from(1))]
|
||||
#[case(6000, 6000, 3000, Slot::from(0), 3000, Slot::from(1))]
|
||||
// Different slot durations
|
||||
#[case(3000, 1000, 0, Slot::from(0), 2000, Slot::from(1))]
|
||||
#[case(3000, 3000, 0, Slot::from(1), 3000, Slot::from(2))]
|
||||
#[case(12000, 6000, 0, Slot::from(0), 6000, Slot::from(1))]
|
||||
#[case(12000, 12000, 0, Slot::from(1), 12000, Slot::from(2))]
|
||||
// Edge cases - at slot boundary
|
||||
#[case(6000, 5999, 0, Slot::from(0), 1, Slot::from(1))]
|
||||
#[case(6000, 11999, 0, Slot::from(1), 1, Slot::from(2))]
|
||||
fn test_compute_time_until_next_slot_change(
|
||||
#[case] para_slot_millis: u64,
|
||||
#[case] time_now: u64,
|
||||
#[case] offset_millis: u64,
|
||||
#[case] last_reported_slot: Slot,
|
||||
#[case] expected_duration: u128,
|
||||
#[case] expected_next_slot: Slot,
|
||||
) {
|
||||
let para_slot_duration = SlotDuration::from_millis(para_slot_millis);
|
||||
let time_now = Duration::from_millis(time_now);
|
||||
let offset = Duration::from_millis(offset_millis);
|
||||
|
||||
let result = compute_time_until_next_slot_change(
|
||||
para_slot_duration,
|
||||
time_now,
|
||||
offset,
|
||||
last_reported_slot,
|
||||
);
|
||||
|
||||
assert!(result.is_some(), "Expected result to be Some");
|
||||
let (duration, next_slot) = result.unwrap();
|
||||
assert_eq!(duration.as_millis(), expected_duration, "Duration mismatch");
|
||||
assert_eq!(next_slot, expected_next_slot, "Next slot mismatch");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
// Various scenarios for 2s block production adjustment.
|
||||
#[case::blocks_2s_fits_next_block(
|
||||
Duration::from_millis(2000), // Authoring duration
|
||||
(Duration::from_millis(2000), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(4000), Slot::from(2)), // Next slot change
|
||||
true, // Different authors
|
||||
Some(Duration::from_millis(2000)), // Expected
|
||||
)]
|
||||
#[case::blocks_2s_closer_next_slot(
|
||||
Duration::from_millis(2000), // Authoring duration
|
||||
(Duration::from_millis(1950), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(4000), Slot::from(2)), // Next slot change
|
||||
true, // Different authors
|
||||
Some(Duration::from_millis(1950)), // Expected
|
||||
)]
|
||||
#[case::blocks_2s_closer_next_slot_bigger(
|
||||
Duration::from_millis(2000), // Authoring duration
|
||||
(Duration::from_millis(1500), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(4000), Slot::from(2)), // Next slot change
|
||||
true, // Different authors
|
||||
Some(Duration::from_millis(1500)), // Expected
|
||||
)]
|
||||
#[case::blocks_2s_reduce_by_1s(
|
||||
Duration::from_millis(2000), // Authoring duration
|
||||
(Duration::from_millis(2000), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(2000), Slot::from(2)), // Next slot change
|
||||
true, // Different authors
|
||||
Some(Duration::from_millis(1000)), // Expected
|
||||
)]
|
||||
#[case::blocks_2s_reduce_by_1s_plus_offset(
|
||||
Duration::from_millis(2000), // Authoring duration
|
||||
(Duration::from_millis(1950), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(1950), Slot::from(2)), // Next slot change
|
||||
true, // Different authors
|
||||
Some(Duration::from_millis(950)), // Expected
|
||||
)]
|
||||
#[case::blocks_2s_reduce_to_minimum(
|
||||
Duration::from_millis(2000), // Authoring duration
|
||||
(Duration::from_millis(1400), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(1400), Slot::from(2)), // Next slot change
|
||||
true, // Different authors
|
||||
Some(Duration::from_millis(400)), // Expected
|
||||
)]
|
||||
#[case::blocks_2s_reduce_below_minimum(
|
||||
Duration::from_millis(2000), // Authoring duration
|
||||
(Duration::from_millis(1300), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(1300), Slot::from(2)), // Next slot change
|
||||
true, // Different authors
|
||||
None, // Expected to reduce below minimum
|
||||
)]
|
||||
#[case::blocks_2s_same_author(
|
||||
Duration::from_millis(2000), // Authoring duration
|
||||
(Duration::from_millis(1400), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(1400), Slot::from(2)), // Next slot change
|
||||
false, // Different authors
|
||||
Some(Duration::from_millis(1400)), // Expected no adjustment for last second.
|
||||
)]
|
||||
// Various scenarios for 500ms block production adjustment.
|
||||
#[case::blocks_500ms_fits_next_block(
|
||||
Duration::from_millis(500), // Authoring duration
|
||||
(Duration::from_millis(500), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(2000), Slot::from(2)), // Next slot change
|
||||
true, // Different authors
|
||||
Some(Duration::from_millis(500)), // Expected
|
||||
)]
|
||||
#[case::blocks_500ms_closer_next_slot(
|
||||
Duration::from_millis(500), // Authoring duration
|
||||
(Duration::from_millis(450), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(2000), Slot::from(2)), // Next slot change
|
||||
true, // Different authors
|
||||
Some(Duration::from_millis(450)), // Expected
|
||||
)]
|
||||
#[case::blocks_500ms_closer_next_slot_bigger(
|
||||
Duration::from_millis(500), // Authoring duration
|
||||
(Duration::from_millis(400), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(1500), Slot::from(2)), // Next slot change
|
||||
true, // Different authors
|
||||
Some(Duration::from_millis(400)), // Expected
|
||||
)]
|
||||
#[case::blocks_500ms_reduce_by_1s(
|
||||
Duration::from_millis(500), // Authoring duration
|
||||
(Duration::from_millis(500), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(1000), Slot::from(2)), // Next slot change
|
||||
true, // Different authors
|
||||
None, // Expected
|
||||
)]
|
||||
#[case::blocks_500ms_reduce_by_1s_closer(
|
||||
Duration::from_millis(500), // Authoring duration
|
||||
(Duration::from_millis(500), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(500), Slot::from(2)), // Next slot change
|
||||
true, // Different authors
|
||||
None, // Expected
|
||||
)]
|
||||
// If we are producing with 1 collator for 500ms authoring duration,
|
||||
// we must produce the last two slots and ignore the 1s adjustment.
|
||||
#[case::blocks_500ms_same_author(
|
||||
Duration::from_millis(500), // Authoring duration
|
||||
(Duration::from_millis(410), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(1000), Slot::from(2)), // Next slot change
|
||||
false, // Different authors
|
||||
Some(Duration::from_millis(410)), // Expected no adjustment for last second.
|
||||
)]
|
||||
#[case::blocks_500ms_same_author_closer(
|
||||
Duration::from_millis(500), // Authoring duration
|
||||
(Duration::from_millis(400), Slot::from(1)), // Next block
|
||||
(Duration::from_millis(400), Slot::from(2)), // Next slot change
|
||||
false, // Different authors
|
||||
Some(Duration::from_millis(400)), // Expected no adjustment for last second.
|
||||
)]
|
||||
fn test_adjust_authoring_duration(
|
||||
#[case] authoring_duration: Duration,
|
||||
#[case] next_block: (Duration, Slot),
|
||||
#[case] next_slot_change: (Duration, Slot),
|
||||
#[case] different_authors: bool,
|
||||
#[case] expected: Option<Duration>,
|
||||
) {
|
||||
pezsp_tracing::init_for_tests();
|
||||
|
||||
let result = adjust_authoring_duration(
|
||||
authoring_duration,
|
||||
next_block,
|
||||
next_slot_change,
|
||||
different_authors,
|
||||
);
|
||||
tracing::debug!("Adjusted authoring duration: {:?}", result);
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,730 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use super::{
|
||||
block_builder_task::{determine_core, offset_relay_parent_find_descendants},
|
||||
relay_chain_data_cache::{RelayChainData, RelayChainDataCache},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use codec::Encode;
|
||||
use cumulus_primitives_core::{ClaimQueueOffset, CoreInfo, CoreSelector, CumulusDigestItem};
|
||||
use cumulus_relay_chain_interface::*;
|
||||
use futures::Stream;
|
||||
use pezkuwi_node_subsystem_util::runtime::ClaimQueueSnapshot;
|
||||
use pezkuwi_primitives::{
|
||||
CandidateEvent, CommittedCandidateReceiptV2, CoreIndex, Hash as RelayHash,
|
||||
Header as RelayHeader, Id as ParaId,
|
||||
};
|
||||
use rstest::rstest;
|
||||
use pezsc_consensus_babe::{
|
||||
AuthorityId, ConsensusLog as BabeConsensusLog, NextEpochDescriptor, BABE_ENGINE_ID,
|
||||
};
|
||||
use pezsp_core::sr25519;
|
||||
use pezsp_runtime::{generic::BlockId, testing::Header as TestHeader, traits::Header};
|
||||
use pezsp_version::RuntimeVersion;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, VecDeque},
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn offset_test_zero_offset() {
|
||||
let (headers, best_hash) = create_header_chain();
|
||||
|
||||
let client = TestRelayClient::new(headers);
|
||||
|
||||
let mut cache = RelayChainDataCache::new(client, 1.into());
|
||||
|
||||
let result = offset_relay_parent_find_descendants(&mut cache, best_hash, 0).await;
|
||||
assert!(result.is_ok());
|
||||
let data = result.unwrap().unwrap();
|
||||
assert_eq!(data.descendants_len(), 0);
|
||||
assert_eq!(data.relay_parent().hash(), best_hash);
|
||||
assert!(data.into_inherent_descendant_list().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn offset_test_two_offset() {
|
||||
let (headers, best_hash) = create_header_chain();
|
||||
|
||||
let client = TestRelayClient::new(headers);
|
||||
|
||||
let mut cache = RelayChainDataCache::new(client, 1.into());
|
||||
|
||||
let result = offset_relay_parent_find_descendants(&mut cache, best_hash, 2).await;
|
||||
assert!(result.is_ok());
|
||||
let data = result.unwrap().unwrap();
|
||||
assert_eq!(data.descendants_len(), 2);
|
||||
assert_eq!(*data.relay_parent().number(), 98);
|
||||
let descendant_list = data.into_inherent_descendant_list();
|
||||
assert_eq!(descendant_list.len(), 3);
|
||||
assert_eq!(*descendant_list.first().unwrap().number(), 98);
|
||||
assert_eq!(*descendant_list.last().unwrap().number(), 100);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn offset_test_five_offset() {
|
||||
let (headers, best_hash) = create_header_chain();
|
||||
|
||||
let client = TestRelayClient::new(headers);
|
||||
|
||||
let mut cache = RelayChainDataCache::new(client, 1.into());
|
||||
|
||||
let result = offset_relay_parent_find_descendants(&mut cache, best_hash, 5).await;
|
||||
assert!(result.is_ok());
|
||||
let data = result.unwrap().unwrap();
|
||||
assert_eq!(data.descendants_len(), 5);
|
||||
assert_eq!(*data.relay_parent().number(), 95);
|
||||
let descendant_list = data.into_inherent_descendant_list();
|
||||
assert_eq!(descendant_list.len(), 6);
|
||||
assert_eq!(*descendant_list.first().unwrap().number(), 95);
|
||||
assert_eq!(*descendant_list.last().unwrap().number(), 100);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn offset_test_too_long() {
|
||||
let (headers, _best_hash) = create_header_chain();
|
||||
|
||||
let client = TestRelayClient::new(headers);
|
||||
|
||||
let mut cache = RelayChainDataCache::new(client, 1.into());
|
||||
|
||||
let result = offset_relay_parent_find_descendants(&mut cache, _best_hash, 200).await;
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = offset_relay_parent_find_descendants(&mut cache, _best_hash, 101).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum HasEpochChange {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::in_best(
|
||||
&[HasEpochChange::No, HasEpochChange::No, HasEpochChange::Yes],
|
||||
)]
|
||||
#[case::in_first_ancestor(
|
||||
&[HasEpochChange::No, HasEpochChange::Yes, HasEpochChange::No],
|
||||
)]
|
||||
#[case::in_second_ancestor(
|
||||
&[HasEpochChange::Yes, HasEpochChange::No, HasEpochChange::No],
|
||||
)]
|
||||
#[tokio::test]
|
||||
async fn offset_returns_none_when_epoch_change_encountered(#[case] flags: &[HasEpochChange]) {
|
||||
let (headers, best_hash) = build_headers_with_epoch_flags(flags);
|
||||
let client = TestRelayClient::new(headers);
|
||||
let mut cache = RelayChainDataCache::new(client, 1.into());
|
||||
|
||||
let result = offset_relay_parent_find_descendants(&mut cache, best_hash, 3).await;
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn determine_core_new_relay_parent() {
|
||||
let (headers, _best_hash) = create_header_chain();
|
||||
let client = TestRelayClient::new(headers);
|
||||
let mut cache = RelayChainDataCache::new(client, 1.into());
|
||||
|
||||
// Create a test relay parent header
|
||||
let relay_parent = RelayHeader {
|
||||
parent_hash: Default::default(),
|
||||
number: 100,
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest: Default::default(),
|
||||
};
|
||||
|
||||
// Create a test para parent header at block 0 (genesis)
|
||||
let para_parent = TestHeader::new_from_number(0);
|
||||
|
||||
// Setup claim queue data for the cache
|
||||
cache.set_test_data(relay_parent.clone(), vec![CoreIndex(0), CoreIndex(1)]);
|
||||
|
||||
let result = determine_core(&mut cache, &relay_parent, 1.into(), ¶_parent, 0).await;
|
||||
|
||||
let core = result.unwrap();
|
||||
let core = core.unwrap();
|
||||
assert_eq!(core.core_selector(), CoreSelector(0));
|
||||
assert_eq!(core.core_index(), CoreIndex(0));
|
||||
assert_eq!(core.total_cores(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn determine_core_with_core_info() {
|
||||
let (headers, best_hash) = create_header_chain();
|
||||
let client = TestRelayClient::new(headers);
|
||||
let mut cache = RelayChainDataCache::new(client, 1.into());
|
||||
|
||||
// Create a test relay parent header
|
||||
let relay_parent = RelayHeader {
|
||||
parent_hash: best_hash,
|
||||
number: 101,
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest: Default::default(),
|
||||
};
|
||||
|
||||
// Create a para parent header with core info in digest
|
||||
let core_info = CoreInfo {
|
||||
selector: CoreSelector(0),
|
||||
claim_queue_offset: ClaimQueueOffset(0),
|
||||
number_of_cores: 3.into(),
|
||||
};
|
||||
let mut digest = pezsp_runtime::generic::Digest::default();
|
||||
digest.push(CumulusDigestItem::CoreInfo(core_info).to_digest_item());
|
||||
// Add relay parent storage root to make it a non-new relay parent
|
||||
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
|
||||
*relay_parent.state_root(),
|
||||
*relay_parent.number(),
|
||||
));
|
||||
|
||||
let para_parent = TestHeader {
|
||||
parent_hash: best_hash.into(),
|
||||
number: 1,
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest,
|
||||
};
|
||||
|
||||
// Setup claim queue data for the cache
|
||||
cache.set_test_data(relay_parent.clone(), vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)]);
|
||||
|
||||
let result = determine_core(&mut cache, &relay_parent, 1.into(), ¶_parent, 0).await;
|
||||
|
||||
match result {
|
||||
Ok(Some(core)) => {
|
||||
assert_eq!(core.core_selector(), CoreSelector(1)); // Should be next selector (0 + 1)
|
||||
assert_eq!(core.core_index(), CoreIndex(1));
|
||||
assert_eq!(core.total_cores(), 3);
|
||||
},
|
||||
Ok(None) => panic!("Expected Some core, got None"),
|
||||
Err(()) => panic!("determine_core returned error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn determine_core_no_cores_available() {
|
||||
let (headers, _best_hash) = create_header_chain();
|
||||
let client = TestRelayClient::new(headers);
|
||||
let mut cache = RelayChainDataCache::new(client, 1.into());
|
||||
|
||||
// Create a test relay parent header
|
||||
let relay_parent = RelayHeader {
|
||||
parent_hash: Default::default(),
|
||||
number: 100,
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest: Default::default(),
|
||||
};
|
||||
|
||||
// Create a test para parent header at block 0 (genesis)
|
||||
let para_parent = TestHeader::new_from_number(0);
|
||||
|
||||
// Setup empty claim queue
|
||||
cache.set_test_data(relay_parent.clone(), vec![]);
|
||||
|
||||
let result = determine_core(&mut cache, &relay_parent, 1.into(), ¶_parent, 0).await;
|
||||
|
||||
let core = result.unwrap();
|
||||
assert!(core.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn determine_core_selector_overflow() {
|
||||
let (headers, best_hash) = create_header_chain();
|
||||
let client = TestRelayClient::new(headers);
|
||||
let mut cache = RelayChainDataCache::new(client, 1.into());
|
||||
|
||||
// Create a test relay parent header
|
||||
let relay_parent = RelayHeader {
|
||||
parent_hash: best_hash,
|
||||
number: 101,
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest: Default::default(),
|
||||
};
|
||||
|
||||
let core_info = CoreInfo {
|
||||
selector: CoreSelector(1),
|
||||
claim_queue_offset: ClaimQueueOffset(0),
|
||||
number_of_cores: 2.into(),
|
||||
};
|
||||
let mut digest = pezsp_runtime::generic::Digest::default();
|
||||
digest.push(CumulusDigestItem::CoreInfo(core_info).to_digest_item());
|
||||
// Add relay parent storage root to make it a non-new relay parent
|
||||
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
|
||||
*relay_parent.state_root(),
|
||||
*relay_parent.number(),
|
||||
));
|
||||
|
||||
let para_parent = TestHeader {
|
||||
parent_hash: best_hash.into(),
|
||||
number: 1,
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest,
|
||||
};
|
||||
|
||||
// Setup claim queue with only 2 cores
|
||||
cache.set_test_data(relay_parent.clone(), vec![CoreIndex(0), CoreIndex(1)]);
|
||||
|
||||
let result = determine_core(&mut cache, &relay_parent, 1.into(), ¶_parent, 0).await;
|
||||
|
||||
let core = result.unwrap();
|
||||
assert!(core.is_none()); // Should return None when selector overflows
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn determine_core_uses_last_claimed_core_selector() {
|
||||
let (headers, best_hash) = create_header_chain();
|
||||
let client = TestRelayClient::new(headers);
|
||||
let mut cache = RelayChainDataCache::new(client, 1.into());
|
||||
|
||||
// Create a test relay parent header
|
||||
let relay_parent = RelayHeader {
|
||||
parent_hash: best_hash,
|
||||
number: 101,
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest: Default::default(),
|
||||
};
|
||||
|
||||
// Create a para parent header without core info in digest (non-genesis)
|
||||
// Need to add relay parent storage root to digest to make it a non-new relay parent
|
||||
let mut digest = pezsp_runtime::generic::Digest::default();
|
||||
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
|
||||
*relay_parent.state_root(),
|
||||
*relay_parent.number(),
|
||||
));
|
||||
|
||||
let para_parent = TestHeader {
|
||||
parent_hash: best_hash.into(),
|
||||
number: 1,
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest,
|
||||
};
|
||||
|
||||
// Setup claim queue data with last_claimed_core_selector set to 1
|
||||
cache.set_test_data_with_last_selector(
|
||||
relay_parent.clone(),
|
||||
vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)],
|
||||
Some(CoreSelector(1)),
|
||||
);
|
||||
|
||||
let result = determine_core(&mut cache, &relay_parent, 1.into(), ¶_parent, 0).await;
|
||||
|
||||
match result {
|
||||
Ok(Some(core)) => {
|
||||
// Should use last_claimed_core_selector (1) + 1 = 2
|
||||
assert_eq!(core.core_selector(), CoreSelector(2));
|
||||
assert_eq!(core.core_index(), CoreIndex(2));
|
||||
assert_eq!(core.total_cores(), 3);
|
||||
},
|
||||
Ok(None) => panic!("Expected Some core, got None"),
|
||||
Err(()) => panic!("determine_core returned error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn determine_core_uses_last_claimed_core_selector_wraps_around() {
|
||||
let (headers, best_hash) = create_header_chain();
|
||||
let client = TestRelayClient::new(headers);
|
||||
let mut cache = RelayChainDataCache::new(client, 1.into());
|
||||
|
||||
// Create a test relay parent header
|
||||
let relay_parent = RelayHeader {
|
||||
parent_hash: best_hash,
|
||||
number: 101,
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest: Default::default(),
|
||||
};
|
||||
|
||||
// Create a para parent header without core info in digest (non-genesis)
|
||||
// Need to add relay parent storage root to digest to make it a non-new relay parent
|
||||
let mut digest = pezsp_runtime::generic::Digest::default();
|
||||
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
|
||||
*relay_parent.state_root(),
|
||||
*relay_parent.number(),
|
||||
));
|
||||
|
||||
let para_parent = TestHeader {
|
||||
parent_hash: best_hash.into(),
|
||||
number: 1,
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest,
|
||||
};
|
||||
|
||||
// Setup claim queue data with last_claimed_core_selector set to 2 (last index)
|
||||
// Next selector should wrap around to out of bounds and return None
|
||||
cache.set_test_data_with_last_selector(
|
||||
relay_parent.clone(),
|
||||
vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)],
|
||||
Some(CoreSelector(2)),
|
||||
);
|
||||
|
||||
let result = determine_core(&mut cache, &relay_parent, 1.into(), ¶_parent, 0).await;
|
||||
|
||||
match result {
|
||||
Ok(Some(_)) => panic!("Expected None due to selector overflow"),
|
||||
Ok(None) => {
|
||||
// This is expected - selector 2 + 1 = 3, but only cores 0,1,2 available
|
||||
},
|
||||
Err(()) => panic!("determine_core returned error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn determine_core_no_last_claimed_core_selector() {
|
||||
let (headers, best_hash) = create_header_chain();
|
||||
let client = TestRelayClient::new(headers);
|
||||
let mut cache = RelayChainDataCache::new(client, 1.into());
|
||||
|
||||
// Create a test relay parent header
|
||||
let relay_parent = RelayHeader {
|
||||
parent_hash: best_hash,
|
||||
number: 101,
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest: Default::default(),
|
||||
};
|
||||
|
||||
// Create a para parent header without core info in digest (non-genesis)
|
||||
// Need to add relay parent storage root to digest to make it a non-new relay parent
|
||||
let mut digest = pezsp_runtime::generic::Digest::default();
|
||||
digest.push(cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
|
||||
*relay_parent.state_root(),
|
||||
*relay_parent.number(),
|
||||
));
|
||||
|
||||
let para_parent = TestHeader {
|
||||
parent_hash: best_hash.into(),
|
||||
number: 1,
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest,
|
||||
};
|
||||
|
||||
// Setup claim queue data with no last_claimed_core_selector (None)
|
||||
cache.set_test_data_with_last_selector(
|
||||
relay_parent.clone(),
|
||||
vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)],
|
||||
None,
|
||||
);
|
||||
|
||||
let result = determine_core(&mut cache, &relay_parent, 1.into(), ¶_parent, 0).await;
|
||||
|
||||
match result {
|
||||
Ok(Some(core)) => {
|
||||
// Should start from selector 0 + 1 = 1 when no last selector
|
||||
assert_eq!(core.core_selector(), CoreSelector(1));
|
||||
assert_eq!(core.core_index(), CoreIndex(1));
|
||||
assert_eq!(core.total_cores(), 3);
|
||||
},
|
||||
Ok(None) => panic!("Expected Some core, got None"),
|
||||
Err(()) => panic!("determine_core returned error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestRelayClient {
|
||||
headers: HashMap<RelayHash, RelayHeader>,
|
||||
}
|
||||
|
||||
impl TestRelayClient {
|
||||
fn new(headers: HashMap<RelayHash, RelayHeader>) -> Self {
|
||||
Self { headers }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RelayChainInterface for TestRelayClient {
|
||||
async fn validators(&self, _: RelayHash) -> RelayChainResult<Vec<ValidatorId>> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn best_block_hash(&self) -> RelayChainResult<RelayHash> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
async fn finalized_block_hash(&self) -> RelayChainResult<RelayHash> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn retrieve_dmq_contents(
|
||||
&self,
|
||||
_: ParaId,
|
||||
_: RelayHash,
|
||||
) -> RelayChainResult<Vec<InboundDownwardMessage>> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn retrieve_all_inbound_hrmp_channel_contents(
|
||||
&self,
|
||||
_: ParaId,
|
||||
_: RelayHash,
|
||||
) -> RelayChainResult<BTreeMap<ParaId, Vec<InboundHrmpMessage>>> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn persisted_validation_data(
|
||||
&self,
|
||||
_: RelayHash,
|
||||
_: ParaId,
|
||||
_: OccupiedCoreAssumption,
|
||||
) -> RelayChainResult<Option<PersistedValidationData>> {
|
||||
use cumulus_primitives_core::PersistedValidationData;
|
||||
Ok(Some(PersistedValidationData {
|
||||
parent_head: Default::default(),
|
||||
relay_parent_number: 100,
|
||||
relay_parent_storage_root: Default::default(),
|
||||
max_pov_size: 1024 * 1024,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn validation_code_hash(
|
||||
&self,
|
||||
_: RelayHash,
|
||||
_: ParaId,
|
||||
_: OccupiedCoreAssumption,
|
||||
) -> RelayChainResult<Option<ValidationCodeHash>> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn candidate_pending_availability(
|
||||
&self,
|
||||
_: RelayHash,
|
||||
_: ParaId,
|
||||
) -> RelayChainResult<Option<CommittedCandidateReceiptV2>> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn candidates_pending_availability(
|
||||
&self,
|
||||
_: RelayHash,
|
||||
_: ParaId,
|
||||
) -> RelayChainResult<Vec<CommittedCandidateReceiptV2>> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn session_index_for_child(&self, _: RelayHash) -> RelayChainResult<SessionIndex> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn import_notification_stream(
|
||||
&self,
|
||||
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn finality_notification_stream(
|
||||
&self,
|
||||
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn is_major_syncing(&self) -> RelayChainResult<bool> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
fn overseer_handle(&self) -> RelayChainResult<OverseerHandle> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn get_storage_by_key(
|
||||
&self,
|
||||
_: RelayHash,
|
||||
_: &[u8],
|
||||
) -> RelayChainResult<Option<StorageValue>> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn prove_read(
|
||||
&self,
|
||||
_: RelayHash,
|
||||
_: &Vec<Vec<u8>>,
|
||||
) -> RelayChainResult<pezsc_client_api::StorageProof> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn wait_for_block(&self, _: RelayHash) -> RelayChainResult<()> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn new_best_notification_stream(
|
||||
&self,
|
||||
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn header(
|
||||
&self,
|
||||
block_id: BlockId<pezkuwi_primitives::Block>,
|
||||
) -> RelayChainResult<Option<PHeader>> {
|
||||
let hash = match block_id {
|
||||
BlockId::Hash(hash) => hash,
|
||||
BlockId::Number(_) => unimplemented!("Not needed for test"),
|
||||
};
|
||||
let header = self.headers.get(&hash);
|
||||
|
||||
Ok(header.cloned())
|
||||
}
|
||||
|
||||
async fn availability_cores(
|
||||
&self,
|
||||
_relay_parent: RelayHash,
|
||||
) -> RelayChainResult<Vec<CoreState<RelayHash, BlockNumber>>> {
|
||||
unimplemented!("Not needed for test");
|
||||
}
|
||||
|
||||
async fn version(&self, _: RelayHash) -> RelayChainResult<RuntimeVersion> {
|
||||
unimplemented!("Not needed for test");
|
||||
}
|
||||
|
||||
async fn claim_queue(
|
||||
&self,
|
||||
_: RelayHash,
|
||||
) -> RelayChainResult<BTreeMap<CoreIndex, VecDeque<ParaId>>> {
|
||||
// Return empty claim queue for offset tests
|
||||
Ok(BTreeMap::new())
|
||||
}
|
||||
|
||||
async fn call_runtime_api(
|
||||
&self,
|
||||
_method_name: &'static str,
|
||||
_hash: RelayHash,
|
||||
_payload: &[u8],
|
||||
) -> RelayChainResult<Vec<u8>> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn scheduling_lookahead(&self, _: RelayHash) -> RelayChainResult<u32> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
|
||||
async fn candidate_events(&self, _: RelayHash) -> RelayChainResult<Vec<CandidateEvent>> {
|
||||
unimplemented!("Not needed for test")
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a consecutive set of relay headers whose digest entries optionally carry a BABE
|
||||
/// epoch-change marker, returning the underlying map and the hash of the last header.
|
||||
fn build_headers_with_epoch_flags(
|
||||
flags: &[HasEpochChange],
|
||||
) -> (HashMap<RelayHash, RelayHeader>, RelayHash) {
|
||||
let mut headers = HashMap::new();
|
||||
let mut parent_hash = RelayHash::default();
|
||||
let mut last_hash = RelayHash::default();
|
||||
|
||||
for (index, has_epoch_change) in flags.iter().enumerate() {
|
||||
let digest = if *has_epoch_change == HasEpochChange::Yes {
|
||||
babe_epoch_change_digest()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let header = RelayHeader {
|
||||
parent_hash,
|
||||
number: (index as u32 + 1),
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest,
|
||||
};
|
||||
|
||||
let hash = header.hash();
|
||||
headers.insert(hash, header);
|
||||
parent_hash = hash;
|
||||
last_hash = hash;
|
||||
}
|
||||
|
||||
(headers, last_hash)
|
||||
}
|
||||
|
||||
/// Create a digest containing a single BABE `NextEpochData` item for use in tests.
|
||||
fn babe_epoch_change_digest() -> pezsp_runtime::generic::Digest {
|
||||
let mut digest = pezsp_runtime::generic::Digest::default();
|
||||
let authority_id = AuthorityId::from(sr25519::Public::from_raw([1u8; 32]));
|
||||
let next_epoch =
|
||||
NextEpochDescriptor { authorities: vec![(authority_id, 1u64)], randomness: [0u8; 32] };
|
||||
let log = BabeConsensusLog::NextEpochData(next_epoch);
|
||||
digest.push(pezsp_runtime::generic::DigestItem::Consensus(BABE_ENGINE_ID, log.encode()));
|
||||
digest
|
||||
}
|
||||
|
||||
fn create_header_chain() -> (HashMap<RelayHash, RelayHeader>, RelayHash) {
|
||||
let mut headers = HashMap::new();
|
||||
let mut current_parent = None;
|
||||
let mut header_hash = RelayHash::repeat_byte(0x1);
|
||||
|
||||
// Create chain from highest to lowest number
|
||||
for number in 1..=100 {
|
||||
let mut header = RelayHeader {
|
||||
parent_hash: Default::default(),
|
||||
number,
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest: Default::default(),
|
||||
};
|
||||
if let Some(hash) = current_parent {
|
||||
header.parent_hash = hash;
|
||||
}
|
||||
|
||||
header_hash = header.hash();
|
||||
// Store header and update parent for next iteration
|
||||
headers.insert(header_hash, header.clone());
|
||||
current_parent = Some(header_hash);
|
||||
}
|
||||
|
||||
(headers, header_hash)
|
||||
}
|
||||
|
||||
// Test extension for RelayChainDataCache
|
||||
impl RelayChainDataCache<TestRelayClient> {
|
||||
fn set_test_data(&mut self, relay_parent_header: RelayHeader, cores: Vec<CoreIndex>) {
|
||||
self.set_test_data_with_last_selector(relay_parent_header, cores, None);
|
||||
}
|
||||
|
||||
fn set_test_data_with_last_selector(
|
||||
&mut self,
|
||||
relay_parent_header: RelayHeader,
|
||||
cores: Vec<CoreIndex>,
|
||||
last_claimed_core_selector: Option<CoreSelector>,
|
||||
) {
|
||||
let relay_parent_hash = relay_parent_header.hash();
|
||||
|
||||
let mut claim_queue = BTreeMap::new();
|
||||
for core_index in cores {
|
||||
claim_queue.insert(core_index, [ParaId::from(1)].into());
|
||||
}
|
||||
|
||||
let claim_queue_snapshot = ClaimQueueSnapshot::from(claim_queue);
|
||||
|
||||
let data = RelayChainData {
|
||||
relay_parent_header,
|
||||
claim_queue: claim_queue_snapshot,
|
||||
max_pov_size: 1024 * 1024,
|
||||
last_claimed_core_selector,
|
||||
};
|
||||
|
||||
self.insert_test_data(relay_parent_hash, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// An import queue which provides some equivocation resistance with lenient trait bounds.
|
||||
///
|
||||
/// Equivocation resistance in general is a hard problem, as different nodes in the network
|
||||
/// may see equivocations in a different order, and therefore may not agree on which blocks
|
||||
/// should be thrown out and which ones should be kept.
|
||||
use codec::Codec;
|
||||
use cumulus_client_consensus_common::TeyrchainBlockImportMarker;
|
||||
use cumulus_primitives_core::{CumulusDigestItem, RelayBlockIdentifier};
|
||||
use parking_lot::Mutex;
|
||||
use pezkuwi_primitives::Hash as RHash;
|
||||
use pezsc_consensus::{
|
||||
import_queue::{BasicQueue, Verifier as VerifierT},
|
||||
BlockImport, BlockImportParams, ForkChoiceStrategy,
|
||||
};
|
||||
use pezsc_consensus_aura::{standalone as aura_internal, AuthoritiesTracker};
|
||||
use pezsc_telemetry::{telemetry, TelemetryHandle, CONSENSUS_DEBUG, CONSENSUS_TRACE};
|
||||
use schnellru::{ByLength, LruMap};
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_block_builder::BlockBuilder as BlockBuilderApi;
|
||||
use pezsp_blockchain::{HeaderBackend, HeaderMetadata};
|
||||
use pezsp_consensus::{error::Error as ConsensusError, BlockOrigin};
|
||||
use pezsp_consensus_aura::{AuraApi, Slot, SlotDuration};
|
||||
use pezsp_core::crypto::Pair;
|
||||
use pezsp_inherents::CreateInherentDataProviders;
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
const LRU_WINDOW: u32 = 512;
|
||||
const EQUIVOCATION_LIMIT: usize = 16;
|
||||
|
||||
struct NaiveEquivocationDefender<N> {
|
||||
/// We distinguish blocks by `(Slot, BlockNumber, RelayParent)`.
|
||||
cache: LruMap<(u64, N, RHash), usize>,
|
||||
}
|
||||
|
||||
impl<N: std::hash::Hash + PartialEq> Default for NaiveEquivocationDefender<N> {
|
||||
fn default() -> Self {
|
||||
NaiveEquivocationDefender { cache: LruMap::new(ByLength::new(LRU_WINDOW)) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: std::hash::Hash + PartialEq> NaiveEquivocationDefender<N> {
|
||||
// Returns `true` if equivocation is beyond the limit.
|
||||
fn insert_and_check(&mut self, slot: Slot, block_number: N, relay_chain_parent: RHash) -> bool {
|
||||
let val = self
|
||||
.cache
|
||||
.get_or_insert((*slot, block_number, relay_chain_parent), || 0)
|
||||
.expect("insertion with ByLength limiter always succeeds; qed");
|
||||
|
||||
if *val == EQUIVOCATION_LIMIT {
|
||||
true
|
||||
} else {
|
||||
*val += 1;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A teyrchain block import verifier that checks for equivocation limits within each slot.
|
||||
pub struct Verifier<P: Pair, Client, Block: BlockT, CIDP> {
|
||||
client: Arc<Client>,
|
||||
create_inherent_data_providers: CIDP,
|
||||
defender: Mutex<NaiveEquivocationDefender<NumberFor<Block>>>,
|
||||
telemetry: Option<TelemetryHandle>,
|
||||
// Unused for now. Will be plugged in with a later PR.
|
||||
_authorities_tracker: AuthoritiesTracker<P, Block, Client>,
|
||||
}
|
||||
|
||||
impl<P, Client, Block, CIDP> Verifier<P, Client, Block, CIDP>
|
||||
where
|
||||
P: Pair,
|
||||
P::Signature: Codec,
|
||||
P::Public: Codec + Debug,
|
||||
Block: BlockT,
|
||||
Client: ProvideRuntimeApi<Block> + Send + Sync,
|
||||
<Client as ProvideRuntimeApi<Block>>::Api: BlockBuilderApi<Block> + AuraApi<Block, P::Public>,
|
||||
|
||||
CIDP: CreateInherentDataProviders<Block, ()>,
|
||||
{
|
||||
/// Creates a new Verifier instance for handling teyrchain block import verification in Aura
|
||||
/// consensus.
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
inherent_data_provider: CIDP,
|
||||
telemetry: Option<TelemetryHandle>,
|
||||
) -> Self {
|
||||
Self {
|
||||
client: client.clone(),
|
||||
create_inherent_data_providers: inherent_data_provider,
|
||||
defender: Mutex::new(NaiveEquivocationDefender::default()),
|
||||
telemetry,
|
||||
_authorities_tracker: AuthoritiesTracker::new(client),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<P, Client, Block, CIDP> VerifierT<Block> for Verifier<P, Client, Block, CIDP>
|
||||
where
|
||||
P: Pair,
|
||||
P::Signature: Codec,
|
||||
P::Public: Codec + Debug,
|
||||
Block: BlockT,
|
||||
Client: HeaderBackend<Block>
|
||||
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>
|
||||
+ ProvideRuntimeApi<Block>
|
||||
+ Send
|
||||
+ Sync,
|
||||
<Client as ProvideRuntimeApi<Block>>::Api: BlockBuilderApi<Block> + AuraApi<Block, P::Public>,
|
||||
|
||||
CIDP: CreateInherentDataProviders<Block, ()>,
|
||||
{
|
||||
async fn verify(
|
||||
&self,
|
||||
mut block_params: BlockImportParams<Block>,
|
||||
) -> Result<BlockImportParams<Block>, String> {
|
||||
// Skip checks that include execution, if being told so, or when importing only state.
|
||||
//
|
||||
// This is done for example when gap syncing and it is expected that the block after the gap
|
||||
// was checked/chosen properly, e.g. by warp syncing to this block using a finality proof.
|
||||
if block_params.state_action.skip_execution_checks() || block_params.with_state() {
|
||||
block_params.fork_choice = Some(ForkChoiceStrategy::Custom(block_params.with_state()));
|
||||
return Ok(block_params);
|
||||
}
|
||||
|
||||
let post_hash = block_params.header.hash();
|
||||
let parent_hash = *block_params.header.parent_hash();
|
||||
|
||||
// check seal and update pre-hash/post-hash
|
||||
{
|
||||
let authorities = aura_internal::fetch_authorities(self.client.as_ref(), parent_hash)
|
||||
.map_err(|e| {
|
||||
format!("Could not fetch authorities at {:?}: {}", parent_hash, e)
|
||||
})?;
|
||||
|
||||
let slot_duration = self
|
||||
.client
|
||||
.runtime_api()
|
||||
.slot_duration(parent_hash)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let slot_now = slot_now(slot_duration);
|
||||
let res = aura_internal::check_header_slot_and_seal::<Block, P>(
|
||||
slot_now,
|
||||
block_params.header,
|
||||
&authorities,
|
||||
);
|
||||
|
||||
match res {
|
||||
Ok((pre_header, slot, seal_digest)) => {
|
||||
telemetry!(
|
||||
self.telemetry;
|
||||
CONSENSUS_TRACE;
|
||||
"aura.checked_and_importing";
|
||||
"pre_header" => ?pre_header,
|
||||
);
|
||||
|
||||
// We need some kind of identifier for the relay parent, in the worst case we
|
||||
// take the all `0` hash.
|
||||
let relay_parent =
|
||||
match CumulusDigestItem::find_relay_block_identifier(pre_header.digest()) {
|
||||
None => Default::default(),
|
||||
Some(RelayBlockIdentifier::ByHash(h)) |
|
||||
Some(RelayBlockIdentifier::ByStorageRoot {
|
||||
storage_root: h, ..
|
||||
}) => h,
|
||||
};
|
||||
|
||||
block_params.header = pre_header;
|
||||
block_params.post_digests.push(seal_digest);
|
||||
block_params.fork_choice = Some(ForkChoiceStrategy::LongestChain);
|
||||
block_params.post_hash = Some(post_hash);
|
||||
|
||||
// Check for and reject egregious amounts of equivocations.
|
||||
//
|
||||
// If the `origin` is `ConsensusBroadcast`, we ignore the result of the
|
||||
// equivocation check. This `origin` is for example used by pov-recovery.
|
||||
if self.defender.lock().insert_and_check(
|
||||
slot,
|
||||
*block_params.header.number(),
|
||||
relay_parent,
|
||||
) && !matches!(block_params.origin, BlockOrigin::ConsensusBroadcast)
|
||||
{
|
||||
return Err(format!(
|
||||
"Rejecting block {:?} due to excessive equivocations at slot",
|
||||
post_hash,
|
||||
));
|
||||
}
|
||||
},
|
||||
Err(aura_internal::SealVerificationError::Deferred(hdr, slot)) => {
|
||||
telemetry!(
|
||||
self.telemetry;
|
||||
CONSENSUS_DEBUG;
|
||||
"aura.header_too_far_in_future";
|
||||
"hash" => ?post_hash,
|
||||
"a" => ?hdr,
|
||||
"b" => ?slot,
|
||||
);
|
||||
|
||||
return Err(format!(
|
||||
"Rejecting block ({:?}) from future slot {:?}",
|
||||
post_hash, slot
|
||||
));
|
||||
},
|
||||
Err(e) =>
|
||||
return Err(format!(
|
||||
"Rejecting block ({:?}) with invalid seal ({:?})",
|
||||
post_hash, e
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// Check inherents.
|
||||
if let Some(body) = block_params.body.clone() {
|
||||
let block = Block::new(block_params.header.clone(), body);
|
||||
let create_inherent_data_providers = self
|
||||
.create_inherent_data_providers
|
||||
.create_inherent_data_providers(parent_hash, ())
|
||||
.await
|
||||
.map_err(|e| format!("Could not create inherent data {:?}", e))?;
|
||||
|
||||
pezsp_block_builder::check_inherents(
|
||||
self.client.clone(),
|
||||
parent_hash,
|
||||
block,
|
||||
&create_inherent_data_providers,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Error checking block inherents {:?}", e))?;
|
||||
}
|
||||
|
||||
Ok(block_params)
|
||||
}
|
||||
}
|
||||
|
||||
fn slot_now(slot_duration: SlotDuration) -> Slot {
|
||||
let timestamp = pezsp_timestamp::InherentDataProvider::from_system_time().timestamp();
|
||||
Slot::from_timestamp(timestamp, slot_duration)
|
||||
}
|
||||
|
||||
/// Start an import queue for a Pezcumulus node which checks blocks' seals and inherent data.
|
||||
///
|
||||
/// Pass in only inherent data providers which don't include aura or teyrchain consensus inherents,
|
||||
/// e.g. things like timestamp and custom inherents for the runtime.
|
||||
///
|
||||
/// The others are generated explicitly internally.
|
||||
///
|
||||
/// This should only be used for runtimes where the runtime does not check all inherents and
|
||||
/// seals in `execute_block` (see <https://github.com/pezkuwichain/kurdistan-sdk/issues/91>)
|
||||
pub fn fully_verifying_import_queue<P, Client, Block: BlockT, I, CIDP>(
|
||||
client: Arc<Client>,
|
||||
block_import: I,
|
||||
create_inherent_data_providers: CIDP,
|
||||
spawner: &impl pezsp_core::traits::SpawnEssentialNamed,
|
||||
registry: Option<&prometheus_endpoint::Registry>,
|
||||
telemetry: Option<TelemetryHandle>,
|
||||
) -> BasicQueue<Block>
|
||||
where
|
||||
P: Pair + 'static,
|
||||
P::Signature: Codec,
|
||||
P::Public: Codec + Debug,
|
||||
I: BlockImport<Block, Error = ConsensusError>
|
||||
+ TeyrchainBlockImportMarker
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
Client: HeaderBackend<Block>
|
||||
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>
|
||||
+ ProvideRuntimeApi<Block>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
<Client as ProvideRuntimeApi<Block>>::Api: BlockBuilderApi<Block> + AuraApi<Block, P::Public>,
|
||||
CIDP: CreateInherentDataProviders<Block, ()> + 'static,
|
||||
{
|
||||
let verifier = Verifier::<P, _, _, _> {
|
||||
client: client.clone(),
|
||||
create_inherent_data_providers,
|
||||
defender: Mutex::new(NaiveEquivocationDefender::default()),
|
||||
telemetry,
|
||||
_authorities_tracker: AuthoritiesTracker::new(client.clone()),
|
||||
};
|
||||
|
||||
BasicQueue::new(verifier, Box::new(block_import), None, spawner, registry)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use codec::Encode;
|
||||
use cumulus_test_client::{
|
||||
runtime::Block, seal_block, Client, InitBlockBuilder, TestClientBuilder,
|
||||
TestClientBuilderExt,
|
||||
};
|
||||
use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder;
|
||||
use futures::FutureExt;
|
||||
use pezkuwi_primitives::{HeadData, PersistedValidationData};
|
||||
use pezsc_client_api::HeaderBackend;
|
||||
use pezsp_consensus_aura::sr25519;
|
||||
use pezsp_tracing::try_init_simple;
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
|
||||
#[test]
|
||||
fn import_equivocated_blocks_from_recovery() {
|
||||
try_init_simple();
|
||||
|
||||
let client = Arc::new(TestClientBuilder::default().build());
|
||||
|
||||
let verifier = Verifier::<sr25519::AuthorityPair, Client, Block, _> {
|
||||
client: client.clone(),
|
||||
create_inherent_data_providers: |_, _| async move {
|
||||
Ok(pezsp_timestamp::InherentDataProvider::from_system_time())
|
||||
},
|
||||
defender: Mutex::new(NaiveEquivocationDefender::default()),
|
||||
telemetry: None,
|
||||
_authorities_tracker: AuthoritiesTracker::new(client.clone()),
|
||||
};
|
||||
|
||||
let genesis = client.info().best_hash;
|
||||
let mut sproof = RelayStateSproofBuilder::default();
|
||||
sproof.included_para_head = Some(HeadData(client.header(genesis).unwrap().encode()));
|
||||
sproof.para_id = cumulus_test_client::runtime::TEYRCHAIN_ID.into();
|
||||
|
||||
let validation_data = PersistedValidationData {
|
||||
relay_parent_number: 1,
|
||||
parent_head: client.header(genesis).unwrap().encode().into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let block_builder = client.init_block_builder(Some(validation_data), sproof);
|
||||
let block = block_builder.block_builder.build().unwrap();
|
||||
|
||||
let mut blocks = Vec::new();
|
||||
for _ in 0..EQUIVOCATION_LIMIT + 1 {
|
||||
blocks.push(seal_block(block.block.clone(), &client))
|
||||
}
|
||||
|
||||
// sr25519 should generate a different signature every time you sign something and thus, all
|
||||
// blocks get a different hash (even if they are the same block).
|
||||
assert_eq!(blocks.iter().map(|b| b.hash()).collect::<HashSet<_>>().len(), blocks.len());
|
||||
|
||||
blocks.iter().take(EQUIVOCATION_LIMIT).for_each(|block| {
|
||||
let mut params =
|
||||
BlockImportParams::new(BlockOrigin::NetworkBroadcast, block.header().clone());
|
||||
params.body = Some(block.extrinsics().to_vec());
|
||||
verifier.verify(params).now_or_never().unwrap().unwrap();
|
||||
});
|
||||
|
||||
// Now let's try some previously verified block and a block we have not verified yet.
|
||||
//
|
||||
// Verify should fail, because we are above the limit. However, when we change the origin to
|
||||
// `ConsensusBroadcast`, it should work.
|
||||
let extra_blocks =
|
||||
vec![blocks[EQUIVOCATION_LIMIT / 2].clone(), blocks.last().unwrap().clone()];
|
||||
|
||||
extra_blocks.into_iter().for_each(|block| {
|
||||
let mut params =
|
||||
BlockImportParams::new(BlockOrigin::NetworkBroadcast, block.header().clone());
|
||||
params.body = Some(block.extrinsics().to_vec());
|
||||
assert!(verifier
|
||||
.verify(params)
|
||||
.now_or_never()
|
||||
.unwrap()
|
||||
.map(drop)
|
||||
.unwrap_err()
|
||||
.contains("excessive equivocations at slot"));
|
||||
|
||||
// When it comes from `pov-recovery`, we will accept it
|
||||
let mut params =
|
||||
BlockImportParams::new(BlockOrigin::ConsensusBroadcast, block.header().clone());
|
||||
params.body = Some(block.extrinsics().to_vec());
|
||||
assert!(verifier.verify(params).now_or_never().unwrap().is_ok());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Teyrchain specific wrapper for the AuRa import queue.
|
||||
|
||||
use codec::Codec;
|
||||
use cumulus_client_consensus_common::TeyrchainBlockImportMarker;
|
||||
use prometheus_endpoint::Registry;
|
||||
use pezsc_client_api::{backend::AuxStore, BlockOf, UsageProvider};
|
||||
use pezsc_consensus::{import_queue::DefaultImportQueue, BlockImport};
|
||||
use pezsc_consensus_aura::{AuraVerifier, CompatibilityMode};
|
||||
use pezsc_consensus_slots::InherentDataProviderExt;
|
||||
use pezsc_telemetry::TelemetryHandle;
|
||||
use pezsp_api::{ApiExt, ProvideRuntimeApi};
|
||||
use pezsp_block_builder::BlockBuilder as BlockBuilderApi;
|
||||
use pezsp_blockchain::{HeaderBackend, HeaderMetadata};
|
||||
use pezsp_consensus::Error as ConsensusError;
|
||||
use pezsp_consensus_aura::AuraApi;
|
||||
use pezsp_core::crypto::Pair;
|
||||
use pezsp_inherents::CreateInherentDataProviders;
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
/// Parameters for [`import_queue`].
|
||||
pub struct ImportQueueParams<'a, I, C, CIDP, S> {
|
||||
/// The block import to use.
|
||||
pub block_import: I,
|
||||
/// The client to interact with the chain.
|
||||
pub client: Arc<C>,
|
||||
/// The inherent data providers, to create the inherent data.
|
||||
pub create_inherent_data_providers: CIDP,
|
||||
/// The spawner to spawn background tasks.
|
||||
pub spawner: &'a S,
|
||||
/// The prometheus registry.
|
||||
pub registry: Option<&'a Registry>,
|
||||
/// The telemetry handle.
|
||||
pub telemetry: Option<TelemetryHandle>,
|
||||
}
|
||||
|
||||
/// Start an import queue for the Aura consensus algorithm.
|
||||
pub fn import_queue<P, Block, I, C, S, CIDP>(
|
||||
ImportQueueParams {
|
||||
block_import,
|
||||
client,
|
||||
create_inherent_data_providers,
|
||||
spawner,
|
||||
registry,
|
||||
telemetry,
|
||||
}: ImportQueueParams<'_, I, C, CIDP, S>,
|
||||
) -> Result<DefaultImportQueue<Block>, pezsp_consensus::Error>
|
||||
where
|
||||
Block: BlockT,
|
||||
C::Api: BlockBuilderApi<Block> + AuraApi<Block, P::Public> + ApiExt<Block>,
|
||||
C: 'static
|
||||
+ ProvideRuntimeApi<Block>
|
||||
+ BlockOf
|
||||
+ Send
|
||||
+ Sync
|
||||
+ AuxStore
|
||||
+ UsageProvider<Block>
|
||||
+ HeaderBackend<Block>
|
||||
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>,
|
||||
I: BlockImport<Block, Error = ConsensusError>
|
||||
+ TeyrchainBlockImportMarker
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
P: Pair + 'static,
|
||||
P::Public: Debug + Codec,
|
||||
P::Signature: Codec,
|
||||
S: pezsp_core::traits::SpawnEssentialNamed,
|
||||
CIDP: CreateInherentDataProviders<Block, ()> + Sync + Send + 'static,
|
||||
CIDP::InherentDataProviders: InherentDataProviderExt + Send + Sync,
|
||||
{
|
||||
pezsc_consensus_aura::import_queue::<P, _, _, _, _, _>(pezsc_consensus_aura::ImportQueueParams {
|
||||
block_import,
|
||||
justification_import: None,
|
||||
client,
|
||||
create_inherent_data_providers,
|
||||
spawner,
|
||||
registry,
|
||||
check_for_equivocation: pezsc_consensus_aura::CheckForEquivocation::No,
|
||||
telemetry,
|
||||
compatibility_mode: CompatibilityMode::None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parameters of [`build_verifier`].
|
||||
pub struct BuildVerifierParams<C, CIDP> {
|
||||
/// The client to interact with the chain.
|
||||
pub client: Arc<C>,
|
||||
/// The inherent data providers, to create the inherent data.
|
||||
pub create_inherent_data_providers: CIDP,
|
||||
/// The telemetry handle.
|
||||
pub telemetry: Option<TelemetryHandle>,
|
||||
}
|
||||
|
||||
/// Build the [`AuraVerifier`].
|
||||
pub fn build_verifier<P: Pair, C, CIDP, B: BlockT>(
|
||||
BuildVerifierParams { client, create_inherent_data_providers, telemetry }: BuildVerifierParams<
|
||||
C,
|
||||
CIDP,
|
||||
>,
|
||||
) -> AuraVerifier<C, P, CIDP, B> {
|
||||
pezsc_consensus_aura::build_verifier(pezsc_consensus_aura::BuildVerifierParams {
|
||||
client,
|
||||
create_inherent_data_providers,
|
||||
telemetry,
|
||||
check_for_equivocation: pezsc_consensus_aura::CheckForEquivocation::No,
|
||||
compatibility_mode: CompatibilityMode::None,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! The AuRa consensus algorithm for teyrchains.
|
||||
//!
|
||||
//! This extends the Bizinikiwi provided AuRa consensus implementation to make it compatible for
|
||||
//! teyrchains.
|
||||
//!
|
||||
//! For more information about AuRa, the Bizinikiwi crate should be checked.
|
||||
|
||||
use codec::Encode;
|
||||
use cumulus_primitives_core::PersistedValidationData;
|
||||
|
||||
use cumulus_primitives_core::relay_chain::HeadData;
|
||||
use pezkuwi_primitives::{BlockNumber as RBlockNumber, Hash as RHash};
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
|
||||
use std::{fs, fs::File, path::PathBuf};
|
||||
|
||||
mod import_queue;
|
||||
|
||||
pub use import_queue::{build_verifier, import_queue, BuildVerifierParams, ImportQueueParams};
|
||||
use pezkuwi_node_primitives::PoV;
|
||||
pub use pezsc_consensus_aura::{
|
||||
slot_duration, standalone::slot_duration_at, AuraVerifier, BuildAuraWorkerParams,
|
||||
SlotProportion,
|
||||
};
|
||||
pub use pezsc_consensus_slots::InherentDataProviderExt;
|
||||
|
||||
pub mod collator;
|
||||
pub mod collators;
|
||||
pub mod equivocation_import_queue;
|
||||
|
||||
const LOG_TARGET: &str = "aura::pezcumulus";
|
||||
|
||||
/// Export the given `pov` to the file system at `path`.
|
||||
///
|
||||
/// The file will be named `block_hash_block_number.pov`.
|
||||
///
|
||||
/// The `parent_header`, `relay_parent_storage_root` and `relay_parent_number` will also be
|
||||
/// stored in the file alongside the `pov`. This enables stateless validation of the `pov`.
|
||||
pub(crate) fn export_pov_to_path<Block: BlockT>(
|
||||
path: PathBuf,
|
||||
pov: PoV,
|
||||
block_hash: Block::Hash,
|
||||
block_number: NumberFor<Block>,
|
||||
parent_header: Block::Header,
|
||||
relay_parent_storage_root: RHash,
|
||||
relay_parent_number: RBlockNumber,
|
||||
max_pov_size: u32,
|
||||
) {
|
||||
if let Err(error) = fs::create_dir_all(&path) {
|
||||
tracing::error!(target: LOG_TARGET, %error, path = %path.display(), "Failed to create PoV export directory");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut file = match File::create(path.join(format!("{block_hash:?}_{block_number}.pov"))) {
|
||||
Ok(f) => f,
|
||||
Err(error) => {
|
||||
tracing::error!(target: LOG_TARGET, %error, "Failed to export PoV.");
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
pov.encode_to(&mut file);
|
||||
PersistedValidationData {
|
||||
parent_head: HeadData(parent_header.encode()),
|
||||
relay_parent_number,
|
||||
relay_parent_storage_root,
|
||||
max_pov_size,
|
||||
}
|
||||
.encode_to(&mut file);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
[package]
|
||||
name = "pezcumulus-client-consensus-common"
|
||||
description = "Pezcumulus specific common consensus implementations"
|
||||
version = "0.7.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
codec = { features = ["derive"], workspace = true, default-features = true }
|
||||
dyn-clone = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
log = { workspace = true, default-features = true }
|
||||
tracing = { workspace = true, default-features = true }
|
||||
|
||||
# Bizinikiwi
|
||||
prometheus-endpoint = { workspace = true, default-features = true }
|
||||
pezsc-client-api = { workspace = true, default-features = true }
|
||||
pezsc-consensus = { workspace = true, default-features = true }
|
||||
pezsc-consensus-babe = { workspace = true, default-features = true }
|
||||
pezsc-network = { workspace = true, default-features = true }
|
||||
pezsp-blockchain = { workspace = true, default-features = true }
|
||||
pezsp-consensus = { workspace = true, default-features = true }
|
||||
pezsp-consensus-slots = { workspace = true, default-features = true }
|
||||
pezsp-core = { workspace = true, default-features = true }
|
||||
pezsp-runtime = { workspace = true, default-features = true }
|
||||
pezsp-timestamp = { workspace = true, default-features = true }
|
||||
pezsp-trie = { workspace = true, default-features = true }
|
||||
pezsp-version = { workspace = true, default-features = true }
|
||||
|
||||
# Pezkuwi
|
||||
pezkuwi-primitives = { workspace = true, default-features = true }
|
||||
|
||||
# Pezcumulus
|
||||
pezcumulus-client-pov-recovery = { workspace = true, default-features = true }
|
||||
pezcumulus-primitives-core = { workspace = true, default-features = true }
|
||||
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
|
||||
pezcumulus-relay-chain-streams = { workspace = true, default-features = true }
|
||||
schnellru = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
futures-timer = { workspace = true }
|
||||
tokio = { features = ["macros"], workspace = true }
|
||||
|
||||
# Bizinikiwi
|
||||
pezsp-tracing = { workspace = true, default-features = true }
|
||||
|
||||
# Pezcumulus
|
||||
pezcumulus-test-client = { workspace = true }
|
||||
pezcumulus-test-relay-sproof-builder = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezcumulus-client-pov-recovery/runtime-benchmarks",
|
||||
"pezcumulus-primitives-core/runtime-benchmarks",
|
||||
"pezcumulus-relay-chain-interface/runtime-benchmarks",
|
||||
"pezcumulus-relay-chain-streams/runtime-benchmarks",
|
||||
"pezcumulus-test-client/runtime-benchmarks",
|
||||
"pezcumulus-test-relay-sproof-builder/runtime-benchmarks",
|
||||
"pezkuwi-primitives/runtime-benchmarks",
|
||||
"pezsc-client-api/runtime-benchmarks",
|
||||
"pezsc-consensus-babe/runtime-benchmarks",
|
||||
"pezsc-consensus/runtime-benchmarks",
|
||||
"pezsc-network/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus-slots/runtime-benchmarks",
|
||||
"pezsp-consensus/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"pezsp-timestamp/runtime-benchmarks",
|
||||
"pezsp-trie/runtime-benchmarks",
|
||||
"pezsp-version/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! (unstable) Composable utilities for constructing import queues for teyrchains.
|
||||
//!
|
||||
//! Unlike standalone chains, teyrchains have the requirement that all consensus logic
|
||||
//! must be checked within the runtime. This property means that work which is normally
|
||||
//! done in the import queue per-block, such as checking signatures, quorums, and whether
|
||||
//! inherent extrinsics were constructed faithfully do not need to be done, per se.
|
||||
//!
|
||||
//! It may seem that it would be beneficial for the client to do these checks regardless,
|
||||
//! but in practice this means that clients would just reject blocks which are _valid_ according
|
||||
//! to their Teyrchain Validation Function, which is the ultimate source of consensus truth.
|
||||
//!
|
||||
//! However, teyrchain runtimes expose two different access points for executing blocks
|
||||
//! in full nodes versus executing those blocks in the teyrchain validation environment.
|
||||
//! At the time of writing, the inherent and consensus checks in most Pezcumulus runtimes
|
||||
//! are only performed during teyrchain validation, not full node block execution.
|
||||
//!
|
||||
//! See <https://github.com/pezkuwichain/kurdistan-sdk/issues/91> for details.
|
||||
|
||||
use pezsp_consensus::error::Error as ConsensusError;
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
use pezsc_consensus::{
|
||||
block_import::{BlockImport, BlockImportParams},
|
||||
import_queue::{BasicQueue, Verifier},
|
||||
};
|
||||
|
||||
use crate::TeyrchainBlockImportMarker;
|
||||
|
||||
/// A [`Verifier`] for blocks which verifies absolutely nothing.
|
||||
///
|
||||
/// This should only be used when the runtime is responsible for checking block seals and inherents.
|
||||
pub struct VerifyNothing;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<Block: BlockT> Verifier<Block> for VerifyNothing {
|
||||
async fn verify(
|
||||
&self,
|
||||
params: BlockImportParams<Block>,
|
||||
) -> Result<BlockImportParams<Block>, String> {
|
||||
Ok(params)
|
||||
}
|
||||
}
|
||||
|
||||
/// An import queue which does no verification.
|
||||
///
|
||||
/// This should only be used when the runtime is responsible for checking block seals and inherents.
|
||||
pub fn verify_nothing_import_queue<Block: BlockT, I>(
|
||||
block_import: I,
|
||||
spawner: &impl pezsp_core::traits::SpawnEssentialNamed,
|
||||
registry: Option<&prometheus_endpoint::Registry>,
|
||||
) -> BasicQueue<Block>
|
||||
where
|
||||
I: BlockImport<Block, Error = ConsensusError>
|
||||
+ TeyrchainBlockImportMarker
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
BasicQueue::new(VerifyNothing, Box::new(block_import), None, spawner, registry)
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use pezsc_client_api::{blockchain::Backend as _, Backend, HeaderBackend as _};
|
||||
use pezsp_blockchain::{HashAndNumber, HeaderMetadata, TreeRoute};
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor, One, Saturating, UniqueSaturatedInto, Zero};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
const LOG_TARGET: &str = "level-monitor";
|
||||
|
||||
/// Value good enough to be used with teyrchains using the current backend implementation
|
||||
/// that ships with Bizinikiwi. This value may change in the future.
|
||||
pub const MAX_LEAVES_PER_LEVEL_SENSIBLE_DEFAULT: usize = 32;
|
||||
|
||||
// Counter threshold after which we are going to eventually cleanup our internal data.
|
||||
const CLEANUP_THRESHOLD: u32 = 32;
|
||||
|
||||
/// Upper bound to the number of leaves allowed for each level of the blockchain.
|
||||
///
|
||||
/// If the limit is set and more leaves are detected on block import, then the older ones are
|
||||
/// dropped to make space for the fresh blocks.
|
||||
///
|
||||
/// In environments where blocks confirmations from the relay chain may be "slow", then
|
||||
/// setting an upper bound helps keeping the chain health by dropping old (presumably) stale
|
||||
/// leaves and prevents discarding new blocks because we've reached the backend max value.
|
||||
pub enum LevelLimit {
|
||||
/// Limit set to [`MAX_LEAVES_PER_LEVEL_SENSIBLE_DEFAULT`].
|
||||
Default,
|
||||
/// No explicit limit, however a limit may be implicitly imposed by the backend implementation.
|
||||
None,
|
||||
/// Custom value.
|
||||
Some(usize),
|
||||
}
|
||||
|
||||
/// Support structure to constrain the number of leaves at each level.
|
||||
pub struct LevelMonitor<Block: BlockT, BE> {
|
||||
/// Max number of leaves for each level.
|
||||
level_limit: usize,
|
||||
/// Monotonic counter used to keep track of block freshness.
|
||||
pub(crate) import_counter: NumberFor<Block>,
|
||||
/// Map between blocks hashes and freshness.
|
||||
pub(crate) freshness: HashMap<Block::Hash, NumberFor<Block>>,
|
||||
/// Blockchain levels cache.
|
||||
pub(crate) levels: HashMap<NumberFor<Block>, HashSet<Block::Hash>>,
|
||||
/// Lower level number stored by the levels map.
|
||||
lowest_level: NumberFor<Block>,
|
||||
/// Backend reference to remove blocks on level saturation.
|
||||
backend: Arc<BE>,
|
||||
}
|
||||
|
||||
/// Contains information about the target scheduled for removal.
|
||||
struct TargetInfo<Block: BlockT> {
|
||||
/// Index of freshest leaf in the leaves array.
|
||||
freshest_leaf_idx: usize,
|
||||
/// Route from target to its freshest leaf.
|
||||
freshest_route: TreeRoute<Block>,
|
||||
}
|
||||
|
||||
impl<Block, BE> LevelMonitor<Block, BE>
|
||||
where
|
||||
Block: BlockT,
|
||||
BE: Backend<Block>,
|
||||
{
|
||||
/// Instance a new monitor structure.
|
||||
pub fn new(level_limit: usize, backend: Arc<BE>) -> Self {
|
||||
let mut monitor = LevelMonitor {
|
||||
level_limit,
|
||||
import_counter: Zero::zero(),
|
||||
freshness: HashMap::new(),
|
||||
levels: HashMap::new(),
|
||||
lowest_level: Zero::zero(),
|
||||
backend,
|
||||
};
|
||||
monitor.restore();
|
||||
monitor
|
||||
}
|
||||
|
||||
/// Restore the structure using the backend.
|
||||
///
|
||||
/// Blocks freshness values are inferred from the height and not from the effective import
|
||||
/// moment. This is a not accurate but "good-enough" best effort solution.
|
||||
///
|
||||
/// Level limits are not enforced during this phase.
|
||||
fn restore(&mut self) {
|
||||
let info = self.backend.blockchain().info();
|
||||
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Restoring chain level monitor from last finalized block: {} {}",
|
||||
info.finalized_number,
|
||||
info.finalized_hash
|
||||
);
|
||||
|
||||
self.lowest_level = info.finalized_number;
|
||||
self.import_counter = info.finalized_number;
|
||||
|
||||
for leaf in self.backend.blockchain().leaves().unwrap_or_default() {
|
||||
let Ok(mut meta) = self.backend.blockchain().header_metadata(leaf) else {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Could not fetch header metadata for leaf: {leaf:?}",
|
||||
);
|
||||
|
||||
continue;
|
||||
};
|
||||
|
||||
self.import_counter = self.import_counter.max(meta.number);
|
||||
|
||||
// Populate the monitor until we don't hit an already imported branch
|
||||
while !self.freshness.contains_key(&meta.hash) {
|
||||
self.freshness.insert(meta.hash, meta.number);
|
||||
self.levels.entry(meta.number).or_default().insert(meta.hash);
|
||||
if meta.number <= self.lowest_level {
|
||||
break;
|
||||
}
|
||||
|
||||
meta = match self.backend.blockchain().header_metadata(meta.parent) {
|
||||
Ok(m) => m,
|
||||
Err(_) => {
|
||||
// This can happen after we have warp synced a node.
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Could not fetch header metadata for parent: {:?}",
|
||||
meta.parent,
|
||||
);
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Restored chain level monitor up to height {}",
|
||||
self.import_counter
|
||||
);
|
||||
}
|
||||
|
||||
/// Check and enforce the limit bound at the given height.
|
||||
///
|
||||
/// In practice this will enforce the given height in having a number of blocks less than
|
||||
/// the limit passed to the constructor.
|
||||
///
|
||||
/// If the given level is found to have a number of blocks greater than or equal the limit
|
||||
/// then the limit is enforced by choosing one (or more) blocks to remove.
|
||||
///
|
||||
/// The removal strategy is driven by the block freshness.
|
||||
///
|
||||
/// A block freshness is determined by the most recent leaf freshness descending from the block
|
||||
/// itself. In other words its freshness is equal to its more "fresh" descendant.
|
||||
///
|
||||
/// The least "fresh" blocks are eventually removed.
|
||||
pub fn enforce_limit(&mut self, number: NumberFor<Block>) {
|
||||
let level_len = self.levels.get(&number).map(|l| l.len()).unwrap_or_default();
|
||||
if level_len < self.level_limit {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort leaves by freshness only once (less fresh first) and keep track of
|
||||
// leaves that were invalidated on removal.
|
||||
let mut leaves = self.backend.blockchain().leaves().unwrap_or_default();
|
||||
leaves.sort_unstable_by(|a, b| self.freshness.get(a).cmp(&self.freshness.get(b)));
|
||||
let mut invalidated_leaves = HashSet::new();
|
||||
|
||||
// This may not be the most efficient way to remove **multiple** entries, but is the easy
|
||||
// one :-). Should be considered that in "normal" conditions the number of blocks to remove
|
||||
// is 0 or 1, it is not worth to complicate the code too much. One condition that may
|
||||
// trigger multiple removals (2+) is if we restart the node using an existing db and a
|
||||
// smaller limit wrt the one previously used.
|
||||
let remove_count = level_len - self.level_limit + 1;
|
||||
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Detected leaves overflow at height {number}, removing {remove_count} obsolete blocks",
|
||||
);
|
||||
|
||||
(0..remove_count).all(|_| {
|
||||
self.find_target(number, &leaves, &invalidated_leaves).map_or(false, |target| {
|
||||
self.remove_target(target, number, &leaves, &mut invalidated_leaves);
|
||||
true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to find the best candidate to be removed.
|
||||
//
|
||||
// Given a set of blocks with height equal to `number` (potential candidates)
|
||||
// 1. For each candidate fetch all the leaves that are descending from it.
|
||||
// 2. Set the candidate freshness equal to the fresher of its descending leaves.
|
||||
// 3. The target is set as the candidate that is less fresh.
|
||||
//
|
||||
// Input `leaves` are assumed to be already ordered by "freshness" (less fresh first).
|
||||
//
|
||||
// Returns the index of the target fresher leaf within `leaves` and the route from target to
|
||||
// such leaf.
|
||||
fn find_target(
|
||||
&self,
|
||||
number: NumberFor<Block>,
|
||||
leaves: &[Block::Hash],
|
||||
invalidated_leaves: &HashSet<usize>,
|
||||
) -> Option<TargetInfo<Block>> {
|
||||
let mut target_info: Option<TargetInfo<Block>> = None;
|
||||
let blockchain = self.backend.blockchain();
|
||||
let best_hash = blockchain.info().best_hash;
|
||||
|
||||
// Leaves that where already assigned to some node and thus can be skipped
|
||||
// during the search.
|
||||
let mut assigned_leaves = HashSet::new();
|
||||
|
||||
let level = self.levels.get(&number)?;
|
||||
|
||||
for blk_hash in level.iter().filter(|hash| **hash != best_hash) {
|
||||
// Search for the fresher leaf information for this block
|
||||
let candidate_info = leaves
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(leaf_idx, _)| {
|
||||
!assigned_leaves.contains(leaf_idx) && !invalidated_leaves.contains(leaf_idx)
|
||||
})
|
||||
.rev()
|
||||
.find_map(|(leaf_idx, leaf_hash)| {
|
||||
if blk_hash == leaf_hash {
|
||||
let entry = HashAndNumber { number, hash: *blk_hash };
|
||||
TreeRoute::new(vec![entry], 0).ok().map(|freshest_route| TargetInfo {
|
||||
freshest_leaf_idx: leaf_idx,
|
||||
freshest_route,
|
||||
})
|
||||
} else {
|
||||
match pezsp_blockchain::tree_route(blockchain, *blk_hash, *leaf_hash) {
|
||||
Ok(route) if route.retracted().is_empty() => Some(TargetInfo {
|
||||
freshest_leaf_idx: leaf_idx,
|
||||
freshest_route: route,
|
||||
}),
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"(Lookup) Unable getting route from {:?} to {:?}: {}",
|
||||
blk_hash,
|
||||
leaf_hash,
|
||||
err,
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let candidate_info = match candidate_info {
|
||||
Some(candidate_info) => {
|
||||
assigned_leaves.insert(candidate_info.freshest_leaf_idx);
|
||||
candidate_info
|
||||
},
|
||||
None => {
|
||||
// This should never happen
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"Unable getting route to any leaf from {:?} (this is a bug)",
|
||||
blk_hash,
|
||||
);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
// Found fresher leaf for this candidate.
|
||||
// This candidate is set as the new target if:
|
||||
// 1. its fresher leaf is less fresh than the previous target fresher leaf AND
|
||||
// 2. best block is not in its route
|
||||
|
||||
let is_less_fresh = || {
|
||||
target_info
|
||||
.as_ref()
|
||||
.map(|ti| candidate_info.freshest_leaf_idx < ti.freshest_leaf_idx)
|
||||
.unwrap_or(true)
|
||||
};
|
||||
let not_contains_best = || {
|
||||
candidate_info
|
||||
.freshest_route
|
||||
.enacted()
|
||||
.iter()
|
||||
.all(|entry| entry.hash != best_hash)
|
||||
};
|
||||
|
||||
if is_less_fresh() && not_contains_best() {
|
||||
let early_stop = candidate_info.freshest_leaf_idx == 0;
|
||||
target_info = Some(candidate_info);
|
||||
if early_stop {
|
||||
// We will never find a candidate with an worst freshest leaf than this.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target_info
|
||||
}
|
||||
|
||||
// Remove the target block and all its descendants.
|
||||
//
|
||||
// Leaves should have already been ordered by "freshness" (less fresh first).
|
||||
fn remove_target(
|
||||
&mut self,
|
||||
target: TargetInfo<Block>,
|
||||
number: NumberFor<Block>,
|
||||
leaves: &[Block::Hash],
|
||||
invalidated_leaves: &mut HashSet<usize>,
|
||||
) {
|
||||
let mut remove_leaf = |number, hash| {
|
||||
log::debug!(target: LOG_TARGET, "Removing block (@{}) {:?}", number, hash);
|
||||
if let Err(err) = self.backend.remove_leaf_block(hash) {
|
||||
log::debug!(target: LOG_TARGET, "Remove not possible for {}: {}", hash, err);
|
||||
return false;
|
||||
}
|
||||
self.levels.get_mut(&number).map(|level| level.remove(&hash));
|
||||
self.freshness.remove(&hash);
|
||||
true
|
||||
};
|
||||
|
||||
invalidated_leaves.insert(target.freshest_leaf_idx);
|
||||
|
||||
// Takes care of route removal. Starts from the leaf and stops as soon as an error is
|
||||
// encountered. In this case an error is interpreted as the block being not a leaf
|
||||
// and it will be removed while removing another route from the same block but to a
|
||||
// different leaf.
|
||||
let mut remove_route = |route: TreeRoute<Block>| {
|
||||
route.enacted().iter().rev().all(|elem| remove_leaf(elem.number, elem.hash));
|
||||
};
|
||||
|
||||
let target_hash = target.freshest_route.common_block().hash;
|
||||
debug_assert_eq!(
|
||||
target.freshest_route.common_block().number,
|
||||
number,
|
||||
"This is a bug in LevelMonitor::find_target() or the Backend is corrupted"
|
||||
);
|
||||
|
||||
// Remove freshest (cached) route first.
|
||||
remove_route(target.freshest_route);
|
||||
|
||||
// Don't bother trying with leaves we already found to not be our descendants.
|
||||
let to_skip = leaves.len() - target.freshest_leaf_idx;
|
||||
leaves.iter().enumerate().rev().skip(to_skip).for_each(|(leaf_idx, leaf_hash)| {
|
||||
if invalidated_leaves.contains(&leaf_idx) {
|
||||
return;
|
||||
}
|
||||
match pezsp_blockchain::tree_route(self.backend.blockchain(), target_hash, *leaf_hash) {
|
||||
Ok(route) if route.retracted().is_empty() => {
|
||||
invalidated_leaves.insert(leaf_idx);
|
||||
remove_route(route);
|
||||
},
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"(Removal) unable getting route from {:?} to {:?}: {}",
|
||||
target_hash,
|
||||
leaf_hash,
|
||||
err,
|
||||
);
|
||||
},
|
||||
_ => (),
|
||||
};
|
||||
});
|
||||
|
||||
remove_leaf(number, target_hash);
|
||||
}
|
||||
|
||||
/// Add a new imported block information to the monitor.
|
||||
pub fn block_imported(&mut self, number: NumberFor<Block>, hash: Block::Hash) {
|
||||
let finalized_num = self.backend.blockchain().info().finalized_number;
|
||||
|
||||
if number > finalized_num {
|
||||
// Only blocks above the last finalized block should be added to the monitor
|
||||
self.import_counter += One::one();
|
||||
self.freshness.insert(hash, self.import_counter);
|
||||
self.levels.entry(number).or_default().insert(hash);
|
||||
}
|
||||
|
||||
let delta: u32 = finalized_num.saturating_sub(self.lowest_level).unique_saturated_into();
|
||||
if delta >= CLEANUP_THRESHOLD {
|
||||
// Do cleanup once in a while, we are allowed to have some obsolete information.
|
||||
for i in 0..delta {
|
||||
let number = self.lowest_level + i.unique_saturated_into();
|
||||
self.levels.remove(&number).map(|level| {
|
||||
level.iter().for_each(|hash| {
|
||||
self.freshness.remove(hash);
|
||||
})
|
||||
});
|
||||
}
|
||||
self.lowest_level = finalized_num;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use codec::Decode;
|
||||
use pezkuwi_primitives::{Block as PBlock, Hash as PHash, Header as PHeader, ValidationCodeHash};
|
||||
|
||||
use cumulus_primitives_core::{relay_chain, AbridgedHostConfiguration};
|
||||
use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface};
|
||||
|
||||
use pezsc_client_api::Backend;
|
||||
use pezsc_consensus::{shared_data::SharedData, BlockImport, ImportResult};
|
||||
use pezsp_consensus_slots::Slot;
|
||||
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
|
||||
use pezsp_timestamp::Timestamp;
|
||||
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
mod level_monitor;
|
||||
mod parent_search;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod teyrchain_consensus;
|
||||
|
||||
pub use parent_search::*;
|
||||
|
||||
pub use cumulus_relay_chain_streams::finalized_heads;
|
||||
pub use teyrchain_consensus::spawn_teyrchain_consensus_tasks;
|
||||
|
||||
use level_monitor::LevelMonitor;
|
||||
pub use level_monitor::{LevelLimit, MAX_LEAVES_PER_LEVEL_SENSIBLE_DEFAULT};
|
||||
|
||||
pub mod import_queue;
|
||||
|
||||
/// Provides the hash of validation code used for authoring/execution of blocks at a given
|
||||
/// hash.
|
||||
pub trait ValidationCodeHashProvider<Hash> {
|
||||
fn code_hash_at(&self, at: Hash) -> Option<ValidationCodeHash>;
|
||||
}
|
||||
|
||||
impl<F, Hash> ValidationCodeHashProvider<Hash> for F
|
||||
where
|
||||
F: Fn(Hash) -> Option<ValidationCodeHash>,
|
||||
{
|
||||
fn code_hash_at(&self, at: Hash) -> Option<ValidationCodeHash> {
|
||||
(self)(at)
|
||||
}
|
||||
}
|
||||
|
||||
/// The result from building a collation.
|
||||
pub struct TeyrchainCandidate<B> {
|
||||
/// The block that was built for this candidate.
|
||||
pub block: B,
|
||||
/// The proof that was recorded while building the block.
|
||||
pub proof: pezsp_trie::StorageProof,
|
||||
}
|
||||
|
||||
/// Teyrchain specific block import.
|
||||
///
|
||||
/// Specialized block import for teyrchains. It supports to delay setting the best block until the
|
||||
/// relay chain has included a candidate in its best block. By default the delayed best block
|
||||
/// setting is disabled. The block import also monitors the imported blocks and prunes by default if
|
||||
/// there are too many blocks at the same height. Too many blocks at the same height can for example
|
||||
/// happen if the relay chain is rejecting the teyrchain blocks in the validation.
|
||||
pub struct TeyrchainBlockImport<Block: BlockT, BI, BE> {
|
||||
inner: BI,
|
||||
monitor: Option<SharedData<LevelMonitor<Block, BE>>>,
|
||||
delayed_best_block: bool,
|
||||
}
|
||||
|
||||
impl<Block: BlockT, BI, BE: Backend<Block>> TeyrchainBlockImport<Block, BI, BE> {
|
||||
/// Create a new instance.
|
||||
///
|
||||
/// The number of leaves per level limit is set to `LevelLimit::Default`.
|
||||
pub fn new(inner: BI, backend: Arc<BE>) -> Self {
|
||||
Self::new_with_limit(inner, backend, LevelLimit::Default)
|
||||
}
|
||||
|
||||
/// Create a new instance with an explicit limit to the number of leaves per level.
|
||||
///
|
||||
/// This function alone doesn't enforce the limit on levels for old imported blocks,
|
||||
/// the limit is eventually enforced only when new blocks are imported.
|
||||
pub fn new_with_limit(inner: BI, backend: Arc<BE>, level_leaves_max: LevelLimit) -> Self {
|
||||
let level_limit = match level_leaves_max {
|
||||
LevelLimit::None => None,
|
||||
LevelLimit::Some(limit) => Some(limit),
|
||||
LevelLimit::Default => Some(MAX_LEAVES_PER_LEVEL_SENSIBLE_DEFAULT),
|
||||
};
|
||||
|
||||
let monitor =
|
||||
level_limit.map(|level_limit| SharedData::new(LevelMonitor::new(level_limit, backend)));
|
||||
|
||||
Self { inner, monitor, delayed_best_block: false }
|
||||
}
|
||||
|
||||
/// Create a new instance which delays setting the best block.
|
||||
///
|
||||
/// The number of leaves per level limit is set to `LevelLimit::Default`.
|
||||
pub fn new_with_delayed_best_block(inner: BI, backend: Arc<BE>) -> Self {
|
||||
Self {
|
||||
delayed_best_block: true,
|
||||
..Self::new_with_limit(inner, backend, LevelLimit::Default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block: BlockT, I: Clone, BE> Clone for TeyrchainBlockImport<Block, I, BE> {
|
||||
fn clone(&self) -> Self {
|
||||
TeyrchainBlockImport {
|
||||
inner: self.inner.clone(),
|
||||
monitor: self.monitor.clone(),
|
||||
delayed_best_block: self.delayed_best_block,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<Block, BI, BE> BlockImport<Block> for TeyrchainBlockImport<Block, BI, BE>
|
||||
where
|
||||
Block: BlockT,
|
||||
BI: BlockImport<Block> + Send + Sync,
|
||||
BE: Backend<Block>,
|
||||
{
|
||||
type Error = BI::Error;
|
||||
|
||||
async fn check_block(
|
||||
&self,
|
||||
block: pezsc_consensus::BlockCheckParams<Block>,
|
||||
) -> Result<pezsc_consensus::ImportResult, Self::Error> {
|
||||
self.inner.check_block(block).await
|
||||
}
|
||||
|
||||
async fn import_block(
|
||||
&self,
|
||||
mut params: pezsc_consensus::BlockImportParams<Block>,
|
||||
) -> Result<pezsc_consensus::ImportResult, Self::Error> {
|
||||
// Blocks are stored within the backend by using POST hash.
|
||||
let hash = params.post_hash();
|
||||
let number = *params.header.number();
|
||||
|
||||
if params.with_state() {
|
||||
// Force imported state finality.
|
||||
// Required for warp sync. We assume that preconditions have been
|
||||
// checked properly and we are importing a finalized block with state.
|
||||
params.finalized = true;
|
||||
}
|
||||
|
||||
if self.delayed_best_block {
|
||||
// Best block is determined by the relay chain, or if we are doing the initial sync
|
||||
// we import all blocks as new best.
|
||||
params.fork_choice = Some(pezsc_consensus::ForkChoiceStrategy::Custom(
|
||||
params.origin == pezsp_consensus::BlockOrigin::NetworkInitialSync,
|
||||
));
|
||||
}
|
||||
|
||||
let maybe_lock = self.monitor.as_ref().map(|monitor_lock| {
|
||||
let mut monitor = monitor_lock.shared_data_locked();
|
||||
monitor.enforce_limit(number);
|
||||
monitor.release_mutex()
|
||||
});
|
||||
|
||||
let res = self.inner.import_block(params).await?;
|
||||
|
||||
if let (Some(mut monitor_lock), ImportResult::Imported(_)) = (maybe_lock, &res) {
|
||||
let mut monitor = monitor_lock.upgrade();
|
||||
monitor.block_imported(number, hash);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker trait denoting a block import type that fits the teyrchain requirements.
|
||||
pub trait TeyrchainBlockImportMarker {}
|
||||
|
||||
impl<B: BlockT, BI, BE> TeyrchainBlockImportMarker for TeyrchainBlockImport<B, BI, BE> {}
|
||||
|
||||
/// Get the relay-parent slot and timestamp from a header.
|
||||
pub fn relay_slot_and_timestamp(
|
||||
relay_parent_header: &PHeader,
|
||||
relay_chain_slot_duration: Duration,
|
||||
) -> Option<(Slot, Timestamp)> {
|
||||
pezsc_consensus_babe::find_pre_digest::<PBlock>(relay_parent_header)
|
||||
.map(|babe_pre_digest| {
|
||||
let slot = babe_pre_digest.slot();
|
||||
let t = Timestamp::new(relay_chain_slot_duration.as_millis() as u64 * *slot);
|
||||
|
||||
(slot, t)
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Reads abridged host configuration from the relay chain storage at the given relay parent.
|
||||
pub async fn load_abridged_host_configuration(
|
||||
relay_parent: PHash,
|
||||
relay_client: &impl RelayChainInterface,
|
||||
) -> Result<Option<AbridgedHostConfiguration>, RelayChainError> {
|
||||
relay_client
|
||||
.get_storage_by_key(relay_parent, relay_chain::well_known_keys::ACTIVE_CONFIG)
|
||||
.await?
|
||||
.map(|bytes| {
|
||||
AbridgedHostConfiguration::decode(&mut &bytes[..])
|
||||
.map_err(RelayChainError::DeserializationError)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use codec::Decode;
|
||||
use pezkuwi_primitives::Hash as RelayHash;
|
||||
|
||||
use cumulus_primitives_core::{
|
||||
relay_chain::{BlockId as RBlockId, OccupiedCoreAssumption},
|
||||
ParaId,
|
||||
};
|
||||
use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface};
|
||||
|
||||
use pezsc_client_api::{Backend, HeaderBackend};
|
||||
|
||||
use pezsp_blockchain::{Backend as BlockchainBackend, TreeRoute};
|
||||
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
|
||||
|
||||
const PARENT_SEARCH_LOG_TARGET: &str = "consensus::common::find_potential_parents";
|
||||
|
||||
/// Parameters when searching for suitable parents to build on top of.
|
||||
#[derive(Debug)]
|
||||
pub struct ParentSearchParams {
|
||||
/// The relay-parent that is intended to be used.
|
||||
pub relay_parent: RelayHash,
|
||||
/// The ID of the teyrchain.
|
||||
pub para_id: ParaId,
|
||||
/// A limitation on the age of relay parents for teyrchain blocks that are being
|
||||
/// considered. This is relative to the `relay_parent` number.
|
||||
pub ancestry_lookback: usize,
|
||||
/// How "deep" parents can be relative to the included teyrchain block at the relay-parent.
|
||||
/// The included block has depth 0.
|
||||
pub max_depth: usize,
|
||||
/// Whether to only ignore "alternative" branches, i.e. branches of the chain
|
||||
/// which do not contain the block pending availability.
|
||||
pub ignore_alternative_branches: bool,
|
||||
}
|
||||
|
||||
/// A potential parent block returned from [`find_potential_parents`]
|
||||
#[derive(PartialEq)]
|
||||
pub struct PotentialParent<B: BlockT> {
|
||||
/// The hash of the block.
|
||||
pub hash: B::Hash,
|
||||
/// The header of the block.
|
||||
pub header: B::Header,
|
||||
/// The depth of the block with respect to the included block.
|
||||
pub depth: usize,
|
||||
/// Whether the block is the included block, is itself pending on-chain, or descends
|
||||
/// from the block pending availability.
|
||||
pub aligned_with_pending: bool,
|
||||
}
|
||||
|
||||
impl<B: BlockT> std::fmt::Debug for PotentialParent<B> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("PotentialParent")
|
||||
.field("hash", &self.hash)
|
||||
.field("depth", &self.depth)
|
||||
.field("aligned_with_pending", &self.aligned_with_pending)
|
||||
.field("number", &self.header.number())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a recursive search through blocks to find potential
|
||||
/// parent blocks for a new block.
|
||||
///
|
||||
/// This accepts a relay-chain block to be used as an anchor and a maximum search depth,
|
||||
/// along with some arguments for filtering teyrchain blocks and performs a recursive search
|
||||
/// for teyrchain blocks. The search begins at the last included teyrchain block and returns
|
||||
/// a set of [`PotentialParent`]s which could be potential parents of a new block with this
|
||||
/// relay-parent according to the search parameters.
|
||||
///
|
||||
/// A teyrchain block is a potential parent if it is either the last included teyrchain block, the
|
||||
/// pending teyrchain block (when `max_depth` >= 1), or all of the following hold:
|
||||
/// * its parent is a potential parent
|
||||
/// * its relay-parent is within `ancestry_lookback` of the targeted relay-parent.
|
||||
/// * its relay-parent is within the same session as the targeted relay-parent.
|
||||
/// * the block number is within `max_depth` blocks of the included block
|
||||
pub async fn find_potential_parents<B: BlockT>(
|
||||
params: ParentSearchParams,
|
||||
backend: &impl Backend<B>,
|
||||
relay_client: &impl RelayChainInterface,
|
||||
) -> Result<Vec<PotentialParent<B>>, RelayChainError> {
|
||||
tracing::trace!("Parent search parameters: {params:?}");
|
||||
// Get the included block.
|
||||
let Some((included_header, included_hash)) =
|
||||
fetch_included_from_relay_chain(relay_client, backend, params.para_id, params.relay_parent)
|
||||
.await?
|
||||
else {
|
||||
return Ok(Default::default());
|
||||
};
|
||||
|
||||
let only_included = vec![PotentialParent {
|
||||
hash: included_hash,
|
||||
header: included_header.clone(),
|
||||
depth: 0,
|
||||
aligned_with_pending: true,
|
||||
}];
|
||||
|
||||
if params.max_depth == 0 {
|
||||
return Ok(only_included);
|
||||
};
|
||||
|
||||
// Pending header and hash.
|
||||
let maybe_pending = {
|
||||
// Fetch the most recent pending header from the relay chain. We use
|
||||
// `OccupiedCoreAssumption::Included` so the candidate pending availability gets enacted
|
||||
// before being returned to us.
|
||||
let pending_header = relay_client
|
||||
.persisted_validation_data(
|
||||
params.relay_parent,
|
||||
params.para_id,
|
||||
OccupiedCoreAssumption::Included,
|
||||
)
|
||||
.await?
|
||||
.and_then(|p| B::Header::decode(&mut &p.parent_head.0[..]).ok())
|
||||
.filter(|x| x.hash() != included_hash);
|
||||
|
||||
// If the pending block is not locally known, we can't do anything.
|
||||
if let Some(header) = pending_header {
|
||||
let pending_hash = header.hash();
|
||||
match backend.blockchain().header(pending_hash) {
|
||||
// We are supposed to ignore branches that don't contain the pending block, but we
|
||||
// do not know the pending block locally.
|
||||
Ok(None) | Err(_) if params.ignore_alternative_branches => {
|
||||
tracing::warn!(
|
||||
target: PARENT_SEARCH_LOG_TARGET,
|
||||
%pending_hash,
|
||||
"Failed to get header for pending block.",
|
||||
);
|
||||
return Ok(Default::default());
|
||||
},
|
||||
Ok(Some(_)) => Some((header, pending_hash)),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let maybe_route_to_last_pending = maybe_pending
|
||||
.as_ref()
|
||||
.map(|(_, pending)| {
|
||||
pezsp_blockchain::tree_route(backend.blockchain(), included_hash, *pending)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
// If we want to ignore alternative branches there is no reason to start
|
||||
// the parent search at the included block. We can add the included block and
|
||||
// the path to the pending block to the potential parents directly (limited by max_depth).
|
||||
let (frontier, potential_parents) = match (
|
||||
&maybe_pending,
|
||||
params.ignore_alternative_branches,
|
||||
&maybe_route_to_last_pending,
|
||||
) {
|
||||
(Some((pending_header, pending_hash)), true, Some(ref route_to_pending)) => {
|
||||
let mut potential_parents = only_included;
|
||||
|
||||
// This is a defensive check, should never happen.
|
||||
if !route_to_pending.retracted().is_empty() {
|
||||
tracing::warn!(target: PARENT_SEARCH_LOG_TARGET, "Included block not an ancestor of pending block. This should not happen.");
|
||||
return Ok(Default::default());
|
||||
}
|
||||
|
||||
// Add all items on the path included -> pending - 1 to the potential parents, but
|
||||
// not more than `max_depth`.
|
||||
let num_parents_on_path =
|
||||
route_to_pending.enacted().len().saturating_sub(1).min(params.max_depth);
|
||||
for (num, block) in
|
||||
route_to_pending.enacted().iter().take(num_parents_on_path).enumerate()
|
||||
{
|
||||
let Ok(Some(header)) = backend.blockchain().header(block.hash) else { continue };
|
||||
|
||||
potential_parents.push(PotentialParent {
|
||||
hash: block.hash,
|
||||
header,
|
||||
depth: 1 + num,
|
||||
aligned_with_pending: true,
|
||||
});
|
||||
}
|
||||
|
||||
// The search for additional potential parents should now start at the children of
|
||||
// the pending block.
|
||||
(
|
||||
vec![PotentialParent {
|
||||
hash: *pending_hash,
|
||||
header: pending_header.clone(),
|
||||
depth: route_to_pending.enacted().len(),
|
||||
aligned_with_pending: true,
|
||||
}],
|
||||
potential_parents,
|
||||
)
|
||||
},
|
||||
_ => (only_included, Default::default()),
|
||||
};
|
||||
|
||||
if potential_parents.len() > params.max_depth {
|
||||
return Ok(potential_parents);
|
||||
}
|
||||
|
||||
// Build up the ancestry record of the relay chain to compare against.
|
||||
let rp_ancestry =
|
||||
build_relay_parent_ancestry(params.ancestry_lookback, params.relay_parent, relay_client)
|
||||
.await?;
|
||||
|
||||
Ok(search_child_branches_for_parents(
|
||||
frontier,
|
||||
maybe_route_to_last_pending,
|
||||
included_header,
|
||||
maybe_pending.map(|(_, hash)| hash),
|
||||
backend,
|
||||
params.max_depth,
|
||||
params.ignore_alternative_branches,
|
||||
rp_ancestry,
|
||||
potential_parents,
|
||||
))
|
||||
}
|
||||
|
||||
/// Fetch the included block from the relay chain.
|
||||
async fn fetch_included_from_relay_chain<B: BlockT>(
|
||||
relay_client: &impl RelayChainInterface,
|
||||
backend: &impl Backend<B>,
|
||||
para_id: ParaId,
|
||||
relay_parent: RelayHash,
|
||||
) -> Result<Option<(B::Header, B::Hash)>, RelayChainError> {
|
||||
// Fetch the pending header from the relay chain. We use `OccupiedCoreAssumption::TimedOut`
|
||||
// so that even if there is a pending candidate, we assume it is timed out and we get the
|
||||
// included head.
|
||||
let included_header = relay_client
|
||||
.persisted_validation_data(relay_parent, para_id, OccupiedCoreAssumption::TimedOut)
|
||||
.await?;
|
||||
let included_header = match included_header {
|
||||
Some(pvd) => pvd.parent_head,
|
||||
None => return Ok(None), // this implies the para doesn't exist.
|
||||
};
|
||||
|
||||
let included_header = match B::Header::decode(&mut &included_header.0[..]).ok() {
|
||||
None => return Ok(None),
|
||||
Some(x) => x,
|
||||
};
|
||||
|
||||
let included_hash = included_header.hash();
|
||||
// If the included block is not locally known, we can't do anything.
|
||||
match backend.blockchain().header(included_hash) {
|
||||
Ok(None) => {
|
||||
tracing::warn!(
|
||||
target: PARENT_SEARCH_LOG_TARGET,
|
||||
%included_hash,
|
||||
"Failed to get header for included block.",
|
||||
);
|
||||
return Ok(None);
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
target: PARENT_SEARCH_LOG_TARGET,
|
||||
%included_hash,
|
||||
%e,
|
||||
"Failed to get header for included block.",
|
||||
);
|
||||
return Ok(None);
|
||||
},
|
||||
_ => {},
|
||||
};
|
||||
|
||||
Ok(Some((included_header, included_hash)))
|
||||
}
|
||||
|
||||
/// Build an ancestry of relay parents that are acceptable.
|
||||
///
|
||||
/// An acceptable relay parent is one that is no more than `ancestry_lookback` + 1 blocks below the
|
||||
/// relay parent we want to build on. Teyrchain blocks anchored on relay parents older than that can
|
||||
/// not be considered potential parents for block building. They have no chance of still getting
|
||||
/// included, so our newly build teyrchain block would also not get included.
|
||||
///
|
||||
/// On success, returns a vector of `(header_hash, state_root)` of the relevant relay chain
|
||||
/// ancestry blocks.
|
||||
async fn build_relay_parent_ancestry(
|
||||
ancestry_lookback: usize,
|
||||
relay_parent: RelayHash,
|
||||
relay_client: &impl RelayChainInterface,
|
||||
) -> Result<Vec<(RelayHash, RelayHash)>, RelayChainError> {
|
||||
let mut ancestry = Vec::with_capacity(ancestry_lookback + 1);
|
||||
let mut current_rp = relay_parent;
|
||||
let mut required_session = None;
|
||||
while ancestry.len() <= ancestry_lookback {
|
||||
let Some(header) = relay_client.header(RBlockId::hash(current_rp)).await? else { break };
|
||||
|
||||
let session = relay_client.session_index_for_child(current_rp).await?;
|
||||
if required_session.get_or_insert(session) != &session {
|
||||
// Respect the relay-chain rule not to cross session boundaries.
|
||||
break;
|
||||
}
|
||||
|
||||
ancestry.push((current_rp, *header.state_root()));
|
||||
current_rp = *header.parent_hash();
|
||||
|
||||
// don't iterate back into the genesis block.
|
||||
if header.number == 1 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(ancestry)
|
||||
}
|
||||
|
||||
/// Start search for child blocks that can be used as parents.
|
||||
pub fn search_child_branches_for_parents<Block: BlockT>(
|
||||
mut frontier: Vec<PotentialParent<Block>>,
|
||||
maybe_route_to_last_pending: Option<TreeRoute<Block>>,
|
||||
included_header: Block::Header,
|
||||
pending_hash: Option<Block::Hash>,
|
||||
backend: &impl Backend<Block>,
|
||||
max_depth: usize,
|
||||
ignore_alternative_branches: bool,
|
||||
rp_ancestry: Vec<(RelayHash, RelayHash)>,
|
||||
mut potential_parents: Vec<PotentialParent<Block>>,
|
||||
) -> Vec<PotentialParent<Block>> {
|
||||
let included_hash = included_header.hash();
|
||||
let is_hash_in_ancestry = |hash| rp_ancestry.iter().any(|x| x.0 == hash);
|
||||
let is_root_in_ancestry = |root| rp_ancestry.iter().any(|x| x.1 == root);
|
||||
|
||||
// The distance between pending and included block. Is later used to check if a child
|
||||
// is aligned with pending when it is between pending and included block.
|
||||
let pending_distance = maybe_route_to_last_pending.as_ref().map(|route| route.enacted().len());
|
||||
|
||||
// If a block is on the path included -> pending, we consider it `aligned_with_pending`.
|
||||
let is_child_pending = |hash| {
|
||||
maybe_route_to_last_pending
|
||||
.as_ref()
|
||||
.map_or(true, |route| route.enacted().iter().any(|x| x.hash == hash))
|
||||
};
|
||||
|
||||
tracing::trace!(
|
||||
target: PARENT_SEARCH_LOG_TARGET,
|
||||
?included_hash,
|
||||
included_num = ?included_header.number(),
|
||||
?pending_hash ,
|
||||
?rp_ancestry,
|
||||
"Searching relay chain ancestry."
|
||||
);
|
||||
while let Some(entry) = frontier.pop() {
|
||||
let is_pending = pending_hash.as_ref().map_or(false, |h| &entry.hash == h);
|
||||
let is_included = included_hash == entry.hash;
|
||||
|
||||
// note: even if the pending block or included block have a relay parent
|
||||
// outside of the expected part of the relay chain, they are always allowed
|
||||
// because they have already been posted on chain.
|
||||
let is_potential = is_pending || is_included || {
|
||||
let digest = entry.header.digest();
|
||||
let is_hash_in_ancestry_check = cumulus_primitives_core::extract_relay_parent(digest)
|
||||
.map_or(false, is_hash_in_ancestry);
|
||||
let is_root_in_ancestry_check =
|
||||
cumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(digest)
|
||||
.map(|(r, _n)| r)
|
||||
.map_or(false, is_root_in_ancestry);
|
||||
|
||||
is_hash_in_ancestry_check || is_root_in_ancestry_check
|
||||
};
|
||||
|
||||
let parent_aligned_with_pending = entry.aligned_with_pending;
|
||||
let child_depth = entry.depth + 1;
|
||||
let hash = entry.hash;
|
||||
|
||||
tracing::trace!(
|
||||
target: PARENT_SEARCH_LOG_TARGET,
|
||||
?hash,
|
||||
is_potential,
|
||||
is_pending,
|
||||
is_included,
|
||||
"Checking potential parent."
|
||||
);
|
||||
|
||||
if is_potential {
|
||||
potential_parents.push(entry);
|
||||
}
|
||||
|
||||
if !is_potential || child_depth > max_depth {
|
||||
continue;
|
||||
}
|
||||
|
||||
// push children onto search frontier.
|
||||
for child in backend.blockchain().children(hash).ok().into_iter().flatten() {
|
||||
tracing::trace!(target: PARENT_SEARCH_LOG_TARGET, ?child, child_depth, ?pending_distance, "Looking at child.");
|
||||
|
||||
let aligned_with_pending = parent_aligned_with_pending &&
|
||||
(pending_distance.map_or(true, |dist| child_depth > dist) ||
|
||||
is_child_pending(child));
|
||||
|
||||
if ignore_alternative_branches && !aligned_with_pending {
|
||||
tracing::trace!(target: PARENT_SEARCH_LOG_TARGET, ?child, "Child is not aligned with pending block.");
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(Some(header)) = backend.blockchain().header(child) else { continue };
|
||||
|
||||
frontier.push(PotentialParent {
|
||||
hash: child,
|
||||
header,
|
||||
depth: child_depth,
|
||||
aligned_with_pending,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
potential_parents
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,544 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use cumulus_relay_chain_streams::{finalized_heads, new_best_heads};
|
||||
use pezsc_client_api::{
|
||||
Backend, BlockBackend, BlockImportNotification, BlockchainEvents, Finalizer, UsageProvider,
|
||||
};
|
||||
use pezsc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy};
|
||||
use schnellru::{ByLength, LruMap};
|
||||
use pezsp_blockchain::Error as ClientError;
|
||||
use pezsp_consensus::{BlockOrigin, BlockStatus};
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
|
||||
|
||||
use cumulus_client_pov_recovery::{RecoveryKind, RecoveryRequest};
|
||||
use cumulus_relay_chain_interface::RelayChainInterface;
|
||||
|
||||
use pezkuwi_primitives::Id as ParaId;
|
||||
|
||||
use codec::Decode;
|
||||
use futures::{
|
||||
channel::mpsc::{Sender, UnboundedSender},
|
||||
pin_mut, select, FutureExt, SinkExt, Stream, StreamExt,
|
||||
};
|
||||
use pezsp_core::traits::SpawnEssentialNamed;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
const LOG_TARGET: &str = "pezcumulus-consensus";
|
||||
const FINALIZATION_CACHE_SIZE: u32 = 40;
|
||||
|
||||
fn handle_new_finalized_head<P, Block, B>(
|
||||
teyrchain: &Arc<P>,
|
||||
header: Block::Header,
|
||||
last_seen_finalized_hashes: &mut LruMap<Block::Hash, ()>,
|
||||
) where
|
||||
Block: BlockT,
|
||||
B: Backend<Block>,
|
||||
P: Finalizer<Block, B> + UsageProvider<Block> + BlockchainEvents<Block>,
|
||||
{
|
||||
let hash = header.hash();
|
||||
|
||||
last_seen_finalized_hashes.insert(hash, ());
|
||||
|
||||
// Only finalize if we are below the incoming finalized teyrchain head
|
||||
if teyrchain.usage_info().chain.finalized_number < *header.number() {
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
block_hash = ?hash,
|
||||
"Attempting to finalize header.",
|
||||
);
|
||||
if let Err(e) = teyrchain.finalize_block(hash, None, true) {
|
||||
match e {
|
||||
ClientError::UnknownBlock(_) => tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
block_hash = ?hash,
|
||||
"Could not finalize block because it is unknown.",
|
||||
),
|
||||
_ => tracing::warn!(
|
||||
target: LOG_TARGET,
|
||||
error = ?e,
|
||||
block_hash = ?hash,
|
||||
"Failed to finalize block",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Streams finalized teyrchain heads from the relay chain.
|
||||
///
|
||||
/// This worker continuously monitors the relay chain for finalized blocks and extracts
|
||||
/// the corresponding teyrchain head data for the given `para_id`. The extracted head
|
||||
/// data is sent through the provided channel for consumption by the consensus system.
|
||||
///
|
||||
/// This is necessary because finalization of blocks can take a long
|
||||
/// time. During this blocking operation, we should not keep references to finality notifications,
|
||||
/// because that prevents the corresponding blocks from getting pruned.
|
||||
pub async fn finalized_head_stream_worker<R: RelayChainInterface + Clone, Block: BlockT>(
|
||||
mut tx: UnboundedSender<Block::Header>,
|
||||
para_id: ParaId,
|
||||
relay_chain: R,
|
||||
) {
|
||||
let finalized_heads = match finalized_heads(relay_chain.clone(), para_id).await {
|
||||
Ok(finalized_heads_stream) => finalized_heads_stream.fuse(),
|
||||
Err(err) => {
|
||||
tracing::error!(target: LOG_TARGET, error = ?err, "Unable to retrieve finalized heads stream.");
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
pin_mut!(finalized_heads);
|
||||
loop {
|
||||
if let Some((head_data, _)) = finalized_heads.next().await {
|
||||
let header = match Block::Header::decode(&mut &head_data[..]) {
|
||||
Ok(header) => header,
|
||||
Err(err) => {
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
error = ?err,
|
||||
"Could not decode teyrchain header while following finalized heads.",
|
||||
);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
if let Err(e) = tx.send(header).await {
|
||||
tracing::error!(target: LOG_TARGET, ?e, "Error while sending finalized head.");
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Follow the finalized head of the given teyrchain.
|
||||
///
|
||||
/// For every finalized block of the relay chain, it will get the included teyrchain header
|
||||
/// corresponding to `para_id` and will finalize it in the teyrchain.
|
||||
async fn follow_finalized_head<P, Block, B>(
|
||||
teyrchain: Arc<P>,
|
||||
finalized_head_stream: Box<impl Stream<Item = Block::Header> + Unpin + Send>,
|
||||
) where
|
||||
Block: BlockT,
|
||||
P: Finalizer<Block, B> + UsageProvider<Block> + BlockchainEvents<Block>,
|
||||
B: Backend<Block>,
|
||||
{
|
||||
let mut imported_blocks = teyrchain.import_notification_stream().fuse();
|
||||
let mut finalized_head_stream = finalized_head_stream.fuse();
|
||||
|
||||
// We use this cache to finalize blocks that are imported late.
|
||||
// For example, a block that has been recovered via PoV-Recovery
|
||||
// on a full node can have several minutes delay. With this cache
|
||||
// we have some "memory" of recently finalized blocks.
|
||||
let mut last_seen_finalized_hashes = LruMap::new(ByLength::new(FINALIZATION_CACHE_SIZE));
|
||||
|
||||
loop {
|
||||
select! {
|
||||
fin = finalized_head_stream.next() => {
|
||||
match fin {
|
||||
Some(finalized_head) =>
|
||||
handle_new_finalized_head(&teyrchain, finalized_head, &mut last_seen_finalized_hashes),
|
||||
None => {
|
||||
tracing::debug!(target: LOG_TARGET, "Stopping following finalized head.");
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
imported = imported_blocks.next() => {
|
||||
match imported {
|
||||
Some(imported_block) => {
|
||||
// When we see a block import that is already finalized, we immediately finalize it.
|
||||
if last_seen_finalized_hashes.peek(&imported_block.hash).is_some() {
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
block_hash = ?imported_block.hash,
|
||||
"Setting newly imported block as finalized.",
|
||||
);
|
||||
|
||||
if let Err(e) = teyrchain.finalize_block(imported_block.hash, None, true) {
|
||||
match e {
|
||||
ClientError::UnknownBlock(_) => tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
block_hash = ?imported_block.hash,
|
||||
"Could not finalize block because it is unknown.",
|
||||
),
|
||||
_ => tracing::warn!(
|
||||
target: LOG_TARGET,
|
||||
error = ?e,
|
||||
block_hash = ?imported_block.hash,
|
||||
"Failed to finalize block",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Stopping following imported blocks.",
|
||||
);
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns the essential finalization tasks for teyrchain consensus.
|
||||
///
|
||||
/// This function creates and spawns two critical background tasks:
|
||||
/// 1. A finalized head stream worker that monitors relay chain finality and extracts included
|
||||
/// headers
|
||||
/// 2. The main teyrchain consensus task that handles finalization and best block updates
|
||||
pub fn spawn_teyrchain_consensus_tasks<P, R, Block, B, S>(
|
||||
para_id: ParaId,
|
||||
teyrchain: Arc<P>,
|
||||
relay_chain: R,
|
||||
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
|
||||
recovery_chan_tx: Option<Sender<RecoveryRequest<Block>>>,
|
||||
spawn_handle: S,
|
||||
) where
|
||||
Block: BlockT,
|
||||
P: Finalizer<Block, B>
|
||||
+ UsageProvider<Block>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ BlockBackend<Block>
|
||||
+ BlockchainEvents<Block>
|
||||
+ 'static,
|
||||
for<'a> &'a P: BlockImport<Block>,
|
||||
R: RelayChainInterface + Clone + 'static,
|
||||
S: SpawnEssentialNamed + 'static,
|
||||
B: Backend<Block> + 'static,
|
||||
{
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
let worker = finalized_head_stream_worker::<_, Block>(tx, para_id, relay_chain.clone());
|
||||
let consensus = run_teyrchain_consensus(
|
||||
para_id,
|
||||
teyrchain,
|
||||
relay_chain,
|
||||
announce_block,
|
||||
Box::new(rx),
|
||||
recovery_chan_tx,
|
||||
);
|
||||
|
||||
spawn_handle.spawn_essential_blocking("pezcumulus-consensus", None, Box::pin(consensus));
|
||||
spawn_handle.spawn_essential_blocking(
|
||||
"pezcumulus-consensus-finality-stream",
|
||||
None,
|
||||
Box::pin(worker),
|
||||
);
|
||||
}
|
||||
|
||||
/// Run the teyrchain consensus.
|
||||
///
|
||||
/// This will follow the given `relay_chain` to act as consensus for the teyrchain that corresponds
|
||||
/// to the given `para_id`. It will set the new best block of the teyrchain as it gets aware of it.
|
||||
/// The same happens for the finalized block.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This will access the backend of the teyrchain and thus, this future should be spawned as
|
||||
/// blocking task.
|
||||
pub async fn run_teyrchain_consensus<P, R, Block, B>(
|
||||
para_id: ParaId,
|
||||
teyrchain: Arc<P>,
|
||||
relay_chain: R,
|
||||
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
|
||||
finalized_head_stream: Box<impl Stream<Item = Block::Header> + Unpin + Send>,
|
||||
recovery_chan_tx: Option<Sender<RecoveryRequest<Block>>>,
|
||||
) where
|
||||
Block: BlockT,
|
||||
P: Finalizer<Block, B>
|
||||
+ UsageProvider<Block>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ BlockBackend<Block>
|
||||
+ BlockchainEvents<Block>,
|
||||
for<'a> &'a P: BlockImport<Block>,
|
||||
R: RelayChainInterface + Clone,
|
||||
B: Backend<Block>,
|
||||
{
|
||||
let follow_new_best = follow_new_best(
|
||||
para_id,
|
||||
teyrchain.clone(),
|
||||
relay_chain.clone(),
|
||||
announce_block,
|
||||
recovery_chan_tx,
|
||||
);
|
||||
let follow_finalized_head = follow_finalized_head(teyrchain, finalized_head_stream);
|
||||
select! {
|
||||
_ = follow_new_best.fuse() => {},
|
||||
_ = follow_finalized_head.fuse() => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// Follow the relay chain new best head, to update the Teyrchain new best head.
|
||||
async fn follow_new_best<P, R, Block, B>(
|
||||
para_id: ParaId,
|
||||
teyrchain: Arc<P>,
|
||||
relay_chain: R,
|
||||
announce_block: Arc<dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync>,
|
||||
mut recovery_chan_tx: Option<Sender<RecoveryRequest<Block>>>,
|
||||
) where
|
||||
Block: BlockT,
|
||||
P: Finalizer<Block, B>
|
||||
+ UsageProvider<Block>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ BlockBackend<Block>
|
||||
+ BlockchainEvents<Block>,
|
||||
for<'a> &'a P: BlockImport<Block>,
|
||||
R: RelayChainInterface + Clone,
|
||||
B: Backend<Block>,
|
||||
{
|
||||
let new_best_heads = match new_best_heads(relay_chain, para_id).await {
|
||||
Ok(best_heads_stream) => best_heads_stream.fuse(),
|
||||
Err(err) => {
|
||||
tracing::error!(target: LOG_TARGET, error = ?err, "Unable to retrieve best heads stream.");
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
pin_mut!(new_best_heads);
|
||||
|
||||
let mut imported_blocks = teyrchain.import_notification_stream().fuse();
|
||||
// The unset best header of the teyrchain. Will be `Some(_)` when we have imported a relay chain
|
||||
// block before the associated teyrchain block. In this case we need to wait for this block to
|
||||
// be imported to set it as new best.
|
||||
let mut unset_best_header = None;
|
||||
|
||||
loop {
|
||||
select! {
|
||||
h = new_best_heads.next() => {
|
||||
match h {
|
||||
Some(h) => handle_new_best_teyrchain_head(
|
||||
h,
|
||||
&*teyrchain,
|
||||
&mut unset_best_header,
|
||||
recovery_chan_tx.as_mut(),
|
||||
).await,
|
||||
None => {
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Stopping following new best.",
|
||||
);
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
i = imported_blocks.next() => {
|
||||
match i {
|
||||
Some(i) => handle_new_block_imported(
|
||||
i,
|
||||
&mut unset_best_header,
|
||||
&*teyrchain,
|
||||
&*announce_block,
|
||||
).await,
|
||||
None => {
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Stopping following imported blocks.",
|
||||
);
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a new import block of the teyrchain.
|
||||
async fn handle_new_block_imported<Block, P>(
|
||||
notification: BlockImportNotification<Block>,
|
||||
unset_best_header_opt: &mut Option<Block::Header>,
|
||||
teyrchain: &P,
|
||||
announce_block: &(dyn Fn(Block::Hash, Option<Vec<u8>>) + Send + Sync),
|
||||
) where
|
||||
Block: BlockT,
|
||||
P: UsageProvider<Block> + Send + Sync + BlockBackend<Block>,
|
||||
for<'a> &'a P: BlockImport<Block>,
|
||||
{
|
||||
// HACK
|
||||
//
|
||||
// Remove after https://github.com/pezkuwichain/kurdistan-sdk/issues/76 or similar is merged
|
||||
if notification.origin != BlockOrigin::Own {
|
||||
announce_block(notification.hash, None);
|
||||
}
|
||||
|
||||
let unset_best_header = match (notification.is_new_best, &unset_best_header_opt) {
|
||||
// If this is the new best block or we don't have any unset block, we can end it here.
|
||||
(true, _) | (_, None) => return,
|
||||
(false, Some(ref u)) => u,
|
||||
};
|
||||
|
||||
let unset_hash = if notification.header.number() < unset_best_header.number() {
|
||||
return;
|
||||
} else if notification.header.number() == unset_best_header.number() {
|
||||
let unset_hash = unset_best_header.hash();
|
||||
|
||||
if unset_hash != notification.hash {
|
||||
return;
|
||||
} else {
|
||||
unset_hash
|
||||
}
|
||||
} else {
|
||||
unset_best_header.hash()
|
||||
};
|
||||
|
||||
match teyrchain.block_status(unset_hash) {
|
||||
Ok(BlockStatus::InChainWithState) => {
|
||||
let unset_best_header = unset_best_header_opt
|
||||
.take()
|
||||
.expect("We checked above that the value is set; qed");
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
?unset_hash,
|
||||
"Importing block as new best for teyrchain.",
|
||||
);
|
||||
import_block_as_new_best(unset_hash, unset_best_header, teyrchain).await;
|
||||
},
|
||||
state => tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
?unset_best_header,
|
||||
?notification.header,
|
||||
?state,
|
||||
"Unexpected state for unset best header.",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle the new best teyrchain head as extracted from the new best relay chain.
|
||||
async fn handle_new_best_teyrchain_head<Block, P>(
|
||||
head: Vec<u8>,
|
||||
teyrchain: &P,
|
||||
unset_best_header: &mut Option<Block::Header>,
|
||||
mut recovery_chan_tx: Option<&mut Sender<RecoveryRequest<Block>>>,
|
||||
) where
|
||||
Block: BlockT,
|
||||
P: UsageProvider<Block> + Send + Sync + BlockBackend<Block>,
|
||||
for<'a> &'a P: BlockImport<Block>,
|
||||
{
|
||||
let teyrchain_head = match <<Block as BlockT>::Header>::decode(&mut &head[..]) {
|
||||
Ok(header) => header,
|
||||
Err(err) => {
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
error = ?err,
|
||||
"Could not decode Teyrchain header while following best heads.",
|
||||
);
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
let hash = teyrchain_head.hash();
|
||||
|
||||
if teyrchain.usage_info().chain.best_hash == hash {
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
block_hash = ?hash,
|
||||
"Skipping set new best block, because block is already the best.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the block is already known or otherwise we skip setting new best.
|
||||
match teyrchain.block_status(hash) {
|
||||
Ok(BlockStatus::InChainWithState) => {
|
||||
unset_best_header.take();
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
included = ?hash,
|
||||
"Importing block as new best for teyrchain.",
|
||||
);
|
||||
import_block_as_new_best(hash, teyrchain_head, teyrchain).await;
|
||||
},
|
||||
Ok(BlockStatus::InChainPruned) => {
|
||||
tracing::error!(
|
||||
target: LOG_TARGET,
|
||||
block_hash = ?hash,
|
||||
"Trying to set pruned block as new best!",
|
||||
);
|
||||
},
|
||||
Ok(BlockStatus::Unknown) => {
|
||||
*unset_best_header = Some(teyrchain_head);
|
||||
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
block_hash = ?hash,
|
||||
"Teyrchain block not yet imported, waiting for import to enact as best block.",
|
||||
);
|
||||
|
||||
if let Some(ref mut recovery_chan_tx) = recovery_chan_tx {
|
||||
// Best effort channel to actively encourage block recovery.
|
||||
// An error here is not fatal; the relay chain continuously re-announces
|
||||
// the best block, thus we will have other opportunities to retry.
|
||||
let req = RecoveryRequest { hash, kind: RecoveryKind::Full };
|
||||
if let Err(err) = recovery_chan_tx.try_send(req) {
|
||||
tracing::warn!(
|
||||
target: LOG_TARGET,
|
||||
block_hash = ?hash,
|
||||
error = ?err,
|
||||
"Unable to notify block recovery subsystem"
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
target: LOG_TARGET,
|
||||
block_hash = ?hash,
|
||||
error = ?e,
|
||||
"Failed to get block status of block.",
|
||||
);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
async fn import_block_as_new_best<Block, P>(hash: Block::Hash, header: Block::Header, teyrchain: &P)
|
||||
where
|
||||
Block: BlockT,
|
||||
P: UsageProvider<Block> + Send + Sync + BlockBackend<Block>,
|
||||
for<'a> &'a P: BlockImport<Block>,
|
||||
{
|
||||
let best_number = teyrchain.usage_info().chain.best_number;
|
||||
if *header.number() < best_number {
|
||||
tracing::debug!(
|
||||
target: LOG_TARGET,
|
||||
%best_number,
|
||||
block_number = %header.number(),
|
||||
"Skipping importing block as new best block, because there already exists a \
|
||||
best block with an higher number",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make it the new best block
|
||||
let mut block_import_params = BlockImportParams::new(BlockOrigin::ConsensusBroadcast, header);
|
||||
block_import_params.fork_choice = Some(ForkChoiceStrategy::Custom(true));
|
||||
block_import_params.import_existing = true;
|
||||
|
||||
if let Err(err) = teyrchain.import_block(block_import_params).await {
|
||||
tracing::warn!(
|
||||
target: LOG_TARGET,
|
||||
block_hash = ?hash,
|
||||
error = ?err,
|
||||
"Failed to set new best block.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "pezcumulus-client-consensus-proposer"
|
||||
description = "A Bizinikiwi `Proposer` for building teyrchain blocks"
|
||||
version = "0.7.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true, default-features = true }
|
||||
async-trait = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
# Bizinikiwi
|
||||
pezsc-basic-authorship = { workspace = true }
|
||||
pezsc-block-builder = { workspace = true }
|
||||
pezsc-transaction-pool-api = { workspace = true }
|
||||
pezsp-api = { workspace = true, default-features = true }
|
||||
pezsp-blockchain = { workspace = true, default-features = true }
|
||||
pezsp-consensus = { workspace = true, default-features = true }
|
||||
pezsp-inherents = { workspace = true, default-features = true }
|
||||
pezsp-runtime = { workspace = true, default-features = true }
|
||||
pezsp-state-machine = { workspace = true, default-features = true }
|
||||
|
||||
# Pezcumulus
|
||||
pezcumulus-primitives-teyrchain-inherent = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezcumulus-primitives-teyrchain-inherent/runtime-benchmarks",
|
||||
"pezsc-basic-authorship/runtime-benchmarks",
|
||||
"pezsc-block-builder/runtime-benchmarks",
|
||||
"pezsc-transaction-pool-api/runtime-benchmarks",
|
||||
"pezsp-api/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus/runtime-benchmarks",
|
||||
"pezsp-inherents/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"pezsp-state-machine/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! The Pezcumulus [`ProposerInterface`] is an extension of the Bizinikiwi [`ProposerFactory`]
|
||||
//! for creating new teyrchain blocks.
|
||||
//!
|
||||
//! This utility is designed to be composed within any collator consensus algorithm.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use cumulus_primitives_teyrchain_inherent::TeyrchainInherentData;
|
||||
use pezsc_basic_authorship::{ProposeArgs, ProposerFactory};
|
||||
use pezsc_block_builder::BlockBuilderApi;
|
||||
use pezsc_transaction_pool_api::TransactionPool;
|
||||
use pezsp_api::{ApiExt, CallApiAt, ProvideRuntimeApi};
|
||||
use pezsp_blockchain::HeaderBackend;
|
||||
use pezsp_consensus::{EnableProofRecording, Environment, Proposal};
|
||||
use pezsp_inherents::{InherentData, InherentDataProvider};
|
||||
use pezsp_runtime::{traits::Block as BlockT, Digest};
|
||||
use pezsp_state_machine::StorageProof;
|
||||
use std::{fmt::Debug, time::Duration};
|
||||
|
||||
/// Errors that can occur when proposing a teyrchain block.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error(transparent)]
|
||||
pub struct Error {
|
||||
inner: anyhow::Error,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Create an error tied to the creation of a proposer.
|
||||
pub fn proposer_creation(err: impl Into<anyhow::Error>) -> Self {
|
||||
Error { inner: err.into().context("Proposer Creation") }
|
||||
}
|
||||
|
||||
/// Create an error tied to the proposing logic itself.
|
||||
pub fn proposing(err: impl Into<anyhow::Error>) -> Self {
|
||||
Error { inner: err.into().context("Proposing") }
|
||||
}
|
||||
}
|
||||
|
||||
/// A type alias for easily referring to the type of a proposal produced by a specific
|
||||
/// [`ProposerInterface`].
|
||||
pub type ProposalOf<B> = Proposal<B, StorageProof>;
|
||||
|
||||
/// An interface for proposers.
|
||||
#[async_trait]
|
||||
pub trait ProposerInterface<Block: BlockT> {
|
||||
/// Propose a collation using the supplied `InherentData` and the provided
|
||||
/// `TeyrchainInherentData`.
|
||||
///
|
||||
/// Also specify any required inherent digests, the maximum proposal duration,
|
||||
/// and the block size limit in bytes. See the documentation on
|
||||
/// [`pezsp_consensus::Proposer::propose`] for more details on how to interpret these parameters.
|
||||
///
|
||||
/// The `InherentData` and `Digest` are left deliberately general in order to accommodate
|
||||
/// all possible collator selection algorithms or inherent creation mechanisms,
|
||||
/// while the `TeyrchainInherentData` is made explicit so it will be constructed appropriately.
|
||||
///
|
||||
/// If the `InherentData` passed into this function already has a `TeyrchainInherentData`,
|
||||
/// this should throw an error.
|
||||
async fn propose(
|
||||
&mut self,
|
||||
parent_header: &Block::Header,
|
||||
paras_inherent_data: &TeyrchainInherentData,
|
||||
other_inherent_data: InherentData,
|
||||
inherent_digests: Digest,
|
||||
max_duration: Duration,
|
||||
block_size_limit: Option<usize>,
|
||||
) -> Result<Option<Proposal<Block, StorageProof>>, Error>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<Block, A, C> ProposerInterface<Block> for ProposerFactory<A, C, EnableProofRecording>
|
||||
where
|
||||
A: TransactionPool<Block = Block> + 'static,
|
||||
C: HeaderBackend<Block> + ProvideRuntimeApi<Block> + CallApiAt<Block> + Send + Sync + 'static,
|
||||
C::Api: ApiExt<Block> + BlockBuilderApi<Block>,
|
||||
Block: pezsp_runtime::traits::Block,
|
||||
{
|
||||
async fn propose(
|
||||
&mut self,
|
||||
parent_header: &Block::Header,
|
||||
paras_inherent_data: &TeyrchainInherentData,
|
||||
other_inherent_data: InherentData,
|
||||
inherent_digests: Digest,
|
||||
max_duration: Duration,
|
||||
block_size_limit: Option<usize>,
|
||||
) -> Result<Option<Proposal<Block, StorageProof>>, Error> {
|
||||
let proposer = self
|
||||
.init(parent_header)
|
||||
.await
|
||||
.map_err(|e| Error::proposer_creation(anyhow::Error::new(e)))?;
|
||||
|
||||
let mut inherent_data = other_inherent_data;
|
||||
paras_inherent_data
|
||||
.provide_inherent_data(&mut inherent_data)
|
||||
.await
|
||||
.map_err(|e| Error::proposing(anyhow::Error::new(e)))?;
|
||||
|
||||
proposer
|
||||
.propose_block(ProposeArgs {
|
||||
inherent_data,
|
||||
inherent_digests,
|
||||
max_duration,
|
||||
block_size_limit,
|
||||
ignored_nodes_by_proof_recording: None,
|
||||
})
|
||||
.await
|
||||
.map(Some)
|
||||
.map_err(|e| Error::proposing(anyhow::Error::new(e)).into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
[package]
|
||||
name = "pezcumulus-client-consensus-relay-chain"
|
||||
description = "The relay-chain provided consensus algorithm"
|
||||
version = "0.7.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
tracing = { workspace = true, default-features = true }
|
||||
|
||||
# Bizinikiwi
|
||||
prometheus-endpoint = { workspace = true, default-features = true }
|
||||
pezsc-consensus = { workspace = true, default-features = true }
|
||||
pezsp-api = { workspace = true, default-features = true }
|
||||
pezsp-block-builder = { workspace = true, default-features = true }
|
||||
pezsp-blockchain = { workspace = true, default-features = true }
|
||||
pezsp-consensus = { workspace = true, default-features = true }
|
||||
pezsp-core = { workspace = true, default-features = true }
|
||||
pezsp-inherents = { workspace = true, default-features = true }
|
||||
pezsp-runtime = { workspace = true, default-features = true }
|
||||
|
||||
# Pezcumulus
|
||||
pezcumulus-client-consensus-common = { workspace = true, default-features = true }
|
||||
pezcumulus-primitives-core = { workspace = true, default-features = true }
|
||||
pezcumulus-relay-chain-interface = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezcumulus-client-consensus-common/runtime-benchmarks",
|
||||
"pezcumulus-primitives-core/runtime-benchmarks",
|
||||
"pezcumulus-relay-chain-interface/runtime-benchmarks",
|
||||
"pezsc-consensus/runtime-benchmarks",
|
||||
"pezsp-api/runtime-benchmarks",
|
||||
"pezsp-block-builder/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus/runtime-benchmarks",
|
||||
"pezsp-inherents/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
|
||||
use pezsc_consensus::{import_queue::Verifier as VerifierT, BlockImportParams};
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_block_builder::BlockBuilder as BlockBuilderApi;
|
||||
use pezsp_inherents::CreateInherentDataProviders;
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
|
||||
|
||||
/// A verifier that just checks the inherents.
|
||||
pub struct Verifier<Client, Block, CIDP> {
|
||||
client: Arc<Client>,
|
||||
create_inherent_data_providers: CIDP,
|
||||
_marker: PhantomData<Block>,
|
||||
}
|
||||
|
||||
impl<Client, Block, CIDP> Verifier<Client, Block, CIDP> {
|
||||
/// Create a new instance.
|
||||
pub fn new(client: Arc<Client>, create_inherent_data_providers: CIDP) -> Self {
|
||||
Self { client, create_inherent_data_providers, _marker: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<Client, Block, CIDP> VerifierT<Block> for Verifier<Client, Block, CIDP>
|
||||
where
|
||||
Block: BlockT,
|
||||
Client: ProvideRuntimeApi<Block> + Send + Sync,
|
||||
<Client as ProvideRuntimeApi<Block>>::Api: BlockBuilderApi<Block>,
|
||||
CIDP: CreateInherentDataProviders<Block, ()>,
|
||||
{
|
||||
async fn verify(
|
||||
&self,
|
||||
mut block_params: BlockImportParams<Block>,
|
||||
) -> Result<BlockImportParams<Block>, String> {
|
||||
block_params.fork_choice = Some(pezsc_consensus::ForkChoiceStrategy::Custom(
|
||||
block_params.origin == pezsp_consensus::BlockOrigin::NetworkInitialSync,
|
||||
));
|
||||
|
||||
// Skip checks that include execution, if being told so, or when importing only state.
|
||||
//
|
||||
// This is done for example when gap syncing and it is expected that the block after the gap
|
||||
// was checked/chosen properly, e.g. by warp syncing to this block using a finality proof.
|
||||
if block_params.state_action.skip_execution_checks() || block_params.with_state() {
|
||||
return Ok(block_params);
|
||||
}
|
||||
|
||||
if let Some(inner_body) = block_params.body.take() {
|
||||
let inherent_data_providers = self
|
||||
.create_inherent_data_providers
|
||||
.create_inherent_data_providers(*block_params.header.parent_hash(), ())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let block = Block::new(block_params.header.clone(), inner_body);
|
||||
pezsp_block_builder::check_inherents(
|
||||
self.client.clone(),
|
||||
*block.header().parent_hash(),
|
||||
block.clone(),
|
||||
&inherent_data_providers,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Error checking block inherents {:?}", e))?;
|
||||
|
||||
let (_, inner_body) = block.deconstruct();
|
||||
block_params.body = Some(inner_body);
|
||||
}
|
||||
|
||||
block_params.post_hash = Some(block_params.header.hash());
|
||||
Ok(block_params)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! The relay-chain provided consensus algorithm for teyrchains.
|
||||
//!
|
||||
//! This is the simplest consensus algorithm you can use when developing a teyrchain. It is a
|
||||
//! permission-less consensus algorithm that doesn't require any staking or similar to join as a
|
||||
//! collator. In this algorithm the consensus is provided by the relay-chain. This works in the
|
||||
//! following way.
|
||||
//!
|
||||
//! 1. Each node that sees itself as a collator is free to build a teyrchain candidate.
|
||||
//!
|
||||
//! 2. This teyrchain candidate is send to the teyrchain validators that are part of the relay
|
||||
//! chain.
|
||||
//!
|
||||
//! 3. The teyrchain validators validate at most X different teyrchain candidates, where X is the
|
||||
//! total number of teyrchain validators.
|
||||
//!
|
||||
//! 4. The teyrchain candidate that is backed by the most validators is chosen by the relay-chain
|
||||
//! block producer to be added as backed candidate on chain.
|
||||
//!
|
||||
//! 5. After the teyrchain candidate got backed and included, all collators start at 1.
|
||||
|
||||
mod import_queue;
|
||||
pub use import_queue::Verifier;
|
||||
Reference in New Issue
Block a user