Parachain Consensus abstractions (#329)

* Move consensus to consensus-common crate

* Move the parachain consensus out of the collator

* Add first relay chain consensus stuff

* Remove some warnings

* Fix more stuff

* Fix collator test

* Change `ParachainConsensus` to take a mutable self

* Make everything compile

* Feedback
This commit is contained in:
Bastian Köcher
2021-02-16 12:45:30 +01:00
committed by GitHub
parent ec08d11d39
commit 4820fa16b1
15 changed files with 707 additions and 551 deletions
@@ -1,6 +1,6 @@
[package]
name = "cumulus-client-consensus"
description = "Proxy Polkadot's consensus as a consensus engine for Substrate"
name = "cumulus-client-consensus-common"
description = "Cumulus specific common consensus implementations"
version = "0.1.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
@@ -15,6 +15,7 @@ sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master
sp-block-builder = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-blockchain = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-api = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-trie = { git = "https://github.com/paritytech/substrate", branch = "master" }
substrate-prometheus-endpoint = { git = "https://github.com/paritytech/substrate", branch = "master" }
# Polkadot deps
@@ -26,14 +27,16 @@ futures = { version = "0.3.8", features = ["compat"] }
tokio = "0.1.22"
codec = { package = "parity-scale-codec", version = "2.0.0", features = [ "derive" ] }
tracing = "0.1.22"
async-trait = "0.1.42"
dyn-clone = "1.0.4"
[dev-dependencies]
# Substrate deps
sp-tracing = { git = "https://github.com/paritytech/substrate", branch = "master" }
# Cumulus dependencies
cumulus-test-runtime = { path = "../../test/runtime" }
cumulus-test-client = { path = "../../test/client" }
cumulus-test-runtime = { path = "../../../test/runtime" }
cumulus-test-client = { path = "../../../test/client" }
# Other deps
futures-timer = "3.0.2"
@@ -29,7 +29,8 @@ use sp_runtime::{
};
use polkadot_primitives::v1::{
Block as PBlock, Id as ParaId, OccupiedCoreAssumption, ParachainHost,
Block as PBlock, Hash as PHash, Id as ParaId, OccupiedCoreAssumption, ParachainHost,
PersistedValidationData,
};
use codec::Decode;
@@ -37,8 +38,6 @@ use futures::{future, select, FutureExt, Stream, StreamExt};
use std::{marker::PhantomData, sync::Arc};
pub mod import_queue;
/// Errors that can occur while following the polkadot relay-chain.
#[derive(Debug)]
pub enum Error {
@@ -511,6 +510,52 @@ where
}
}
/// The result of [`ParachainConsensus::produce_candidate`].
pub struct ParachainCandidate<B> {
/// The block that was build for this candidate.
pub block: B,
/// The proof that was recorded while building the block.
pub proof: sp_trie::StorageProof,
}
/// A specific parachain consensus implementation that can be used by a collator to produce candidates.
///
/// The collator will call [`Self::produce_candidate`] every time there is a free core for the parachain
/// this collator is collating for. It is the job of the consensus implementation to decide if this
/// specific collator should build candidate for the given relay chain block. The consensus
/// implementation could for example check if this specific collator is part of the validator.
#[async_trait::async_trait]
pub trait ParachainConsensus<B: BlockT>: Send + Sync + dyn_clone::DynClone {
/// Produce a new candidate at the given parent block.
///
/// Should return `None` if the consensus implementation decided that it shouldn't build a
/// candidate or if there occurred any error.
///
/// # NOTE
///
/// It is expected that the block is already imported when the future resolves.
async fn produce_candidate(
&mut self,
parent: &B::Header,
relay_parent: PHash,
validation_data: &PersistedValidationData,
) -> Option<ParachainCandidate<B>>;
}
dyn_clone::clone_trait_object!(<B> ParachainConsensus<B> where B: BlockT);
#[async_trait::async_trait]
impl<B: BlockT> ParachainConsensus<B> for Box<dyn ParachainConsensus<B> + Send + Sync> {
async fn produce_candidate(
&mut self,
parent: &B::Header,
relay_parent: PHash,
validation_data: &PersistedValidationData,
) -> Option<ParachainCandidate<B>> {
(*self).produce_candidate(parent, relay_parent, validation_data).await
}
}
#[cfg(test)]
mod tests {
use super::*;
+33
View File
@@ -0,0 +1,33 @@
[package]
name = "cumulus-client-consensus-relay-chain"
description = "The relay-chain provided consensus algorithm"
version = "0.1.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
[dependencies]
# Substrate deps
sp-consensus = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-inherents = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-blockchain = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-block-builder = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-api = { git = "https://github.com/paritytech/substrate", branch = "master" }
sc-client-api = { git = "https://github.com/paritytech/substrate", branch = "master" }
substrate-prometheus-endpoint = { git = "https://github.com/paritytech/substrate", branch = "master" }
# Polkadot dependencies
polkadot-service = { git = "https://github.com/paritytech/polkadot", branch = "master", features = [ "real-overseer" ] }
# Cumulus dependencies
cumulus-client-consensus-common = { path = "../common" }
cumulus-primitives-core = { path = "../../../primitives/core" }
cumulus-primitives-parachain-inherent = { path = "../../../primitives/parachain-inherent" }
# Other deps
futures = { version = "0.3.8", features = ["compat"] }
codec = { package = "parity-scale-codec", version = "2.0.0", features = [ "derive" ] }
tracing = "0.1.22"
async-trait = "0.1.42"
parking_lot = "0.9"
+364
View File
@@ -0,0 +1,364 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Cumulus.
// Cumulus 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.
// Cumulus 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 Cumulus. If not, see <http://www.gnu.org/licenses/>.
//! The relay-chain provided consensus algoritm for parachains.
//!
//! This is the simplest consensus algorithm you can use when developing a parachain. 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 parachain candidate.
//!
//! 2. This parachain candidate is send to the parachain validators that are part of the relay chain.
//!
//! 3. The parachain validators validate at most X different parachain candidates, where X is the
//! total number of parachain validators.
//!
//! 4. The parachain candidate that is backed by the most validators is choosen by the relay-chain
//! block producer to be added as backed candidate on chain.
//!
//! 5. After the parachain candidate got backed and included, all collators start at 1.
use cumulus_client_consensus_common::{ParachainCandidate, ParachainConsensus};
use cumulus_primitives_core::{
relay_chain::v1::{Block as PBlock, Hash as PHash, ParachainHost},
ParaId, PersistedValidationData,
};
use cumulus_primitives_parachain_inherent::ParachainInherentData;
pub use import_queue::import_queue;
use parking_lot::Mutex;
use polkadot_service::ClientHandle;
use sc_client_api::Backend;
use sp_api::ProvideRuntimeApi;
use sp_consensus::{
BlockImport, BlockImportParams, BlockOrigin, Environment, ForkChoiceStrategy, Proposal,
Proposer, RecordProof,
};
use sp_inherents::{InherentData, InherentDataProviders};
use sp_runtime::traits::{Block as BlockT, HashFor, Header as HeaderT};
use std::{marker::PhantomData, sync::Arc, time::Duration};
mod import_queue;
const LOG_TARGET: &str = "cumulus-consensus-relay-chain";
/// The implementation of the relay-chain provided consensus for parachains.
pub struct RelayChainConsensus<B, PF, BI, RClient, RBackend> {
para_id: ParaId,
_phantom: PhantomData<B>,
proposer_factory: Arc<Mutex<PF>>,
inherent_data_providers: InherentDataProviders,
block_import: Arc<Mutex<BI>>,
relay_chain_client: Arc<RClient>,
relay_chain_backend: Arc<RBackend>,
}
impl<B, PF, BI, RClient, RBackend> Clone for RelayChainConsensus<B, PF, BI, RClient, RBackend> {
fn clone(&self) -> Self {
Self {
para_id: self.para_id,
_phantom: PhantomData,
proposer_factory: self.proposer_factory.clone(),
inherent_data_providers: self.inherent_data_providers.clone(),
block_import: self.block_import.clone(),
relay_chain_backend: self.relay_chain_backend.clone(),
relay_chain_client: self.relay_chain_client.clone(),
}
}
}
impl<B, PF, BI, RClient, RBackend> RelayChainConsensus<B, PF, BI, RClient, RBackend>
where
B: BlockT,
RClient: ProvideRuntimeApi<PBlock>,
RClient::Api: ParachainHost<PBlock>,
RBackend: Backend<PBlock>,
{
/// Create a new instance of relay-chain provided consensus.
pub fn new(
para_id: ParaId,
proposer_factory: PF,
inherent_data_providers: InherentDataProviders,
block_import: BI,
polkadot_client: Arc<RClient>,
polkadot_backend: Arc<RBackend>,
) -> Self {
Self {
para_id,
proposer_factory: Arc::new(Mutex::new(proposer_factory)),
inherent_data_providers,
block_import: Arc::new(Mutex::new(block_import)),
relay_chain_backend: polkadot_backend,
relay_chain_client: polkadot_client,
_phantom: PhantomData,
}
}
/// Get the inherent data with validation function parameters injected
fn inherent_data(
&self,
validation_data: &PersistedValidationData,
relay_parent: PHash,
) -> Option<InherentData> {
let mut inherent_data = self
.inherent_data_providers
.create_inherent_data()
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Failed to create inherent data.",
)
})
.ok()?;
let parachain_inherent_data = ParachainInherentData::create_at(
relay_parent,
&*self.relay_chain_client,
&*self.relay_chain_backend,
validation_data,
self.para_id,
)?;
inherent_data
.put_data(
cumulus_primitives_parachain_inherent::INHERENT_IDENTIFIER,
&parachain_inherent_data,
)
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error = ?e,
"Failed to put the system inherent into inherent data.",
)
})
.ok()?;
Some(inherent_data)
}
}
#[async_trait::async_trait]
impl<B, PF, BI, RClient, RBackend> ParachainConsensus<B>
for RelayChainConsensus<B, PF, BI, RClient, RBackend>
where
B: BlockT,
RClient: ProvideRuntimeApi<PBlock> + Send + Sync,
RClient::Api: ParachainHost<PBlock>,
RBackend: Backend<PBlock>,
BI: BlockImport<B> + Send + Sync,
PF: Environment<B> + Send + Sync,
PF::Proposer: Proposer<B, Transaction = BI::Transaction>,
{
async fn produce_candidate(
&mut self,
parent: &B::Header,
relay_parent: PHash,
validation_data: &PersistedValidationData,
) -> Option<ParachainCandidate<B>> {
let proposer_future = self.proposer_factory.lock().init(&parent);
let proposer = proposer_future
.await
.map_err(
|e| tracing::error!(target: LOG_TARGET, error = ?e, "Could not create proposer."),
)
.ok()?;
let inherent_data = self.inherent_data(&validation_data, relay_parent)?;
let Proposal {
block,
storage_changes,
proof,
} = proposer
.propose(
inherent_data,
Default::default(),
//TODO: Fix this.
Duration::from_millis(500),
RecordProof::Yes,
)
.await
.map_err(|e| tracing::error!(target: LOG_TARGET, error = ?e, "Proposing failed."))
.ok()?;
let proof = match proof {
Some(proof) => proof,
None => {
tracing::error!(
target: LOG_TARGET,
"Proposer did not return the requested proof.",
);
return None;
}
};
let (header, extrinsics) = block.clone().deconstruct();
let mut block_import_params = BlockImportParams::new(BlockOrigin::Own, header);
block_import_params.body = Some(extrinsics);
// Best block is determined by the relay chain.
block_import_params.fork_choice = Some(ForkChoiceStrategy::Custom(false));
block_import_params.storage_changes = Some(storage_changes);
if let Err(err) = self
.block_import
.lock()
.import_block(block_import_params, Default::default())
{
tracing::error!(
target: LOG_TARGET,
at = ?parent.hash(),
error = ?err,
"Error importing build block.",
);
return None;
}
Some(ParachainCandidate { block, proof })
}
}
/// Paramaters of [`build_relay_chain_consensus`].
pub struct BuildRelayChainConsensusParams<PF, BI, RBackend> {
pub para_id: ParaId,
pub proposer_factory: PF,
pub inherent_data_providers: InherentDataProviders,
pub block_import: BI,
pub relay_chain_client: polkadot_service::Client,
pub relay_chain_backend: Arc<RBackend>,
}
/// Build the [`RelayChainConsensus`].
///
/// Returns a boxed [`ParachainConsensus`].
pub fn build_relay_chain_consensus<Block, PF, BI, RBackend>(
BuildRelayChainConsensusParams {
para_id,
proposer_factory,
inherent_data_providers,
block_import,
relay_chain_client,
relay_chain_backend,
}: BuildRelayChainConsensusParams<PF, BI, RBackend>,
) -> Box<dyn ParachainConsensus<Block>>
where
Block: BlockT,
PF: Environment<Block> + Send + Sync + 'static,
PF::Proposer: Proposer<Block, Transaction = BI::Transaction>,
BI: BlockImport<Block> + Send + Sync + 'static,
RBackend: Backend<PBlock> + 'static,
// Rust bug: https://github.com/rust-lang/rust/issues/24159
sc_client_api::StateBackendFor<RBackend, PBlock>: sc_client_api::StateBackend<HashFor<PBlock>>,
{
RelayChainConsensusBuilder::new(
para_id,
proposer_factory,
block_import,
inherent_data_providers,
relay_chain_client,
relay_chain_backend,
)
.build()
}
/// Relay chain consensus builder.
///
/// Builds a [`RelayChainConsensus`] for a parachain. As this requires
/// a concrete relay chain client instance, the builder takes a [`polkadot_service::Client`]
/// that wraps this concrete instanace. By using [`polkadot_service::ExecuteWithClient`]
/// the builder gets access to this concrete instance.
struct RelayChainConsensusBuilder<Block, PF, BI, RBackend> {
para_id: ParaId,
_phantom: PhantomData<Block>,
proposer_factory: PF,
inherent_data_providers: InherentDataProviders,
block_import: BI,
relay_chain_backend: Arc<RBackend>,
relay_chain_client: polkadot_service::Client,
}
impl<Block, PF, BI, RBackend> RelayChainConsensusBuilder<Block, PF, BI, RBackend>
where
Block: BlockT,
// Rust bug: https://github.com/rust-lang/rust/issues/24159
sc_client_api::StateBackendFor<RBackend, PBlock>: sc_client_api::StateBackend<HashFor<PBlock>>,
PF: Environment<Block> + Send + Sync + 'static,
PF::Proposer: Proposer<Block, Transaction = BI::Transaction>,
BI: BlockImport<Block> + Send + Sync + 'static,
RBackend: Backend<PBlock> + 'static,
{
/// Create a new instance of the builder.
fn new(
para_id: ParaId,
proposer_factory: PF,
block_import: BI,
inherent_data_providers: InherentDataProviders,
relay_chain_client: polkadot_service::Client,
relay_chain_backend: Arc<RBackend>,
) -> Self {
Self {
para_id,
_phantom: PhantomData,
proposer_factory,
block_import,
inherent_data_providers,
relay_chain_backend,
relay_chain_client,
}
}
/// Build the relay chain consensus.
fn build(self) -> Box<dyn ParachainConsensus<Block>> {
self.relay_chain_client.clone().execute_with(self)
}
}
impl<Block, PF, BI, RBackend> polkadot_service::ExecuteWithClient
for RelayChainConsensusBuilder<Block, PF, BI, RBackend>
where
Block: BlockT,
// Rust bug: https://github.com/rust-lang/rust/issues/24159
sc_client_api::StateBackendFor<RBackend, PBlock>: sc_client_api::StateBackend<HashFor<PBlock>>,
PF: Environment<Block> + Send + Sync + 'static,
PF::Proposer: Proposer<Block, Transaction = BI::Transaction>,
BI: BlockImport<Block> + Send + Sync + 'static,
RBackend: Backend<PBlock> + 'static,
{
type Output = Box<dyn ParachainConsensus<Block>>;
fn execute_with_client<PClient, Api, PBackend>(self, client: Arc<PClient>) -> Self::Output
where
<Api as sp_api::ApiExt<PBlock>>::StateBackend: sp_api::StateBackend<HashFor<PBlock>>,
PBackend: Backend<PBlock>,
PBackend::State: sp_api::StateBackend<sp_runtime::traits::BlakeTwo256>,
Api: polkadot_service::RuntimeApiCollection<StateBackend = PBackend::State>,
PClient: polkadot_service::AbstractClient<PBlock, PBackend, Api = Api> + 'static,
{
Box::new(RelayChainConsensus::new(
self.para_id,
self.proposer_factory,
self.inherent_data_providers,
self.block_import,
client.clone(),
self.relay_chain_backend,
))
}
}