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,74 @@
|
||||
[package]
|
||||
name = "pezsc-consensus-aura"
|
||||
version = "0.34.0"
|
||||
authors.workspace = true
|
||||
description = "Aura consensus algorithm for bizinikiwi"
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
codec = { workspace = true, default-features = true }
|
||||
fork-tree = { workspace = true, default-features = true }
|
||||
futures = { workspace = true }
|
||||
log = { workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
prometheus-endpoint = { workspace = true, default-features = true }
|
||||
pezsc-block-builder = { workspace = true, default-features = true }
|
||||
pezsc-client-api = { workspace = true, default-features = true }
|
||||
pezsc-consensus = { workspace = true, default-features = true }
|
||||
pezsc-consensus-slots = { workspace = true, default-features = true }
|
||||
pezsc-telemetry = { 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-consensus-slots = { 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 }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezsc-keystore = { workspace = true, default-features = true }
|
||||
pezsc-network = { workspace = true, default-features = true }
|
||||
pezsc-network-test = { workspace = true }
|
||||
pezsp-keyring = { workspace = true, default-features = true }
|
||||
pezsp-timestamp = { workspace = true, default-features = true }
|
||||
pezsp-tracing = { workspace = true, default-features = true }
|
||||
bizinikiwi-test-runtime-client = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tokio = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezsc-block-builder/runtime-benchmarks",
|
||||
"pezsc-client-api/runtime-benchmarks",
|
||||
"pezsc-consensus-slots/runtime-benchmarks",
|
||||
"pezsc-consensus/runtime-benchmarks",
|
||||
"pezsc-network-test/runtime-benchmarks",
|
||||
"pezsc-network/runtime-benchmarks",
|
||||
"pezsp-api/runtime-benchmarks",
|
||||
"pezsp-block-builder/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus-aura/runtime-benchmarks",
|
||||
"pezsp-consensus-slots/runtime-benchmarks",
|
||||
"pezsp-consensus/runtime-benchmarks",
|
||||
"pezsp-inherents/runtime-benchmarks",
|
||||
"pezsp-keyring/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"pezsp-timestamp/runtime-benchmarks",
|
||||
"bizinikiwi-test-runtime-client/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
Aura (Authority-round) consensus in Bizinikiwi.
|
||||
|
||||
Aura works by having a list of authorities A who are expected to roughly
|
||||
agree on the current time. Time is divided up into discrete slots of t
|
||||
seconds each. For each slot s, the author of that slot is A[s % |A|].
|
||||
|
||||
The author is allowed to issue one block but not more during that slot,
|
||||
and it will be built upon the longest valid chain that has been seen.
|
||||
|
||||
Blocks from future steps will be either deferred or rejected depending on how
|
||||
far in the future they are.
|
||||
|
||||
NOTE: Aura itself is designed to be generic over the crypto used.
|
||||
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,182 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Module implementing the logic for verifying and importing AuRa blocks.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use codec::Codec;
|
||||
use fork_tree::ForkTree;
|
||||
use parking_lot::RwLock;
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_blockchain::{HeaderBackend, HeaderMetadata};
|
||||
use pezsp_consensus_aura::{AuraApi, ConsensusLog, AURA_ENGINE_ID};
|
||||
use pezsp_core::Pair;
|
||||
use pezsp_runtime::{
|
||||
generic::OpaqueDigestItemId,
|
||||
traits::{Block, Header, NumberFor},
|
||||
};
|
||||
|
||||
use crate::{fetch_authorities_from_runtime, AuthorityId, CompatibilityMode};
|
||||
|
||||
const LOG_TARGET: &str = "aura::authorities_tracker";
|
||||
|
||||
/// AURA authorities tracker. Updates authorities based on the AURA authorities change
|
||||
/// digest in the block header.
|
||||
pub struct AuthoritiesTracker<P: Pair, B: Block, C> {
|
||||
authorities: RwLock<ForkTree<B::Hash, NumberFor<B>, Vec<AuthorityId<P>>>>,
|
||||
client: Arc<C>,
|
||||
}
|
||||
|
||||
impl<P: Pair, B: Block, C> AuthoritiesTracker<P, B, C> {
|
||||
/// Create a new `AuthoritiesTracker`.
|
||||
pub fn new(client: Arc<C>) -> Self {
|
||||
Self { authorities: RwLock::new(ForkTree::new()), client }
|
||||
}
|
||||
}
|
||||
|
||||
impl<P, B, C> AuthoritiesTracker<P, B, C>
|
||||
where
|
||||
P: Pair,
|
||||
B: Block,
|
||||
C: HeaderBackend<B> + HeaderMetadata<B, Error = pezsp_blockchain::Error> + ProvideRuntimeApi<B>,
|
||||
P::Public: Codec + Debug,
|
||||
C::Api: AuraApi<B, AuthorityId<P>>,
|
||||
{
|
||||
/// Fetch authorities from the tracker, if available. If not available, fetch from the client
|
||||
/// and update the tracker.
|
||||
pub fn fetch_or_update(
|
||||
&self,
|
||||
header: &B::Header,
|
||||
compatibility_mode: &CompatibilityMode<NumberFor<B>>,
|
||||
) -> Result<Vec<AuthorityId<P>>, String> {
|
||||
let hash = header.hash();
|
||||
let number = *header.number();
|
||||
let parent_hash = *header.parent_hash();
|
||||
|
||||
// Fetch authorities from cache, if available.
|
||||
let authorities = {
|
||||
let is_descendent_of =
|
||||
pezsc_client_api::utils::is_descendent_of(&*self.client, Some((hash, parent_hash)));
|
||||
let authorities_cache = self.authorities.read();
|
||||
authorities_cache
|
||||
.find_node_where(&hash, &number, &is_descendent_of, &|_| true)
|
||||
.map_err(|e| {
|
||||
format!("Could not find authorities for block {hash:?} at number {number}: {e}")
|
||||
})?
|
||||
.map(|node| node.data.clone())
|
||||
};
|
||||
|
||||
match authorities {
|
||||
Some(authorities) => {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Authorities for block {:?} at number {} found in cache",
|
||||
hash,
|
||||
number,
|
||||
);
|
||||
Ok(authorities)
|
||||
},
|
||||
None => {
|
||||
// Authorities are missing from the cache. Fetch them from the runtime and cache
|
||||
// them.
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Authorities for block {:?} at number {} not found in cache, fetching from runtime",
|
||||
hash,
|
||||
number
|
||||
);
|
||||
let authorities = fetch_authorities_from_runtime(
|
||||
&*self.client,
|
||||
parent_hash,
|
||||
number,
|
||||
compatibility_mode,
|
||||
)
|
||||
.map_err(|e| format!("Could not fetch authorities at {:?}: {}", parent_hash, e))?;
|
||||
let is_descendent_of = pezsc_client_api::utils::is_descendent_of(&*self.client, None);
|
||||
let mut authorities_cache = self.authorities.write();
|
||||
authorities_cache
|
||||
.import(
|
||||
parent_hash,
|
||||
number - 1u32.into(),
|
||||
authorities.clone(),
|
||||
&is_descendent_of,
|
||||
)
|
||||
.map_err(|e| {
|
||||
format!("Could not import authorities for block {parent_hash:?} at number {}: {e}", number - 1u32.into())
|
||||
})?;
|
||||
Ok(authorities)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// If there is an authorities change digest in the header, import it into the tracker.
|
||||
pub fn import(&self, post_header: &B::Header) -> Result<(), String> {
|
||||
if let Some(authorities_change) = find_authorities_change_digest::<B, P>(&post_header) {
|
||||
let hash = post_header.hash();
|
||||
let parent_hash = *post_header.parent_hash();
|
||||
let number = *post_header.number();
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Importing authorities change for block {:?} at number {} found in header digest",
|
||||
hash,
|
||||
number,
|
||||
);
|
||||
self.prune_finalized()?;
|
||||
let is_descendent_of =
|
||||
pezsc_client_api::utils::is_descendent_of(&*self.client, Some((hash, parent_hash)));
|
||||
let mut authorities_cache = self.authorities.write();
|
||||
authorities_cache
|
||||
.import(hash, number, authorities_change, &is_descendent_of)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Could not import authorities for block {hash:?} at number {number}: {e}"
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prune_finalized(&self) -> Result<(), String> {
|
||||
let is_descendent_of = pezsc_client_api::utils::is_descendent_of(&*self.client, None);
|
||||
let info = self.client.info();
|
||||
let mut authorities_cache = self.authorities.write();
|
||||
let _pruned = authorities_cache
|
||||
.prune(&info.finalized_hash, &info.finalized_number, &is_descendent_of, &|_| true)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the AURA authorities change digest from the given header, if it exists.
|
||||
fn find_authorities_change_digest<B, P>(header: &B::Header) -> Option<Vec<AuthorityId<P>>>
|
||||
where
|
||||
B: Block,
|
||||
P: Pair,
|
||||
P::Public: Codec,
|
||||
{
|
||||
for log in header.digest().logs() {
|
||||
log::trace!(target: LOG_TARGET, "Checking log {:?}, looking for authorities change digest.", log);
|
||||
let log = log
|
||||
.try_to::<ConsensusLog<AuthorityId<P>>>(OpaqueDigestItemId::Consensus(&AURA_ENGINE_ID));
|
||||
if let Some(ConsensusLog::AuthoritiesChange(authorities)) = log {
|
||||
return Some(authorities);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Module implementing the logic for verifying and importing AuRa blocks.
|
||||
|
||||
use crate::{
|
||||
fetch_authorities_from_runtime, standalone::SealVerificationError, AuthoritiesTracker,
|
||||
AuthorityId, CompatibilityMode, Error, LOG_TARGET,
|
||||
};
|
||||
use codec::Codec;
|
||||
use log::{debug, info, trace};
|
||||
use prometheus_endpoint::Registry;
|
||||
use pezsc_client_api::{backend::AuxStore, BlockOf, UsageProvider};
|
||||
use pezsc_consensus::{
|
||||
block_import::{BlockImport, BlockImportParams, ForkChoiceStrategy},
|
||||
import_queue::{BasicQueue, BoxJustificationImport, DefaultImportQueue, Verifier},
|
||||
};
|
||||
use pezsc_consensus_slots::{check_equivocation, CheckedHeader, InherentDataProviderExt};
|
||||
use pezsc_telemetry::{telemetry, TelemetryHandle, CONSENSUS_DEBUG, CONSENSUS_TRACE};
|
||||
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::{inherents::AuraInherentData, AuraApi};
|
||||
use pezsp_consensus_slots::Slot;
|
||||
use pezsp_core::crypto::Pair;
|
||||
use pezsp_inherents::{CreateInherentDataProviders, InherentDataProvider as _};
|
||||
use pezsp_runtime::{
|
||||
traits::{Block as BlockT, Header, NumberFor},
|
||||
DigestItem,
|
||||
};
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
/// check a header has been signed by the right key. If the slot is too far in the future, an error
|
||||
/// will be returned. If it's successful, returns the pre-header and the digest item
|
||||
/// containing the seal.
|
||||
///
|
||||
/// This digest item will always return `Some` when used with `as_aura_seal`.
|
||||
fn check_header<C, B: BlockT, P: Pair>(
|
||||
client: &C,
|
||||
slot_now: Slot,
|
||||
header: B::Header,
|
||||
hash: B::Hash,
|
||||
authorities: &[AuthorityId<P>],
|
||||
check_for_equivocation: CheckForEquivocation,
|
||||
) -> Result<CheckedHeader<B::Header, (Slot, DigestItem)>, Error<B>>
|
||||
where
|
||||
P::Public: Codec,
|
||||
P::Signature: Codec,
|
||||
C: pezsc_client_api::backend::AuxStore,
|
||||
{
|
||||
let check_result =
|
||||
crate::standalone::check_header_slot_and_seal::<B, P>(slot_now, header, authorities);
|
||||
|
||||
match check_result {
|
||||
Ok((header, slot, seal)) => {
|
||||
let expected_author = crate::standalone::slot_author::<P>(slot, &authorities);
|
||||
let should_equiv_check = check_for_equivocation.check_for_equivocation();
|
||||
if let (true, Some(expected)) = (should_equiv_check, expected_author) {
|
||||
if let Some(equivocation_proof) =
|
||||
check_equivocation(client, slot_now, slot, &header, expected)
|
||||
.map_err(Error::Client)?
|
||||
{
|
||||
info!(
|
||||
target: LOG_TARGET,
|
||||
"Slot author is equivocating at slot {} with headers {:?} and {:?}",
|
||||
slot,
|
||||
equivocation_proof.first_header.hash(),
|
||||
equivocation_proof.second_header.hash(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CheckedHeader::Checked(header, (slot, seal)))
|
||||
},
|
||||
Err(SealVerificationError::Deferred(header, slot)) =>
|
||||
Ok(CheckedHeader::Deferred(header, slot)),
|
||||
Err(SealVerificationError::Unsealed) => Err(Error::HeaderUnsealed(hash)),
|
||||
Err(SealVerificationError::BadSeal) => Err(Error::HeaderBadSeal(hash)),
|
||||
Err(SealVerificationError::BadSignature) => Err(Error::BadSignature(hash)),
|
||||
Err(SealVerificationError::SlotAuthorNotFound) => Err(Error::SlotAuthorNotFound),
|
||||
Err(SealVerificationError::InvalidPreDigest(e)) => Err(Error::from(e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// A verifier for Aura blocks.
|
||||
pub struct AuraVerifier<C, P: Pair, CIDP, B: BlockT> {
|
||||
client: Arc<C>,
|
||||
create_inherent_data_providers: CIDP,
|
||||
check_for_equivocation: CheckForEquivocation,
|
||||
telemetry: Option<TelemetryHandle>,
|
||||
compatibility_mode: CompatibilityMode<NumberFor<B>>,
|
||||
_authorities_tracker: AuthoritiesTracker<P, B, C>,
|
||||
}
|
||||
|
||||
impl<C, P: Pair, CIDP, B: BlockT> AuraVerifier<C, P, CIDP, B> {
|
||||
pub(crate) fn new(
|
||||
client: Arc<C>,
|
||||
create_inherent_data_providers: CIDP,
|
||||
check_for_equivocation: CheckForEquivocation,
|
||||
telemetry: Option<TelemetryHandle>,
|
||||
compatibility_mode: CompatibilityMode<NumberFor<B>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
client: client.clone(),
|
||||
create_inherent_data_providers,
|
||||
check_for_equivocation,
|
||||
telemetry,
|
||||
compatibility_mode,
|
||||
_authorities_tracker: AuthoritiesTracker::new(client),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B, C, P, CIDP> Verifier<B> for AuraVerifier<C, P, CIDP, B>
|
||||
where
|
||||
B: BlockT,
|
||||
C: HeaderBackend<B>
|
||||
+ HeaderMetadata<B, Error = pezsp_blockchain::Error>
|
||||
+ ProvideRuntimeApi<B>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ pezsc_client_api::backend::AuxStore,
|
||||
C::Api: BlockBuilderApi<B> + AuraApi<B, AuthorityId<P>> + ApiExt<B>,
|
||||
P: Pair,
|
||||
P::Public: Codec + Debug,
|
||||
P::Signature: Codec,
|
||||
CIDP: CreateInherentDataProviders<B, ()> + Send + Sync,
|
||||
CIDP::InherentDataProviders: InherentDataProviderExt + Send + Sync,
|
||||
{
|
||||
async fn verify(
|
||||
&self,
|
||||
mut block: BlockImportParams<B>,
|
||||
) -> Result<BlockImportParams<B>, 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.
|
||||
// Or when we are importing state only and can not verify the seal.
|
||||
if block.with_state() || block.state_action.skip_execution_checks() {
|
||||
// When we are importing only the state of a block, it will be the best block.
|
||||
block.fork_choice = Some(ForkChoiceStrategy::Custom(block.with_state()));
|
||||
|
||||
return Ok(block);
|
||||
}
|
||||
|
||||
let hash = block.header.hash();
|
||||
let parent_hash = *block.header.parent_hash();
|
||||
let authorities = fetch_authorities_from_runtime(
|
||||
self.client.as_ref(),
|
||||
parent_hash,
|
||||
*block.header.number(),
|
||||
&self.compatibility_mode,
|
||||
)
|
||||
.map_err(|e| format!("Could not fetch authorities at {:?}: {}", parent_hash, e))?;
|
||||
|
||||
let create_inherent_data_providers = self
|
||||
.create_inherent_data_providers
|
||||
.create_inherent_data_providers(parent_hash, ())
|
||||
.await
|
||||
.map_err(|e| Error::<B>::Client(pezsp_blockchain::Error::Application(e)))?;
|
||||
|
||||
let mut inherent_data = create_inherent_data_providers
|
||||
.create_inherent_data()
|
||||
.await
|
||||
.map_err(Error::<B>::Inherent)?;
|
||||
|
||||
let slot_now = create_inherent_data_providers.slot();
|
||||
|
||||
// we add one to allow for some small drift.
|
||||
// FIXME #1019 in the future, alter this queue to allow deferring of
|
||||
// headers
|
||||
let checked_header = check_header::<C, B, P>(
|
||||
&self.client,
|
||||
slot_now + 1,
|
||||
block.header,
|
||||
hash,
|
||||
&authorities[..],
|
||||
self.check_for_equivocation,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
match checked_header {
|
||||
CheckedHeader::Checked(pre_header, (slot, seal)) => {
|
||||
// if the body is passed through, we need to use the runtime
|
||||
// to check that the internally-set timestamp in the inherents
|
||||
// actually matches the slot set in the seal.
|
||||
if let Some(inner_body) = block.body.take() {
|
||||
let new_block = B::new(pre_header.clone(), inner_body);
|
||||
|
||||
inherent_data.aura_replace_inherent_data(slot);
|
||||
|
||||
// skip the inherents verification if the runtime API is old or not expected to
|
||||
// exist.
|
||||
if self
|
||||
.client
|
||||
.runtime_api()
|
||||
.has_api_with::<dyn BlockBuilderApi<B>, _>(parent_hash, |v| v >= 2)
|
||||
.map_err(|e| e.to_string())?
|
||||
{
|
||||
pezsp_block_builder::check_inherents_with_data(
|
||||
self.client.clone(),
|
||||
parent_hash,
|
||||
new_block.clone(),
|
||||
&create_inherent_data_providers,
|
||||
inherent_data,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Error checking block inherents {:?}", e))?;
|
||||
}
|
||||
|
||||
let (_, inner_body) = new_block.deconstruct();
|
||||
block.body = Some(inner_body);
|
||||
}
|
||||
|
||||
trace!(target: LOG_TARGET, "Checked {:?}; importing.", pre_header);
|
||||
telemetry!(
|
||||
self.telemetry;
|
||||
CONSENSUS_TRACE;
|
||||
"aura.checked_and_importing";
|
||||
"pre_header" => ?pre_header,
|
||||
);
|
||||
|
||||
block.header = pre_header;
|
||||
block.post_digests.push(seal);
|
||||
block.fork_choice = Some(ForkChoiceStrategy::LongestChain);
|
||||
block.post_hash = Some(hash);
|
||||
|
||||
Ok(block)
|
||||
},
|
||||
CheckedHeader::Deferred(a, b) => {
|
||||
debug!(target: LOG_TARGET, "Checking {:?} failed; {:?}, {:?}.", hash, a, b);
|
||||
telemetry!(
|
||||
self.telemetry;
|
||||
CONSENSUS_DEBUG;
|
||||
"aura.header_too_far_in_future";
|
||||
"hash" => ?hash,
|
||||
"a" => ?a,
|
||||
"b" => ?b,
|
||||
);
|
||||
Err(format!("Header {:?} rejected: too far in the future", hash))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Should we check for equivocation of a block author?
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CheckForEquivocation {
|
||||
/// Yes, check for equivocation.
|
||||
///
|
||||
/// This is the default setting for this.
|
||||
Yes,
|
||||
/// No, don't check for equivocation.
|
||||
No,
|
||||
}
|
||||
|
||||
impl CheckForEquivocation {
|
||||
/// Should we check for equivocation?
|
||||
fn check_for_equivocation(self) -> bool {
|
||||
matches!(self, Self::Yes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CheckForEquivocation {
|
||||
fn default() -> Self {
|
||||
Self::Yes
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters of [`import_queue`].
|
||||
pub struct ImportQueueParams<'a, Block: BlockT, I, C, S, CIDP> {
|
||||
/// The block import to use.
|
||||
pub block_import: I,
|
||||
/// The justification import.
|
||||
pub justification_import: Option<BoxJustificationImport<Block>>,
|
||||
/// The client to interact with the chain.
|
||||
pub client: Arc<C>,
|
||||
/// Something that can create the inherent data providers.
|
||||
pub create_inherent_data_providers: CIDP,
|
||||
/// The spawner to spawn background tasks.
|
||||
pub spawner: &'a S,
|
||||
/// The prometheus registry.
|
||||
pub registry: Option<&'a Registry>,
|
||||
/// Should we check for equivocation?
|
||||
pub check_for_equivocation: CheckForEquivocation,
|
||||
/// Telemetry instance used to report telemetry metrics.
|
||||
pub telemetry: Option<TelemetryHandle>,
|
||||
/// Compatibility mode that should be used.
|
||||
///
|
||||
/// If in doubt, use `Default::default()`.
|
||||
pub compatibility_mode: CompatibilityMode<NumberFor<Block>>,
|
||||
}
|
||||
|
||||
/// Start an import queue for the Aura consensus algorithm.
|
||||
pub fn import_queue<P, Block, I, C, S, CIDP>(
|
||||
ImportQueueParams {
|
||||
block_import,
|
||||
justification_import,
|
||||
client,
|
||||
create_inherent_data_providers,
|
||||
spawner,
|
||||
registry,
|
||||
check_for_equivocation,
|
||||
telemetry,
|
||||
compatibility_mode,
|
||||
}: ImportQueueParams<Block, I, C, S, CIDP>,
|
||||
) -> Result<DefaultImportQueue<Block>, pezsp_consensus::Error>
|
||||
where
|
||||
Block: BlockT,
|
||||
C::Api: BlockBuilderApi<Block> + AuraApi<Block, AuthorityId<P>> + 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> + Send + Sync + 'static,
|
||||
P: Pair + 'static,
|
||||
P::Public: Codec + Debug,
|
||||
P::Signature: Codec,
|
||||
S: pezsp_core::traits::SpawnEssentialNamed,
|
||||
CIDP: CreateInherentDataProviders<Block, ()> + Sync + Send + 'static,
|
||||
CIDP::InherentDataProviders: InherentDataProviderExt + Send + Sync,
|
||||
{
|
||||
let verifier = build_verifier::<P, _, _, _>(BuildVerifierParams {
|
||||
client,
|
||||
create_inherent_data_providers,
|
||||
check_for_equivocation,
|
||||
telemetry,
|
||||
compatibility_mode,
|
||||
});
|
||||
|
||||
Ok(BasicQueue::new(verifier, Box::new(block_import), justification_import, spawner, registry))
|
||||
}
|
||||
|
||||
/// Parameters of [`build_verifier`].
|
||||
pub struct BuildVerifierParams<C, CIDP, N> {
|
||||
/// The client to interact with the chain.
|
||||
pub client: Arc<C>,
|
||||
/// Something that can create the inherent data providers.
|
||||
pub create_inherent_data_providers: CIDP,
|
||||
/// Should we check for equivocation?
|
||||
pub check_for_equivocation: CheckForEquivocation,
|
||||
/// Telemetry instance used to report telemetry metrics.
|
||||
pub telemetry: Option<TelemetryHandle>,
|
||||
/// Compatibility mode that should be used.
|
||||
///
|
||||
/// If in doubt, use `Default::default()`.
|
||||
pub compatibility_mode: CompatibilityMode<N>,
|
||||
}
|
||||
|
||||
/// Build the [`AuraVerifier`]
|
||||
pub fn build_verifier<P: Pair, C, CIDP, B: BlockT>(
|
||||
BuildVerifierParams {
|
||||
client,
|
||||
create_inherent_data_providers,
|
||||
check_for_equivocation,
|
||||
telemetry,
|
||||
compatibility_mode,
|
||||
}: BuildVerifierParams<C, CIDP, NumberFor<B>>,
|
||||
) -> AuraVerifier<C, P, CIDP, B> {
|
||||
AuraVerifier::<_, P, _, _>::new(
|
||||
client,
|
||||
create_inherent_data_providers,
|
||||
check_for_equivocation,
|
||||
telemetry,
|
||||
compatibility_mode,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,882 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Aura (Authority-round) consensus in bizinikiwi.
|
||||
//!
|
||||
//! Aura works by having a list of authorities A who are expected to roughly
|
||||
//! agree on the current time. Time is divided up into discrete slots of t
|
||||
//! seconds each. For each slot s, the author of that slot is A[s % |A|].
|
||||
//!
|
||||
//! The author is allowed to issue one block but not more during that slot,
|
||||
//! and it will be built upon the longest valid chain that has been seen.
|
||||
//!
|
||||
//! Blocks from future steps will be either deferred or rejected depending on how
|
||||
//! far in the future they are.
|
||||
//!
|
||||
//! NOTE: Aura itself is designed to be generic over the crypto used.
|
||||
#![forbid(missing_docs, unsafe_code)]
|
||||
use std::{fmt::Debug, marker::PhantomData, pin::Pin, sync::Arc};
|
||||
|
||||
use codec::Codec;
|
||||
use futures::prelude::*;
|
||||
|
||||
use pezsc_client_api::{backend::AuxStore, BlockOf};
|
||||
use pezsc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy, StateAction};
|
||||
use pezsc_consensus_slots::{
|
||||
BackoffAuthoringBlocksStrategy, InherentDataProviderExt, SimpleSlotWorkerToSlotWorker,
|
||||
SlotInfo, StorageChanges,
|
||||
};
|
||||
use pezsc_telemetry::TelemetryHandle;
|
||||
use pezsp_api::{Core, ProvideRuntimeApi};
|
||||
use pezsp_application_crypto::AppPublic;
|
||||
use pezsp_blockchain::HeaderBackend;
|
||||
use pezsp_consensus::{BlockOrigin, Environment, Error as ConsensusError, Proposer, SelectChain};
|
||||
use pezsp_consensus_slots::Slot;
|
||||
use pezsp_core::crypto::Pair;
|
||||
use pezsp_inherents::CreateInherentDataProviders;
|
||||
use pezsp_keystore::KeystorePtr;
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header, Member, NumberFor};
|
||||
|
||||
mod authorities_tracker;
|
||||
mod import_queue;
|
||||
pub mod standalone;
|
||||
|
||||
pub use crate::standalone::{find_pre_digest, slot_duration};
|
||||
pub use authorities_tracker::AuthoritiesTracker;
|
||||
pub use import_queue::{
|
||||
build_verifier, import_queue, AuraVerifier, BuildVerifierParams, CheckForEquivocation,
|
||||
ImportQueueParams,
|
||||
};
|
||||
pub use pezsc_consensus_slots::SlotProportion;
|
||||
pub use pezsp_consensus::SyncOracle;
|
||||
pub use pezsp_consensus_aura::{
|
||||
digests::CompatibleDigestItem,
|
||||
inherents::{InherentDataProvider, InherentType as AuraInherent, INHERENT_IDENTIFIER},
|
||||
AuraApi, ConsensusLog, SlotDuration, AURA_ENGINE_ID,
|
||||
};
|
||||
|
||||
const LOG_TARGET: &str = "aura";
|
||||
|
||||
type AuthorityId<P> = <P as Pair>::Public;
|
||||
|
||||
/// Run `AURA` in a compatibility mode.
|
||||
///
|
||||
/// This is required for when the chain was launched and later there
|
||||
/// was a consensus breaking change.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CompatibilityMode<N> {
|
||||
/// Don't use any compatibility mode.
|
||||
None,
|
||||
/// Call `initialize_block` before doing any runtime calls.
|
||||
///
|
||||
/// Previously the node would execute `initialize_block` before fetching the authorities
|
||||
/// from the runtime. This behaviour changed in: <https://github.com/pezkuwichain/kurdistan-sdk/issues/77>
|
||||
///
|
||||
/// By calling `initialize_block` before fetching the authorities, on a block that
|
||||
/// would enact a new validator set, the block would already be build/sealed by an
|
||||
/// authority of the new set. With this mode disabled (the default) a block that enacts a new
|
||||
/// set isn't sealed/built by an authority of the new set, however to make new nodes be able to
|
||||
/// sync old chains this compatibility mode exists.
|
||||
UseInitializeBlock {
|
||||
/// The block number until this compatibility mode should be executed. The first runtime
|
||||
/// call in the context of the `until` block (importing it/building it) will disable the
|
||||
/// compatibility mode (i.e. at `until` the default rules will apply). When enabling this
|
||||
/// compatibility mode the `until` block should be a future block on which all nodes will
|
||||
/// have upgraded to a release that includes the updated compatibility mode configuration.
|
||||
/// At `until` block there will be a hard fork when the authority set changes, between the
|
||||
/// old nodes (running with `initialize_block`, i.e. without the compatibility mode
|
||||
/// configuration) and the new nodes.
|
||||
until: N,
|
||||
},
|
||||
}
|
||||
|
||||
impl<N> Default for CompatibilityMode<N> {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters of [`start_aura`].
|
||||
pub struct StartAuraParams<C, SC, I, PF, SO, L, CIDP, BS, N> {
|
||||
/// The duration of a slot.
|
||||
pub slot_duration: SlotDuration,
|
||||
/// The client to interact with the chain.
|
||||
pub client: Arc<C>,
|
||||
/// A select chain implementation to select the best block.
|
||||
pub select_chain: SC,
|
||||
/// The block import.
|
||||
pub block_import: I,
|
||||
/// The proposer factory to build proposer instances.
|
||||
pub proposer_factory: PF,
|
||||
/// The sync oracle that can give us the current sync status.
|
||||
pub sync_oracle: SO,
|
||||
/// Hook into the sync module to control the justification sync process.
|
||||
pub justification_sync_link: L,
|
||||
/// Something that can create the inherent data providers.
|
||||
pub create_inherent_data_providers: CIDP,
|
||||
/// Should we force the authoring of blocks?
|
||||
pub force_authoring: bool,
|
||||
/// The backoff strategy when we miss slots.
|
||||
pub backoff_authoring_blocks: Option<BS>,
|
||||
/// The keystore used by the node.
|
||||
pub keystore: KeystorePtr,
|
||||
/// The proportion of the slot dedicated to proposing.
|
||||
///
|
||||
/// The block proposing will be limited to this proportion of the slot from the starting of the
|
||||
/// slot. However, the proposing can still take longer when there is some lenience factor
|
||||
/// applied, because there were no blocks produced for some slots.
|
||||
pub block_proposal_slot_portion: SlotProportion,
|
||||
/// The maximum proportion of the slot dedicated to proposing with any lenience factor applied
|
||||
/// due to no blocks being produced.
|
||||
pub max_block_proposal_slot_portion: Option<SlotProportion>,
|
||||
/// Telemetry instance used to report telemetry metrics.
|
||||
pub telemetry: Option<TelemetryHandle>,
|
||||
/// Compatibility mode that should be used.
|
||||
///
|
||||
/// If in doubt, use `Default::default()`.
|
||||
pub compatibility_mode: CompatibilityMode<N>,
|
||||
}
|
||||
|
||||
/// Start the aura worker. The returned future should be run in a futures executor.
|
||||
pub fn start_aura<P, B, C, SC, I, PF, SO, L, CIDP, BS, Error>(
|
||||
StartAuraParams {
|
||||
slot_duration,
|
||||
client,
|
||||
select_chain,
|
||||
block_import,
|
||||
proposer_factory,
|
||||
sync_oracle,
|
||||
justification_sync_link,
|
||||
create_inherent_data_providers,
|
||||
force_authoring,
|
||||
backoff_authoring_blocks,
|
||||
keystore,
|
||||
block_proposal_slot_portion,
|
||||
max_block_proposal_slot_portion,
|
||||
telemetry,
|
||||
compatibility_mode,
|
||||
}: StartAuraParams<C, SC, I, PF, SO, L, CIDP, BS, NumberFor<B>>,
|
||||
) -> Result<impl Future<Output = ()>, ConsensusError>
|
||||
where
|
||||
P: Pair,
|
||||
P::Public: AppPublic + Member,
|
||||
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
|
||||
B: BlockT,
|
||||
C: ProvideRuntimeApi<B> + BlockOf + AuxStore + HeaderBackend<B> + Send + Sync,
|
||||
C::Api: AuraApi<B, AuthorityId<P>>,
|
||||
SC: SelectChain<B>,
|
||||
I: BlockImport<B> + Send + Sync + 'static,
|
||||
PF: Environment<B, Error = Error> + Send + Sync + 'static,
|
||||
PF::Proposer: Proposer<B, Error = Error>,
|
||||
SO: SyncOracle + Send + Sync + Clone,
|
||||
L: pezsc_consensus::JustificationSyncLink<B>,
|
||||
CIDP: CreateInherentDataProviders<B, ()> + Send + 'static,
|
||||
CIDP::InherentDataProviders: InherentDataProviderExt + Send,
|
||||
BS: BackoffAuthoringBlocksStrategy<NumberFor<B>> + Send + Sync + 'static,
|
||||
Error: std::error::Error + Send + From<ConsensusError> + 'static,
|
||||
{
|
||||
let worker = build_aura_worker::<P, _, _, _, _, _, _, _, _>(BuildAuraWorkerParams {
|
||||
client,
|
||||
block_import,
|
||||
proposer_factory,
|
||||
keystore,
|
||||
sync_oracle: sync_oracle.clone(),
|
||||
justification_sync_link,
|
||||
force_authoring,
|
||||
backoff_authoring_blocks,
|
||||
telemetry,
|
||||
block_proposal_slot_portion,
|
||||
max_block_proposal_slot_portion,
|
||||
compatibility_mode,
|
||||
});
|
||||
|
||||
Ok(pezsc_consensus_slots::start_slot_worker(
|
||||
slot_duration,
|
||||
select_chain,
|
||||
SimpleSlotWorkerToSlotWorker(worker),
|
||||
sync_oracle,
|
||||
create_inherent_data_providers,
|
||||
))
|
||||
}
|
||||
|
||||
/// Parameters of [`build_aura_worker`].
|
||||
pub struct BuildAuraWorkerParams<C, I, PF, SO, L, BS, N> {
|
||||
/// The client to interact with the chain.
|
||||
pub client: Arc<C>,
|
||||
/// The block import.
|
||||
pub block_import: I,
|
||||
/// The proposer factory to build proposer instances.
|
||||
pub proposer_factory: PF,
|
||||
/// The sync oracle that can give us the current sync status.
|
||||
pub sync_oracle: SO,
|
||||
/// Hook into the sync module to control the justification sync process.
|
||||
pub justification_sync_link: L,
|
||||
/// Should we force the authoring of blocks?
|
||||
pub force_authoring: bool,
|
||||
/// The backoff strategy when we miss slots.
|
||||
pub backoff_authoring_blocks: Option<BS>,
|
||||
/// The keystore used by the node.
|
||||
pub keystore: KeystorePtr,
|
||||
/// The proportion of the slot dedicated to proposing.
|
||||
///
|
||||
/// The block proposing will be limited to this proportion of the slot from the starting of the
|
||||
/// slot. However, the proposing can still take longer when there is some lenience factor
|
||||
/// applied, because there were no blocks produced for some slots.
|
||||
pub block_proposal_slot_portion: SlotProportion,
|
||||
/// The maximum proportion of the slot dedicated to proposing with any lenience factor applied
|
||||
/// due to no blocks being produced.
|
||||
pub max_block_proposal_slot_portion: Option<SlotProportion>,
|
||||
/// Telemetry instance used to report telemetry metrics.
|
||||
pub telemetry: Option<TelemetryHandle>,
|
||||
/// Compatibility mode that should be used.
|
||||
///
|
||||
/// If in doubt, use `Default::default()`.
|
||||
pub compatibility_mode: CompatibilityMode<N>,
|
||||
}
|
||||
|
||||
/// Build the aura worker.
|
||||
///
|
||||
/// The caller is responsible for running this worker, otherwise it will do nothing.
|
||||
pub fn build_aura_worker<P, B, C, PF, I, SO, L, BS, Error>(
|
||||
BuildAuraWorkerParams {
|
||||
client,
|
||||
block_import,
|
||||
proposer_factory,
|
||||
sync_oracle,
|
||||
justification_sync_link,
|
||||
backoff_authoring_blocks,
|
||||
keystore,
|
||||
block_proposal_slot_portion,
|
||||
max_block_proposal_slot_portion,
|
||||
telemetry,
|
||||
force_authoring,
|
||||
compatibility_mode,
|
||||
}: BuildAuraWorkerParams<C, I, PF, SO, L, BS, NumberFor<B>>,
|
||||
) -> impl pezsc_consensus_slots::SimpleSlotWorker<
|
||||
B,
|
||||
Proposer = PF::Proposer,
|
||||
BlockImport = I,
|
||||
SyncOracle = SO,
|
||||
JustificationSyncLink = L,
|
||||
Claim = P::Public,
|
||||
AuxData = Vec<AuthorityId<P>>,
|
||||
>
|
||||
where
|
||||
B: BlockT,
|
||||
C: ProvideRuntimeApi<B> + BlockOf + AuxStore + HeaderBackend<B> + Send + Sync,
|
||||
C::Api: AuraApi<B, AuthorityId<P>>,
|
||||
PF: Environment<B, Error = Error> + Send + Sync + 'static,
|
||||
PF::Proposer: Proposer<B, Error = Error>,
|
||||
P: Pair,
|
||||
P::Public: AppPublic + Member,
|
||||
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
|
||||
I: BlockImport<B> + Send + Sync + 'static,
|
||||
Error: std::error::Error + Send + From<ConsensusError> + 'static,
|
||||
SO: SyncOracle + Send + Sync + Clone,
|
||||
L: pezsc_consensus::JustificationSyncLink<B>,
|
||||
BS: BackoffAuthoringBlocksStrategy<NumberFor<B>> + Send + Sync + 'static,
|
||||
{
|
||||
AuraWorker {
|
||||
client,
|
||||
block_import,
|
||||
env: proposer_factory,
|
||||
keystore,
|
||||
sync_oracle,
|
||||
justification_sync_link,
|
||||
force_authoring,
|
||||
backoff_authoring_blocks,
|
||||
telemetry,
|
||||
block_proposal_slot_portion,
|
||||
max_block_proposal_slot_portion,
|
||||
compatibility_mode,
|
||||
_phantom: PhantomData::<fn() -> P>,
|
||||
}
|
||||
}
|
||||
|
||||
struct AuraWorker<C, E, I, P, SO, L, BS, N> {
|
||||
client: Arc<C>,
|
||||
block_import: I,
|
||||
env: E,
|
||||
keystore: KeystorePtr,
|
||||
sync_oracle: SO,
|
||||
justification_sync_link: L,
|
||||
force_authoring: bool,
|
||||
backoff_authoring_blocks: Option<BS>,
|
||||
block_proposal_slot_portion: SlotProportion,
|
||||
max_block_proposal_slot_portion: Option<SlotProportion>,
|
||||
telemetry: Option<TelemetryHandle>,
|
||||
compatibility_mode: CompatibilityMode<N>,
|
||||
_phantom: PhantomData<fn() -> P>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B, C, E, I, P, Error, SO, L, BS> pezsc_consensus_slots::SimpleSlotWorker<B>
|
||||
for AuraWorker<C, E, I, P, SO, L, BS, NumberFor<B>>
|
||||
where
|
||||
B: BlockT,
|
||||
C: ProvideRuntimeApi<B> + BlockOf + HeaderBackend<B> + Sync,
|
||||
C::Api: AuraApi<B, AuthorityId<P>>,
|
||||
E: Environment<B, Error = Error> + Send + Sync,
|
||||
E::Proposer: Proposer<B, Error = Error>,
|
||||
I: BlockImport<B> + Send + Sync + 'static,
|
||||
P: Pair,
|
||||
P::Public: AppPublic + Member,
|
||||
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
|
||||
SO: SyncOracle + Send + Clone + Sync,
|
||||
L: pezsc_consensus::JustificationSyncLink<B>,
|
||||
BS: BackoffAuthoringBlocksStrategy<NumberFor<B>> + Send + Sync + 'static,
|
||||
Error: std::error::Error + Send + From<ConsensusError> + 'static,
|
||||
{
|
||||
type BlockImport = I;
|
||||
type SyncOracle = SO;
|
||||
type JustificationSyncLink = L;
|
||||
type CreateProposer =
|
||||
Pin<Box<dyn Future<Output = Result<E::Proposer, ConsensusError>> + Send + 'static>>;
|
||||
type Proposer = E::Proposer;
|
||||
type Claim = P::Public;
|
||||
type AuxData = Vec<AuthorityId<P>>;
|
||||
|
||||
fn logging_target(&self) -> &'static str {
|
||||
"aura"
|
||||
}
|
||||
|
||||
fn block_import(&mut self) -> &mut Self::BlockImport {
|
||||
&mut self.block_import
|
||||
}
|
||||
|
||||
fn aux_data(&self, header: &B::Header, _slot: Slot) -> Result<Self::AuxData, ConsensusError> {
|
||||
fetch_authorities_from_runtime(
|
||||
self.client.as_ref(),
|
||||
header.hash(),
|
||||
*header.number() + 1u32.into(),
|
||||
&self.compatibility_mode,
|
||||
)
|
||||
}
|
||||
|
||||
fn authorities_len(&self, authorities: &Self::AuxData) -> Option<usize> {
|
||||
Some(authorities.len())
|
||||
}
|
||||
|
||||
async fn claim_slot(
|
||||
&mut self,
|
||||
_header: &B::Header,
|
||||
slot: Slot,
|
||||
authorities: &Self::AuxData,
|
||||
) -> Option<Self::Claim> {
|
||||
crate::standalone::claim_slot::<P>(slot, authorities, &self.keystore).await
|
||||
}
|
||||
|
||||
fn pre_digest_data(&self, slot: Slot, _claim: &Self::Claim) -> Vec<pezsp_runtime::DigestItem> {
|
||||
vec![crate::standalone::pre_digest::<P>(slot)]
|
||||
}
|
||||
|
||||
async fn block_import_params(
|
||||
&self,
|
||||
header: B::Header,
|
||||
header_hash: &B::Hash,
|
||||
body: Vec<B::Extrinsic>,
|
||||
storage_changes: StorageChanges<B>,
|
||||
public: Self::Claim,
|
||||
_authorities: Self::AuxData,
|
||||
) -> Result<pezsc_consensus::BlockImportParams<B>, ConsensusError> {
|
||||
let signature_digest_item =
|
||||
crate::standalone::seal::<_, P>(header_hash, &public, &self.keystore)?;
|
||||
|
||||
let mut import_block = BlockImportParams::new(BlockOrigin::Own, header);
|
||||
import_block.post_digests.push(signature_digest_item);
|
||||
import_block.body = Some(body);
|
||||
import_block.state_action =
|
||||
StateAction::ApplyChanges(pezsc_consensus::StorageChanges::Changes(storage_changes));
|
||||
import_block.fork_choice = Some(ForkChoiceStrategy::LongestChain);
|
||||
|
||||
Ok(import_block)
|
||||
}
|
||||
|
||||
fn force_authoring(&self) -> bool {
|
||||
self.force_authoring
|
||||
}
|
||||
|
||||
fn should_backoff(&self, slot: Slot, chain_head: &B::Header) -> bool {
|
||||
if let Some(ref strategy) = self.backoff_authoring_blocks {
|
||||
if let Ok(chain_head_slot) = find_pre_digest::<B, P::Signature>(chain_head) {
|
||||
return strategy.should_backoff(
|
||||
*chain_head.number(),
|
||||
chain_head_slot,
|
||||
self.client.info().finalized_number,
|
||||
slot,
|
||||
self.logging_target(),
|
||||
);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn sync_oracle(&mut self) -> &mut Self::SyncOracle {
|
||||
&mut self.sync_oracle
|
||||
}
|
||||
|
||||
fn justification_sync_link(&mut self) -> &mut Self::JustificationSyncLink {
|
||||
&mut self.justification_sync_link
|
||||
}
|
||||
|
||||
fn proposer(&mut self, block: &B::Header) -> Self::CreateProposer {
|
||||
self.env
|
||||
.init(block)
|
||||
.map_err(|e| ConsensusError::ClientImport(format!("{:?}", e)))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn telemetry(&self) -> Option<TelemetryHandle> {
|
||||
self.telemetry.clone()
|
||||
}
|
||||
|
||||
fn proposing_remaining_duration(&self, slot_info: &SlotInfo<B>) -> std::time::Duration {
|
||||
let parent_slot = find_pre_digest::<B, P::Signature>(&slot_info.chain_head).ok();
|
||||
|
||||
pezsc_consensus_slots::proposing_remaining_duration(
|
||||
parent_slot,
|
||||
slot_info,
|
||||
&self.block_proposal_slot_portion,
|
||||
self.max_block_proposal_slot_portion.as_ref(),
|
||||
pezsc_consensus_slots::SlotLenienceType::Exponential,
|
||||
self.logging_target(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Aura Errors
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error<B: BlockT> {
|
||||
/// Multiple Aura pre-runtime headers
|
||||
#[error("Multiple Aura pre-runtime headers")]
|
||||
MultipleHeaders,
|
||||
/// No Aura pre-runtime digest found
|
||||
#[error("No Aura pre-runtime digest found")]
|
||||
NoDigestFound,
|
||||
/// Header is unsealed
|
||||
#[error("Header {0:?} is unsealed")]
|
||||
HeaderUnsealed(B::Hash),
|
||||
/// Header has a bad seal
|
||||
#[error("Header {0:?} has a bad seal")]
|
||||
HeaderBadSeal(B::Hash),
|
||||
/// Slot Author not found
|
||||
#[error("Slot Author not found")]
|
||||
SlotAuthorNotFound,
|
||||
/// Bad signature
|
||||
#[error("Bad signature on {0:?}")]
|
||||
BadSignature(B::Hash),
|
||||
/// Client Error
|
||||
#[error(transparent)]
|
||||
Client(pezsp_blockchain::Error),
|
||||
/// Inherents Error
|
||||
#[error("Inherent error: {0}")]
|
||||
Inherent(pezsp_inherents::Error),
|
||||
}
|
||||
|
||||
impl<B: BlockT> From<Error<B>> for String {
|
||||
fn from(error: Error<B>) -> String {
|
||||
error.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT> From<crate::standalone::PreDigestLookupError> for Error<B> {
|
||||
fn from(e: crate::standalone::PreDigestLookupError) -> Self {
|
||||
match e {
|
||||
crate::standalone::PreDigestLookupError::MultipleHeaders => Error::MultipleHeaders,
|
||||
crate::standalone::PreDigestLookupError::NoDigestFound => Error::NoDigestFound,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_authorities_from_runtime<A, B, C>(
|
||||
client: &C,
|
||||
parent_hash: B::Hash,
|
||||
context_block_number: NumberFor<B>,
|
||||
compatibility_mode: &CompatibilityMode<NumberFor<B>>,
|
||||
) -> Result<Vec<A>, ConsensusError>
|
||||
where
|
||||
A: Codec + Debug,
|
||||
B: BlockT,
|
||||
C: ProvideRuntimeApi<B>,
|
||||
C::Api: AuraApi<B, A>,
|
||||
{
|
||||
let runtime_api = client.runtime_api();
|
||||
|
||||
match compatibility_mode {
|
||||
CompatibilityMode::None => {},
|
||||
// Use `initialize_block` until we hit the block that should disable the mode.
|
||||
CompatibilityMode::UseInitializeBlock { until } =>
|
||||
if *until > context_block_number {
|
||||
runtime_api
|
||||
.initialize_block(
|
||||
parent_hash,
|
||||
&B::Header::new(
|
||||
context_block_number,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
parent_hash,
|
||||
Default::default(),
|
||||
),
|
||||
)
|
||||
.map_err(|_| ConsensusError::InvalidAuthoritiesSet)?;
|
||||
},
|
||||
}
|
||||
|
||||
runtime_api
|
||||
.authorities(parent_hash)
|
||||
.ok()
|
||||
.ok_or(ConsensusError::InvalidAuthoritiesSet)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use parking_lot::Mutex;
|
||||
use pezsc_block_builder::BlockBuilderBuilder;
|
||||
use pezsc_client_api::BlockchainEvents;
|
||||
use pezsc_consensus::BoxJustificationImport;
|
||||
use pezsc_consensus_slots::{BackoffAuthoringOnFinalizedHeadLagging, SimpleSlotWorker};
|
||||
use pezsc_keystore::LocalKeystore;
|
||||
use pezsc_network_test::{Block as TestBlock, *};
|
||||
use pezsp_application_crypto::{key_types::AURA, AppCrypto};
|
||||
use pezsp_consensus::{DisableProofRecording, NoNetwork as DummyOracle, Proposal};
|
||||
use pezsp_consensus_aura::sr25519::AuthorityPair;
|
||||
use pezsp_inherents::InherentData;
|
||||
use pezsp_keyring::sr25519::Keyring;
|
||||
use pezsp_keystore::Keystore;
|
||||
use pezsp_runtime::{
|
||||
traits::{Block as BlockT, Header as _},
|
||||
Digest,
|
||||
};
|
||||
use pezsp_timestamp::Timestamp;
|
||||
use std::{
|
||||
task::Poll,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use bizinikiwi_test_runtime_client::{
|
||||
runtime::{Header, H256},
|
||||
TestClient,
|
||||
};
|
||||
|
||||
const SLOT_DURATION_MS: u64 = 1000;
|
||||
|
||||
type Error = pezsp_blockchain::Error;
|
||||
|
||||
struct DummyFactory(Arc<TestClient>);
|
||||
struct DummyProposer(Arc<TestClient>);
|
||||
|
||||
impl Environment<TestBlock> for DummyFactory {
|
||||
type Proposer = DummyProposer;
|
||||
type CreateProposer = futures::future::Ready<Result<DummyProposer, Error>>;
|
||||
type Error = Error;
|
||||
|
||||
fn init(&mut self, _: &<TestBlock as BlockT>::Header) -> Self::CreateProposer {
|
||||
futures::future::ready(Ok(DummyProposer(self.0.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
impl Proposer<TestBlock> for DummyProposer {
|
||||
type Error = Error;
|
||||
type Proposal = future::Ready<Result<Proposal<TestBlock, ()>, Error>>;
|
||||
type ProofRecording = DisableProofRecording;
|
||||
type Proof = ();
|
||||
|
||||
fn propose(
|
||||
self,
|
||||
_: InherentData,
|
||||
digests: Digest,
|
||||
_: Duration,
|
||||
_: Option<usize>,
|
||||
) -> Self::Proposal {
|
||||
let r = BlockBuilderBuilder::new(&*self.0)
|
||||
.on_parent_block(self.0.chain_info().best_hash)
|
||||
.fetch_parent_block_number(&*self.0)
|
||||
.unwrap()
|
||||
.with_inherent_digests(digests)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build();
|
||||
|
||||
future::ready(r.map(|b| Proposal {
|
||||
block: b.block,
|
||||
proof: (),
|
||||
storage_changes: b.storage_changes,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
type AuraVerifier = import_queue::AuraVerifier<
|
||||
PeersFullClient,
|
||||
AuthorityPair,
|
||||
Box<
|
||||
dyn CreateInherentDataProviders<
|
||||
TestBlock,
|
||||
(),
|
||||
InherentDataProviders = (InherentDataProvider,),
|
||||
>,
|
||||
>,
|
||||
TestBlock,
|
||||
>;
|
||||
type AuraPeer = Peer<(), PeersClient>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AuraTestNet {
|
||||
peers: Vec<AuraPeer>,
|
||||
}
|
||||
|
||||
impl TestNetFactory for AuraTestNet {
|
||||
type Verifier = AuraVerifier;
|
||||
type PeerData = ();
|
||||
type BlockImport = PeersClient;
|
||||
|
||||
fn make_verifier(&self, client: PeersClient, _peer_data: &()) -> Self::Verifier {
|
||||
let client = client.as_client();
|
||||
let slot_duration = slot_duration(&*client).expect("slot duration available");
|
||||
|
||||
assert_eq!(slot_duration.as_millis() as u64, SLOT_DURATION_MS);
|
||||
import_queue::AuraVerifier::new(
|
||||
client,
|
||||
Box::new(|_, _| async {
|
||||
let slot = InherentDataProvider::from_timestamp_and_slot_duration(
|
||||
Timestamp::current(),
|
||||
SlotDuration::from_millis(SLOT_DURATION_MS),
|
||||
);
|
||||
Ok((slot,))
|
||||
}),
|
||||
CheckForEquivocation::Yes,
|
||||
None,
|
||||
CompatibilityMode::None,
|
||||
)
|
||||
}
|
||||
|
||||
fn make_block_import(
|
||||
&self,
|
||||
client: PeersClient,
|
||||
) -> (
|
||||
BlockImportAdapter<Self::BlockImport>,
|
||||
Option<BoxJustificationImport<Block>>,
|
||||
Self::PeerData,
|
||||
) {
|
||||
(client.as_block_import(), None, ())
|
||||
}
|
||||
|
||||
fn peer(&mut self, i: usize) -> &mut AuraPeer {
|
||||
&mut self.peers[i]
|
||||
}
|
||||
|
||||
fn peers(&self) -> &Vec<AuraPeer> {
|
||||
&self.peers
|
||||
}
|
||||
|
||||
fn peers_mut(&mut self) -> &mut Vec<AuraPeer> {
|
||||
&mut self.peers
|
||||
}
|
||||
|
||||
fn mut_peers<F: FnOnce(&mut Vec<AuraPeer>)>(&mut self, closure: F) {
|
||||
closure(&mut self.peers);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authoring_blocks() {
|
||||
pezsp_tracing::try_init_simple();
|
||||
let net = AuraTestNet::new(3);
|
||||
|
||||
let peers = &[(0, Keyring::Alice), (1, Keyring::Bob), (2, Keyring::Charlie)];
|
||||
|
||||
let net = Arc::new(Mutex::new(net));
|
||||
let mut import_notifications = Vec::new();
|
||||
let mut aura_futures = Vec::new();
|
||||
|
||||
let mut keystore_paths = Vec::new();
|
||||
for (peer_id, key) in peers {
|
||||
let mut net = net.lock();
|
||||
let peer = net.peer(*peer_id);
|
||||
let client = peer.client().as_client();
|
||||
let select_chain = peer.select_chain().expect("full client has a select chain");
|
||||
let keystore_path = tempfile::tempdir().expect("Creates keystore path");
|
||||
let keystore = Arc::new(
|
||||
LocalKeystore::open(keystore_path.path(), None).expect("Creates keystore."),
|
||||
);
|
||||
|
||||
keystore
|
||||
.sr25519_generate_new(AURA, Some(&key.to_seed()))
|
||||
.expect("Creates authority key");
|
||||
keystore_paths.push(keystore_path);
|
||||
|
||||
let environ = DummyFactory(client.clone());
|
||||
import_notifications.push(
|
||||
client
|
||||
.import_notification_stream()
|
||||
.take_while(|n| {
|
||||
future::ready(!(n.origin != BlockOrigin::Own && n.header.number() < &5))
|
||||
})
|
||||
.for_each(move |_| future::ready(())),
|
||||
);
|
||||
|
||||
let slot_duration = slot_duration(&*client).expect("slot duration available");
|
||||
|
||||
aura_futures.push(
|
||||
start_aura::<AuthorityPair, _, _, _, _, _, _, _, _, _, _>(StartAuraParams {
|
||||
slot_duration,
|
||||
block_import: client.clone(),
|
||||
select_chain,
|
||||
client,
|
||||
proposer_factory: environ,
|
||||
sync_oracle: DummyOracle,
|
||||
justification_sync_link: (),
|
||||
create_inherent_data_providers: |_, _| async {
|
||||
let slot = InherentDataProvider::from_timestamp_and_slot_duration(
|
||||
Timestamp::current(),
|
||||
SlotDuration::from_millis(SLOT_DURATION_MS),
|
||||
);
|
||||
|
||||
Ok((slot,))
|
||||
},
|
||||
force_authoring: false,
|
||||
backoff_authoring_blocks: Some(
|
||||
BackoffAuthoringOnFinalizedHeadLagging::default(),
|
||||
),
|
||||
keystore,
|
||||
block_proposal_slot_portion: SlotProportion::new(0.5),
|
||||
max_block_proposal_slot_portion: None,
|
||||
telemetry: None,
|
||||
compatibility_mode: CompatibilityMode::None,
|
||||
})
|
||||
.expect("Starts aura"),
|
||||
);
|
||||
}
|
||||
|
||||
future::select(
|
||||
future::poll_fn(move |cx| {
|
||||
net.lock().poll(cx);
|
||||
Poll::<()>::Pending
|
||||
}),
|
||||
future::select(future::join_all(aura_futures), future::join_all(import_notifications)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn current_node_authority_should_claim_slot() {
|
||||
let net = AuraTestNet::new(4);
|
||||
|
||||
let mut authorities = vec![
|
||||
Keyring::Alice.public().into(),
|
||||
Keyring::Bob.public().into(),
|
||||
Keyring::Charlie.public().into(),
|
||||
];
|
||||
|
||||
let keystore_path = tempfile::tempdir().expect("Creates keystore path");
|
||||
let keystore = LocalKeystore::open(keystore_path.path(), None).expect("Creates keystore.");
|
||||
let public = keystore
|
||||
.sr25519_generate_new(AuthorityPair::ID, None)
|
||||
.expect("Key should be created");
|
||||
authorities.push(public.into());
|
||||
|
||||
let net = Arc::new(Mutex::new(net));
|
||||
|
||||
let mut net = net.lock();
|
||||
let peer = net.peer(3);
|
||||
let client = peer.client().as_client();
|
||||
let environ = DummyFactory(client.clone());
|
||||
|
||||
let mut worker = AuraWorker {
|
||||
client: client.clone(),
|
||||
block_import: client,
|
||||
env: environ,
|
||||
keystore: keystore.into(),
|
||||
sync_oracle: DummyOracle,
|
||||
justification_sync_link: (),
|
||||
force_authoring: false,
|
||||
backoff_authoring_blocks: Some(BackoffAuthoringOnFinalizedHeadLagging::default()),
|
||||
telemetry: None,
|
||||
block_proposal_slot_portion: SlotProportion::new(0.5),
|
||||
max_block_proposal_slot_portion: None,
|
||||
compatibility_mode: Default::default(),
|
||||
_phantom: PhantomData::<fn() -> AuthorityPair>,
|
||||
};
|
||||
|
||||
let head = Header::new(
|
||||
1,
|
||||
H256::from_low_u64_be(0),
|
||||
H256::from_low_u64_be(0),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
);
|
||||
assert!(worker.claim_slot(&head, 0.into(), &authorities).await.is_none());
|
||||
assert!(worker.claim_slot(&head, 1.into(), &authorities).await.is_none());
|
||||
assert!(worker.claim_slot(&head, 2.into(), &authorities).await.is_none());
|
||||
assert!(worker.claim_slot(&head, 3.into(), &authorities).await.is_some());
|
||||
assert!(worker.claim_slot(&head, 4.into(), &authorities).await.is_none());
|
||||
assert!(worker.claim_slot(&head, 5.into(), &authorities).await.is_none());
|
||||
assert!(worker.claim_slot(&head, 6.into(), &authorities).await.is_none());
|
||||
assert!(worker.claim_slot(&head, 7.into(), &authorities).await.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn on_slot_returns_correct_block() {
|
||||
let net = AuraTestNet::new(4);
|
||||
|
||||
let keystore_path = tempfile::tempdir().expect("Creates keystore path");
|
||||
let keystore = LocalKeystore::open(keystore_path.path(), None).expect("Creates keystore.");
|
||||
keystore
|
||||
.sr25519_generate_new(AuthorityPair::ID, Some(&Keyring::Alice.to_seed()))
|
||||
.expect("Key should be created");
|
||||
|
||||
let net = Arc::new(Mutex::new(net));
|
||||
|
||||
let mut net = net.lock();
|
||||
let peer = net.peer(3);
|
||||
let client = peer.client().as_client();
|
||||
let environ = DummyFactory(client.clone());
|
||||
|
||||
let mut worker = AuraWorker {
|
||||
client: client.clone(),
|
||||
block_import: client.clone(),
|
||||
env: environ,
|
||||
keystore: keystore.into(),
|
||||
sync_oracle: DummyOracle,
|
||||
justification_sync_link: (),
|
||||
force_authoring: false,
|
||||
backoff_authoring_blocks: Option::<()>::None,
|
||||
telemetry: None,
|
||||
block_proposal_slot_portion: SlotProportion::new(0.5),
|
||||
max_block_proposal_slot_portion: None,
|
||||
compatibility_mode: Default::default(),
|
||||
_phantom: PhantomData::<fn() -> AuthorityPair>,
|
||||
};
|
||||
|
||||
let head = client.expect_header(client.info().genesis_hash).unwrap();
|
||||
|
||||
let res = worker
|
||||
.on_slot(SlotInfo {
|
||||
slot: 0.into(),
|
||||
ends_at: Instant::now() + Duration::from_secs(100),
|
||||
create_inherent_data: Box::new(()),
|
||||
duration: Duration::from_millis(1000),
|
||||
chain_head: head,
|
||||
block_size_limit: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The returned block should be imported and we should be able to get its header by now.
|
||||
assert!(client.header(res.block.hash()).unwrap().is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Standalone functions used within the implementation of Aura.
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use log::trace;
|
||||
|
||||
use codec::Codec;
|
||||
|
||||
use pezsc_client_api::UsageProvider;
|
||||
use pezsp_api::{Core, ProvideRuntimeApi};
|
||||
use pezsp_application_crypto::{AppCrypto, AppPublic};
|
||||
use pezsp_blockchain::Result as CResult;
|
||||
use pezsp_consensus::Error as ConsensusError;
|
||||
use pezsp_consensus_slots::Slot;
|
||||
use pezsp_core::crypto::{ByteArray, Pair};
|
||||
use pezsp_keystore::KeystorePtr;
|
||||
use pezsp_runtime::{
|
||||
traits::{Block as BlockT, Header, NumberFor, Zero},
|
||||
DigestItem,
|
||||
};
|
||||
|
||||
pub use pezsc_consensus_slots::check_equivocation;
|
||||
|
||||
use super::{
|
||||
AuraApi, AuthorityId, CompatibilityMode, CompatibleDigestItem, SlotDuration, LOG_TARGET,
|
||||
};
|
||||
|
||||
/// Get the slot duration for Aura by reading from a runtime API at the best block's state.
|
||||
pub fn slot_duration<A, B, C>(client: &C) -> CResult<SlotDuration>
|
||||
where
|
||||
A: Codec,
|
||||
B: BlockT,
|
||||
C: ProvideRuntimeApi<B> + UsageProvider<B>,
|
||||
C::Api: AuraApi<B, A>,
|
||||
{
|
||||
slot_duration_at(client, client.usage_info().chain.best_hash)
|
||||
}
|
||||
|
||||
/// Get the slot duration for Aura by reading from a runtime API at a given block's state.
|
||||
pub fn slot_duration_at<A, B, C>(client: &C, block_hash: B::Hash) -> CResult<SlotDuration>
|
||||
where
|
||||
A: Codec,
|
||||
B: BlockT,
|
||||
C: ProvideRuntimeApi<B>,
|
||||
C::Api: AuraApi<B, A>,
|
||||
{
|
||||
client.runtime_api().slot_duration(block_hash).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
/// Get the slot author for given block along with authorities.
|
||||
pub fn slot_author<P: Pair>(slot: Slot, authorities: &[AuthorityId<P>]) -> Option<&AuthorityId<P>> {
|
||||
if authorities.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let idx = *slot % (authorities.len() as u64);
|
||||
assert!(
|
||||
idx <= usize::MAX as u64,
|
||||
"It is impossible to have a vector with length beyond the address space; qed",
|
||||
);
|
||||
|
||||
let current_author = authorities.get(idx as usize).expect(
|
||||
"authorities not empty; index constrained to list length;this is a valid index; qed",
|
||||
);
|
||||
|
||||
Some(current_author)
|
||||
}
|
||||
|
||||
/// Attempt to claim a slot using a keystore.
|
||||
///
|
||||
/// This returns `None` if the slot author is not locally controlled, and `Some` if it is,
|
||||
/// with the public key of the slot author.
|
||||
pub async fn claim_slot<P: Pair>(
|
||||
slot: Slot,
|
||||
authorities: &[AuthorityId<P>],
|
||||
keystore: &KeystorePtr,
|
||||
) -> Option<P::Public> {
|
||||
let expected_author = slot_author::<P>(slot, authorities);
|
||||
expected_author.and_then(|p| {
|
||||
if keystore.has_keys(&[(p.to_raw_vec(), pezsp_application_crypto::key_types::AURA)]) {
|
||||
Some(p.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Produce the pre-runtime digest containing the slot info.
|
||||
///
|
||||
/// This is intended to be put into the block header prior to runtime execution,
|
||||
/// so the runtime can read the slot in this way.
|
||||
pub fn pre_digest<P: Pair>(slot: Slot) -> pezsp_runtime::DigestItem
|
||||
where
|
||||
P::Signature: Codec,
|
||||
{
|
||||
<DigestItem as CompatibleDigestItem<P::Signature>>::aura_pre_digest(slot)
|
||||
}
|
||||
|
||||
/// Produce the seal digest item by signing the hash of a block.
|
||||
///
|
||||
/// Note that after this is added to a block header, the hash of the block will change.
|
||||
pub fn seal<Hash, P>(
|
||||
header_hash: &Hash,
|
||||
public: &P::Public,
|
||||
keystore: &KeystorePtr,
|
||||
) -> Result<pezsp_runtime::DigestItem, ConsensusError>
|
||||
where
|
||||
Hash: AsRef<[u8]>,
|
||||
P: Pair,
|
||||
P::Signature: Codec + TryFrom<Vec<u8>>,
|
||||
P::Public: AppPublic,
|
||||
{
|
||||
let signature = keystore
|
||||
.sign_with(
|
||||
<AuthorityId<P> as AppCrypto>::ID,
|
||||
<AuthorityId<P> as AppCrypto>::CRYPTO_ID,
|
||||
public.as_slice(),
|
||||
header_hash.as_ref(),
|
||||
)
|
||||
.map_err(|e| ConsensusError::CannotSign(format!("{}. Key: {:?}", e, public)))?
|
||||
.ok_or_else(|| {
|
||||
ConsensusError::CannotSign(format!("Could not find key in keystore. Key: {:?}", public))
|
||||
})?;
|
||||
|
||||
let signature = signature
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| ConsensusError::InvalidSignature(signature, public.to_raw_vec()))?;
|
||||
|
||||
let signature_digest_item =
|
||||
<DigestItem as CompatibleDigestItem<P::Signature>>::aura_seal(signature);
|
||||
|
||||
Ok(signature_digest_item)
|
||||
}
|
||||
|
||||
/// Errors in pre-digest lookup.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PreDigestLookupError {
|
||||
/// Multiple Aura pre-runtime headers
|
||||
#[error("Multiple Aura pre-runtime headers")]
|
||||
MultipleHeaders,
|
||||
/// No Aura pre-runtime digest found
|
||||
#[error("No Aura pre-runtime digest found")]
|
||||
NoDigestFound,
|
||||
}
|
||||
|
||||
/// Extract a pre-digest from a block header.
|
||||
///
|
||||
/// This fails if there is no pre-digest or there are multiple.
|
||||
///
|
||||
/// Returns the `slot` stored in the pre-digest or an error if no pre-digest was found.
|
||||
pub fn find_pre_digest<B: BlockT, Signature: Codec>(
|
||||
header: &B::Header,
|
||||
) -> Result<Slot, PreDigestLookupError> {
|
||||
if header.number().is_zero() {
|
||||
return Ok(0.into());
|
||||
}
|
||||
|
||||
let mut pre_digest: Option<Slot> = None;
|
||||
for log in header.digest().logs() {
|
||||
trace!(target: LOG_TARGET, "Checking log {:?}", log);
|
||||
match (CompatibleDigestItem::<Signature>::as_aura_pre_digest(log), pre_digest.is_some()) {
|
||||
(Some(_), true) => return Err(PreDigestLookupError::MultipleHeaders),
|
||||
(None, _) => trace!(target: LOG_TARGET, "Ignoring digest not meant for us"),
|
||||
(s, false) => pre_digest = s,
|
||||
}
|
||||
}
|
||||
pre_digest.ok_or_else(|| PreDigestLookupError::NoDigestFound)
|
||||
}
|
||||
|
||||
/// Fetch the current set of authorities from the runtime at a specific block.
|
||||
///
|
||||
/// The compatibility mode and context block number informs this function whether
|
||||
/// to initialize the hypothetical block created by the runtime API as backwards compatibility
|
||||
/// for older chains.
|
||||
pub fn fetch_authorities_with_compatibility_mode<A, B, C>(
|
||||
client: &C,
|
||||
parent_hash: B::Hash,
|
||||
context_block_number: NumberFor<B>,
|
||||
compatibility_mode: &CompatibilityMode<NumberFor<B>>,
|
||||
) -> Result<Vec<A>, ConsensusError>
|
||||
where
|
||||
A: Codec + Debug,
|
||||
B: BlockT,
|
||||
C: ProvideRuntimeApi<B>,
|
||||
C::Api: AuraApi<B, A>,
|
||||
{
|
||||
let runtime_api = client.runtime_api();
|
||||
|
||||
match compatibility_mode {
|
||||
CompatibilityMode::None => {},
|
||||
// Use `initialize_block` until we hit the block that should disable the mode.
|
||||
CompatibilityMode::UseInitializeBlock { until } =>
|
||||
if *until > context_block_number {
|
||||
runtime_api
|
||||
.initialize_block(
|
||||
parent_hash,
|
||||
&B::Header::new(
|
||||
context_block_number,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
parent_hash,
|
||||
Default::default(),
|
||||
),
|
||||
)
|
||||
.map_err(|_| ConsensusError::InvalidAuthoritiesSet)?;
|
||||
},
|
||||
}
|
||||
|
||||
runtime_api
|
||||
.authorities(parent_hash)
|
||||
.ok()
|
||||
.ok_or(ConsensusError::InvalidAuthoritiesSet)
|
||||
}
|
||||
|
||||
/// Load the current set of authorities from a runtime at a specific block.
|
||||
pub fn fetch_authorities<A, B, C>(
|
||||
client: &C,
|
||||
parent_hash: B::Hash,
|
||||
) -> Result<Vec<A>, ConsensusError>
|
||||
where
|
||||
A: Codec + Debug,
|
||||
B: BlockT,
|
||||
C: ProvideRuntimeApi<B>,
|
||||
C::Api: AuraApi<B, A>,
|
||||
{
|
||||
client
|
||||
.runtime_api()
|
||||
.authorities(parent_hash)
|
||||
.ok()
|
||||
.ok_or(ConsensusError::InvalidAuthoritiesSet)
|
||||
}
|
||||
|
||||
/// Errors in slot and seal verification.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SealVerificationError<Header> {
|
||||
/// Header is deferred to the future.
|
||||
#[error("Header slot is in the future")]
|
||||
Deferred(Header, Slot),
|
||||
|
||||
/// The header has no seal digest.
|
||||
#[error("Header is unsealed.")]
|
||||
Unsealed,
|
||||
|
||||
/// The header has a malformed seal.
|
||||
#[error("Header has a malformed seal")]
|
||||
BadSeal,
|
||||
|
||||
/// The header has a bad signature.
|
||||
#[error("Header has a bad signature")]
|
||||
BadSignature,
|
||||
|
||||
/// No slot author found.
|
||||
#[error("No slot author for provided slot")]
|
||||
SlotAuthorNotFound,
|
||||
|
||||
/// Header has no valid slot pre-digest.
|
||||
#[error("Header has no valid slot pre-digest")]
|
||||
InvalidPreDigest(PreDigestLookupError),
|
||||
}
|
||||
|
||||
/// Check a header has been signed by the right key. If the slot is too far in the future, an error
|
||||
/// will be returned. If it's successful, returns the pre-header (i.e. without the seal),
|
||||
/// the slot, and the digest item containing the seal.
|
||||
///
|
||||
/// Note that this does not check for equivocations, and [`check_equivocation`] is recommended
|
||||
/// for that purpose.
|
||||
///
|
||||
/// This digest item will always return `Some` when used with `as_aura_seal`.
|
||||
pub fn check_header_slot_and_seal<B: BlockT, P: Pair>(
|
||||
slot_now: Slot,
|
||||
mut header: B::Header,
|
||||
authorities: &[AuthorityId<P>],
|
||||
) -> Result<(B::Header, Slot, DigestItem), SealVerificationError<B::Header>>
|
||||
where
|
||||
P::Signature: Codec,
|
||||
P::Public: Codec + PartialEq + Clone,
|
||||
{
|
||||
let seal = header.digest_mut().pop().ok_or(SealVerificationError::Unsealed)?;
|
||||
|
||||
let sig = seal.as_aura_seal().ok_or(SealVerificationError::BadSeal)?;
|
||||
|
||||
let slot = find_pre_digest::<B, P::Signature>(&header)
|
||||
.map_err(SealVerificationError::InvalidPreDigest)?;
|
||||
|
||||
if slot > slot_now {
|
||||
header.digest_mut().push(seal);
|
||||
return Err(SealVerificationError::Deferred(header, slot));
|
||||
} else {
|
||||
// check the signature is valid under the expected authority and
|
||||
// chain state.
|
||||
let expected_author =
|
||||
slot_author::<P>(slot, authorities).ok_or(SealVerificationError::SlotAuthorNotFound)?;
|
||||
|
||||
let pre_hash = header.hash();
|
||||
|
||||
if P::verify(&sig, pre_hash.as_ref(), expected_author) {
|
||||
Ok((header, slot, seal))
|
||||
} else {
|
||||
Err(SealVerificationError::BadSignature)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pezsp_keyring::sr25519::Keyring;
|
||||
|
||||
#[test]
|
||||
fn authorities_call_works() {
|
||||
let client = bizinikiwi_test_runtime_client::new();
|
||||
|
||||
assert_eq!(client.chain_info().best_number, 0);
|
||||
assert_eq!(
|
||||
fetch_authorities_with_compatibility_mode(
|
||||
&client,
|
||||
client.chain_info().best_hash,
|
||||
1,
|
||||
&CompatibilityMode::None
|
||||
)
|
||||
.unwrap(),
|
||||
vec![
|
||||
Keyring::Alice.public().into(),
|
||||
Keyring::Bob.public().into(),
|
||||
Keyring::Charlie.public().into()
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
fetch_authorities(&client, client.chain_info().best_hash).unwrap(),
|
||||
vec![
|
||||
Keyring::Alice.public().into(),
|
||||
Keyring::Bob.public().into(),
|
||||
Keyring::Charlie.public().into()
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
[package]
|
||||
name = "pezsc-consensus-babe"
|
||||
version = "0.34.0"
|
||||
authors.workspace = true
|
||||
description = "BABE consensus algorithm for bizinikiwi"
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
documentation = "https://docs.rs/pezsc-consensus-babe"
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
codec = { features = ["derive"], workspace = true, default-features = true }
|
||||
fork-tree = { workspace = true, default-features = true }
|
||||
futures = { workspace = true }
|
||||
log = { workspace = true, default-features = true }
|
||||
num-bigint = { workspace = true }
|
||||
num-rational = { workspace = true }
|
||||
num-traits = { workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
prometheus-endpoint = { workspace = true, default-features = true }
|
||||
pezsc-client-api = { workspace = true, default-features = true }
|
||||
pezsc-consensus = { workspace = true, default-features = true }
|
||||
pezsc-consensus-epochs = { workspace = true, default-features = true }
|
||||
pezsc-consensus-slots = { workspace = true, default-features = true }
|
||||
pezsc-telemetry = { workspace = true, default-features = true }
|
||||
pezsc-transaction-pool-api = { 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-babe = { workspace = true, default-features = true }
|
||||
pezsp-consensus-slots = { workspace = true, default-features = true }
|
||||
pezsp-core = { workspace = true, default-features = true }
|
||||
pezsp-crypto-hashing = { 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-timestamp = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezsc-block-builder = { workspace = true, default-features = true }
|
||||
pezsc-network-test = { workspace = true }
|
||||
pezsp-keyring = { workspace = true, default-features = true }
|
||||
pezsp-tracing = { workspace = true, default-features = true }
|
||||
bizinikiwi-test-runtime-client = { workspace = true }
|
||||
tokio = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezsc-block-builder/runtime-benchmarks",
|
||||
"pezsc-client-api/runtime-benchmarks",
|
||||
"pezsc-consensus-epochs/runtime-benchmarks",
|
||||
"pezsc-consensus-slots/runtime-benchmarks",
|
||||
"pezsc-consensus/runtime-benchmarks",
|
||||
"pezsc-network-test/runtime-benchmarks",
|
||||
"pezsc-transaction-pool-api/runtime-benchmarks",
|
||||
"pezsp-api/runtime-benchmarks",
|
||||
"pezsp-block-builder/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus-babe/runtime-benchmarks",
|
||||
"pezsp-consensus-slots/runtime-benchmarks",
|
||||
"pezsp-consensus/runtime-benchmarks",
|
||||
"pezsp-inherents/runtime-benchmarks",
|
||||
"pezsp-keyring/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"pezsp-timestamp/runtime-benchmarks",
|
||||
"bizinikiwi-test-runtime-client/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,48 @@
|
||||
# BABE (Blind Assignment for Blockchain Extension)
|
||||
|
||||
BABE is a slot-based block production mechanism which uses a VRF PRNG to
|
||||
randomly perform the slot allocation. On every slot, all the authorities
|
||||
generate a new random number with the VRF function and if it is lower than a
|
||||
given threshold (which is proportional to their weight/stake) they have a
|
||||
right to produce a block. The proof of the VRF function execution will be
|
||||
used by other peer to validate the legitimacy of the slot claim.
|
||||
|
||||
The engine is also responsible for collecting entropy on-chain which will be
|
||||
used to seed the given VRF PRNG. An epoch is a contiguous number of slots
|
||||
under which we will be using the same authority set. During an epoch all VRF
|
||||
outputs produced as a result of block production will be collected on an
|
||||
on-chain randomness pool. Epoch changes are announced one epoch in advance,
|
||||
i.e. when ending epoch N, we announce the parameters (randomness,
|
||||
authorities, etc.) for epoch N+2.
|
||||
|
||||
Since the slot assignment is randomized, it is possible that a slot is
|
||||
assigned to multiple validators in which case we will have a temporary fork,
|
||||
or that a slot is assigned to no validator in which case no block is
|
||||
produced. Which means that block times are not deterministic.
|
||||
|
||||
The protocol has a parameter `c` [0, 1] for which `1 - c` is the probability
|
||||
of a slot being empty. The choice of this parameter affects the security of
|
||||
the protocol relating to maximum tolerable network delays.
|
||||
|
||||
In addition to the VRF-based slot assignment described above, which we will
|
||||
call primary slots, the engine also supports a deterministic secondary slot
|
||||
assignment. Primary slots take precedence over secondary slots, when
|
||||
authoring the node starts by trying to claim a primary slot and falls back
|
||||
to a secondary slot claim attempt. The secondary slot assignment is done
|
||||
by picking the authority at index:
|
||||
|
||||
`blake2_256(epoch_randomness ++ slot_number) % authorities_len`.
|
||||
|
||||
The secondary slots supports either a `SecondaryPlain` or `SecondaryVRF`
|
||||
variant. Comparing with `SecondaryPlain` variant, the `SecondaryVRF` variant
|
||||
generates an additional VRF output. The output is not included in beacon
|
||||
randomness, but can be consumed by teyrchains.
|
||||
|
||||
The fork choice rule is weight-based, where weight equals the number of
|
||||
primary blocks in the chain. We will pick the heaviest chain (more primary
|
||||
blocks) and will go with the longest one in case of a tie.
|
||||
|
||||
An in-depth description and analysis of the protocol can be found here:
|
||||
<https://research.web3.foundation/PezkuwiChain/protocols/block-production/Babe>
|
||||
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,60 @@
|
||||
[package]
|
||||
name = "pezsc-consensus-babe-rpc"
|
||||
version = "0.34.0"
|
||||
authors.workspace = true
|
||||
description = "RPC extensions for the BABE consensus algorithm"
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
futures = { workspace = true }
|
||||
jsonrpsee = { features = [
|
||||
"client-core",
|
||||
"macros",
|
||||
"server-core",
|
||||
], workspace = true }
|
||||
pezsc-consensus-babe = { workspace = true, default-features = true }
|
||||
pezsc-consensus-epochs = { workspace = true, default-features = true }
|
||||
pezsc-rpc-api = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive"], workspace = true, default-features = true }
|
||||
pezsp-api = { workspace = true, default-features = true }
|
||||
pezsp-application-crypto = { workspace = true, default-features = true }
|
||||
pezsp-blockchain = { workspace = true, default-features = true }
|
||||
pezsp-consensus = { workspace = true, default-features = true }
|
||||
pezsp-consensus-babe = { workspace = true, default-features = true }
|
||||
pezsp-core = { workspace = true, default-features = true }
|
||||
pezsp-keystore = { workspace = true, default-features = true }
|
||||
pezsp-runtime = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezsc-consensus = { workspace = true, default-features = true }
|
||||
pezsc-transaction-pool-api = { workspace = true, default-features = true }
|
||||
pezsp-keyring = { workspace = true, default-features = true }
|
||||
bizinikiwi-test-runtime-client = { workspace = true }
|
||||
tokio = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezsc-consensus-babe/runtime-benchmarks",
|
||||
"pezsc-consensus-epochs/runtime-benchmarks",
|
||||
"pezsc-consensus/runtime-benchmarks",
|
||||
"pezsc-rpc-api/runtime-benchmarks",
|
||||
"pezsc-transaction-pool-api/runtime-benchmarks",
|
||||
"pezsp-api/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus-babe/runtime-benchmarks",
|
||||
"pezsp-consensus/runtime-benchmarks",
|
||||
"pezsp-keyring/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"bizinikiwi-test-runtime-client/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
RPC api for babe.
|
||||
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,282 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! RPC api for babe.
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use futures::TryFutureExt;
|
||||
use jsonrpsee::{
|
||||
core::async_trait,
|
||||
proc_macros::rpc,
|
||||
types::{ErrorObject, ErrorObjectOwned},
|
||||
Extensions,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use pezsc_consensus_babe::{authorship, BabeWorkerHandle};
|
||||
use pezsc_consensus_epochs::Epoch as EpochT;
|
||||
use pezsc_rpc_api::{check_if_safe, UnsafeRpcError};
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_application_crypto::AppCrypto;
|
||||
use pezsp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata};
|
||||
use pezsp_consensus::{Error as ConsensusError, SelectChain};
|
||||
use pezsp_consensus_babe::{digests::PreDigest, AuthorityId, BabeApi as BabeRuntimeApi};
|
||||
use pezsp_core::crypto::ByteArray;
|
||||
use pezsp_keystore::KeystorePtr;
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header as _};
|
||||
|
||||
const BABE_ERROR: i32 = 9000;
|
||||
|
||||
/// Provides rpc methods for interacting with Babe.
|
||||
#[rpc(client, server)]
|
||||
pub trait BabeApi {
|
||||
/// Returns data about which slots (primary or secondary) can be claimed in the current epoch
|
||||
/// with the keys in the keystore.
|
||||
#[method(name = "babe_epochAuthorship", with_extensions)]
|
||||
async fn epoch_authorship(&self) -> Result<HashMap<AuthorityId, EpochAuthorship>, Error>;
|
||||
}
|
||||
|
||||
/// Provides RPC methods for interacting with Babe.
|
||||
pub struct Babe<B: BlockT, C, SC> {
|
||||
/// shared reference to the client.
|
||||
client: Arc<C>,
|
||||
/// A handle to the BABE worker for issuing requests.
|
||||
babe_worker_handle: BabeWorkerHandle<B>,
|
||||
/// shared reference to the Keystore
|
||||
keystore: KeystorePtr,
|
||||
/// The SelectChain strategy
|
||||
select_chain: SC,
|
||||
}
|
||||
|
||||
impl<B: BlockT, C, SC> Babe<B, C, SC> {
|
||||
/// Creates a new instance of the Babe Rpc handler.
|
||||
pub fn new(
|
||||
client: Arc<C>,
|
||||
babe_worker_handle: BabeWorkerHandle<B>,
|
||||
keystore: KeystorePtr,
|
||||
select_chain: SC,
|
||||
) -> Self {
|
||||
Self { client, babe_worker_handle, keystore, select_chain }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<B: BlockT, C, SC> BabeApiServer for Babe<B, C, SC>
|
||||
where
|
||||
B: BlockT,
|
||||
C: ProvideRuntimeApi<B>
|
||||
+ HeaderBackend<B>
|
||||
+ HeaderMetadata<B, Error = BlockChainError>
|
||||
+ 'static,
|
||||
C::Api: BabeRuntimeApi<B>,
|
||||
SC: SelectChain<B> + Clone + 'static,
|
||||
{
|
||||
async fn epoch_authorship(
|
||||
&self,
|
||||
ext: &Extensions,
|
||||
) -> Result<HashMap<AuthorityId, EpochAuthorship>, Error> {
|
||||
check_if_safe(ext)?;
|
||||
|
||||
let best_header = self.select_chain.best_chain().map_err(Error::SelectChain).await?;
|
||||
|
||||
let epoch_start = self
|
||||
.client
|
||||
.runtime_api()
|
||||
.current_epoch_start(best_header.hash())
|
||||
.map_err(|_| Error::FetchEpoch)?;
|
||||
|
||||
let epoch = self
|
||||
.babe_worker_handle
|
||||
.epoch_data_for_child_of(best_header.hash(), *best_header.number(), epoch_start)
|
||||
.await
|
||||
.map_err(|_| Error::FetchEpoch)?;
|
||||
|
||||
let (epoch_start, epoch_end) = (epoch.start_slot(), epoch.end_slot());
|
||||
let mut claims: HashMap<AuthorityId, EpochAuthorship> = HashMap::new();
|
||||
|
||||
let keys = {
|
||||
epoch
|
||||
.authorities
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, a)| {
|
||||
if self.keystore.has_keys(&[(a.0.to_raw_vec(), AuthorityId::ID)]) {
|
||||
Some((a.0.clone(), i))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
for slot in *epoch_start..*epoch_end {
|
||||
if let Some((claim, key)) =
|
||||
authorship::claim_slot_using_keys(slot.into(), &epoch, &self.keystore, &keys)
|
||||
{
|
||||
match claim {
|
||||
PreDigest::Primary { .. } => {
|
||||
claims.entry(key).or_default().primary.push(slot);
|
||||
},
|
||||
PreDigest::SecondaryPlain { .. } => {
|
||||
claims.entry(key).or_default().secondary.push(slot);
|
||||
},
|
||||
PreDigest::SecondaryVRF { .. } => {
|
||||
claims.entry(key).or_default().secondary_vrf.push(slot.into());
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(claims)
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds information about the `slot`'s that can be claimed by a given key.
|
||||
#[derive(Clone, Default, Debug, Deserialize, Serialize)]
|
||||
pub struct EpochAuthorship {
|
||||
/// the array of primary slots that can be claimed
|
||||
primary: Vec<u64>,
|
||||
/// the array of secondary slots that can be claimed
|
||||
secondary: Vec<u64>,
|
||||
/// The array of secondary VRF slots that can be claimed.
|
||||
secondary_vrf: Vec<u64>,
|
||||
}
|
||||
|
||||
/// Top-level error type for the RPC handler.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Failed to fetch the current best header.
|
||||
#[error("Failed to fetch the current best header: {0}")]
|
||||
SelectChain(ConsensusError),
|
||||
/// Failed to fetch epoch data.
|
||||
#[error("Failed to fetch epoch data")]
|
||||
FetchEpoch,
|
||||
/// Consensus error
|
||||
#[error(transparent)]
|
||||
Consensus(#[from] ConsensusError),
|
||||
/// Errors that can be formatted as a String
|
||||
#[error("{0}")]
|
||||
StringError(String),
|
||||
/// Call to an unsafe RPC was denied.
|
||||
#[error(transparent)]
|
||||
UnsafeRpcCalled(#[from] UnsafeRpcError),
|
||||
}
|
||||
|
||||
impl From<Error> for ErrorObjectOwned {
|
||||
fn from(error: Error) -> Self {
|
||||
match error {
|
||||
Error::SelectChain(e) => ErrorObject::owned(BABE_ERROR + 1, e.to_string(), None::<()>),
|
||||
Error::FetchEpoch => ErrorObject::owned(BABE_ERROR + 2, error.to_string(), None::<()>),
|
||||
Error::Consensus(e) => ErrorObject::owned(BABE_ERROR + 3, e.to_string(), None::<()>),
|
||||
Error::StringError(e) => ErrorObject::owned(BABE_ERROR + 4, e, None::<()>),
|
||||
Error::UnsafeRpcCalled(e) => e.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pezsc_consensus_babe::ImportQueueParams;
|
||||
use pezsc_rpc_api::DenyUnsafe;
|
||||
use pezsc_transaction_pool_api::{OffchainTransactionPoolFactory, RejectAllTxPool};
|
||||
use pezsp_consensus_babe::inherents::InherentDataProvider;
|
||||
use pezsp_core::{crypto::key_types::BABE, testing::TaskExecutor};
|
||||
use pezsp_keyring::Sr25519Keyring;
|
||||
use pezsp_keystore::{testing::MemoryKeystore, Keystore};
|
||||
use bizinikiwi_test_runtime_client::{
|
||||
runtime::Block, Backend, DefaultTestClientBuilderExt, TestClient, TestClientBuilder,
|
||||
TestClientBuilderExt,
|
||||
};
|
||||
|
||||
fn create_keystore(authority: Sr25519Keyring) -> KeystorePtr {
|
||||
let keystore = MemoryKeystore::new();
|
||||
keystore
|
||||
.sr25519_generate_new(BABE, Some(&authority.to_seed()))
|
||||
.expect("Creates authority key");
|
||||
keystore.into()
|
||||
}
|
||||
|
||||
fn test_babe_rpc_module() -> Babe<Block, TestClient, pezsc_consensus::LongestChain<Backend, Block>>
|
||||
{
|
||||
let builder = TestClientBuilder::new();
|
||||
let (client, longest_chain) = builder.build_with_longest_chain();
|
||||
let client = Arc::new(client);
|
||||
let task_executor = TaskExecutor::new();
|
||||
let keystore = create_keystore(Sr25519Keyring::Alice);
|
||||
|
||||
let config = pezsc_consensus_babe::configuration(&*client).expect("config available");
|
||||
let slot_duration = config.slot_duration();
|
||||
|
||||
let (block_import, link) = pezsc_consensus_babe::block_import(
|
||||
config.clone(),
|
||||
client.clone(),
|
||||
client.clone(),
|
||||
move |_, _| async move {
|
||||
Ok((InherentDataProvider::from_timestamp_and_slot_duration(
|
||||
0.into(),
|
||||
slot_duration,
|
||||
),))
|
||||
},
|
||||
longest_chain.clone(),
|
||||
OffchainTransactionPoolFactory::new(RejectAllTxPool::default()),
|
||||
)
|
||||
.expect("can initialize block-import");
|
||||
|
||||
let (_, babe_worker_handle) = pezsc_consensus_babe::import_queue(ImportQueueParams {
|
||||
link: link.clone(),
|
||||
block_import: block_import.clone(),
|
||||
justification_import: None,
|
||||
client: client.clone(),
|
||||
slot_duration,
|
||||
spawner: &task_executor,
|
||||
registry: None,
|
||||
telemetry: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Babe::new(client.clone(), babe_worker_handle, keystore, longest_chain)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn epoch_authorship_works() {
|
||||
let babe_rpc = test_babe_rpc_module();
|
||||
let mut api = babe_rpc.into_rpc();
|
||||
api.extensions_mut().insert(DenyUnsafe::No);
|
||||
|
||||
let request = r#"{"jsonrpc":"2.0","id":1,"method":"babe_epochAuthorship","params":[]}"#;
|
||||
let (response, _) = api.raw_json_request(request, 1).await.unwrap();
|
||||
let expected = r#"{"jsonrpc":"2.0","id":1,"result":{"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY":{"primary":[0],"secondary":[],"secondary_vrf":[1,2,4]}}}"#;
|
||||
|
||||
assert_eq!(response, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn epoch_authorship_is_unsafe() {
|
||||
let babe_rpc = test_babe_rpc_module();
|
||||
let mut api = babe_rpc.into_rpc();
|
||||
api.extensions_mut().insert(DenyUnsafe::Yes);
|
||||
|
||||
let request = r#"{"jsonrpc":"2.0","method":"babe_epochAuthorship","params":[],"id":1}"#;
|
||||
let (response, _) = api.raw_json_request(request, 1).await.unwrap();
|
||||
let expected = r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"RPC call is unsafe to be called externally"}}"#;
|
||||
|
||||
assert_eq!(response, expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! BABE authority selection and slot claiming.
|
||||
|
||||
use super::{Epoch, AUTHORING_SCORE_LENGTH, AUTHORING_SCORE_VRF_CONTEXT};
|
||||
use codec::Encode;
|
||||
use pezsc_consensus_epochs::Epoch as EpochT;
|
||||
use pezsp_application_crypto::AppCrypto;
|
||||
use pezsp_consensus_babe::{
|
||||
digests::{PreDigest, PrimaryPreDigest, SecondaryPlainPreDigest, SecondaryVRFPreDigest},
|
||||
make_vrf_sign_data, AuthorityId, BabeAuthorityWeight, Randomness, Slot,
|
||||
};
|
||||
use pezsp_core::{
|
||||
crypto::{ByteArray, Wraps},
|
||||
U256,
|
||||
};
|
||||
use pezsp_keystore::KeystorePtr;
|
||||
|
||||
/// Calculates the primary selection threshold for a given authority, taking
|
||||
/// into account `c` (`1 - c` represents the probability of a slot being empty).
|
||||
pub(super) fn calculate_primary_threshold(
|
||||
c: (u64, u64),
|
||||
authorities: &[(AuthorityId, BabeAuthorityWeight)],
|
||||
authority_index: usize,
|
||||
) -> u128 {
|
||||
use num_bigint::BigUint;
|
||||
use num_rational::BigRational;
|
||||
use num_traits::{cast::ToPrimitive, identities::One};
|
||||
|
||||
// Prevent div by zero and out of bounds access.
|
||||
// While Babe's pallet implementation that ships with FRAME performs a sanity check over
|
||||
// configuration parameters, this is not sufficient to guarantee that `c.1` is non-zero
|
||||
// (i.e. third party implementations are possible).
|
||||
if c.1 == 0 || authority_index >= authorities.len() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let c = c.0 as f64 / c.1 as f64;
|
||||
|
||||
let theta = authorities[authority_index].1 as f64 /
|
||||
authorities.iter().map(|(_, weight)| weight).sum::<u64>() as f64;
|
||||
|
||||
assert!(theta > 0.0, "authority with weight 0.");
|
||||
|
||||
// NOTE: in the equation `p = 1 - (1 - c)^theta` the value of `p` is always
|
||||
// capped by `c`. For all practical purposes `c` should always be set to a
|
||||
// value < 0.5, as such in the computations below we should never be near
|
||||
// edge cases like `0.999999`.
|
||||
|
||||
let p = BigRational::from_float(1f64 - (1f64 - c).powf(theta)).expect(
|
||||
"returns None when the given value is not finite; \
|
||||
c is a configuration parameter defined in (0, 1]; \
|
||||
theta must be > 0 if the given authority's weight is > 0; \
|
||||
theta represents the validator's relative weight defined in (0, 1]; \
|
||||
powf will always return values in (0, 1] given both the \
|
||||
base and exponent are in that domain; \
|
||||
qed.",
|
||||
);
|
||||
|
||||
let numer = p.numer().to_biguint().expect(
|
||||
"returns None when the given value is negative; \
|
||||
p is defined as `1 - n` where n is defined in (0, 1]; \
|
||||
p must be a value in [0, 1); \
|
||||
qed.",
|
||||
);
|
||||
|
||||
let denom = p.denom().to_biguint().expect(
|
||||
"returns None when the given value is negative; \
|
||||
p is defined as `1 - n` where n is defined in (0, 1]; \
|
||||
p must be a value in [0, 1); \
|
||||
qed.",
|
||||
);
|
||||
|
||||
((BigUint::one() << 128usize) * numer / denom).to_u128().expect(
|
||||
"returns None if the underlying value cannot be represented with 128 bits; \
|
||||
we start with 2^128 which is one more than can be represented with 128 bits; \
|
||||
we multiple by p which is defined in [0, 1); \
|
||||
the result must be lower than 2^128 by at least one and thus representable with 128 bits; \
|
||||
qed.",
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the expected secondary author for the given slot and with given
|
||||
/// authorities. This should always assign the slot to some authority unless the
|
||||
/// authorities list is empty.
|
||||
pub(super) fn secondary_slot_author(
|
||||
slot: Slot,
|
||||
authorities: &[(AuthorityId, BabeAuthorityWeight)],
|
||||
randomness: Randomness,
|
||||
) -> Option<&AuthorityId> {
|
||||
if authorities.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let rand =
|
||||
U256::from_big_endian(&(randomness, slot).using_encoded(pezsp_crypto_hashing::blake2_256));
|
||||
|
||||
let authorities_len = U256::from(authorities.len());
|
||||
let idx = rand % authorities_len;
|
||||
|
||||
let expected_author = authorities.get(idx.as_u32() as usize).expect(
|
||||
"authorities not empty; index constrained to list length; \
|
||||
this is a valid index; qed",
|
||||
);
|
||||
|
||||
Some(&expected_author.0)
|
||||
}
|
||||
|
||||
/// Claim a secondary slot if it is our turn to propose, returning the
|
||||
/// pre-digest to use when authoring the block, or `None` if it is not our turn
|
||||
/// to propose.
|
||||
fn claim_secondary_slot(
|
||||
slot: Slot,
|
||||
epoch: &Epoch,
|
||||
keys: &[(AuthorityId, usize)],
|
||||
keystore: &KeystorePtr,
|
||||
author_secondary_vrf: bool,
|
||||
) -> Option<(PreDigest, AuthorityId)> {
|
||||
if epoch.authorities.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut epoch_index = epoch.epoch_index;
|
||||
if epoch.end_slot() <= slot {
|
||||
// Slot doesn't strictly belong to the epoch, create a clone with fixed values.
|
||||
epoch_index = epoch.clone_for_slot(slot).epoch_index;
|
||||
}
|
||||
|
||||
let expected_author = secondary_slot_author(slot, &epoch.authorities, epoch.randomness)?;
|
||||
|
||||
for (authority_id, authority_index) in keys {
|
||||
if authority_id == expected_author {
|
||||
let pre_digest = if author_secondary_vrf {
|
||||
let data = make_vrf_sign_data(&epoch.randomness, slot, epoch_index);
|
||||
let result =
|
||||
keystore.sr25519_vrf_sign(AuthorityId::ID, authority_id.as_ref(), &data);
|
||||
if let Ok(Some(vrf_signature)) = result {
|
||||
Some(PreDigest::SecondaryVRF(SecondaryVRFPreDigest {
|
||||
slot,
|
||||
authority_index: *authority_index as u32,
|
||||
vrf_signature,
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if keystore.has_keys(&[(authority_id.to_raw_vec(), AuthorityId::ID)]) {
|
||||
Some(PreDigest::SecondaryPlain(SecondaryPlainPreDigest {
|
||||
slot,
|
||||
authority_index: *authority_index as u32,
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(pre_digest) = pre_digest {
|
||||
return Some((pre_digest, authority_id.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Tries to claim the given slot number. This method starts by trying to claim
|
||||
/// a primary VRF based slot. If we are not able to claim it, then if we have
|
||||
/// secondary slots enabled for the given epoch, we will fallback to trying to
|
||||
/// claim a secondary slot.
|
||||
pub fn claim_slot(
|
||||
slot: Slot,
|
||||
epoch: &Epoch,
|
||||
keystore: &KeystorePtr,
|
||||
) -> Option<(PreDigest, AuthorityId)> {
|
||||
let authorities = epoch
|
||||
.authorities
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, a)| (a.0.clone(), index))
|
||||
.collect::<Vec<_>>();
|
||||
claim_slot_using_keys(slot, epoch, keystore, &authorities)
|
||||
}
|
||||
|
||||
/// Like `claim_slot`, but allows passing an explicit set of key pairs. Useful if we intend
|
||||
/// to make repeated calls for different slots using the same key pairs.
|
||||
pub fn claim_slot_using_keys(
|
||||
slot: Slot,
|
||||
epoch: &Epoch,
|
||||
keystore: &KeystorePtr,
|
||||
keys: &[(AuthorityId, usize)],
|
||||
) -> Option<(PreDigest, AuthorityId)> {
|
||||
claim_primary_slot(slot, epoch, epoch.config.c, keystore, keys).or_else(|| {
|
||||
if epoch.config.allowed_slots.is_secondary_plain_slots_allowed() ||
|
||||
epoch.config.allowed_slots.is_secondary_vrf_slots_allowed()
|
||||
{
|
||||
claim_secondary_slot(
|
||||
slot,
|
||||
epoch,
|
||||
keys,
|
||||
keystore,
|
||||
epoch.config.allowed_slots.is_secondary_vrf_slots_allowed(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Claim a primary slot if it is our turn. Returns `None` if it is not our turn.
|
||||
/// This hashes the slot number, epoch, genesis hash, and chain randomness into
|
||||
/// the VRF. If the VRF produces a value less than `threshold`, it is our turn,
|
||||
/// so it returns `Some(_)`. Otherwise, it returns `None`.
|
||||
fn claim_primary_slot(
|
||||
slot: Slot,
|
||||
epoch: &Epoch,
|
||||
c: (u64, u64),
|
||||
keystore: &KeystorePtr,
|
||||
keys: &[(AuthorityId, usize)],
|
||||
) -> Option<(PreDigest, AuthorityId)> {
|
||||
let mut epoch_index = epoch.epoch_index;
|
||||
if epoch.end_slot() <= slot {
|
||||
// Slot doesn't strictly belong to the epoch, create a clone with fixed values.
|
||||
epoch_index = epoch.clone_for_slot(slot).epoch_index;
|
||||
}
|
||||
|
||||
let data = make_vrf_sign_data(&epoch.randomness, slot, epoch_index);
|
||||
|
||||
for (authority_id, authority_index) in keys {
|
||||
let result = keystore.sr25519_vrf_sign(AuthorityId::ID, authority_id.as_ref(), &data);
|
||||
if let Ok(Some(vrf_signature)) = result {
|
||||
let threshold = calculate_primary_threshold(c, &epoch.authorities, *authority_index);
|
||||
|
||||
let can_claim = authority_id
|
||||
.as_inner_ref()
|
||||
.make_bytes::<AUTHORING_SCORE_LENGTH>(
|
||||
AUTHORING_SCORE_VRF_CONTEXT,
|
||||
&data.as_ref(),
|
||||
&vrf_signature.pre_output,
|
||||
)
|
||||
.map(|bytes| u128::from_le_bytes(bytes) < threshold)
|
||||
.unwrap_or_default();
|
||||
|
||||
if can_claim {
|
||||
let pre_digest = PreDigest::Primary(PrimaryPreDigest {
|
||||
slot,
|
||||
authority_index: *authority_index as u32,
|
||||
vrf_signature,
|
||||
});
|
||||
|
||||
return Some((pre_digest, authority_id.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pezsp_consensus_babe::{
|
||||
AllowedSlots, AuthorityId, BabeEpochConfiguration, Epoch, RANDOMNESS_LENGTH,
|
||||
};
|
||||
use pezsp_core::{crypto::Pair as _, sr25519::Pair};
|
||||
use pezsp_keystore::testing::MemoryKeystore;
|
||||
|
||||
#[test]
|
||||
fn claim_secondary_plain_slot_works() {
|
||||
let keystore: KeystorePtr = MemoryKeystore::new().into();
|
||||
let valid_public_key = keystore
|
||||
.sr25519_generate_new(AuthorityId::ID, Some(pezsp_core::crypto::DEV_PHRASE))
|
||||
.unwrap();
|
||||
|
||||
let authorities = vec![
|
||||
(AuthorityId::from(Pair::generate().0.public()), 5),
|
||||
(AuthorityId::from(Pair::generate().0.public()), 7),
|
||||
];
|
||||
|
||||
let mut epoch = Epoch {
|
||||
epoch_index: 10,
|
||||
start_slot: 0.into(),
|
||||
duration: 20,
|
||||
authorities: authorities.clone(),
|
||||
randomness: Default::default(),
|
||||
config: BabeEpochConfiguration {
|
||||
c: (3, 10),
|
||||
allowed_slots: AllowedSlots::PrimaryAndSecondaryPlainSlots,
|
||||
},
|
||||
}
|
||||
.into();
|
||||
|
||||
assert!(claim_slot(10.into(), &epoch, &keystore).is_none());
|
||||
|
||||
epoch.authorities.push((valid_public_key.into(), 10));
|
||||
assert_eq!(claim_slot(10.into(), &epoch, &keystore).unwrap().1, valid_public_key.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secondary_slot_author_selection_works() {
|
||||
let authorities = (0..1000)
|
||||
.map(|i| (AuthorityId::from(Pair::generate().0.public()), i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let randomness = [3; RANDOMNESS_LENGTH];
|
||||
|
||||
assert_eq!(
|
||||
*secondary_slot_author(100.into(), &authorities, randomness).unwrap(),
|
||||
authorities[167].0
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Schema for BABE epoch changes in the aux-db.
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use log::info;
|
||||
|
||||
use crate::{migration::EpochV0, Epoch, LOG_TARGET};
|
||||
use pezsc_client_api::backend::AuxStore;
|
||||
use pezsc_consensus_epochs::{
|
||||
migration::{EpochChangesV0For, EpochChangesV1For},
|
||||
EpochChangesFor, SharedEpochChanges,
|
||||
};
|
||||
use pezsp_blockchain::{Error as ClientError, Result as ClientResult};
|
||||
use pezsp_consensus_babe::{BabeBlockWeight, BabeConfiguration};
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
const BABE_EPOCH_CHANGES_VERSION: &[u8] = b"babe_epoch_changes_version";
|
||||
const BABE_EPOCH_CHANGES_KEY: &[u8] = b"babe_epoch_changes";
|
||||
const BABE_EPOCH_CHANGES_CURRENT_VERSION: u32 = 3;
|
||||
|
||||
/// The aux storage key used to store the block weight of the given block hash.
|
||||
pub fn block_weight_key<H: Encode>(block_hash: H) -> Vec<u8> {
|
||||
(b"block_weight", block_hash).encode()
|
||||
}
|
||||
|
||||
fn load_decode<B, T>(backend: &B, key: &[u8]) -> ClientResult<Option<T>>
|
||||
where
|
||||
B: AuxStore,
|
||||
T: Decode,
|
||||
{
|
||||
let corrupt = |e: codec::Error| {
|
||||
ClientError::Backend(format!("BABE DB is corrupted. Decode error: {}", e))
|
||||
};
|
||||
match backend.get_aux(key)? {
|
||||
None => Ok(None),
|
||||
Some(t) => T::decode(&mut &t[..]).map(Some).map_err(corrupt),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load or initialize persistent epoch change data from backend.
|
||||
pub fn load_epoch_changes<Block: BlockT, B: AuxStore>(
|
||||
backend: &B,
|
||||
config: &BabeConfiguration,
|
||||
) -> ClientResult<SharedEpochChanges<Block, Epoch>> {
|
||||
let version = load_decode::<_, u32>(backend, BABE_EPOCH_CHANGES_VERSION)?;
|
||||
|
||||
let maybe_epoch_changes = match version {
|
||||
None =>
|
||||
load_decode::<_, EpochChangesV0For<Block, EpochV0>>(backend, BABE_EPOCH_CHANGES_KEY)?
|
||||
.map(|v0| v0.migrate().map(|_, _, epoch| epoch.migrate(config))),
|
||||
Some(1) =>
|
||||
load_decode::<_, EpochChangesV1For<Block, EpochV0>>(backend, BABE_EPOCH_CHANGES_KEY)?
|
||||
.map(|v1| v1.migrate().map(|_, _, epoch| epoch.migrate(config))),
|
||||
Some(2) => {
|
||||
// v2 still uses `EpochChanges` v1 format but with a different `Epoch` type.
|
||||
load_decode::<_, EpochChangesV1For<Block, Epoch>>(backend, BABE_EPOCH_CHANGES_KEY)?
|
||||
.map(|v2| v2.migrate())
|
||||
},
|
||||
Some(BABE_EPOCH_CHANGES_CURRENT_VERSION) =>
|
||||
load_decode::<_, EpochChangesFor<Block, Epoch>>(backend, BABE_EPOCH_CHANGES_KEY)?,
|
||||
Some(other) =>
|
||||
return Err(ClientError::Backend(format!("Unsupported BABE DB version: {:?}", other))),
|
||||
};
|
||||
|
||||
let epoch_changes =
|
||||
SharedEpochChanges::<Block, Epoch>::new(maybe_epoch_changes.unwrap_or_else(|| {
|
||||
info!(
|
||||
target: LOG_TARGET,
|
||||
"👶 Creating empty BABE epoch changes on what appears to be first startup.",
|
||||
);
|
||||
EpochChangesFor::<Block, Epoch>::default()
|
||||
}));
|
||||
|
||||
// rebalance the tree after deserialization. this isn't strictly necessary
|
||||
// since the tree is now rebalanced on every update operation. but since the
|
||||
// tree wasn't rebalanced initially it's useful to temporarily leave it here
|
||||
// to avoid having to wait until an import for rebalancing.
|
||||
epoch_changes.shared_data().rebalance();
|
||||
|
||||
Ok(epoch_changes)
|
||||
}
|
||||
|
||||
/// Update the epoch changes on disk after a change.
|
||||
pub(crate) fn write_epoch_changes<Block: BlockT, F, R>(
|
||||
epoch_changes: &EpochChangesFor<Block, Epoch>,
|
||||
write_aux: F,
|
||||
) -> R
|
||||
where
|
||||
F: FnOnce(&[(&'static [u8], &[u8])]) -> R,
|
||||
{
|
||||
BABE_EPOCH_CHANGES_CURRENT_VERSION.using_encoded(|version| {
|
||||
let encoded_epoch_changes = epoch_changes.encode();
|
||||
write_aux(&[
|
||||
(BABE_EPOCH_CHANGES_KEY, encoded_epoch_changes.as_slice()),
|
||||
(BABE_EPOCH_CHANGES_VERSION, version),
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
/// Write the cumulative chain-weight of a block ot aux storage.
|
||||
pub(crate) fn write_block_weight<H: Encode, F, R>(
|
||||
block_hash: H,
|
||||
block_weight: BabeBlockWeight,
|
||||
write_aux: F,
|
||||
) -> R
|
||||
where
|
||||
F: FnOnce(&[(Vec<u8>, &[u8])]) -> R,
|
||||
{
|
||||
let key = block_weight_key(block_hash);
|
||||
block_weight.using_encoded(|s| write_aux(&[(key, s)]))
|
||||
}
|
||||
|
||||
/// Load the cumulative chain-weight associated with a block.
|
||||
pub fn load_block_weight<H: Encode, B: AuxStore>(
|
||||
backend: &B,
|
||||
block_hash: H,
|
||||
) -> ClientResult<Option<BabeBlockWeight>> {
|
||||
load_decode(backend, block_weight_key(block_hash).as_slice())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::migration::EpochV0;
|
||||
use fork_tree::ForkTree;
|
||||
use pezsc_consensus_epochs::{EpochHeader, PersistedEpoch, PersistedEpochHeader};
|
||||
use pezsc_network_test::Block as TestBlock;
|
||||
use pezsp_consensus::Error as ConsensusError;
|
||||
use pezsp_consensus_babe::AllowedSlots;
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::traits::NumberFor;
|
||||
use bizinikiwi_test_runtime_client;
|
||||
|
||||
#[test]
|
||||
fn load_decode_from_v0_epoch_changes() {
|
||||
let epoch = EpochV0 {
|
||||
start_slot: 0.into(),
|
||||
authorities: vec![],
|
||||
randomness: [0; 32],
|
||||
epoch_index: 1,
|
||||
duration: 100,
|
||||
};
|
||||
let client = bizinikiwi_test_runtime_client::new();
|
||||
let mut v0_tree = ForkTree::<H256, NumberFor<TestBlock>, _>::new();
|
||||
v0_tree
|
||||
.import::<_, ConsensusError>(
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
PersistedEpoch::Regular(epoch),
|
||||
&|_, _| Ok(false), // Test is single item only so this can be set to false.
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
.insert_aux(
|
||||
&[(
|
||||
BABE_EPOCH_CHANGES_KEY,
|
||||
&EpochChangesV0For::<TestBlock, EpochV0>::from_raw(v0_tree).encode()[..],
|
||||
)],
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(load_decode::<_, u32>(&client, BABE_EPOCH_CHANGES_VERSION).unwrap(), None);
|
||||
|
||||
let epoch_changes = load_epoch_changes::<TestBlock, _>(
|
||||
&client,
|
||||
&BabeConfiguration {
|
||||
slot_duration: 10,
|
||||
epoch_length: 4,
|
||||
c: (3, 10),
|
||||
authorities: Vec::new(),
|
||||
randomness: Default::default(),
|
||||
allowed_slots: AllowedSlots::PrimaryAndSecondaryPlainSlots,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
epoch_changes
|
||||
.shared_data()
|
||||
.tree()
|
||||
.iter()
|
||||
.map(|(_, _, epoch)| epoch.clone())
|
||||
.collect::<Vec<_>>() ==
|
||||
vec![PersistedEpochHeader::Regular(EpochHeader {
|
||||
start_slot: 0.into(),
|
||||
end_slot: 100.into(),
|
||||
})],
|
||||
); // PersistedEpochHeader does not implement Debug, so we use assert! directly.
|
||||
|
||||
write_epoch_changes::<TestBlock, _, _>(&epoch_changes.shared_data(), |values| {
|
||||
client.insert_aux(values, &[]).unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(load_decode::<_, u32>(&client, BABE_EPOCH_CHANGES_VERSION).unwrap(), Some(3));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{
|
||||
AuthorityId, BabeAuthorityWeight, BabeConfiguration, BabeEpochConfiguration, Epoch,
|
||||
NextEpochDescriptor, Randomness,
|
||||
};
|
||||
use codec::{Decode, Encode};
|
||||
use pezsc_consensus_epochs::Epoch as EpochT;
|
||||
use pezsp_consensus_slots::Slot;
|
||||
|
||||
/// BABE epoch information, version 0.
|
||||
#[derive(Decode, Encode, PartialEq, Eq, Clone, Debug)]
|
||||
pub struct EpochV0 {
|
||||
/// The epoch index.
|
||||
pub epoch_index: u64,
|
||||
/// The starting slot of the epoch.
|
||||
pub start_slot: Slot,
|
||||
/// The duration of this epoch.
|
||||
pub duration: u64,
|
||||
/// The authorities and their weights.
|
||||
pub authorities: Vec<(AuthorityId, BabeAuthorityWeight)>,
|
||||
/// Randomness for this epoch.
|
||||
pub randomness: Randomness,
|
||||
}
|
||||
|
||||
impl EpochT for EpochV0 {
|
||||
type NextEpochDescriptor = NextEpochDescriptor;
|
||||
type Slot = Slot;
|
||||
|
||||
fn increment(&self, descriptor: NextEpochDescriptor) -> EpochV0 {
|
||||
EpochV0 {
|
||||
epoch_index: self.epoch_index + 1,
|
||||
start_slot: self.start_slot + self.duration,
|
||||
duration: self.duration,
|
||||
authorities: descriptor.authorities,
|
||||
randomness: descriptor.randomness,
|
||||
}
|
||||
}
|
||||
|
||||
fn start_slot(&self) -> Slot {
|
||||
self.start_slot
|
||||
}
|
||||
|
||||
fn end_slot(&self) -> Slot {
|
||||
self.start_slot + self.duration
|
||||
}
|
||||
}
|
||||
|
||||
// Implement From<EpochV0> for Epoch
|
||||
impl EpochV0 {
|
||||
/// Migrate the struct to current epoch version.
|
||||
pub fn migrate(self, config: &BabeConfiguration) -> Epoch {
|
||||
pezsp_consensus_babe::Epoch {
|
||||
epoch_index: self.epoch_index,
|
||||
start_slot: self.start_slot,
|
||||
duration: self.duration,
|
||||
authorities: self.authorities,
|
||||
randomness: self.randomness,
|
||||
config: BabeEpochConfiguration { c: config.c, allowed_slots: config.allowed_slots },
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,263 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Verification for BABE headers.
|
||||
use crate::{
|
||||
authorship::{calculate_primary_threshold, secondary_slot_author},
|
||||
babe_err, find_pre_digest, BlockT, Epoch, Error, AUTHORING_SCORE_LENGTH,
|
||||
AUTHORING_SCORE_VRF_CONTEXT, LOG_TARGET,
|
||||
};
|
||||
use log::{debug, trace};
|
||||
use pezsc_consensus_epochs::Epoch as EpochT;
|
||||
use pezsc_consensus_slots::CheckedHeader;
|
||||
use pezsp_consensus_babe::{
|
||||
digests::{
|
||||
CompatibleDigestItem, PreDigest, PrimaryPreDigest, SecondaryPlainPreDigest,
|
||||
SecondaryVRFPreDigest,
|
||||
},
|
||||
make_vrf_sign_data, AuthorityPair, AuthoritySignature,
|
||||
};
|
||||
use pezsp_consensus_slots::Slot;
|
||||
use pezsp_core::{
|
||||
crypto::{VrfPublic, Wraps},
|
||||
Pair,
|
||||
};
|
||||
use pezsp_runtime::{traits::Header, DigestItem};
|
||||
|
||||
/// BABE verification parameters
|
||||
pub(super) struct VerificationParams<'a, B: 'a + BlockT> {
|
||||
/// The header being verified.
|
||||
pub(super) header: B::Header,
|
||||
/// The pre-digest of the header being verified. this is optional - if prior
|
||||
/// verification code had to read it, it can be included here to avoid duplicate
|
||||
/// work.
|
||||
pub(super) pre_digest: Option<PreDigest>,
|
||||
/// The slot number of the current time.
|
||||
pub(super) slot_now: Slot,
|
||||
/// Epoch descriptor of the epoch this block _should_ be under, if it's valid.
|
||||
pub(super) epoch: &'a Epoch,
|
||||
}
|
||||
|
||||
/// Check a header has been signed by the right key. If the slot is too far in
|
||||
/// the future, an error will be returned. If successful, returns the pre-header
|
||||
/// and the digest item containing the seal.
|
||||
///
|
||||
/// The seal must be the last digest. Otherwise, the whole header is considered
|
||||
/// unsigned. This is required for security and must not be changed.
|
||||
///
|
||||
/// This digest item will always return `Some` when used with `as_babe_pre_digest`.
|
||||
///
|
||||
/// The given header can either be from a primary or secondary slot assignment,
|
||||
/// with each having different validation logic.
|
||||
pub(super) fn check_header<B: BlockT + Sized>(
|
||||
params: VerificationParams<B>,
|
||||
) -> Result<CheckedHeader<B::Header, VerifiedHeaderInfo>, Error<B>> {
|
||||
let VerificationParams { mut header, pre_digest, slot_now, epoch } = params;
|
||||
|
||||
let pre_digest = pre_digest.map(Ok).unwrap_or_else(|| find_pre_digest::<B>(&header))?;
|
||||
|
||||
trace!(target: LOG_TARGET, "Checking header");
|
||||
let seal = header
|
||||
.digest_mut()
|
||||
.pop()
|
||||
.ok_or_else(|| babe_err(Error::HeaderUnsealed(header.hash())))?;
|
||||
|
||||
let sig = seal
|
||||
.as_babe_seal()
|
||||
.ok_or_else(|| babe_err(Error::HeaderBadSeal(header.hash())))?;
|
||||
|
||||
// the pre-hash of the header doesn't include the seal
|
||||
// and that's what we sign
|
||||
let pre_hash = header.hash();
|
||||
|
||||
if pre_digest.slot() > slot_now {
|
||||
header.digest_mut().push(seal);
|
||||
return Ok(CheckedHeader::Deferred(header, pre_digest.slot()));
|
||||
}
|
||||
|
||||
match &pre_digest {
|
||||
PreDigest::Primary(primary) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Verifying primary block #{} at slot: {}",
|
||||
header.number(),
|
||||
primary.slot,
|
||||
);
|
||||
|
||||
check_primary_header::<B>(pre_hash, primary, sig, epoch, epoch.config.c)?;
|
||||
},
|
||||
PreDigest::SecondaryPlain(secondary)
|
||||
if epoch.config.allowed_slots.is_secondary_plain_slots_allowed() =>
|
||||
{
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Verifying secondary plain block #{} at slot: {}",
|
||||
header.number(),
|
||||
secondary.slot,
|
||||
);
|
||||
|
||||
check_secondary_plain_header::<B>(pre_hash, secondary, sig, epoch)?;
|
||||
},
|
||||
PreDigest::SecondaryVRF(secondary)
|
||||
if epoch.config.allowed_slots.is_secondary_vrf_slots_allowed() =>
|
||||
{
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Verifying secondary VRF block #{} at slot: {}",
|
||||
header.number(),
|
||||
secondary.slot,
|
||||
);
|
||||
|
||||
check_secondary_vrf_header::<B>(pre_hash, secondary, sig, epoch)?;
|
||||
},
|
||||
_ => return Err(babe_err(Error::SecondarySlotAssignmentsDisabled)),
|
||||
}
|
||||
|
||||
let info = VerifiedHeaderInfo { seal };
|
||||
Ok(CheckedHeader::Checked(header, info))
|
||||
}
|
||||
|
||||
pub(super) struct VerifiedHeaderInfo {
|
||||
pub(super) seal: DigestItem,
|
||||
}
|
||||
|
||||
/// Check a primary slot proposal header. We validate that the given header is
|
||||
/// properly signed by the expected authority, and that the contained VRF proof
|
||||
/// is valid. Additionally, the weight of this block must increase compared to
|
||||
/// its parent since it is a primary block.
|
||||
fn check_primary_header<B: BlockT + Sized>(
|
||||
pre_hash: B::Hash,
|
||||
pre_digest: &PrimaryPreDigest,
|
||||
signature: AuthoritySignature,
|
||||
epoch: &Epoch,
|
||||
c: (u64, u64),
|
||||
) -> Result<(), Error<B>> {
|
||||
let authority_id = &epoch
|
||||
.authorities
|
||||
.get(pre_digest.authority_index as usize)
|
||||
.ok_or(Error::SlotAuthorNotFound)?
|
||||
.0;
|
||||
let mut epoch_index = epoch.epoch_index;
|
||||
|
||||
if epoch.end_slot() <= pre_digest.slot {
|
||||
// Slot doesn't strictly belong to this epoch, create a clone with fixed values.
|
||||
epoch_index = epoch.clone_for_slot(pre_digest.slot).epoch_index;
|
||||
}
|
||||
|
||||
if !AuthorityPair::verify(&signature, pre_hash, authority_id) {
|
||||
return Err(babe_err(Error::BadSignature(pre_hash)));
|
||||
}
|
||||
|
||||
let data = make_vrf_sign_data(&epoch.randomness, pre_digest.slot, epoch_index);
|
||||
|
||||
if !authority_id.as_inner_ref().vrf_verify(&data, &pre_digest.vrf_signature) {
|
||||
return Err(babe_err(Error::VrfVerificationFailed));
|
||||
}
|
||||
|
||||
let threshold =
|
||||
calculate_primary_threshold(c, &epoch.authorities, pre_digest.authority_index as usize);
|
||||
|
||||
let score = authority_id
|
||||
.as_inner_ref()
|
||||
.make_bytes::<AUTHORING_SCORE_LENGTH>(
|
||||
AUTHORING_SCORE_VRF_CONTEXT,
|
||||
&data.as_ref(),
|
||||
&pre_digest.vrf_signature.pre_output,
|
||||
)
|
||||
.map(u128::from_le_bytes)
|
||||
.map_err(|_| babe_err(Error::VrfVerificationFailed))?;
|
||||
|
||||
if score >= threshold {
|
||||
return Err(babe_err(Error::VrfThresholdExceeded(threshold)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check a secondary slot proposal header. We validate that the given header is
|
||||
/// properly signed by the expected authority, which we have a deterministic way
|
||||
/// of computing. Additionally, the weight of this block must stay the same
|
||||
/// compared to its parent since it is a secondary block.
|
||||
fn check_secondary_plain_header<B: BlockT>(
|
||||
pre_hash: B::Hash,
|
||||
pre_digest: &SecondaryPlainPreDigest,
|
||||
signature: AuthoritySignature,
|
||||
epoch: &Epoch,
|
||||
) -> Result<(), Error<B>> {
|
||||
// check the signature is valid under the expected authority and chain state.
|
||||
let expected_author =
|
||||
secondary_slot_author(pre_digest.slot, &epoch.authorities, epoch.randomness)
|
||||
.ok_or(Error::NoSecondaryAuthorExpected)?;
|
||||
|
||||
let author = &epoch
|
||||
.authorities
|
||||
.get(pre_digest.authority_index as usize)
|
||||
.ok_or(Error::SlotAuthorNotFound)?
|
||||
.0;
|
||||
|
||||
if expected_author != author {
|
||||
return Err(Error::InvalidAuthor(expected_author.clone(), author.clone()));
|
||||
}
|
||||
|
||||
if !AuthorityPair::verify(&signature, pre_hash.as_ref(), author) {
|
||||
return Err(Error::BadSignature(pre_hash));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check a secondary VRF slot proposal header.
|
||||
fn check_secondary_vrf_header<B: BlockT>(
|
||||
pre_hash: B::Hash,
|
||||
pre_digest: &SecondaryVRFPreDigest,
|
||||
signature: AuthoritySignature,
|
||||
epoch: &Epoch,
|
||||
) -> Result<(), Error<B>> {
|
||||
// check the signature is valid under the expected authority and chain state.
|
||||
let expected_author =
|
||||
secondary_slot_author(pre_digest.slot, &epoch.authorities, epoch.randomness)
|
||||
.ok_or(Error::NoSecondaryAuthorExpected)?;
|
||||
|
||||
let author = &epoch
|
||||
.authorities
|
||||
.get(pre_digest.authority_index as usize)
|
||||
.ok_or(Error::SlotAuthorNotFound)?
|
||||
.0;
|
||||
|
||||
if expected_author != author {
|
||||
return Err(Error::InvalidAuthor(expected_author.clone(), author.clone()));
|
||||
}
|
||||
|
||||
let mut epoch_index = epoch.epoch_index;
|
||||
if epoch.end_slot() <= pre_digest.slot {
|
||||
// Slot doesn't strictly belong to this epoch, create a clone with fixed values.
|
||||
epoch_index = epoch.clone_for_slot(pre_digest.slot).epoch_index;
|
||||
}
|
||||
|
||||
if !AuthorityPair::verify(&signature, pre_hash.as_ref(), author) {
|
||||
return Err(Error::BadSignature(pre_hash));
|
||||
}
|
||||
|
||||
let data = make_vrf_sign_data(&epoch.randomness, pre_digest.slot, epoch_index);
|
||||
|
||||
if !author.as_inner_ref().vrf_verify(&data, &pre_digest.vrf_signature) {
|
||||
return Err(Error::VrfVerificationFailed);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
[package]
|
||||
name = "pezsc-consensus-beefy"
|
||||
version = "13.0.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
repository.workspace = true
|
||||
description = "BEEFY Client gadget for bizinikiwi"
|
||||
homepage.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
array-bytes = { workspace = true, default-features = true }
|
||||
async-channel = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
codec = { features = ["derive"], workspace = true, default-features = true }
|
||||
futures = { workspace = true }
|
||||
log = { workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
prometheus-endpoint = { workspace = true, default-features = true }
|
||||
pezsc-client-api = { workspace = true, default-features = true }
|
||||
pezsc-consensus = { workspace = true, default-features = true }
|
||||
pezsc-network = { workspace = true, default-features = true }
|
||||
pezsc-network-gossip = { workspace = true, default-features = true }
|
||||
pezsc-network-sync = { workspace = true, default-features = true }
|
||||
pezsc-network-types = { 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-arithmetic = { workspace = true, default-features = true }
|
||||
pezsp-blockchain = { workspace = true, default-features = true }
|
||||
pezsp-consensus = { workspace = true, default-features = true }
|
||||
pezsp-consensus-beefy = { workspace = true, default-features = true }
|
||||
pezsp-core = { workspace = true, default-features = true }
|
||||
pezsp-keystore = { workspace = true, default-features = true }
|
||||
pezsp-runtime = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, default-features = true }
|
||||
wasm-timer = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezsc-block-builder = { workspace = true, default-features = true }
|
||||
pezsc-network-test = { workspace = true }
|
||||
serde = { workspace = true, default-features = true }
|
||||
pezsp-mmr-primitives = { workspace = true, default-features = true }
|
||||
pezsp-tracing = { workspace = true, default-features = true }
|
||||
bizinikiwi-test-runtime-client = { workspace = true }
|
||||
|
||||
[features]
|
||||
# This feature adds BLS crypto primitives. It should not be used in production since
|
||||
# the BLS implementation and interface may still be subject to significant change.
|
||||
bls-experimental = [
|
||||
"pezsp-application-crypto/bls-experimental",
|
||||
"pezsp-consensus-beefy/bls-experimental",
|
||||
"pezsp-core/bls-experimental",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezsc-block-builder/runtime-benchmarks",
|
||||
"pezsc-client-api/runtime-benchmarks",
|
||||
"pezsc-consensus/runtime-benchmarks",
|
||||
"pezsc-network-gossip/runtime-benchmarks",
|
||||
"pezsc-network-sync/runtime-benchmarks",
|
||||
"pezsc-network-test/runtime-benchmarks",
|
||||
"pezsc-network/runtime-benchmarks",
|
||||
"pezsp-api/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus-beefy/runtime-benchmarks",
|
||||
"pezsp-consensus/runtime-benchmarks",
|
||||
"pezsp-mmr-primitives/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"bizinikiwi-test-runtime-client/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,373 @@
|
||||
# BEEFY
|
||||
**BEEFY** (**B**ridge **E**fficiency **E**nabling **F**inality **Y**ielder) is a secondary
|
||||
protocol running along GRANDPA Finality to support efficient bridging with non-Bizinikiwi
|
||||
blockchains, currently mainly ETH mainnet.
|
||||
|
||||
It can be thought of as an (optional) Bridge-specific Gadget to the GRANDPA Finality protocol.
|
||||
The Protocol piggybacks on many assumptions provided by GRANDPA, and is required to be built
|
||||
on top of it to work correctly.
|
||||
|
||||
BEEFY is a consensus protocol designed with efficient trustless bridging in mind. It means
|
||||
that building a light client of BEEFY protocol should be optimized for restricted environments
|
||||
like Ethereum Smart Contracts or On-Chain State Transition Function (e.g. Bizinikiwi Runtime).
|
||||
Note that BEEFY is not a standalone protocol, it is meant to be running alongside GRANDPA, a
|
||||
finality gadget created for Bizinikiwi/PezkuwiChain ecosystem. More details about GRANDPA can be found
|
||||
in the [whitepaper](https://github.com/w3f/consensus/blob/master/pdf/grandpa.pdf).
|
||||
|
||||
# Context
|
||||
|
||||
## Bridges
|
||||
|
||||
We want to be able to "bridge" different blockchains. We do so by safely sharing and verifying
|
||||
information about each chain’s state, i.e. blockchain `A` should be able to verify that blockchain
|
||||
`B` is at block #X.
|
||||
|
||||
## Finality
|
||||
|
||||
Finality in blockchains is a concept that means that after a given block #X has been finalized,
|
||||
it will never be reverted (e.g. due to a re-org). As such, we can be assured that any transaction
|
||||
that exists in this block will never be reverted.
|
||||
|
||||
## GRANDPA
|
||||
|
||||
GRANDPA is our finality gadget. It allows a set of nodes to come to BFT agreement on what is the
|
||||
canonical chain. It requires that 2/3 of the validator set agrees on a prefix of the canonical
|
||||
chain, which then becomes finalized.
|
||||
|
||||

|
||||
|
||||
### Difficulties of GRANDPA finality proofs
|
||||
|
||||
```rust
|
||||
struct Justification<Block: BlockT> {
|
||||
round: u64,
|
||||
commit: Commit<Block>,
|
||||
votes_ancestries: Vec<Block::Header>,
|
||||
}
|
||||
|
||||
struct Commit<Hash, Number, Signature, Id> {
|
||||
target_hash: Hash,
|
||||
target_number: Number,
|
||||
precommits: Vec<SignedPrecommit<Hash, Number, Signature, Id>>,
|
||||
}
|
||||
|
||||
struct SignedPrecommit<Hash, Number, Signature, Id> {
|
||||
precommit: Precommit<Hash, Number>,
|
||||
signature: Signature,
|
||||
id: Id,
|
||||
}
|
||||
|
||||
struct Precommit<Hash, Number> {
|
||||
target_hash: Hash,
|
||||
target_number: Number,
|
||||
}
|
||||
```
|
||||
|
||||
The main difficulty of verifying GRANDPA finality proofs comes from the fact that voters are
|
||||
voting on different things. In GRANDPA each voter will vote for the block they think is the
|
||||
latest one, and the protocol will come to agreement on what is the common ancestor which has >
|
||||
2/3 support.
|
||||
|
||||
This creates two sets of inefficiencies:
|
||||
|
||||
- We may need to have each validator's vote data because they're all potentially different (i.e.
|
||||
just the signature isn't enough).
|
||||
- We may need to attach a couple of headers to the finality proof in order to be able to verify
|
||||
all of the votes' ancestries.
|
||||
|
||||
Additionally, since our interim goal is to bridge to Ethereum there is also a difficulty related
|
||||
to "incompatible" crypto schemes. We use \`ed25519\` signatures in GRANDPA which we can't
|
||||
efficiently verify in the EVM.
|
||||
|
||||
Hence,
|
||||
|
||||
### Goals of BEEFY
|
||||
|
||||
1. Allow customisation of crypto to adapt for different targets. Support thresholds signatures as
|
||||
well eventually.
|
||||
1. Minimize the size of the "signed payload" and the finality proof.
|
||||
1. Unify data types and use backward-compatible versioning so that the protocol can be extended
|
||||
(additional payload, different crypto) without breaking existing light clients.
|
||||
|
||||
And since BEEFY is required to be running on top of GRANDPA. This allows us to take couple of
|
||||
shortcuts:
|
||||
1. BEEFY validator set is **the same** as GRANDPA's (i.e. the same bonded actors), they might be
|
||||
identified by different session keys though.
|
||||
1. BEEFY runs on **finalized** canonical chain, i.e. no forks (note Misbehavior
|
||||
section though).
|
||||
1. From a single validator perspective, BEEFY has at most one active voting round. Since GRANDPA
|
||||
validators are reaching finality, we assume they are on-line and well-connected and have
|
||||
similar view of the state of the blockchain.
|
||||
|
||||
# The BEEFY Protocol
|
||||
|
||||
## Mental Model
|
||||
|
||||
BEEFY should be considered as an extra voting round done by GRANDPA validators for the current
|
||||
best finalized block. Similarly to how GRANDPA is lagging behind best produced (non-finalized)
|
||||
block, BEEFY is going to lag behind best GRANDPA (finalized) block.
|
||||
|
||||
```
|
||||
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
|
||||
│ │ │ │ │ │ │ │ │ │
|
||||
│ B1 │ │ B2 │ │ B3 │ │ B4 │ │ B5 │
|
||||
│ │ │ │ │ │ │ │ │ │
|
||||
└──────┘ └───▲──┘ └──────┘ └───▲──┘ └───▲──┘
|
||||
│ │ │
|
||||
Best BEEFY block───────────────┘ │ │
|
||||
│ │
|
||||
Best GRANDPA block───────────────────────────────┘ │
|
||||
│
|
||||
Best produced block───────────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
A pseudo-algorithm of behaviour for a fully-synced BEEFY validator is:
|
||||
|
||||
```
|
||||
loop {
|
||||
let (best_beefy, best_grandpa) = wait_for_best_blocks();
|
||||
|
||||
let block_to_vote_on = choose_next_beefy_block(
|
||||
best_beefy,
|
||||
best_grandpa
|
||||
);
|
||||
|
||||
let payload_to_vote_on = retrieve_payload(block_to_vote_on);
|
||||
|
||||
let commitment = (block_to_vote_on, payload_to_vote_on);
|
||||
|
||||
let signature = sign_with_current_session_key(commitment);
|
||||
|
||||
broadcast_vote(commitment, signature);
|
||||
}
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
Before we jump into describing how BEEFY works in details, let's agree on the terms we are going
|
||||
to use and actors in the system. All nodes in the network need to participate in the BEEFY
|
||||
networking protocol, but we can identify two distinct actors though: **regular nodes** and
|
||||
**BEEFY validators**.
|
||||
Validators are expected to actively participate in the protocol, by producing and broadcasting
|
||||
**votes**. Votes are simply their signatures over a **Commitment**. A Commitment consists of a
|
||||
**payload** (an opaque blob of bytes extracted from a block or state at that block, expected to
|
||||
be some form of crypto accumulator (like Merkle Tree Hash or Merkle Mountain Range Root Hash))
|
||||
and **block number** from which this payload originates. Additionally, Commitment contains BEEFY
|
||||
**validator set id** at that particular block. Note the block is finalized, so there is no
|
||||
ambiguity despite using block number instead of a hash. A collection of **votes**, or rather
|
||||
a Commitment and a collection of signatures is going to be called **Signed Commitment**. A valid
|
||||
(see later for the rules) Signed Commitment is also called a **BEEFY Justification** or
|
||||
**BEEFY Finality Proof**. For more details on the actual data structures please see
|
||||
[BEEFY primitives definitions](https://github.com/pezkuwichain/pezkuwi-sdk/tree/master/bizinikiwi/primitives/consensus/beefy/src).
|
||||
|
||||
A **round** is an attempt by BEEFY validators to produce a BEEFY Justification. **Round number**
|
||||
is simply defined as a block number the validators are voting for, or to be more precise, the
|
||||
Commitment for that block number. Round ends when the next round is started, which may happen
|
||||
when one of the events occur:
|
||||
1. Either the node collects `2/3rd + 1` valid votes for that round.
|
||||
2. Or the node receives a BEEFY Justification for a block greater than the current best BEEFY block.
|
||||
|
||||
In both cases the node proceeds to determining the new round number using "Round Selection"
|
||||
procedure.
|
||||
|
||||
Regular nodes are expected to:
|
||||
1. Receive & validate votes for the current round and broadcast them to their peers.
|
||||
1. Receive & validate BEEFY Justifications and broadcast them to their peers.
|
||||
1. Return BEEFY Justifications for **Mandatory Blocks** on demand.
|
||||
1. Optionally return BEEFY Justifications for non-mandatory blocks on demand.
|
||||
|
||||
Validators are expected to additionally:
|
||||
1. Produce & broadcast vote for the current round.
|
||||
|
||||
Both kinds of actors are expected to fully participate in the protocol ONLY IF they believe they
|
||||
are up-to-date with the rest of the network, i.e. they are fully synced. Before this happens,
|
||||
the node should continue processing imported BEEFY Justifications and votes without actively
|
||||
voting themselves.
|
||||
|
||||
### Round Selection
|
||||
|
||||
Every node (both regular nodes and validators) need to determine locally what they believe
|
||||
current round number is. The choice is based on their knowledge of:
|
||||
|
||||
1. Best GRANDPA finalized block number (`best_grandpa`).
|
||||
1. Best BEEFY finalized block number (`best_beefy`).
|
||||
1. Starting block of current session (`session_start`).
|
||||
|
||||
**Session** means a period of time (or rather number of blocks) where validator set (keys) do not change.
|
||||
See `pallet_session` for implementation details in `FRAME` context. Since we piggy-back on
|
||||
GRANDPA, session boundaries for BEEFY are exactly the same as the ones for GRANDPA.
|
||||
|
||||
We define two kinds of blocks from the perspective of BEEFY protocol:
|
||||
1. **Mandatory Blocks**
|
||||
2. **Non-mandatory Blocks**
|
||||
|
||||
Mandatory blocks are the ones that MUST have BEEFY justification. That means that the validators
|
||||
will always start and conclude a round at mandatory blocks. For non-mandatory blocks, there may
|
||||
or may not be a justification and validators may never choose these blocks to start a round.
|
||||
|
||||
Every **first block in** each **session** is considered a **mandatory block**. All other blocks
|
||||
in the session are non-mandatory, however validators are encouraged to finalize as many blocks as
|
||||
possible to enable lower latency for light clients and hence end users. Since GRANDPA is
|
||||
considering session boundary blocks as mandatory as well, `session_start` block will always have
|
||||
both GRANDPA and BEEFY Justification.
|
||||
|
||||
Therefore, to determine current round number nodes use a formula:
|
||||
|
||||
```
|
||||
round_number =
|
||||
(1 - M) * session_start
|
||||
+ M * (best_beefy + NEXT_POWER_OF_TWO((best_grandpa - best_beefy + 1) / 2))
|
||||
```
|
||||
|
||||
where:
|
||||
|
||||
- `M` is `1` if mandatory block in current session is already finalized and `0` otherwise.
|
||||
- `NEXT_POWER_OF_TWO(x)` returns the smallest number greater or equal to `x` that is a power of two.
|
||||
|
||||
In other words, the next round number should be the oldest mandatory block without a justification,
|
||||
or the highest GRANDPA-finalized block, whose block number difference with `best_beefy` block is
|
||||
a power of two. The mental model for round selection is to first finalize the mandatory block and
|
||||
then to attempt to pick a block taking into account how fast BEEFY catches up with GRANDPA.
|
||||
In case GRANDPA makes progress, but BEEFY seems to be lagging behind, validators are changing
|
||||
rounds less often to increase the chance of concluding them.
|
||||
|
||||
As mentioned earlier, every time the node picks a new `round_number` (and validator casts a vote)
|
||||
it ends the previous one, no matter if finality was reached (i.e. the round concluded) or not.
|
||||
Votes for an inactive round should not be propagated.
|
||||
|
||||
Note that since BEEFY only votes for GRANDPA-finalized blocks, `session_start` here actually means:
|
||||
"the latest session for which the start of is GRANDPA-finalized", i.e. block production might
|
||||
have already progressed, but BEEFY needs to first finalize the mandatory block of the older
|
||||
session.
|
||||
|
||||
In good networking conditions BEEFY may end up finalizing each and every block (if GRANDPA does
|
||||
the same). Practically, with short block times, it's going to be rare and might be excessive, so
|
||||
it's suggested for implementations to introduce a `min_delta` parameter which will limit the
|
||||
frequency with which new rounds are started. The affected component of the formula would be:
|
||||
`best_beefy + MAX(min_delta, NEXT_POWER_OF_TWO(...))`, so we start a new round only if the
|
||||
power-of-two component is greater than the min delta. Note that if `round_number > best_grandpa`
|
||||
the validators are not expected to start any round.
|
||||
|
||||
### Catch up
|
||||
|
||||
Every session is guaranteed to have at least one BEEFY-finalized block. However it also means
|
||||
that the round at mandatory block must be concluded even though, a new session has already started
|
||||
(i.e. the on-chain component has selected a new validator set and GRANDPA might have already
|
||||
finalized the transition). In such case BEEFY must "catch up" the previous sessions and make sure to
|
||||
conclude rounds for mandatory blocks. Note that older sessions must obviously be finalized by the
|
||||
validator set at that point in time, not the latest/current one.
|
||||
|
||||
### Initial Sync
|
||||
|
||||
It's all rainbows and unicorns when the node is fully synced with the network. However during cold
|
||||
startup it will have hard time determining the current round number. Because of that nodes that
|
||||
are not fully synced should not participate in BEEFY protocol at all.
|
||||
|
||||
During the sync we should make sure to also fetch BEEFY justifications for all mandatory blocks.
|
||||
This can happen asynchronously, but validators, before starting to vote, need to be certain
|
||||
about the last session that contains a concluded round on mandatory block in order to initiate the
|
||||
catch up procedure.
|
||||
|
||||
### Gossip
|
||||
|
||||
Nodes participating in BEEFY protocol are expected to gossip messages around.
|
||||
The protocol defines following messages:
|
||||
|
||||
1. Votes for the current round,
|
||||
2. BEEFY Justifications for recently concluded rounds,
|
||||
3. BEEFY Justification for the latest mandatory block,
|
||||
|
||||
Each message is additionally associated with a **topic**, which can be either:
|
||||
1. the round number (i.e. topic associated with a particular round),
|
||||
2. or the global topic (independent from the rounds).
|
||||
|
||||
Round-specific topic should only be used to gossip the votes, other messages are gossiped
|
||||
periodically on the global topic. Let's now dive into description of the messages.
|
||||
|
||||
- **Votes**
|
||||
- Votes are sent on the round-specific topic.
|
||||
- Vote is considered valid when:
|
||||
- The commitment matches local commitment.
|
||||
- The validator is part of the current validator set.
|
||||
- The signature is correct.
|
||||
|
||||
- **BEEFY Justification**
|
||||
- Justifications are sent on the global topic.
|
||||
- Justification is considered worthwhile to gossip when:
|
||||
- It is for a recent (implementation specific) round or the latest mandatory round.
|
||||
- All signatures are valid and there is at least `2/3rd + 1` of them.
|
||||
- Signatories are part of the current validator set.
|
||||
- Mandatory justifications should be announced periodically.
|
||||
|
||||
## Misbehavior
|
||||
|
||||
Similarly to other PoS protocols, BEEFY considers casting two different votes in the same round a
|
||||
misbehavior. I.e. for a particular `round_number`, the validator produces signatures for 2 different
|
||||
`Commitment`s and broadcasts them. This is called **equivocation**.
|
||||
|
||||
On top of this, voting on an incorrect **payload** is considered a misbehavior as well, and since
|
||||
we piggy-back on GRANDPA there is no ambiguity in terms of the fork validators should be voting for.
|
||||
|
||||
Misbehavior should be penalized. If more validators misbehave in the exact same `round` the
|
||||
penalty should be more severe, up to the entire bonded stake in case we reach `1/3rd + 1`
|
||||
validators misbehaving.
|
||||
|
||||
## Ethereum
|
||||
|
||||
Initial version of BEEFY was made to enable efficient bridging with Ethereum, where the light
|
||||
client is a Solidity Smart Contract compiled to EVM bytecode. Hence the choice of the initial
|
||||
cryptography for BEEFY: `secp256k1` and usage of `keccak256` hashing function.
|
||||
|
||||
### Future: Supporting multiple crypto
|
||||
|
||||
While BEEFY currently works with `secp256k1` signatures, we intend in the future to support
|
||||
multiple signature schemes.
|
||||
This means that multiple kinds of `SignedCommitment`s might exist and only together they form a
|
||||
full `BEEFY Justification`.
|
||||
|
||||
## BEEFY Key
|
||||
|
||||
The current cryptographic scheme used by BEEFY is `ecdsa`. This is **different** from other
|
||||
schemes like `sr25519` and `ed25519` which are commonly used in Bizinikiwi configurations for
|
||||
other pallets (BABE, GRANDPA, AuRa, etc). The most noticeable difference is that an `ecdsa`
|
||||
public key is `33` bytes long, instead of `32` bytes for a `sr25519` based public key. So, a
|
||||
BEEFY key [sticks out](https://github.com/paritytech/polkadot/blob/25951e45b1907853f120c752aaa01631a0b3e783/node/service/src/chain_spec.rs#L738)
|
||||
among the other public keys a bit.
|
||||
|
||||
For other crypto (using the default Bizinikiwi configuration) the `AccountId` (32-bytes) matches
|
||||
the `PublicKey`, but note that it's not the case for BEEFY. As a consequence of this, you can
|
||||
**not** convert the `AccountId` raw bytes into a BEEFY `PublicKey`.
|
||||
|
||||
The easiest way to generate or view hex-encoded or SS58-encoded BEEFY Public Key is by using the
|
||||
[Subkey](https://bizinikiwi.dev/docs/en/knowledgebase/integrate/subkey) tool. Generate a BEEFY key
|
||||
using the following command
|
||||
|
||||
```sh
|
||||
subkey generate --scheme ecdsa
|
||||
```
|
||||
|
||||
The output will look something like
|
||||
|
||||
```sh
|
||||
Secret phrase `sunset anxiety liberty mention dwarf actress advice stove peasant olive kite rebuild` is account:
|
||||
Secret seed: 0x9f844e21444683c8fcf558c4c11231a14ed9dea6f09a8cc505604368ef204a61
|
||||
Public key (hex): 0x02d69740c3bbfbdbb365886c8270c4aafd17cbffb2e04ecef581e6dced5aded2cd
|
||||
Public key (SS58): KW7n1vMENCBLQpbT5FWtmYWHNvEyGjSrNL4JE32mDds3xnXTf
|
||||
Account ID: 0x295509ae9a9b04ade5f1756b5f58f4161cf57037b4543eac37b3b555644f6aed
|
||||
SS58 Address: 5Czu5hudL79ETnQt6GAkVJHGhDQ6Qv3VWq54zN1CPKzKzYGu
|
||||
|
||||
```
|
||||
|
||||
In case your BEEFY keys are using the wrong cryptographic scheme, you will see an invalid public
|
||||
key format message at node startup. Basically something like
|
||||
|
||||
```sh
|
||||
...
|
||||
2021-05-28 12:37:51 [Relaychain] Invalid BEEFY PublicKey format!
|
||||
...
|
||||
```
|
||||
|
||||
# BEEFY Light Client
|
||||
|
||||
TODO
|
||||
@@ -0,0 +1,47 @@
|
||||
[package]
|
||||
name = "pezsc-consensus-beefy-rpc"
|
||||
version = "13.0.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
repository.workspace = true
|
||||
description = "RPC for the BEEFY Client gadget for bizinikiwi"
|
||||
homepage.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codec = { features = ["derive"], workspace = true, default-features = true }
|
||||
futures = { workspace = true }
|
||||
jsonrpsee = { features = [
|
||||
"client-core",
|
||||
"macros",
|
||||
"server-core",
|
||||
], workspace = true }
|
||||
log = { workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
pezsc-consensus-beefy = { workspace = true, default-features = true }
|
||||
pezsc-rpc = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive"], workspace = true, default-features = true }
|
||||
pezsp-application-crypto = { workspace = true, default-features = true }
|
||||
pezsp-consensus-beefy = { workspace = true, default-features = true }
|
||||
pezsp-core = { workspace = true, default-features = true }
|
||||
pezsp-runtime = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezsc-rpc = { features = [
|
||||
"test-helpers",
|
||||
], workspace = true, default-features = true }
|
||||
bizinikiwi-test-runtime-client = { workspace = true }
|
||||
tokio = { features = ["macros"], workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezsc-consensus-beefy/runtime-benchmarks",
|
||||
"pezsc-rpc/runtime-benchmarks",
|
||||
"pezsp-consensus-beefy/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"bizinikiwi-test-runtime-client/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,305 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! RPC API for BEEFY.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use parking_lot::RwLock;
|
||||
use pezsp_consensus_beefy::AuthorityIdBound;
|
||||
use std::sync::Arc;
|
||||
|
||||
use pezsc_rpc::{
|
||||
utils::{BoundedVecDeque, PendingSubscription},
|
||||
SubscriptionTaskExecutor,
|
||||
};
|
||||
use pezsp_application_crypto::RuntimeAppPublic;
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
use futures::{task::SpawnError, FutureExt, StreamExt};
|
||||
use jsonrpsee::{
|
||||
core::async_trait,
|
||||
proc_macros::rpc,
|
||||
types::{ErrorObject, ErrorObjectOwned},
|
||||
PendingSubscriptionSink,
|
||||
};
|
||||
use log::warn;
|
||||
|
||||
use pezsc_consensus_beefy::communication::notification::{
|
||||
BeefyBestBlockStream, BeefyVersionedFinalityProofStream,
|
||||
};
|
||||
|
||||
mod notification;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
/// Top-level error type for the RPC handler
|
||||
pub enum Error {
|
||||
/// The BEEFY RPC endpoint is not ready.
|
||||
#[error("BEEFY RPC endpoint not ready")]
|
||||
EndpointNotReady,
|
||||
/// The BEEFY RPC background task failed to spawn.
|
||||
#[error("BEEFY RPC background task failed to spawn")]
|
||||
RpcTaskFailure(#[from] SpawnError),
|
||||
}
|
||||
|
||||
/// The error codes returned by jsonrpc.
|
||||
pub enum ErrorCode {
|
||||
/// Returned when BEEFY RPC endpoint is not ready.
|
||||
NotReady = 1,
|
||||
/// Returned on BEEFY RPC background task failure.
|
||||
TaskFailure = 2,
|
||||
}
|
||||
|
||||
impl From<Error> for ErrorCode {
|
||||
fn from(error: Error) -> Self {
|
||||
match error {
|
||||
Error::EndpointNotReady => ErrorCode::NotReady,
|
||||
Error::RpcTaskFailure(_) => ErrorCode::TaskFailure,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for ErrorObjectOwned {
|
||||
fn from(error: Error) -> Self {
|
||||
let message = error.to_string();
|
||||
let code = ErrorCode::from(error);
|
||||
ErrorObject::owned(code as i32, message, None::<()>)
|
||||
}
|
||||
}
|
||||
|
||||
// Provides RPC methods for interacting with BEEFY.
|
||||
#[rpc(client, server)]
|
||||
pub trait BeefyApi<Notification, Hash> {
|
||||
/// Returns the block most recently finalized by BEEFY, alongside its justification.
|
||||
#[subscription(
|
||||
name = "beefy_subscribeJustifications" => "beefy_justifications",
|
||||
unsubscribe = "beefy_unsubscribeJustifications",
|
||||
item = Notification,
|
||||
)]
|
||||
fn subscribe_justifications(&self);
|
||||
|
||||
/// Returns hash of the latest BEEFY finalized block as seen by this client.
|
||||
///
|
||||
/// The latest BEEFY block might not be available if the BEEFY gadget is not running
|
||||
/// in the network or if the client is still initializing or syncing with the network.
|
||||
/// In such case an error would be returned.
|
||||
#[method(name = "beefy_getFinalizedHead")]
|
||||
async fn latest_finalized(&self) -> Result<Hash, Error>;
|
||||
}
|
||||
|
||||
/// Implements the BeefyApi RPC trait for interacting with BEEFY.
|
||||
pub struct Beefy<Block: BlockT, AuthorityId: AuthorityIdBound> {
|
||||
finality_proof_stream: BeefyVersionedFinalityProofStream<Block, AuthorityId>,
|
||||
beefy_best_block: Arc<RwLock<Option<Block::Hash>>>,
|
||||
executor: SubscriptionTaskExecutor,
|
||||
}
|
||||
|
||||
impl<Block, AuthorityId> Beefy<Block, AuthorityId>
|
||||
where
|
||||
Block: BlockT,
|
||||
AuthorityId: AuthorityIdBound,
|
||||
{
|
||||
/// Creates a new Beefy Rpc handler instance.
|
||||
pub fn new(
|
||||
finality_proof_stream: BeefyVersionedFinalityProofStream<Block, AuthorityId>,
|
||||
best_block_stream: BeefyBestBlockStream<Block>,
|
||||
executor: SubscriptionTaskExecutor,
|
||||
) -> Result<Self, Error> {
|
||||
let beefy_best_block = Arc::new(RwLock::new(None));
|
||||
|
||||
let stream = best_block_stream.subscribe(100_000);
|
||||
let closure_clone = beefy_best_block.clone();
|
||||
let future = stream.for_each(move |best_beefy| {
|
||||
let async_clone = closure_clone.clone();
|
||||
async move { *async_clone.write() = Some(best_beefy) }
|
||||
});
|
||||
|
||||
executor.spawn("bizinikiwi-rpc-subscription", Some("rpc"), future.map(drop).boxed());
|
||||
Ok(Self { finality_proof_stream, beefy_best_block, executor })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<Block, AuthorityId> BeefyApiServer<notification::EncodedVersionedFinalityProof, Block::Hash>
|
||||
for Beefy<Block, AuthorityId>
|
||||
where
|
||||
Block: BlockT,
|
||||
AuthorityId: AuthorityIdBound,
|
||||
<AuthorityId as RuntimeAppPublic>::Signature: Send + Sync,
|
||||
{
|
||||
fn subscribe_justifications(&self, pending: PendingSubscriptionSink) {
|
||||
let stream = self
|
||||
.finality_proof_stream
|
||||
.subscribe(100_000)
|
||||
.map(|vfp| notification::EncodedVersionedFinalityProof::new::<Block, AuthorityId>(vfp));
|
||||
|
||||
pezsc_rpc::utils::spawn_subscription_task(
|
||||
&self.executor,
|
||||
PendingSubscription::from(pending).pipe_from_stream(stream, BoundedVecDeque::default()),
|
||||
);
|
||||
}
|
||||
|
||||
async fn latest_finalized(&self) -> Result<Block::Hash, Error> {
|
||||
self.beefy_best_block.read().as_ref().cloned().ok_or(Error::EndpointNotReady)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use jsonrpsee::{core::EmptyServerParams as EmptyParams, RpcModule};
|
||||
use pezsc_consensus_beefy::{
|
||||
communication::notification::BeefyVersionedFinalityProofSender,
|
||||
justification::BeefyVersionedFinalityProof,
|
||||
};
|
||||
use pezsp_consensus_beefy::{ecdsa_crypto, known_payloads, Payload, SignedCommitment};
|
||||
use pezsp_runtime::traits::{BlakeTwo256, Hash};
|
||||
use bizinikiwi_test_runtime_client::runtime::Block;
|
||||
|
||||
fn setup_io_handler() -> (
|
||||
RpcModule<Beefy<Block, ecdsa_crypto::AuthorityId>>,
|
||||
BeefyVersionedFinalityProofSender<Block, ecdsa_crypto::AuthorityId>,
|
||||
) {
|
||||
let (_, stream) = BeefyBestBlockStream::<Block>::channel();
|
||||
setup_io_handler_with_best_block_stream(stream)
|
||||
}
|
||||
|
||||
fn setup_io_handler_with_best_block_stream(
|
||||
best_block_stream: BeefyBestBlockStream<Block>,
|
||||
) -> (
|
||||
RpcModule<Beefy<Block, ecdsa_crypto::AuthorityId>>,
|
||||
BeefyVersionedFinalityProofSender<Block, ecdsa_crypto::AuthorityId>,
|
||||
) {
|
||||
let (finality_proof_sender, finality_proof_stream) =
|
||||
BeefyVersionedFinalityProofStream::<Block, ecdsa_crypto::AuthorityId>::channel();
|
||||
|
||||
let handler =
|
||||
Beefy::new(finality_proof_stream, best_block_stream, pezsc_rpc::testing::test_executor())
|
||||
.expect("Setting up the BEEFY RPC handler works");
|
||||
|
||||
(handler.into_rpc(), finality_proof_sender)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn uninitialized_rpc_handler() {
|
||||
let (rpc, _) = setup_io_handler();
|
||||
let request = r#"{"jsonrpc":"2.0","method":"beefy_getFinalizedHead","params":[],"id":1}"#;
|
||||
let expected_response = r#"{"jsonrpc":"2.0","id":1,"error":{"code":1,"message":"BEEFY RPC endpoint not ready"}}"#;
|
||||
let (response, _) = rpc.raw_json_request(&request, 1).await.unwrap();
|
||||
|
||||
assert_eq!(expected_response, response);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn latest_finalized_rpc() {
|
||||
let (sender, stream) = BeefyBestBlockStream::<Block>::channel();
|
||||
let (io, _) = setup_io_handler_with_best_block_stream(stream);
|
||||
|
||||
let hash = BlakeTwo256::hash(b"42");
|
||||
let r: Result<(), ()> = sender.notify(|| Ok(hash));
|
||||
r.unwrap();
|
||||
|
||||
// Verify RPC `beefy_getFinalizedHead` returns expected hash.
|
||||
let request = r#"{"jsonrpc":"2.0","method":"beefy_getFinalizedHead","params":[],"id":1}"#;
|
||||
let expected = "{\
|
||||
\"jsonrpc\":\"2.0\",\
|
||||
\"id\":1,\
|
||||
\"result\":\"0x2f0039e93a27221fcf657fb877a1d4f60307106113e885096cb44a461cd0afbf\"\
|
||||
}";
|
||||
let not_ready: &str = "{\
|
||||
\"jsonrpc\":\"2.0\",\
|
||||
\"id\":1,\
|
||||
\"error\":{\"code\":1,\"message\":\"BEEFY RPC endpoint not ready\"}\
|
||||
}";
|
||||
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||
while std::time::Instant::now() < deadline {
|
||||
let (response, _) = io.raw_json_request(request, 1).await.expect("RPC requests work");
|
||||
if response != not_ready {
|
||||
assert_eq!(response, expected);
|
||||
// Success
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
}
|
||||
|
||||
panic!(
|
||||
"Deadline reached while waiting for best BEEFY block to update. Perhaps the background task is broken?"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribe_and_unsubscribe_with_wrong_id() {
|
||||
let (rpc, _) = setup_io_handler();
|
||||
// Subscribe call.
|
||||
let _sub = rpc
|
||||
.subscribe_unbounded("beefy_subscribeJustifications", EmptyParams::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Unsubscribe with wrong ID
|
||||
let (response, _) = rpc
|
||||
.raw_json_request(
|
||||
r#"{"jsonrpc":"2.0","method":"beefy_unsubscribeJustifications","params":["FOO"],"id":1}"#,
|
||||
1,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let expected = r#"{"jsonrpc":"2.0","id":1,"result":false}"#;
|
||||
|
||||
assert_eq!(response, expected);
|
||||
}
|
||||
|
||||
fn create_finality_proof() -> BeefyVersionedFinalityProof<Block, ecdsa_crypto::AuthorityId> {
|
||||
let payload =
|
||||
Payload::from_single_entry(known_payloads::MMR_ROOT_ID, "Hello World!".encode());
|
||||
BeefyVersionedFinalityProof::<Block, ecdsa_crypto::AuthorityId>::V1(SignedCommitment {
|
||||
commitment: pezsp_consensus_beefy::Commitment {
|
||||
payload,
|
||||
block_number: 5,
|
||||
validator_set_id: 0,
|
||||
},
|
||||
signatures: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribe_and_listen_to_one_justification() {
|
||||
let (rpc, finality_proof_sender) = setup_io_handler();
|
||||
|
||||
// Subscribe
|
||||
let mut sub = rpc
|
||||
.subscribe_unbounded("beefy_subscribeJustifications", EmptyParams::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Notify with finality_proof
|
||||
let finality_proof = create_finality_proof();
|
||||
let r: Result<(), ()> = finality_proof_sender.notify(|| Ok(finality_proof.clone()));
|
||||
r.unwrap();
|
||||
|
||||
// Inspect what we received
|
||||
let (bytes, recv_sub_id) = sub.next::<pezsp_core::Bytes>().await.unwrap().unwrap();
|
||||
let recv_finality_proof: BeefyVersionedFinalityProof<Block, ecdsa_crypto::AuthorityId> =
|
||||
Decode::decode(&mut &bytes[..]).unwrap();
|
||||
assert_eq!(&recv_sub_id, sub.subscription_id());
|
||||
assert_eq!(recv_finality_proof, finality_proof);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use codec::Encode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use pezsp_consensus_beefy::AuthorityIdBound;
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
/// An encoded finality proof proving that the given header has been finalized.
|
||||
/// The given bytes should be the SCALE-encoded representation of a
|
||||
/// `pezsp_consensus_beefy::VersionedFinalityProof`.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct EncodedVersionedFinalityProof(pezsp_core::Bytes);
|
||||
|
||||
impl EncodedVersionedFinalityProof {
|
||||
pub fn new<Block, AuthorityId>(
|
||||
finality_proof: pezsc_consensus_beefy::justification::BeefyVersionedFinalityProof<
|
||||
Block,
|
||||
AuthorityId,
|
||||
>,
|
||||
) -> Self
|
||||
where
|
||||
Block: BlockT,
|
||||
AuthorityId: AuthorityIdBound,
|
||||
{
|
||||
EncodedVersionedFinalityProof(finality_proof.encode().into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Schema for BEEFY state persisted in the aux-db.
|
||||
|
||||
use crate::{error::Error, worker::PersistedState, LOG_TARGET};
|
||||
use codec::{Decode, Encode};
|
||||
use log::{debug, trace, warn};
|
||||
use pezsc_client_api::{backend::AuxStore, Backend};
|
||||
use pezsp_blockchain::{Error as ClientError, Result as ClientResult};
|
||||
use pezsp_consensus_beefy::AuthorityIdBound;
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
const VERSION_KEY: &[u8] = b"beefy_auxschema_version";
|
||||
const WORKER_STATE_KEY: &[u8] = b"beefy_voter_state";
|
||||
|
||||
const CURRENT_VERSION: u32 = 4;
|
||||
|
||||
pub(crate) fn write_current_version<BE: AuxStore>(backend: &BE) -> Result<(), Error> {
|
||||
debug!(target: LOG_TARGET, "🥩 write aux schema version {:?}", CURRENT_VERSION);
|
||||
AuxStore::insert_aux(backend, &[(VERSION_KEY, CURRENT_VERSION.encode().as_slice())], &[])
|
||||
.map_err(|e| Error::Backend(e.to_string()))
|
||||
}
|
||||
|
||||
/// Write voter state.
|
||||
pub(crate) fn write_voter_state<B: BlockT, BE: AuxStore, AuthorityId: AuthorityIdBound>(
|
||||
backend: &BE,
|
||||
state: &PersistedState<B, AuthorityId>,
|
||||
) -> ClientResult<()> {
|
||||
trace!(target: LOG_TARGET, "🥩 persisting {:?}", state);
|
||||
AuxStore::insert_aux(backend, &[(WORKER_STATE_KEY, state.encode().as_slice())], &[])
|
||||
}
|
||||
|
||||
fn load_decode<BE: AuxStore, T: Decode>(backend: &BE, key: &[u8]) -> ClientResult<Option<T>> {
|
||||
match backend.get_aux(key)? {
|
||||
None => Ok(None),
|
||||
Some(t) => T::decode(&mut &t[..])
|
||||
.map_err(|e| ClientError::Backend(format!("BEEFY DB is corrupted: {}", e)))
|
||||
.map(Some),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load or initialize persistent data from backend.
|
||||
pub(crate) fn load_persistent<B, BE, AuthorityId: AuthorityIdBound>(
|
||||
backend: &BE,
|
||||
) -> ClientResult<Option<PersistedState<B, AuthorityId>>>
|
||||
where
|
||||
B: BlockT,
|
||||
BE: Backend<B>,
|
||||
{
|
||||
let version: Option<u32> = load_decode(backend, VERSION_KEY)?;
|
||||
|
||||
match version {
|
||||
None => (),
|
||||
|
||||
Some(v) if 1 <= v && v <= 3 =>
|
||||
// versions 1, 2 & 3 are obsolete and should be ignored
|
||||
{
|
||||
warn!(target: LOG_TARGET, "🥩 backend contains a BEEFY state of an obsolete version {v}. ignoring...")
|
||||
},
|
||||
Some(4) =>
|
||||
return load_decode::<_, PersistedState<B, AuthorityId>>(backend, WORKER_STATE_KEY),
|
||||
other =>
|
||||
return Err(ClientError::Backend(format!("Unsupported BEEFY DB version: {:?}", other))),
|
||||
}
|
||||
|
||||
// No persistent state found in DB.
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::tests::BeefyTestNet;
|
||||
use pezsc_network_test::TestNetFactory;
|
||||
use pezsp_consensus_beefy::ecdsa_crypto;
|
||||
|
||||
// also used in tests.rs
|
||||
pub fn verify_persisted_version<B: BlockT, BE: Backend<B>>(backend: &BE) -> bool {
|
||||
let version: u32 = load_decode(backend, VERSION_KEY).unwrap().unwrap();
|
||||
version == CURRENT_VERSION
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_load_persistent_sanity_checks() {
|
||||
let mut net = BeefyTestNet::new(1);
|
||||
let backend = net.peer(0).client().as_backend();
|
||||
|
||||
// version not available in db -> None
|
||||
assert_eq!(load_persistent::<_, _, ecdsa_crypto::AuthorityId>(&*backend).unwrap(), None);
|
||||
|
||||
// populate version in db
|
||||
write_current_version(&*backend).unwrap();
|
||||
// verify correct version is retrieved
|
||||
assert_eq!(load_decode(&*backend, VERSION_KEY).unwrap(), Some(CURRENT_VERSION));
|
||||
|
||||
// version is available in db but state isn't -> None
|
||||
assert_eq!(load_persistent::<_, _, ecdsa_crypto::AuthorityId>(&*backend).unwrap(), None);
|
||||
|
||||
// full `PersistedState` load is tested in `tests.rs`.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,917 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{collections::BTreeSet, sync::Arc, time::Duration};
|
||||
|
||||
use pezsc_network::{NetworkPeers, ReputationChange};
|
||||
use pezsc_network_gossip::{MessageIntent, ValidationResult, Validator, ValidatorContext};
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_runtime::traits::{Block, Hash, Header, NumberFor};
|
||||
|
||||
use codec::{Decode, DecodeAll, Encode};
|
||||
use log::{debug, trace};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use wasm_timer::Instant;
|
||||
|
||||
use crate::{
|
||||
communication::{benefit, cost, peers::KnownPeers},
|
||||
justification::{
|
||||
proof_block_num_and_set_id, verify_with_validator_set, BeefyVersionedFinalityProof,
|
||||
},
|
||||
keystore::BeefyKeystore,
|
||||
LOG_TARGET,
|
||||
};
|
||||
use pezsp_application_crypto::RuntimeAppPublic;
|
||||
use pezsp_consensus_beefy::{AuthorityIdBound, ValidatorSet, ValidatorSetId, VoteMessage};
|
||||
|
||||
// Timeout for rebroadcasting messages.
|
||||
#[cfg(not(test))]
|
||||
const REBROADCAST_AFTER: Duration = Duration::from_secs(60);
|
||||
#[cfg(test)]
|
||||
const REBROADCAST_AFTER: Duration = Duration::from_secs(5);
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(super) enum Action<H> {
|
||||
// repropagate under given topic, to the given peers, applying cost/benefit to originator.
|
||||
Keep(H, ReputationChange),
|
||||
// discard, applying cost/benefit to originator.
|
||||
Discard(ReputationChange),
|
||||
// ignore, no cost/benefit applied to originator.
|
||||
DiscardNoReport,
|
||||
}
|
||||
|
||||
/// An outcome of examining a message.
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
enum Consider {
|
||||
/// Accept the message.
|
||||
Accept,
|
||||
/// Message is too early. Reject.
|
||||
RejectPast,
|
||||
/// Message is from the future. Reject.
|
||||
RejectFuture,
|
||||
/// Message cannot be evaluated. Reject.
|
||||
CannotEvaluate,
|
||||
}
|
||||
|
||||
/// BEEFY gossip message type that gets encoded and sent on the network.
|
||||
#[derive(Debug, Encode, Decode)]
|
||||
pub(crate) enum GossipMessage<B: Block, AuthorityId: AuthorityIdBound> {
|
||||
/// BEEFY message with commitment and single signature.
|
||||
Vote(VoteMessage<NumberFor<B>, AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature>),
|
||||
/// BEEFY justification with commitment and signatures.
|
||||
FinalityProof(BeefyVersionedFinalityProof<B, AuthorityId>),
|
||||
}
|
||||
|
||||
impl<B: Block, AuthorityId: AuthorityIdBound> GossipMessage<B, AuthorityId> {
|
||||
/// Return inner vote if this message is a Vote.
|
||||
pub fn unwrap_vote(
|
||||
self,
|
||||
) -> Option<VoteMessage<NumberFor<B>, AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature>>
|
||||
{
|
||||
match self {
|
||||
GossipMessage::Vote(vote) => Some(vote),
|
||||
GossipMessage::FinalityProof(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return inner finality proof if this message is a FinalityProof.
|
||||
pub fn unwrap_finality_proof(self) -> Option<BeefyVersionedFinalityProof<B, AuthorityId>> {
|
||||
match self {
|
||||
GossipMessage::Vote(_) => None,
|
||||
GossipMessage::FinalityProof(proof) => Some(proof),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gossip engine votes messages topic
|
||||
pub(crate) fn votes_topic<B: Block>() -> B::Hash
|
||||
where
|
||||
B: Block,
|
||||
{
|
||||
<<B::Header as Header>::Hashing as Hash>::hash(b"beefy-votes")
|
||||
}
|
||||
|
||||
/// Gossip engine justifications messages topic
|
||||
pub(crate) fn proofs_topic<B: Block>() -> B::Hash
|
||||
where
|
||||
B: Block,
|
||||
{
|
||||
<<B::Header as Header>::Hashing as Hash>::hash(b"beefy-justifications")
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct GossipFilterCfg<'a, B: Block, AuthorityId: AuthorityIdBound> {
|
||||
pub start: NumberFor<B>,
|
||||
pub end: NumberFor<B>,
|
||||
pub validator_set: &'a ValidatorSet<AuthorityId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct FilterInner<B: Block, AuthorityId: AuthorityIdBound> {
|
||||
pub start: NumberFor<B>,
|
||||
pub end: NumberFor<B>,
|
||||
pub validator_set: ValidatorSet<AuthorityId>,
|
||||
}
|
||||
|
||||
struct Filter<B: Block, AuthorityId: AuthorityIdBound> {
|
||||
// specifies live rounds
|
||||
inner: Option<FilterInner<B, AuthorityId>>,
|
||||
// cache of seen valid justifications in active rounds
|
||||
rounds_with_valid_proofs: BTreeSet<NumberFor<B>>,
|
||||
}
|
||||
|
||||
impl<B: Block, AuthorityId: AuthorityIdBound> Filter<B, AuthorityId> {
|
||||
pub fn new() -> Self {
|
||||
Self { inner: None, rounds_with_valid_proofs: BTreeSet::new() }
|
||||
}
|
||||
|
||||
/// Update filter to new `start` and `set_id`.
|
||||
fn update(&mut self, cfg: GossipFilterCfg<B, AuthorityId>) {
|
||||
self.rounds_with_valid_proofs
|
||||
.retain(|&round| round >= cfg.start && round <= cfg.end);
|
||||
// only clone+overwrite big validator_set if set_id changed
|
||||
match self.inner.as_mut() {
|
||||
Some(f) if f.validator_set.id() == cfg.validator_set.id() => {
|
||||
f.start = cfg.start;
|
||||
f.end = cfg.end;
|
||||
},
|
||||
_ =>
|
||||
self.inner = Some(FilterInner {
|
||||
start: cfg.start,
|
||||
end: cfg.end,
|
||||
validator_set: cfg.validator_set.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept if `max(session_start, best_beefy) <= round <= best_grandpa`,
|
||||
/// and vote `set_id` matches session set id.
|
||||
///
|
||||
/// Latest concluded round is still considered alive to allow proper gossiping for it.
|
||||
fn consider_vote(&self, round: NumberFor<B>, set_id: ValidatorSetId) -> Consider {
|
||||
self.inner
|
||||
.as_ref()
|
||||
.map(|f|
|
||||
// only from current set and only [filter.start, filter.end]
|
||||
if set_id < f.validator_set.id() || round < f.start {
|
||||
Consider::RejectPast
|
||||
} else if set_id > f.validator_set.id() || round > f.end {
|
||||
Consider::RejectFuture
|
||||
} else {
|
||||
Consider::Accept
|
||||
})
|
||||
.unwrap_or(Consider::CannotEvaluate)
|
||||
}
|
||||
|
||||
/// Return true if `round` is >= than `max(session_start, best_beefy)`,
|
||||
/// and proof `set_id` matches session set id.
|
||||
///
|
||||
/// Latest concluded round is still considered alive to allow proper gossiping for it.
|
||||
fn consider_finality_proof(&self, round: NumberFor<B>, set_id: ValidatorSetId) -> Consider {
|
||||
self.inner
|
||||
.as_ref()
|
||||
.map(|f|
|
||||
// only from current set and only >= filter.start
|
||||
if round < f.start || set_id < f.validator_set.id() {
|
||||
Consider::RejectPast
|
||||
} else if set_id > f.validator_set.id() {
|
||||
Consider::RejectFuture
|
||||
} else {
|
||||
Consider::Accept
|
||||
}
|
||||
)
|
||||
.unwrap_or(Consider::CannotEvaluate)
|
||||
}
|
||||
|
||||
/// Add new _known_ `round` to the set of seen valid justifications.
|
||||
fn mark_round_as_proven(&mut self, round: NumberFor<B>) {
|
||||
self.rounds_with_valid_proofs.insert(round);
|
||||
}
|
||||
|
||||
/// Check if `round` is already part of seen valid justifications.
|
||||
fn is_already_proven(&self, round: NumberFor<B>) -> bool {
|
||||
self.rounds_with_valid_proofs.contains(&round)
|
||||
}
|
||||
|
||||
fn validator_set(&self) -> Option<&ValidatorSet<AuthorityId>> {
|
||||
self.inner.as_ref().map(|f| &f.validator_set)
|
||||
}
|
||||
}
|
||||
|
||||
/// BEEFY gossip validator
|
||||
///
|
||||
/// Validate BEEFY gossip messages and limit the number of live BEEFY voting rounds.
|
||||
///
|
||||
/// Allows messages for 'rounds >= last concluded' to flow, everything else gets
|
||||
/// rejected/expired.
|
||||
///
|
||||
///All messaging is handled in a single BEEFY global topic.
|
||||
pub(crate) struct GossipValidator<B, N, AuthorityId: AuthorityIdBound>
|
||||
where
|
||||
B: Block,
|
||||
{
|
||||
votes_topic: B::Hash,
|
||||
justifs_topic: B::Hash,
|
||||
gossip_filter: RwLock<Filter<B, AuthorityId>>,
|
||||
next_rebroadcast: Mutex<Instant>,
|
||||
known_peers: Arc<Mutex<KnownPeers<B>>>,
|
||||
network: Arc<N>,
|
||||
}
|
||||
|
||||
impl<B, N, AuthorityId> GossipValidator<B, N, AuthorityId>
|
||||
where
|
||||
B: Block,
|
||||
AuthorityId: AuthorityIdBound,
|
||||
{
|
||||
pub(crate) fn new(known_peers: Arc<Mutex<KnownPeers<B>>>, network: Arc<N>) -> Self {
|
||||
Self {
|
||||
votes_topic: votes_topic::<B>(),
|
||||
justifs_topic: proofs_topic::<B>(),
|
||||
gossip_filter: RwLock::new(Filter::new()),
|
||||
next_rebroadcast: Mutex::new(Instant::now() + REBROADCAST_AFTER),
|
||||
known_peers,
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update gossip validator filter.
|
||||
///
|
||||
/// Only votes for `set_id` and rounds `start <= round <= end` will be accepted.
|
||||
pub(crate) fn update_filter(&self, filter: GossipFilterCfg<B, AuthorityId>) {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 New gossip filter: start {:?}, end {:?}, validator set id {:?}",
|
||||
filter.start, filter.end, filter.validator_set.id()
|
||||
);
|
||||
self.gossip_filter.write().update(filter);
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, N, AuthorityId> GossipValidator<B, N, AuthorityId>
|
||||
where
|
||||
B: Block,
|
||||
N: NetworkPeers,
|
||||
AuthorityId: AuthorityIdBound,
|
||||
{
|
||||
fn report(&self, who: PeerId, cost_benefit: ReputationChange) {
|
||||
self.network.report_peer(who, cost_benefit);
|
||||
}
|
||||
|
||||
fn validate_vote(
|
||||
&self,
|
||||
vote: VoteMessage<NumberFor<B>, AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature>,
|
||||
sender: &PeerId,
|
||||
) -> Action<B::Hash> {
|
||||
let round = vote.commitment.block_number;
|
||||
let set_id = vote.commitment.validator_set_id;
|
||||
self.known_peers.lock().note_vote_for(*sender, round);
|
||||
|
||||
// Verify general usefulness of the message.
|
||||
// We are going to discard old votes right away (without verification).
|
||||
{
|
||||
let filter = self.gossip_filter.read();
|
||||
|
||||
match filter.consider_vote(round, set_id) {
|
||||
Consider::RejectPast => return Action::Discard(cost::OUTDATED_MESSAGE),
|
||||
Consider::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE),
|
||||
// When we can't evaluate, it's our fault (e.g. filter not initialized yet), we
|
||||
// discard the vote without punishing or rewarding the sending peer.
|
||||
Consider::CannotEvaluate => return Action::DiscardNoReport,
|
||||
Consider::Accept => {},
|
||||
}
|
||||
|
||||
// ensure authority is part of the set.
|
||||
if !filter
|
||||
.validator_set()
|
||||
.map(|set| set.validators().contains(&vote.id))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
debug!(target: LOG_TARGET, "Message from voter not in validator set: {}", vote.id);
|
||||
return Action::Discard(cost::UNKNOWN_VOTER);
|
||||
}
|
||||
}
|
||||
|
||||
if BeefyKeystore::verify(&vote.id, &vote.signature, &vote.commitment.encode()) {
|
||||
Action::Keep(self.votes_topic, benefit::VOTE_MESSAGE)
|
||||
} else {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Bad signature on message: {:?}, from: {:?}", vote, sender
|
||||
);
|
||||
Action::Discard(cost::BAD_SIGNATURE)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_finality_proof(
|
||||
&self,
|
||||
proof: BeefyVersionedFinalityProof<B, AuthorityId>,
|
||||
sender: &PeerId,
|
||||
) -> Action<B::Hash> {
|
||||
let (round, set_id) = proof_block_num_and_set_id::<B, AuthorityId>(&proof);
|
||||
self.known_peers.lock().note_vote_for(*sender, round);
|
||||
|
||||
let action = {
|
||||
let guard = self.gossip_filter.read();
|
||||
|
||||
// Verify general usefulness of the justification.
|
||||
match guard.consider_finality_proof(round, set_id) {
|
||||
Consider::RejectPast => return Action::Discard(cost::OUTDATED_MESSAGE),
|
||||
Consider::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE),
|
||||
// When we can't evaluate, it's our fault (e.g. filter not initialized yet), we
|
||||
// discard the proof without punishing or rewarding the sending peer.
|
||||
Consider::CannotEvaluate => return Action::DiscardNoReport,
|
||||
Consider::Accept => {},
|
||||
}
|
||||
|
||||
if guard.is_already_proven(round) {
|
||||
return Action::Discard(benefit::NOT_INTERESTED);
|
||||
}
|
||||
|
||||
// Verify justification signatures.
|
||||
guard
|
||||
.validator_set()
|
||||
.map(|validator_set| {
|
||||
if let Err((_, signatures_checked)) =
|
||||
verify_with_validator_set::<B, AuthorityId>(round, validator_set, &proof)
|
||||
{
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Bad signatures on message: {:?}, from: {:?}", proof, sender
|
||||
);
|
||||
let mut cost = cost::INVALID_PROOF;
|
||||
cost.value +=
|
||||
cost::PER_SIGNATURE_CHECKED.saturating_mul(signatures_checked as i32);
|
||||
Action::Discard(cost)
|
||||
} else {
|
||||
Action::Keep(self.justifs_topic, benefit::VALIDATED_PROOF)
|
||||
}
|
||||
})
|
||||
// When we can't evaluate, it's our fault (e.g. filter not initialized yet), we
|
||||
// discard the proof without punishing or rewarding the sending peer.
|
||||
.unwrap_or(Action::DiscardNoReport)
|
||||
};
|
||||
if matches!(action, Action::Keep(_, _)) {
|
||||
self.gossip_filter.write().mark_round_as_proven(round);
|
||||
}
|
||||
action
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, N, AuthorityId> Validator<B> for GossipValidator<B, N, AuthorityId>
|
||||
where
|
||||
B: Block,
|
||||
AuthorityId: AuthorityIdBound,
|
||||
N: NetworkPeers + Send + Sync,
|
||||
{
|
||||
fn peer_disconnected(&self, _context: &mut dyn ValidatorContext<B>, who: &PeerId) {
|
||||
self.known_peers.lock().remove(who);
|
||||
}
|
||||
|
||||
fn validate(
|
||||
&self,
|
||||
context: &mut dyn ValidatorContext<B>,
|
||||
sender: &PeerId,
|
||||
mut data: &[u8],
|
||||
) -> ValidationResult<B::Hash> {
|
||||
let raw = data;
|
||||
let action = match GossipMessage::<B, AuthorityId>::decode_all(&mut data) {
|
||||
Ok(GossipMessage::Vote(msg)) => self.validate_vote(msg, sender),
|
||||
Ok(GossipMessage::FinalityProof(proof)) => self.validate_finality_proof(proof, sender),
|
||||
Err(e) => {
|
||||
debug!(target: LOG_TARGET, "Error decoding message: {}", e);
|
||||
let bytes = raw.len().min(i32::MAX as usize) as i32;
|
||||
let cost = ReputationChange::new(
|
||||
bytes.saturating_mul(cost::PER_UNDECODABLE_BYTE),
|
||||
"BEEFY: Bad packet",
|
||||
);
|
||||
Action::Discard(cost)
|
||||
},
|
||||
};
|
||||
match action {
|
||||
Action::Keep(topic, cb) => {
|
||||
self.report(*sender, cb);
|
||||
context.broadcast_message(topic, data.to_vec(), false);
|
||||
ValidationResult::ProcessAndKeep(topic)
|
||||
},
|
||||
Action::Discard(cb) => {
|
||||
self.report(*sender, cb);
|
||||
ValidationResult::Discard
|
||||
},
|
||||
Action::DiscardNoReport => ValidationResult::Discard,
|
||||
}
|
||||
}
|
||||
|
||||
fn message_expired<'a>(&'a self) -> Box<dyn FnMut(B::Hash, &[u8]) -> bool + 'a> {
|
||||
let filter = self.gossip_filter.read();
|
||||
Box::new(move |_topic, mut data| {
|
||||
match GossipMessage::<B, AuthorityId>::decode_all(&mut data) {
|
||||
Ok(GossipMessage::Vote(msg)) => {
|
||||
let round = msg.commitment.block_number;
|
||||
let set_id = msg.commitment.validator_set_id;
|
||||
let expired = filter.consider_vote(round, set_id) != Consider::Accept;
|
||||
trace!(target: LOG_TARGET, "🥩 Vote for round #{} expired: {}", round, expired);
|
||||
expired
|
||||
},
|
||||
Ok(GossipMessage::FinalityProof(proof)) => {
|
||||
let (round, set_id) = proof_block_num_and_set_id::<B, AuthorityId>(&proof);
|
||||
let expired = filter.consider_finality_proof(round, set_id) != Consider::Accept;
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Finality proof for round #{} expired: {}",
|
||||
round,
|
||||
expired
|
||||
);
|
||||
expired
|
||||
},
|
||||
Err(_) => true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn message_allowed<'a>(
|
||||
&'a self,
|
||||
) -> Box<dyn FnMut(&PeerId, MessageIntent, &B::Hash, &[u8]) -> bool + 'a> {
|
||||
let do_rebroadcast = {
|
||||
let now = Instant::now();
|
||||
let mut next_rebroadcast = self.next_rebroadcast.lock();
|
||||
if now >= *next_rebroadcast {
|
||||
trace!(target: LOG_TARGET, "🥩 Gossip rebroadcast");
|
||||
*next_rebroadcast = now + REBROADCAST_AFTER;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let filter = self.gossip_filter.read();
|
||||
Box::new(move |_who, intent, _topic, mut data| {
|
||||
if let MessageIntent::PeriodicRebroadcast = intent {
|
||||
return do_rebroadcast;
|
||||
}
|
||||
|
||||
match GossipMessage::<B, AuthorityId>::decode_all(&mut data) {
|
||||
Ok(GossipMessage::Vote(msg)) => {
|
||||
let round = msg.commitment.block_number;
|
||||
let set_id = msg.commitment.validator_set_id;
|
||||
let allowed = filter.consider_vote(round, set_id) == Consider::Accept;
|
||||
trace!(target: LOG_TARGET, "🥩 Vote for round #{} allowed: {}", round, allowed);
|
||||
allowed
|
||||
},
|
||||
Ok(GossipMessage::FinalityProof(proof)) => {
|
||||
let (round, set_id) = proof_block_num_and_set_id::<B, AuthorityId>(&proof);
|
||||
let allowed = filter.consider_finality_proof(round, set_id) == Consider::Accept;
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Finality proof for round #{} allowed: {}",
|
||||
round,
|
||||
allowed
|
||||
);
|
||||
allowed
|
||||
},
|
||||
Err(_) => false,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::{communication::peers::PeerReport, keystore::BeefyKeystore};
|
||||
use pezsc_network_test::Block;
|
||||
use pezsp_application_crypto::key_types::BEEFY as BEEFY_KEY_TYPE;
|
||||
use pezsp_consensus_beefy::{
|
||||
ecdsa_crypto, known_payloads, test_utils::Keyring, Commitment, MmrRootHash, Payload,
|
||||
SignedCommitment, VoteMessage,
|
||||
};
|
||||
use pezsp_keystore::{testing::MemoryKeystore, Keystore};
|
||||
|
||||
pub(crate) struct TestNetwork {
|
||||
report_sender: futures::channel::mpsc::UnboundedSender<PeerReport>,
|
||||
}
|
||||
|
||||
impl TestNetwork {
|
||||
pub fn new() -> (Self, futures::channel::mpsc::UnboundedReceiver<PeerReport>) {
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
|
||||
(Self { report_sender: tx }, rx)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NetworkPeers for TestNetwork {
|
||||
fn set_authorized_peers(&self, _: std::collections::HashSet<PeerId>) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_authorized_only(&self, _: bool) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn add_known_address(&self, _: PeerId, _: pezsc_network::Multiaddr) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn report_peer(&self, peer_id: PeerId, cost_benefit: ReputationChange) {
|
||||
let _ = self.report_sender.unbounded_send(PeerReport { who: peer_id, cost_benefit });
|
||||
}
|
||||
|
||||
fn peer_reputation(&self, _: &PeerId) -> i32 {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn disconnect_peer(&self, _: PeerId, _: pezsc_network::ProtocolName) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn accept_unreserved_peers(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn deny_unreserved_peers(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn add_reserved_peer(
|
||||
&self,
|
||||
_: pezsc_network::config::MultiaddrWithPeerId,
|
||||
) -> Result<(), String> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn remove_reserved_peer(&self, _: PeerId) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_reserved_peers(
|
||||
&self,
|
||||
_: pezsc_network::ProtocolName,
|
||||
_: std::collections::HashSet<pezsc_network::Multiaddr>,
|
||||
) -> Result<(), String> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn add_peers_to_reserved_set(
|
||||
&self,
|
||||
_: pezsc_network::ProtocolName,
|
||||
_: std::collections::HashSet<pezsc_network::Multiaddr>,
|
||||
) -> Result<(), String> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn remove_peers_from_reserved_set(
|
||||
&self,
|
||||
_: pezsc_network::ProtocolName,
|
||||
_: Vec<PeerId>,
|
||||
) -> Result<(), String> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn sync_num_connected(&self) -> usize {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn peer_role(&self, _: PeerId, _: Vec<u8>) -> Option<pezsc_network::ObservedRole> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn reserved_peers(&self) -> Result<Vec<PeerId>, ()> {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
|
||||
struct TestContext;
|
||||
impl<B: pezsp_runtime::traits::Block> ValidatorContext<B> for TestContext {
|
||||
fn broadcast_topic(&mut self, _topic: B::Hash, _force: bool) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn broadcast_message(&mut self, _topic: B::Hash, _message: Vec<u8>, _force: bool) {}
|
||||
|
||||
fn send_message(&mut self, _who: &pezsc_network_types::PeerId, _message: Vec<u8>) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn send_topic(&mut self, _who: &pezsc_network_types::PeerId, _topic: B::Hash, _force: bool) {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign_commitment<BN: Encode>(
|
||||
who: &Keyring<ecdsa_crypto::AuthorityId>,
|
||||
commitment: &Commitment<BN>,
|
||||
) -> ecdsa_crypto::Signature {
|
||||
let store = MemoryKeystore::new();
|
||||
store.ecdsa_generate_new(BEEFY_KEY_TYPE, Some(&who.to_seed())).unwrap();
|
||||
let beefy_keystore: BeefyKeystore<ecdsa_crypto::AuthorityId> = Some(store.into()).into();
|
||||
beefy_keystore.sign(&who.public(), &commitment.encode()).unwrap()
|
||||
}
|
||||
|
||||
fn dummy_vote(
|
||||
block_number: u64,
|
||||
) -> VoteMessage<u64, ecdsa_crypto::AuthorityId, ecdsa_crypto::Signature> {
|
||||
let payload = Payload::from_single_entry(
|
||||
known_payloads::MMR_ROOT_ID,
|
||||
MmrRootHash::default().encode(),
|
||||
);
|
||||
let commitment = Commitment { payload, block_number, validator_set_id: 0 };
|
||||
let signature = sign_commitment(&Keyring::Alice, &commitment);
|
||||
|
||||
VoteMessage { commitment, id: Keyring::Alice.public(), signature }
|
||||
}
|
||||
|
||||
pub fn dummy_proof(
|
||||
block_number: u64,
|
||||
validator_set: &ValidatorSet<ecdsa_crypto::AuthorityId>,
|
||||
) -> BeefyVersionedFinalityProof<Block, ecdsa_crypto::AuthorityId> {
|
||||
let payload = Payload::from_single_entry(
|
||||
known_payloads::MMR_ROOT_ID,
|
||||
MmrRootHash::default().encode(),
|
||||
);
|
||||
let commitment = Commitment { payload, block_number, validator_set_id: validator_set.id() };
|
||||
let signatures = validator_set
|
||||
.validators()
|
||||
.iter()
|
||||
.map(|validator: &ecdsa_crypto::AuthorityId| {
|
||||
Some(sign_commitment(
|
||||
&Keyring::<ecdsa_crypto::AuthorityId>::from_public(validator).unwrap(),
|
||||
&commitment,
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
BeefyVersionedFinalityProof::<Block, ecdsa_crypto::AuthorityId>::V1(SignedCommitment {
|
||||
commitment,
|
||||
signatures,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_validate_messages() {
|
||||
let keys = vec![Keyring::<ecdsa_crypto::AuthorityId>::Alice.public()];
|
||||
let validator_set =
|
||||
ValidatorSet::<ecdsa_crypto::AuthorityId>::new(keys.clone(), 0).unwrap();
|
||||
|
||||
let (network, mut report_stream) = TestNetwork::new();
|
||||
|
||||
let gv = GossipValidator::<Block, _, ecdsa_crypto::AuthorityId>::new(
|
||||
Arc::new(Mutex::new(KnownPeers::new())),
|
||||
Arc::new(network),
|
||||
);
|
||||
let sender = PeerId::random();
|
||||
let mut context = TestContext;
|
||||
|
||||
// reject message, decoding error
|
||||
let bad_encoding = b"0000000000".as_slice();
|
||||
let expected_cost = ReputationChange::new(
|
||||
(bad_encoding.len() as i32).saturating_mul(cost::PER_UNDECODABLE_BYTE),
|
||||
"BEEFY: Bad packet",
|
||||
);
|
||||
let mut expected_report = PeerReport { who: sender, cost_benefit: expected_cost };
|
||||
let res = gv.validate(&mut context, &sender, bad_encoding);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
|
||||
|
||||
// verify votes validation
|
||||
|
||||
let vote = dummy_vote(3);
|
||||
let encoded =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::Vote(vote.clone()).encode();
|
||||
|
||||
// filter not initialized
|
||||
let res = gv.validate(&mut context, &sender, &encoded);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
// nothing reported
|
||||
assert!(report_stream.try_next().is_err());
|
||||
|
||||
gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set });
|
||||
// nothing in cache first time
|
||||
let res = gv.validate(&mut context, &sender, &encoded);
|
||||
assert!(matches!(res, ValidationResult::ProcessAndKeep(_)));
|
||||
expected_report.cost_benefit = benefit::VOTE_MESSAGE;
|
||||
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
|
||||
|
||||
// reject vote, voter not in validator set
|
||||
let mut bad_vote = vote.clone();
|
||||
bad_vote.id = Keyring::Bob.public();
|
||||
let bad_vote = GossipMessage::<Block, ecdsa_crypto::AuthorityId>::Vote(bad_vote).encode();
|
||||
let res = gv.validate(&mut context, &sender, &bad_vote);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
expected_report.cost_benefit = cost::UNKNOWN_VOTER;
|
||||
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
|
||||
|
||||
// reject if the round is not GRANDPA finalized
|
||||
gv.update_filter(GossipFilterCfg { start: 1, end: 2, validator_set: &validator_set });
|
||||
let number = vote.commitment.block_number;
|
||||
let set_id = vote.commitment.validator_set_id;
|
||||
assert_eq!(gv.gossip_filter.read().consider_vote(number, set_id), Consider::RejectFuture);
|
||||
let res = gv.validate(&mut context, &sender, &encoded);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
expected_report.cost_benefit = cost::FUTURE_MESSAGE;
|
||||
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
|
||||
|
||||
// reject if the round is not live anymore
|
||||
gv.update_filter(GossipFilterCfg { start: 7, end: 10, validator_set: &validator_set });
|
||||
let number = vote.commitment.block_number;
|
||||
let set_id = vote.commitment.validator_set_id;
|
||||
assert_eq!(gv.gossip_filter.read().consider_vote(number, set_id), Consider::RejectPast);
|
||||
let res = gv.validate(&mut context, &sender, &encoded);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
expected_report.cost_benefit = cost::OUTDATED_MESSAGE;
|
||||
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
|
||||
|
||||
// now verify proofs validation
|
||||
|
||||
// reject old proof
|
||||
let proof = dummy_proof(5, &validator_set);
|
||||
let encoded_proof =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
|
||||
let res = gv.validate(&mut context, &sender, &encoded_proof);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
expected_report.cost_benefit = cost::OUTDATED_MESSAGE;
|
||||
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
|
||||
|
||||
// accept next proof with good set_id
|
||||
let proof = dummy_proof(7, &validator_set);
|
||||
let encoded_proof =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
|
||||
let res = gv.validate(&mut context, &sender, &encoded_proof);
|
||||
assert!(matches!(res, ValidationResult::ProcessAndKeep(_)));
|
||||
expected_report.cost_benefit = benefit::VALIDATED_PROOF;
|
||||
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
|
||||
|
||||
// accept future proof with good set_id
|
||||
let proof = dummy_proof(20, &validator_set);
|
||||
let encoded_proof =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
|
||||
let res = gv.validate(&mut context, &sender, &encoded_proof);
|
||||
assert!(matches!(res, ValidationResult::ProcessAndKeep(_)));
|
||||
expected_report.cost_benefit = benefit::VALIDATED_PROOF;
|
||||
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
|
||||
|
||||
// reject proof, future set_id
|
||||
let bad_validator_set = ValidatorSet::<ecdsa_crypto::AuthorityId>::new(keys, 1).unwrap();
|
||||
let proof = dummy_proof(20, &bad_validator_set);
|
||||
let encoded_proof =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
|
||||
let res = gv.validate(&mut context, &sender, &encoded_proof);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
expected_report.cost_benefit = cost::FUTURE_MESSAGE;
|
||||
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
|
||||
|
||||
// reject proof, bad signatures (Bob instead of Alice)
|
||||
let bad_validator_set =
|
||||
ValidatorSet::<ecdsa_crypto::AuthorityId>::new(vec![Keyring::Bob.public()], 0).unwrap();
|
||||
let proof = dummy_proof(21, &bad_validator_set);
|
||||
let encoded_proof =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
|
||||
let res = gv.validate(&mut context, &sender, &encoded_proof);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
expected_report.cost_benefit = cost::INVALID_PROOF;
|
||||
expected_report.cost_benefit.value += cost::PER_SIGNATURE_CHECKED;
|
||||
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn messages_allowed_and_expired() {
|
||||
let keys = vec![Keyring::Alice.public()];
|
||||
let validator_set =
|
||||
ValidatorSet::<ecdsa_crypto::AuthorityId>::new(keys.clone(), 0).unwrap();
|
||||
let gv = GossipValidator::<Block, _, ecdsa_crypto::AuthorityId>::new(
|
||||
Arc::new(Mutex::new(KnownPeers::new())),
|
||||
Arc::new(TestNetwork::new().0),
|
||||
);
|
||||
gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set });
|
||||
let sender = pezsc_network_types::PeerId::random();
|
||||
let topic = Default::default();
|
||||
let intent = MessageIntent::Broadcast;
|
||||
|
||||
// conclude 2
|
||||
gv.update_filter(GossipFilterCfg { start: 2, end: 10, validator_set: &validator_set });
|
||||
let mut allowed = gv.message_allowed();
|
||||
let mut expired = gv.message_expired();
|
||||
|
||||
// check bad vote format
|
||||
assert!(!allowed(&sender, intent, &topic, &mut [0u8; 16]));
|
||||
assert!(expired(topic, &mut [0u8; 16]));
|
||||
|
||||
// inactive round 1 -> expired
|
||||
let vote = dummy_vote(1);
|
||||
let mut encoded_vote =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::Vote(vote).encode();
|
||||
assert!(!allowed(&sender, intent, &topic, &mut encoded_vote));
|
||||
assert!(expired(topic, &mut encoded_vote));
|
||||
let proof = dummy_proof(1, &validator_set);
|
||||
let mut encoded_proof =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
|
||||
assert!(!allowed(&sender, intent, &topic, &mut encoded_proof));
|
||||
assert!(expired(topic, &mut encoded_proof));
|
||||
|
||||
// active round 2 -> !expired - concluded but still gossiped
|
||||
let vote = dummy_vote(2);
|
||||
let mut encoded_vote =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::Vote(vote).encode();
|
||||
assert!(allowed(&sender, intent, &topic, &mut encoded_vote));
|
||||
assert!(!expired(topic, &mut encoded_vote));
|
||||
let proof = dummy_proof(2, &validator_set);
|
||||
let mut encoded_proof =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
|
||||
assert!(allowed(&sender, intent, &topic, &mut encoded_proof));
|
||||
assert!(!expired(topic, &mut encoded_proof));
|
||||
// using wrong set_id -> !allowed, expired
|
||||
let bad_validator_set =
|
||||
ValidatorSet::<ecdsa_crypto::AuthorityId>::new(keys.clone(), 1).unwrap();
|
||||
let proof = dummy_proof(2, &bad_validator_set);
|
||||
let mut encoded_proof =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
|
||||
assert!(!allowed(&sender, intent, &topic, &mut encoded_proof));
|
||||
assert!(expired(topic, &mut encoded_proof));
|
||||
|
||||
// in progress round 3 -> !expired
|
||||
let vote = dummy_vote(3);
|
||||
let mut encoded_vote =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::Vote(vote).encode();
|
||||
assert!(allowed(&sender, intent, &topic, &mut encoded_vote));
|
||||
assert!(!expired(topic, &mut encoded_vote));
|
||||
let proof = dummy_proof(3, &validator_set);
|
||||
let mut encoded_proof =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
|
||||
assert!(allowed(&sender, intent, &topic, &mut encoded_proof));
|
||||
assert!(!expired(topic, &mut encoded_proof));
|
||||
|
||||
// unseen round 4 -> !expired
|
||||
let vote = dummy_vote(4);
|
||||
let mut encoded_vote =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::Vote(vote).encode();
|
||||
assert!(allowed(&sender, intent, &topic, &mut encoded_vote));
|
||||
assert!(!expired(topic, &mut encoded_vote));
|
||||
let proof = dummy_proof(4, &validator_set);
|
||||
let mut encoded_proof =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
|
||||
assert!(allowed(&sender, intent, &topic, &mut encoded_proof));
|
||||
assert!(!expired(topic, &mut encoded_proof));
|
||||
|
||||
// future round 11 -> expired
|
||||
let vote = dummy_vote(11);
|
||||
let mut encoded_vote =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::Vote(vote).encode();
|
||||
assert!(!allowed(&sender, intent, &topic, &mut encoded_vote));
|
||||
assert!(expired(topic, &mut encoded_vote));
|
||||
// future proofs allowed while same set_id -> allowed
|
||||
let proof = dummy_proof(11, &validator_set);
|
||||
let mut encoded_proof =
|
||||
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
|
||||
assert!(allowed(&sender, intent, &topic, &mut encoded_proof));
|
||||
assert!(!expired(topic, &mut encoded_proof));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn messages_rebroadcast() {
|
||||
let keys = vec![Keyring::Alice.public()];
|
||||
let validator_set =
|
||||
ValidatorSet::<ecdsa_crypto::AuthorityId>::new(keys.clone(), 0).unwrap();
|
||||
let gv = GossipValidator::<Block, _, ecdsa_crypto::AuthorityId>::new(
|
||||
Arc::new(Mutex::new(KnownPeers::new())),
|
||||
Arc::new(TestNetwork::new().0),
|
||||
);
|
||||
gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set });
|
||||
let sender = pezsc_network_types::PeerId::random();
|
||||
let topic = Default::default();
|
||||
|
||||
let vote = dummy_vote(1);
|
||||
let mut encoded_vote = vote.encode();
|
||||
|
||||
// re-broadcasting only allowed at `REBROADCAST_AFTER` intervals
|
||||
let intent = MessageIntent::PeriodicRebroadcast;
|
||||
let mut allowed = gv.message_allowed();
|
||||
|
||||
// rebroadcast not allowed so soon after GossipValidator creation
|
||||
assert!(!allowed(&sender, intent, &topic, &mut encoded_vote));
|
||||
|
||||
// hack the inner deadline to be `now`
|
||||
*gv.next_rebroadcast.lock() = Instant::now();
|
||||
|
||||
// still not allowed on old `allowed` closure result
|
||||
assert!(!allowed(&sender, intent, &topic, &mut encoded_vote));
|
||||
|
||||
// renew closure result
|
||||
let mut allowed = gv.message_allowed();
|
||||
// rebroadcast should be allowed now
|
||||
assert!(allowed(&sender, intent, &topic, &mut encoded_vote));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Communication streams for the BEEFY networking protocols.
|
||||
|
||||
pub mod notification;
|
||||
pub mod request_response;
|
||||
|
||||
pub(crate) mod gossip;
|
||||
pub(crate) mod peers;
|
||||
|
||||
pub(crate) mod beefy_protocol_name {
|
||||
use array_bytes::bytes2hex;
|
||||
use pezsc_network::ProtocolName;
|
||||
|
||||
/// BEEFY votes gossip protocol name suffix.
|
||||
const GOSSIP_NAME: &str = "/beefy/2";
|
||||
/// BEEFY justifications protocol name suffix.
|
||||
const JUSTIFICATIONS_NAME: &str = "/beefy/justifications/1";
|
||||
|
||||
/// Name of the votes gossip protocol used by BEEFY.
|
||||
///
|
||||
/// Must be registered towards the networking in order for BEEFY voter to properly function.
|
||||
pub fn gossip_protocol_name<Hash: AsRef<[u8]>>(
|
||||
genesis_hash: Hash,
|
||||
fork_id: Option<&str>,
|
||||
) -> ProtocolName {
|
||||
let genesis_hash = genesis_hash.as_ref();
|
||||
if let Some(fork_id) = fork_id {
|
||||
format!("/{}/{}{}", bytes2hex("", genesis_hash), fork_id, GOSSIP_NAME).into()
|
||||
} else {
|
||||
format!("/{}{}", bytes2hex("", genesis_hash), GOSSIP_NAME).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Name of the BEEFY justifications request-response protocol.
|
||||
pub fn justifications_protocol_name<Hash: AsRef<[u8]>>(
|
||||
genesis_hash: Hash,
|
||||
fork_id: Option<&str>,
|
||||
) -> ProtocolName {
|
||||
let genesis_hash = genesis_hash.as_ref();
|
||||
if let Some(fork_id) = fork_id {
|
||||
format!("/{}/{}{}", bytes2hex("", genesis_hash), fork_id, JUSTIFICATIONS_NAME).into()
|
||||
} else {
|
||||
format!("/{}{}", bytes2hex("", genesis_hash), JUSTIFICATIONS_NAME).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the configuration value to put in
|
||||
/// [`pezsc_network::config::FullNetworkConfiguration`].
|
||||
/// For standard protocol name see [`beefy_protocol_name::gossip_protocol_name`].
|
||||
pub fn beefy_peers_set_config<
|
||||
B: pezsp_runtime::traits::Block,
|
||||
N: pezsc_network::NetworkBackend<B, <B as pezsp_runtime::traits::Block>::Hash>,
|
||||
>(
|
||||
gossip_protocol_name: pezsc_network::ProtocolName,
|
||||
metrics: pezsc_network::service::NotificationMetrics,
|
||||
peer_store_handle: std::sync::Arc<dyn pezsc_network::peer_store::PeerStoreProvider>,
|
||||
) -> (N::NotificationProtocolConfig, Box<dyn pezsc_network::NotificationService>) {
|
||||
let (cfg, notification_service) = N::notification_config(
|
||||
gossip_protocol_name,
|
||||
Vec::new(),
|
||||
1024 * 1024,
|
||||
None,
|
||||
pezsc_network::config::SetConfig {
|
||||
in_peers: 25,
|
||||
out_peers: 25,
|
||||
reserved_nodes: Vec::new(),
|
||||
non_reserved_mode: pezsc_network::config::NonReservedPeerMode::Accept,
|
||||
},
|
||||
metrics,
|
||||
peer_store_handle,
|
||||
);
|
||||
(cfg, notification_service)
|
||||
}
|
||||
|
||||
// cost scalars for reporting peers.
|
||||
mod cost {
|
||||
use pezsc_network::ReputationChange as Rep;
|
||||
// Message that's for an outdated round.
|
||||
pub(super) const OUTDATED_MESSAGE: Rep = Rep::new(-50, "BEEFY: Past message");
|
||||
// Message that's from the future relative to our current set-id.
|
||||
pub(super) const FUTURE_MESSAGE: Rep = Rep::new(-100, "BEEFY: Future message");
|
||||
// Vote message containing bad signature.
|
||||
pub(super) const BAD_SIGNATURE: Rep = Rep::new(-100, "BEEFY: Bad signature");
|
||||
// Message received with vote from voter not in validator set.
|
||||
pub(super) const UNKNOWN_VOTER: Rep = Rep::new(-150, "BEEFY: Unknown voter");
|
||||
// Message containing invalid proof.
|
||||
pub(super) const INVALID_PROOF: Rep = Rep::new(-5000, "BEEFY: Invalid commit");
|
||||
// Reputation cost per signature checked for invalid proof.
|
||||
pub(super) const PER_SIGNATURE_CHECKED: i32 = -25;
|
||||
// Reputation cost per byte for un-decodable message.
|
||||
pub(super) const PER_UNDECODABLE_BYTE: i32 = -5;
|
||||
// On-demand request was refused by peer.
|
||||
pub(super) const REFUSAL_RESPONSE: Rep = Rep::new(-100, "BEEFY: Proof request refused");
|
||||
// On-demand request for a proof that can't be found in the backend.
|
||||
pub(super) const UNKNOWN_PROOF_REQUEST: Rep = Rep::new(-150, "BEEFY: Unknown proof request");
|
||||
}
|
||||
|
||||
// benefit scalars for reporting peers.
|
||||
mod benefit {
|
||||
use pezsc_network::ReputationChange as Rep;
|
||||
pub(super) const VOTE_MESSAGE: Rep = Rep::new(100, "BEEFY: Round vote message");
|
||||
pub(super) const NOT_INTERESTED: Rep = Rep::new(10, "BEEFY: Not interested in round");
|
||||
pub(super) const VALIDATED_PROOF: Rep = Rep::new(100, "BEEFY: Justification");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use pezsp_core::H256;
|
||||
|
||||
#[test]
|
||||
fn beefy_protocols_names() {
|
||||
use beefy_protocol_name::{gossip_protocol_name, justifications_protocol_name};
|
||||
// Create protocol name using random genesis hash.
|
||||
let genesis_hash = H256::random();
|
||||
let genesis_hex = array_bytes::bytes2hex("", genesis_hash);
|
||||
|
||||
let expected_gossip_name = format!("/{}/beefy/2", genesis_hex);
|
||||
let gossip_proto_name = gossip_protocol_name(&genesis_hash, None);
|
||||
assert_eq!(gossip_proto_name.to_string(), expected_gossip_name);
|
||||
|
||||
let expected_justif_name = format!("/{}/beefy/justifications/1", genesis_hex);
|
||||
let justif_proto_name = justifications_protocol_name(&genesis_hash, None);
|
||||
assert_eq!(justif_proto_name.to_string(), expected_justif_name);
|
||||
|
||||
// Create protocol name using hardcoded genesis hash. Verify exact representation.
|
||||
let genesis_hash = [
|
||||
50, 4, 60, 123, 58, 106, 216, 246, 194, 188, 139, 193, 33, 212, 202, 171, 9, 55, 123,
|
||||
94, 8, 43, 12, 251, 187, 57, 173, 19, 188, 74, 205, 147,
|
||||
];
|
||||
let genesis_hex = "32043c7b3a6ad8f6c2bc8bc121d4caab09377b5e082b0cfbbb39ad13bc4acd93";
|
||||
|
||||
let expected_gossip_name = format!("/{}/beefy/2", genesis_hex);
|
||||
let gossip_proto_name = gossip_protocol_name(&genesis_hash, None);
|
||||
assert_eq!(gossip_proto_name.to_string(), expected_gossip_name);
|
||||
|
||||
let expected_justif_name = format!("/{}/beefy/justifications/1", genesis_hex);
|
||||
let justif_proto_name = justifications_protocol_name(&genesis_hash, None);
|
||||
assert_eq!(justif_proto_name.to_string(), expected_justif_name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use pezsc_utils::notification::{NotificationSender, NotificationStream, TracingKeyStr};
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
use crate::justification::BeefyVersionedFinalityProof;
|
||||
|
||||
/// The sending half of the notifications channel(s) used to send
|
||||
/// notifications about best BEEFY block from the gadget side.
|
||||
pub type BeefyBestBlockSender<Block> = NotificationSender<<Block as BlockT>::Hash>;
|
||||
|
||||
/// The receiving half of a notifications channel used to receive
|
||||
/// notifications about best BEEFY blocks determined on the gadget side.
|
||||
pub type BeefyBestBlockStream<Block> =
|
||||
NotificationStream<<Block as BlockT>::Hash, BeefyBestBlockTracingKey>;
|
||||
|
||||
/// The sending half of the notifications channel(s) used to send notifications
|
||||
/// about versioned finality proof generated at the end of a BEEFY round.
|
||||
pub type BeefyVersionedFinalityProofSender<Block, AuthorityId> =
|
||||
NotificationSender<BeefyVersionedFinalityProof<Block, AuthorityId>>;
|
||||
|
||||
/// The receiving half of a notifications channel used to receive notifications
|
||||
/// about versioned finality proof generated at the end of a BEEFY round.
|
||||
pub type BeefyVersionedFinalityProofStream<Block, AuthorityId> = NotificationStream<
|
||||
BeefyVersionedFinalityProof<Block, AuthorityId>,
|
||||
BeefyVersionedFinalityProofTracingKey,
|
||||
>;
|
||||
|
||||
/// Provides tracing key for BEEFY best block stream.
|
||||
#[derive(Clone)]
|
||||
pub struct BeefyBestBlockTracingKey;
|
||||
impl TracingKeyStr for BeefyBestBlockTracingKey {
|
||||
const TRACING_KEY: &'static str = "mpsc_beefy_best_block_notification_stream";
|
||||
}
|
||||
|
||||
/// Provides tracing key for BEEFY versioned finality proof stream.
|
||||
#[derive(Clone)]
|
||||
pub struct BeefyVersionedFinalityProofTracingKey;
|
||||
impl TracingKeyStr for BeefyVersionedFinalityProofTracingKey {
|
||||
const TRACING_KEY: &'static str = "mpsc_beefy_versioned_finality_proof_notification_stream";
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Logic for keeping track of BEEFY peers.
|
||||
|
||||
use pezsc_network::ReputationChange;
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_runtime::traits::{Block, NumberFor, Zero};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
/// Report specifying a reputation change for a given peer.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct PeerReport {
|
||||
pub who: PeerId,
|
||||
pub cost_benefit: ReputationChange,
|
||||
}
|
||||
|
||||
struct PeerData<B: Block> {
|
||||
last_voted_on: NumberFor<B>,
|
||||
}
|
||||
|
||||
impl<B: Block> Default for PeerData<B> {
|
||||
fn default() -> Self {
|
||||
PeerData { last_voted_on: Zero::zero() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep a simple map of connected peers
|
||||
/// and the most recent voting round they participated in.
|
||||
pub struct KnownPeers<B: Block> {
|
||||
live: HashMap<PeerId, PeerData<B>>,
|
||||
}
|
||||
|
||||
impl<B: Block> KnownPeers<B> {
|
||||
pub fn new() -> Self {
|
||||
Self { live: HashMap::new() }
|
||||
}
|
||||
|
||||
/// Note vote round number for `peer`.
|
||||
pub fn note_vote_for(&mut self, peer: PeerId, round: NumberFor<B>) {
|
||||
let data = self.live.entry(peer).or_default();
|
||||
data.last_voted_on = round.max(data.last_voted_on);
|
||||
}
|
||||
|
||||
/// Remove connected `peer`.
|
||||
pub fn remove(&mut self, peer: &PeerId) {
|
||||
self.live.remove(peer);
|
||||
}
|
||||
|
||||
/// Return _filtered and cloned_ list of peers that have voted on higher than `block`.
|
||||
pub fn further_than(&self, block: NumberFor<B>) -> VecDeque<PeerId> {
|
||||
self.live
|
||||
.iter()
|
||||
.filter_map(|(k, v)| (v.last_voted_on > block).then_some(k))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Answer whether `peer` is part of `KnownPeers` set.
|
||||
pub fn contains(&self, peer: &PeerId) -> bool {
|
||||
self.live.contains_key(peer)
|
||||
}
|
||||
|
||||
/// Number of peers in the set.
|
||||
pub fn len(&self) -> usize {
|
||||
self.live.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_track_known_peers_progress() {
|
||||
let (alice, bob, charlie) = (PeerId::random(), PeerId::random(), PeerId::random());
|
||||
let mut peers = KnownPeers::<pezsc_network_test::Block>::new();
|
||||
assert!(peers.live.is_empty());
|
||||
|
||||
// 'Tracked' Bob seen voting for 5.
|
||||
peers.note_vote_for(bob, 5);
|
||||
// Previously unseen Charlie now seen voting for 10.
|
||||
peers.note_vote_for(charlie, 10);
|
||||
|
||||
assert_eq!(peers.live.len(), 2);
|
||||
assert!(!peers.contains(&alice));
|
||||
assert!(peers.contains(&bob));
|
||||
assert!(peers.contains(&charlie));
|
||||
|
||||
// Get peers at block > 4
|
||||
let further_than_4 = peers.further_than(4);
|
||||
// Should be Bob and Charlie
|
||||
assert_eq!(further_than_4.len(), 2);
|
||||
assert!(further_than_4.contains(&bob));
|
||||
assert!(further_than_4.contains(&charlie));
|
||||
|
||||
// 'Tracked' Alice seen voting for 10.
|
||||
peers.note_vote_for(alice, 10);
|
||||
|
||||
// Get peers at block > 9
|
||||
let further_than_9 = peers.further_than(9);
|
||||
// Should be Charlie and Alice
|
||||
assert_eq!(further_than_9.len(), 2);
|
||||
assert!(further_than_9.contains(&charlie));
|
||||
assert!(further_than_9.contains(&alice));
|
||||
|
||||
// Remove Alice
|
||||
peers.remove(&alice);
|
||||
assert_eq!(peers.live.len(), 2);
|
||||
assert!(!peers.contains(&alice));
|
||||
|
||||
// Get peers at block >= 9
|
||||
let further_than_9 = peers.further_than(9);
|
||||
// Now should be just Charlie
|
||||
assert_eq!(further_than_9.len(), 1);
|
||||
assert!(further_than_9.contains(&charlie));
|
||||
}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Bizinikiwi.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// Bizinikiwi 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.
|
||||
|
||||
// Bizinikiwi 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 Bizinikiwi. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Helper for handling (i.e. answering) BEEFY justifications requests from a remote peer.
|
||||
|
||||
use codec::DecodeAll;
|
||||
use futures::{channel::oneshot, StreamExt};
|
||||
use log::{debug, trace};
|
||||
use pezsc_client_api::BlockBackend;
|
||||
use pezsc_network::{
|
||||
config as netconfig, service::traits::RequestResponseConfig, types::ProtocolName,
|
||||
NetworkBackend, ReputationChange,
|
||||
};
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_consensus_beefy::BEEFY_ENGINE_ID;
|
||||
use pezsp_runtime::traits::Block;
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
communication::{
|
||||
cost,
|
||||
request_response::{
|
||||
on_demand_justifications_protocol_config, Error, JustificationRequest,
|
||||
BEEFY_SYNC_LOG_TARGET,
|
||||
},
|
||||
},
|
||||
metric_inc,
|
||||
metrics::{register_metrics, OnDemandIncomingRequestsMetrics},
|
||||
};
|
||||
|
||||
/// A request coming in, including a sender for sending responses.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct IncomingRequest<B: Block> {
|
||||
/// `PeerId` of sending peer.
|
||||
pub peer: PeerId,
|
||||
/// The sent request.
|
||||
pub payload: JustificationRequest<B>,
|
||||
/// Sender for sending response back.
|
||||
pub pending_response: oneshot::Sender<netconfig::OutgoingResponse>,
|
||||
}
|
||||
|
||||
impl<B: Block> IncomingRequest<B> {
|
||||
/// Create new `IncomingRequest`.
|
||||
pub fn new(
|
||||
peer: PeerId,
|
||||
payload: JustificationRequest<B>,
|
||||
pending_response: oneshot::Sender<netconfig::OutgoingResponse>,
|
||||
) -> Self {
|
||||
Self { peer, payload, pending_response }
|
||||
}
|
||||
|
||||
/// Try building from raw network request.
|
||||
///
|
||||
/// This function will fail if the request cannot be decoded and will apply passed in
|
||||
/// reputation changes in that case.
|
||||
///
|
||||
/// Params:
|
||||
/// - The raw request to decode
|
||||
/// - Reputation changes to apply for the peer in case decoding fails.
|
||||
pub fn try_from_raw<F>(
|
||||
raw: netconfig::IncomingRequest,
|
||||
reputation_changes_on_err: F,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
F: FnOnce(usize) -> Vec<ReputationChange>,
|
||||
{
|
||||
let netconfig::IncomingRequest { payload, peer, pending_response } = raw;
|
||||
let payload = match JustificationRequest::decode_all(&mut payload.as_ref()) {
|
||||
Ok(payload) => payload,
|
||||
Err(err) => {
|
||||
let response = netconfig::OutgoingResponse {
|
||||
result: Err(()),
|
||||
reputation_changes: reputation_changes_on_err(payload.len()),
|
||||
sent_feedback: None,
|
||||
};
|
||||
if let Err(_) = pending_response.send(response) {
|
||||
return Err(Error::DecodingErrorNoReputationChange(peer, err));
|
||||
}
|
||||
return Err(Error::DecodingError(peer, err));
|
||||
},
|
||||
};
|
||||
Ok(Self::new(peer, payload, pending_response))
|
||||
}
|
||||
}
|
||||
|
||||
/// Receiver for incoming BEEFY justifications requests.
|
||||
///
|
||||
/// Takes care of decoding and handling of invalid encoded requests.
|
||||
pub(crate) struct IncomingRequestReceiver {
|
||||
raw: async_channel::Receiver<netconfig::IncomingRequest>,
|
||||
}
|
||||
|
||||
impl IncomingRequestReceiver {
|
||||
pub fn new(inner: async_channel::Receiver<netconfig::IncomingRequest>) -> Self {
|
||||
Self { raw: inner }
|
||||
}
|
||||
|
||||
/// Try to receive the next incoming request.
|
||||
///
|
||||
/// Any received request will be decoded, on decoding errors the provided reputation changes
|
||||
/// will be applied and an error will be reported.
|
||||
pub async fn recv<B, F>(&mut self, reputation_changes: F) -> Result<IncomingRequest<B>, Error>
|
||||
where
|
||||
B: Block,
|
||||
F: FnOnce(usize) -> Vec<ReputationChange>,
|
||||
{
|
||||
let req = match self.raw.next().await {
|
||||
None => return Err(Error::RequestChannelExhausted),
|
||||
Some(raw) => IncomingRequest::<B>::try_from_raw(raw, reputation_changes)?,
|
||||
};
|
||||
Ok(req)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for incoming BEEFY justifications requests from a remote peer.
|
||||
pub struct BeefyJustifsRequestHandler<B, Client> {
|
||||
pub(crate) request_receiver: IncomingRequestReceiver,
|
||||
pub(crate) justif_protocol_name: ProtocolName,
|
||||
pub(crate) client: Arc<Client>,
|
||||
pub(crate) metrics: Option<OnDemandIncomingRequestsMetrics>,
|
||||
pub(crate) _block: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<B, Client> BeefyJustifsRequestHandler<B, Client>
|
||||
where
|
||||
B: Block,
|
||||
Client: BlockBackend<B> + Send + Sync,
|
||||
{
|
||||
/// Create a new [`BeefyJustifsRequestHandler`].
|
||||
pub fn new<Hash: AsRef<[u8]>, Network: NetworkBackend<B, <B as Block>::Hash>>(
|
||||
genesis_hash: Hash,
|
||||
fork_id: Option<&str>,
|
||||
client: Arc<Client>,
|
||||
prometheus_registry: Option<prometheus_endpoint::Registry>,
|
||||
) -> (Self, Network::RequestResponseProtocolConfig) {
|
||||
let (request_receiver, config): (_, Network::RequestResponseProtocolConfig) =
|
||||
on_demand_justifications_protocol_config::<_, _, Network>(genesis_hash, fork_id);
|
||||
let justif_protocol_name = config.protocol_name().clone();
|
||||
let metrics = register_metrics(prometheus_registry);
|
||||
(
|
||||
Self { request_receiver, justif_protocol_name, client, metrics, _block: PhantomData },
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
/// Network request-response protocol name used by this handler.
|
||||
pub fn protocol_name(&self) -> ProtocolName {
|
||||
self.justif_protocol_name.clone()
|
||||
}
|
||||
|
||||
// Sends back justification response if justification found in client backend.
|
||||
fn handle_request(&self, request: IncomingRequest<B>) -> Result<(), Error> {
|
||||
let mut reputation_changes = vec![];
|
||||
let maybe_encoded_proof = self
|
||||
.client
|
||||
.block_hash(request.payload.begin)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|hash| self.client.justifications(hash).ok().flatten())
|
||||
.and_then(|justifs| justifs.get(BEEFY_ENGINE_ID).cloned())
|
||||
.ok_or_else(|| reputation_changes.push(cost::UNKNOWN_PROOF_REQUEST));
|
||||
request
|
||||
.pending_response
|
||||
.send(netconfig::OutgoingResponse {
|
||||
result: maybe_encoded_proof,
|
||||
reputation_changes,
|
||||
sent_feedback: None,
|
||||
})
|
||||
.map_err(|_| Error::SendResponse)
|
||||
}
|
||||
|
||||
/// Run [`BeefyJustifsRequestHandler`].
|
||||
///
|
||||
/// Should never end, returns `Error` otherwise.
|
||||
pub async fn run(&mut self) -> Error {
|
||||
trace!(target: BEEFY_SYNC_LOG_TARGET, "🥩 Running BeefyJustifsRequestHandler");
|
||||
|
||||
while let Ok(request) = self
|
||||
.request_receiver
|
||||
.recv(|bytes| {
|
||||
let bytes = bytes.min(i32::MAX as usize) as i32;
|
||||
vec![ReputationChange::new(
|
||||
bytes.saturating_mul(cost::PER_UNDECODABLE_BYTE),
|
||||
"BEEFY: Bad request payload",
|
||||
)]
|
||||
})
|
||||
.await
|
||||
{
|
||||
let peer = request.peer;
|
||||
match self.handle_request(request) {
|
||||
Ok(()) => {
|
||||
metric_inc!(self.metrics, beefy_successful_justification_responses);
|
||||
debug!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 Handled BEEFY justification request from {:?}.", peer
|
||||
)
|
||||
},
|
||||
Err(e) => {
|
||||
// peer reputation changes already applied in `self.handle_request()`
|
||||
metric_inc!(self.metrics, beefy_failed_justification_responses);
|
||||
debug!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 Failed to handle BEEFY justification request from {:?}: {}", peer, e,
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
Error::RequestsReceiverStreamClosed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Request/response protocol for syncing BEEFY justifications.
|
||||
|
||||
mod incoming_requests_handler;
|
||||
pub(crate) mod outgoing_requests_engine;
|
||||
|
||||
pub use incoming_requests_handler::BeefyJustifsRequestHandler;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use codec::{Decode, Encode, Error as CodecError};
|
||||
use pezsc_network::NetworkBackend;
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_runtime::traits::{Block, NumberFor};
|
||||
|
||||
use crate::communication::{beefy_protocol_name::justifications_protocol_name, peers::PeerReport};
|
||||
use incoming_requests_handler::IncomingRequestReceiver;
|
||||
|
||||
// 10 seems reasonable, considering justifs are explicitly requested only
|
||||
// for mandatory blocks, by nodes that are syncing/catching-up.
|
||||
const JUSTIF_CHANNEL_SIZE: usize = 10;
|
||||
|
||||
const MAX_RESPONSE_SIZE: u64 = 1024 * 1024;
|
||||
const JUSTIF_REQUEST_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
|
||||
const BEEFY_SYNC_LOG_TARGET: &str = "beefy::sync";
|
||||
|
||||
/// Get the configuration for the BEEFY justifications Request/response protocol.
|
||||
///
|
||||
/// Returns a receiver for messages received on this protocol and the requested
|
||||
/// `ProtocolConfig`.
|
||||
///
|
||||
/// Consider using [`BeefyJustifsRequestHandler`] instead of this low-level function.
|
||||
pub(crate) fn on_demand_justifications_protocol_config<
|
||||
Hash: AsRef<[u8]>,
|
||||
B: Block,
|
||||
Network: NetworkBackend<B, <B as Block>::Hash>,
|
||||
>(
|
||||
genesis_hash: Hash,
|
||||
fork_id: Option<&str>,
|
||||
) -> (IncomingRequestReceiver, Network::RequestResponseProtocolConfig) {
|
||||
let name = justifications_protocol_name(genesis_hash, fork_id);
|
||||
let fallback_names = vec![];
|
||||
let (tx, rx) = async_channel::bounded(JUSTIF_CHANNEL_SIZE);
|
||||
let rx = IncomingRequestReceiver::new(rx);
|
||||
let cfg = Network::request_response_config(
|
||||
name,
|
||||
fallback_names,
|
||||
32,
|
||||
MAX_RESPONSE_SIZE,
|
||||
// We are connected to all validators:
|
||||
JUSTIF_REQUEST_TIMEOUT,
|
||||
Some(tx),
|
||||
);
|
||||
(rx, cfg)
|
||||
}
|
||||
|
||||
/// BEEFY justification request.
|
||||
#[derive(Debug, Clone, Encode, Decode)]
|
||||
pub struct JustificationRequest<B: Block> {
|
||||
/// Start collecting proofs from this block.
|
||||
pub begin: NumberFor<B>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Client(#[from] pezsp_blockchain::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
RuntimeApi(#[from] pezsp_api::ApiError),
|
||||
|
||||
/// Decoding failed, we were able to change the peer's reputation accordingly.
|
||||
#[error("Decoding request failed for peer {0}.")]
|
||||
DecodingError(PeerId, #[source] CodecError),
|
||||
|
||||
/// Decoding failed, but sending reputation change failed.
|
||||
#[error("Decoding request failed for peer {0}, and changing reputation failed.")]
|
||||
DecodingErrorNoReputationChange(PeerId, #[source] CodecError),
|
||||
|
||||
/// Incoming request stream exhausted. Should only happen on shutdown.
|
||||
#[error("Incoming request channel got closed.")]
|
||||
RequestChannelExhausted,
|
||||
|
||||
#[error("Failed to send response.")]
|
||||
SendResponse,
|
||||
|
||||
#[error("Received invalid response.")]
|
||||
InvalidResponse(PeerReport),
|
||||
|
||||
#[error("Internal error while getting response.")]
|
||||
ResponseError,
|
||||
|
||||
#[error("On-demand requests receiver stream terminated.")]
|
||||
RequestsReceiverStreamClosed,
|
||||
}
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Generating request logic for request/response protocol for syncing BEEFY justifications.
|
||||
|
||||
use codec::Encode;
|
||||
use futures::channel::{oneshot, oneshot::Canceled};
|
||||
use log::{debug, warn};
|
||||
use parking_lot::Mutex;
|
||||
use pezsc_network::{
|
||||
request_responses::{IfDisconnected, RequestFailure},
|
||||
NetworkRequest, ProtocolName,
|
||||
};
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsp_consensus_beefy::{AuthorityIdBound, ValidatorSet};
|
||||
use pezsp_runtime::traits::{Block, NumberFor};
|
||||
use std::{collections::VecDeque, result::Result, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
communication::{
|
||||
benefit, cost,
|
||||
peers::PeerReport,
|
||||
request_response::{Error, JustificationRequest, BEEFY_SYNC_LOG_TARGET},
|
||||
},
|
||||
justification::{decode_and_verify_finality_proof, BeefyVersionedFinalityProof},
|
||||
metric_inc, metric_set,
|
||||
metrics::{register_metrics, OnDemandOutgoingRequestsMetrics},
|
||||
KnownPeers,
|
||||
};
|
||||
|
||||
/// Response type received from network.
|
||||
type Response = Result<(Vec<u8>, ProtocolName), RequestFailure>;
|
||||
/// Used to receive a response from the network.
|
||||
type ResponseReceiver = oneshot::Receiver<Response>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct RequestInfo<B: Block, AuthorityId: AuthorityIdBound> {
|
||||
block: NumberFor<B>,
|
||||
active_set: ValidatorSet<AuthorityId>,
|
||||
}
|
||||
|
||||
enum State<B: Block, AuthorityId: AuthorityIdBound> {
|
||||
Idle,
|
||||
AwaitingResponse(PeerId, RequestInfo<B, AuthorityId>, ResponseReceiver),
|
||||
}
|
||||
|
||||
/// Possible engine responses.
|
||||
pub(crate) enum ResponseInfo<B: Block, AuthorityId: AuthorityIdBound> {
|
||||
/// No peer response available yet.
|
||||
Pending,
|
||||
/// Valid justification provided alongside peer reputation changes.
|
||||
ValidProof(BeefyVersionedFinalityProof<B, AuthorityId>, PeerReport),
|
||||
/// No justification yet, only peer reputation changes.
|
||||
PeerReport(PeerReport),
|
||||
}
|
||||
|
||||
pub struct OnDemandJustificationsEngine<B: Block, AuthorityId: AuthorityIdBound> {
|
||||
network: Arc<dyn NetworkRequest + Send + Sync>,
|
||||
protocol_name: ProtocolName,
|
||||
|
||||
live_peers: Arc<Mutex<KnownPeers<B>>>,
|
||||
peers_cache: VecDeque<PeerId>,
|
||||
|
||||
state: State<B, AuthorityId>,
|
||||
metrics: Option<OnDemandOutgoingRequestsMetrics>,
|
||||
}
|
||||
|
||||
impl<B: Block, AuthorityId: AuthorityIdBound> OnDemandJustificationsEngine<B, AuthorityId> {
|
||||
pub fn new(
|
||||
network: Arc<dyn NetworkRequest + Send + Sync>,
|
||||
protocol_name: ProtocolName,
|
||||
live_peers: Arc<Mutex<KnownPeers<B>>>,
|
||||
prometheus_registry: Option<prometheus_endpoint::Registry>,
|
||||
) -> Self {
|
||||
let metrics = register_metrics(prometheus_registry);
|
||||
Self {
|
||||
network,
|
||||
protocol_name,
|
||||
live_peers,
|
||||
peers_cache: VecDeque::new(),
|
||||
state: State::Idle,
|
||||
metrics,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_peers_cache_for_block(&mut self, block: NumberFor<B>) {
|
||||
self.peers_cache = self.live_peers.lock().further_than(block);
|
||||
}
|
||||
|
||||
fn try_next_peer(&mut self) -> Option<PeerId> {
|
||||
let live = self.live_peers.lock();
|
||||
while let Some(peer) = self.peers_cache.pop_front() {
|
||||
if live.contains(&peer) {
|
||||
return Some(peer);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn request_from_peer(&mut self, peer: PeerId, req_info: RequestInfo<B, AuthorityId>) {
|
||||
debug!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 requesting justif #{:?} from peer {:?}", req_info.block, peer,
|
||||
);
|
||||
|
||||
let payload = JustificationRequest::<B> { begin: req_info.block }.encode();
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
self.network.start_request(
|
||||
peer,
|
||||
self.protocol_name.clone(),
|
||||
payload,
|
||||
None,
|
||||
tx,
|
||||
IfDisconnected::ImmediateError,
|
||||
);
|
||||
|
||||
self.state = State::AwaitingResponse(peer, req_info, rx);
|
||||
}
|
||||
|
||||
/// Start new justification request for `block`, if no other request is in progress.
|
||||
///
|
||||
/// `active_set` will be used to verify validity of potential responses.
|
||||
pub fn request(&mut self, block: NumberFor<B>, active_set: ValidatorSet<AuthorityId>) {
|
||||
// ignore new requests while there's already one pending
|
||||
if matches!(self.state, State::AwaitingResponse(_, _, _)) {
|
||||
return;
|
||||
}
|
||||
self.reset_peers_cache_for_block(block);
|
||||
|
||||
// Start the requests engine - each unsuccessful received response will automatically
|
||||
// trigger a new request to the next peer in the `peers_cache` until there are none left.
|
||||
if let Some(peer) = self.try_next_peer() {
|
||||
self.request_from_peer(peer, RequestInfo { block, active_set });
|
||||
} else {
|
||||
metric_inc!(self.metrics, beefy_on_demand_justification_no_peer_to_request_from);
|
||||
debug!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 no good peers to request justif #{:?} from", block
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel any pending request for block numbers smaller or equal to `block`.
|
||||
pub fn cancel_requests_older_than(&mut self, block: NumberFor<B>) {
|
||||
match &self.state {
|
||||
State::AwaitingResponse(_, req_info, _) if req_info.block <= block => {
|
||||
debug!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 cancel pending request for justification #{:?}", req_info.block
|
||||
);
|
||||
self.state = State::Idle;
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_response(
|
||||
&mut self,
|
||||
peer: &PeerId,
|
||||
req_info: &RequestInfo<B, AuthorityId>,
|
||||
response: Result<Response, Canceled>,
|
||||
) -> Result<BeefyVersionedFinalityProof<B, AuthorityId>, Error> {
|
||||
response
|
||||
.map_err(|e| {
|
||||
debug!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 on-demand sc-network channel sender closed, err: {:?}", e
|
||||
);
|
||||
Error::ResponseError
|
||||
})?
|
||||
.map_err(|e| {
|
||||
debug!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 for on demand justification #{:?}, peer {:?} error: {:?}",
|
||||
req_info.block,
|
||||
peer,
|
||||
e
|
||||
);
|
||||
match e {
|
||||
RequestFailure::Refused => {
|
||||
metric_inc!(self.metrics, beefy_on_demand_justification_peer_refused);
|
||||
let peer_report =
|
||||
PeerReport { who: *peer, cost_benefit: cost::REFUSAL_RESPONSE };
|
||||
Error::InvalidResponse(peer_report)
|
||||
},
|
||||
_ => {
|
||||
metric_inc!(self.metrics, beefy_on_demand_justification_peer_error);
|
||||
Error::ResponseError
|
||||
},
|
||||
}
|
||||
})
|
||||
.and_then(|(encoded, _)| {
|
||||
decode_and_verify_finality_proof::<B, AuthorityId>(
|
||||
&encoded[..],
|
||||
req_info.block,
|
||||
&req_info.active_set,
|
||||
)
|
||||
.map_err(|(err, signatures_checked)| {
|
||||
metric_inc!(self.metrics, beefy_on_demand_justification_invalid_proof);
|
||||
debug!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 for on demand justification #{:?}, peer {:?} responded with invalid proof: {:?}",
|
||||
req_info.block, peer, err
|
||||
);
|
||||
let mut cost = cost::INVALID_PROOF;
|
||||
cost.value +=
|
||||
cost::PER_SIGNATURE_CHECKED.saturating_mul(signatures_checked as i32);
|
||||
Error::InvalidResponse(PeerReport { who: *peer, cost_benefit: cost })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn next(&mut self) -> ResponseInfo<B, AuthorityId> {
|
||||
let (peer, req_info, resp) = match &mut self.state {
|
||||
State::Idle => {
|
||||
futures::future::pending::<()>().await;
|
||||
return ResponseInfo::Pending;
|
||||
},
|
||||
State::AwaitingResponse(peer, req_info, receiver) => {
|
||||
let resp = receiver.await;
|
||||
(*peer, req_info.clone(), resp)
|
||||
},
|
||||
};
|
||||
// We received the awaited response. Our 'receiver' will never generate any other response,
|
||||
// meaning we're done with current state. Move the engine to `State::Idle`.
|
||||
self.state = State::Idle;
|
||||
|
||||
metric_set!(self.metrics, beefy_on_demand_live_peers, self.live_peers.lock().len() as u64);
|
||||
|
||||
let block = req_info.block;
|
||||
match self.process_response(&peer, &req_info, resp) {
|
||||
Err(err) => {
|
||||
// No valid justification received, try next peer in our set.
|
||||
if let Some(peer) = self.try_next_peer() {
|
||||
self.request_from_peer(peer, req_info);
|
||||
} else {
|
||||
metric_inc!(
|
||||
self.metrics,
|
||||
beefy_on_demand_justification_no_peer_to_request_from
|
||||
);
|
||||
|
||||
let num_cache = self.peers_cache.len();
|
||||
let num_live = self.live_peers.lock().len();
|
||||
warn!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 ran out of peers to request justif #{block:?} from num_cache={num_cache} num_live={num_live} err={err:?}",
|
||||
);
|
||||
}
|
||||
// Report peer based on error type.
|
||||
if let Error::InvalidResponse(peer_report) = err {
|
||||
ResponseInfo::PeerReport(peer_report)
|
||||
} else {
|
||||
ResponseInfo::Pending
|
||||
}
|
||||
},
|
||||
Ok(proof) => {
|
||||
metric_inc!(self.metrics, beefy_on_demand_justification_good_proof);
|
||||
debug!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 received valid on-demand justif #{block:?} from {peer:?}",
|
||||
);
|
||||
let peer_report = PeerReport { who: peer, cost_benefit: benefit::VALIDATED_PROOF };
|
||||
ResponseInfo::ValidProof(proof, peer_report)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! BEEFY gadget specific errors
|
||||
//!
|
||||
//! Used for BEEFY gadget internal error handling only
|
||||
|
||||
use pezsp_blockchain::Error as ClientError;
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Backend: {0}")]
|
||||
Backend(String),
|
||||
#[error("Keystore error: {0}")]
|
||||
Keystore(String),
|
||||
#[error("Runtime api error: {0}")]
|
||||
RuntimeApi(pezsp_api::ApiError),
|
||||
#[error("Signature error: {0}")]
|
||||
Signature(String),
|
||||
#[error("Session uninitialized")]
|
||||
UninitSession,
|
||||
#[error("pezpallet-beefy was reset")]
|
||||
ConsensusReset,
|
||||
#[error("Block import stream terminated")]
|
||||
BlockImportStreamTerminated,
|
||||
#[error("Gossip Engine terminated")]
|
||||
GossipEngineTerminated,
|
||||
#[error("Finality proofs gossiping stream terminated")]
|
||||
FinalityProofGossipStreamTerminated,
|
||||
#[error("Finality stream terminated")]
|
||||
FinalityStreamTerminated,
|
||||
#[error("Votes gossiping stream terminated")]
|
||||
VotesGossipStreamTerminated,
|
||||
}
|
||||
|
||||
impl From<ClientError> for Error {
|
||||
fn from(e: ClientError) -> Self {
|
||||
Self::Backend(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl PartialEq for Error {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Error::Backend(s1), Error::Backend(s2)) => s1 == s2,
|
||||
(Error::Keystore(s1), Error::Keystore(s2)) => s1 == s2,
|
||||
(Error::RuntimeApi(_), Error::RuntimeApi(_)) => true,
|
||||
(Error::Signature(s1), Error::Signature(s2)) => s1 == s2,
|
||||
(Error::UninitSession, Error::UninitSession) => true,
|
||||
(Error::ConsensusReset, Error::ConsensusReset) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{error::Error, keystore::BeefyKeystore, round::Rounds, LOG_TARGET};
|
||||
use log::{debug, error, warn};
|
||||
use pezsc_client_api::Backend;
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_application_crypto::RuntimeAppPublic;
|
||||
use pezsp_blockchain::HeaderBackend;
|
||||
use pezsp_consensus_beefy::{
|
||||
check_double_voting_proof, AuthorityIdBound, BeefyApi, BeefySignatureHasher, DoubleVotingProof,
|
||||
OpaqueKeyOwnershipProof, ValidatorSetId,
|
||||
};
|
||||
use pezsp_runtime::{
|
||||
generic::BlockId,
|
||||
traits::{Block, NumberFor},
|
||||
};
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
|
||||
/// Helper struct containing the key ownership proof for a validator.
|
||||
pub struct ProvedValidator {
|
||||
pub key_owner_proof: OpaqueKeyOwnershipProof,
|
||||
}
|
||||
|
||||
/// Helper used to check and report equivocations.
|
||||
pub struct Fisherman<B, BE, RuntimeApi, AuthorityId: AuthorityIdBound> {
|
||||
backend: Arc<BE>,
|
||||
runtime: Arc<RuntimeApi>,
|
||||
key_store: Arc<BeefyKeystore<AuthorityId>>,
|
||||
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<B: Block, BE: Backend<B>, RuntimeApi: ProvideRuntimeApi<B>, AuthorityId>
|
||||
Fisherman<B, BE, RuntimeApi, AuthorityId>
|
||||
where
|
||||
RuntimeApi::Api: BeefyApi<B, AuthorityId>,
|
||||
AuthorityId: AuthorityIdBound,
|
||||
{
|
||||
pub fn new(
|
||||
backend: Arc<BE>,
|
||||
runtime: Arc<RuntimeApi>,
|
||||
keystore: Arc<BeefyKeystore<AuthorityId>>,
|
||||
) -> Self {
|
||||
Self { backend, runtime, key_store: keystore, _phantom: Default::default() }
|
||||
}
|
||||
|
||||
fn prove_offenders<'a>(
|
||||
&self,
|
||||
at: BlockId<B>,
|
||||
offender_ids: impl Iterator<Item = &'a AuthorityId>,
|
||||
validator_set_id: ValidatorSetId,
|
||||
) -> Result<Vec<ProvedValidator>, Error> {
|
||||
let hash = match at {
|
||||
BlockId::Hash(hash) => hash,
|
||||
BlockId::Number(number) => self
|
||||
.backend
|
||||
.blockchain()
|
||||
.expect_block_hash_from_id(&BlockId::Number(number))
|
||||
.map_err(|err| {
|
||||
Error::Backend(format!(
|
||||
"Couldn't get hash for block #{:?} (error: {:?}). \
|
||||
Skipping report for equivocation",
|
||||
at, err
|
||||
))
|
||||
})?,
|
||||
};
|
||||
|
||||
let runtime_api = self.runtime.runtime_api();
|
||||
let mut proved_offenders = vec![];
|
||||
for offender_id in offender_ids {
|
||||
match runtime_api.generate_key_ownership_proof(
|
||||
hash,
|
||||
validator_set_id,
|
||||
offender_id.clone(),
|
||||
) {
|
||||
Ok(Some(key_owner_proof)) => {
|
||||
proved_offenders.push(ProvedValidator { key_owner_proof });
|
||||
},
|
||||
Ok(None) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Equivocation offender {} not part of the authority set {}.",
|
||||
offender_id, validator_set_id
|
||||
);
|
||||
},
|
||||
Err(e) => {
|
||||
error!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Error generating key ownership proof for equivocation offender {} \
|
||||
in authority set {}: {}",
|
||||
offender_id, validator_set_id, e
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Ok(proved_offenders)
|
||||
}
|
||||
|
||||
/// Report the given equivocation to the BEEFY runtime module. This method
|
||||
/// generates a session membership proof of the offender and then submits an
|
||||
/// extrinsic to report the equivocation. In particular, the session membership
|
||||
/// proof must be generated at the block at which the given set was active which
|
||||
/// isn't necessarily the best block if there are pending authority set changes.
|
||||
pub fn report_double_voting(
|
||||
&self,
|
||||
proof: DoubleVotingProof<
|
||||
NumberFor<B>,
|
||||
AuthorityId,
|
||||
<AuthorityId as RuntimeAppPublic>::Signature,
|
||||
>,
|
||||
active_rounds: &Rounds<B, AuthorityId>,
|
||||
) -> Result<(), Error> {
|
||||
let (validators, validator_set_id) =
|
||||
(active_rounds.validators(), active_rounds.validator_set_id());
|
||||
let offender_id = proof.offender_id();
|
||||
|
||||
if !check_double_voting_proof::<_, _, BeefySignatureHasher>(&proof) {
|
||||
debug!(target: LOG_TARGET, "🥩 Skipping report for bad equivocation {:?}", proof);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(local_id) = self.key_store.authority_id(validators) {
|
||||
if offender_id == &local_id {
|
||||
warn!(target: LOG_TARGET, "🥩 Skipping report for own equivocation");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let key_owner_proofs = self.prove_offenders(
|
||||
BlockId::Number(*proof.round_number()),
|
||||
vec![offender_id].into_iter(),
|
||||
validator_set_id,
|
||||
)?;
|
||||
|
||||
// submit equivocation report at **best** block
|
||||
let best_block_hash = self.backend.blockchain().info().best_hash;
|
||||
for ProvedValidator { key_owner_proof, .. } in key_owner_proofs {
|
||||
self.runtime
|
||||
.runtime_api()
|
||||
.submit_report_double_voting_unsigned_extrinsic(
|
||||
best_block_hash,
|
||||
proof.clone(),
|
||||
key_owner_proof,
|
||||
)
|
||||
.map_err(Error::RuntimeApi)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use log::debug;
|
||||
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_consensus::Error as ConsensusError;
|
||||
use pezsp_consensus_beefy::{AuthorityIdBound, BeefyApi, BEEFY_ENGINE_ID};
|
||||
use pezsp_runtime::{
|
||||
traits::{Block as BlockT, Header as HeaderT, NumberFor},
|
||||
EncodedJustification,
|
||||
};
|
||||
|
||||
use pezsc_client_api::{backend::Backend, TrieCacheContext};
|
||||
use pezsc_consensus::{BlockCheckParams, BlockImport, BlockImportParams, ImportResult};
|
||||
|
||||
use crate::{
|
||||
communication::notification::BeefyVersionedFinalityProofSender,
|
||||
justification::{decode_and_verify_finality_proof, BeefyVersionedFinalityProof},
|
||||
metric_inc,
|
||||
metrics::BlockImportMetrics,
|
||||
LOG_TARGET,
|
||||
};
|
||||
|
||||
/// A block-import handler for BEEFY.
|
||||
///
|
||||
/// This scans each imported block for BEEFY justifications and verifies them.
|
||||
/// Wraps a `inner: BlockImport` and ultimately defers to it.
|
||||
///
|
||||
/// When using BEEFY, the block import worker should be using this block import object.
|
||||
pub struct BeefyBlockImport<Block: BlockT, Backend, RuntimeApi, I, AuthorityId: AuthorityIdBound> {
|
||||
backend: Arc<Backend>,
|
||||
runtime: Arc<RuntimeApi>,
|
||||
inner: I,
|
||||
justification_sender: BeefyVersionedFinalityProofSender<Block, AuthorityId>,
|
||||
metrics: Option<BlockImportMetrics>,
|
||||
}
|
||||
|
||||
impl<Block: BlockT, BE, Runtime, I: Clone, AuthorityId: AuthorityIdBound> Clone
|
||||
for BeefyBlockImport<Block, BE, Runtime, I, AuthorityId>
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
BeefyBlockImport {
|
||||
backend: self.backend.clone(),
|
||||
runtime: self.runtime.clone(),
|
||||
inner: self.inner.clone(),
|
||||
justification_sender: self.justification_sender.clone(),
|
||||
metrics: self.metrics.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block: BlockT, BE, Runtime, I, AuthorityId: AuthorityIdBound>
|
||||
BeefyBlockImport<Block, BE, Runtime, I, AuthorityId>
|
||||
{
|
||||
/// Create a new BeefyBlockImport.
|
||||
pub fn new(
|
||||
backend: Arc<BE>,
|
||||
runtime: Arc<Runtime>,
|
||||
inner: I,
|
||||
justification_sender: BeefyVersionedFinalityProofSender<Block, AuthorityId>,
|
||||
metrics: Option<BlockImportMetrics>,
|
||||
) -> BeefyBlockImport<Block, BE, Runtime, I, AuthorityId> {
|
||||
BeefyBlockImport { backend, runtime, inner, justification_sender, metrics }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block, BE, Runtime, I, AuthorityId> BeefyBlockImport<Block, BE, Runtime, I, AuthorityId>
|
||||
where
|
||||
Block: BlockT,
|
||||
BE: Backend<Block>,
|
||||
Runtime: ProvideRuntimeApi<Block>,
|
||||
Runtime::Api: BeefyApi<Block, AuthorityId> + Send,
|
||||
AuthorityId: AuthorityIdBound,
|
||||
{
|
||||
fn decode_and_verify(
|
||||
&self,
|
||||
encoded: &EncodedJustification,
|
||||
number: NumberFor<Block>,
|
||||
hash: <Block as BlockT>::Hash,
|
||||
) -> Result<BeefyVersionedFinalityProof<Block, AuthorityId>, ConsensusError> {
|
||||
use ConsensusError::ClientImport as ImportError;
|
||||
let beefy_genesis = self
|
||||
.runtime
|
||||
.runtime_api()
|
||||
.beefy_genesis(hash)
|
||||
.map_err(|e| ImportError(e.to_string()))?
|
||||
.ok_or_else(|| ImportError("Unknown BEEFY genesis".to_string()))?;
|
||||
if number < beefy_genesis {
|
||||
return Err(ImportError("BEEFY genesis is set for future block".to_string()));
|
||||
}
|
||||
let validator_set = self
|
||||
.runtime
|
||||
.runtime_api()
|
||||
.validator_set(hash)
|
||||
.map_err(|e| ImportError(e.to_string()))?
|
||||
.ok_or_else(|| ImportError("Unknown validator set".to_string()))?;
|
||||
|
||||
decode_and_verify_finality_proof::<Block, AuthorityId>(&encoded[..], number, &validator_set)
|
||||
.map_err(|(err, _)| err)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<Block, BE, Runtime, I, AuthorityId> BlockImport<Block>
|
||||
for BeefyBlockImport<Block, BE, Runtime, I, AuthorityId>
|
||||
where
|
||||
Block: BlockT,
|
||||
BE: Backend<Block>,
|
||||
I: BlockImport<Block, Error = ConsensusError> + Send + Sync,
|
||||
Runtime: ProvideRuntimeApi<Block> + Send + Sync,
|
||||
Runtime::Api: BeefyApi<Block, AuthorityId>,
|
||||
AuthorityId: AuthorityIdBound,
|
||||
{
|
||||
type Error = ConsensusError;
|
||||
|
||||
async fn import_block(
|
||||
&self,
|
||||
mut block: BlockImportParams<Block>,
|
||||
) -> Result<ImportResult, Self::Error> {
|
||||
let hash = block.post_hash();
|
||||
let number = *block.header.number();
|
||||
|
||||
let beefy_encoded = block.justifications.as_mut().and_then(|just| {
|
||||
let encoded = just.get(BEEFY_ENGINE_ID).cloned();
|
||||
// Remove BEEFY justification from the list before giving to `inner`; we send it to the
|
||||
// voter (beefy-gadget) and it will append it to the backend after block is finalized.
|
||||
just.remove(BEEFY_ENGINE_ID);
|
||||
encoded
|
||||
});
|
||||
|
||||
// Run inner block import.
|
||||
let inner_import_result = self.inner.import_block(block).await?;
|
||||
|
||||
match self.backend.state_at(hash, TrieCacheContext::Untrusted) {
|
||||
Ok(_) => {},
|
||||
Err(_) => {
|
||||
// The block is imported as part of some chain sync.
|
||||
// The voter doesn't need to process it now.
|
||||
// It will be detected and processed as part of the voter state init.
|
||||
return Ok(inner_import_result);
|
||||
},
|
||||
}
|
||||
|
||||
match (beefy_encoded, &inner_import_result) {
|
||||
(Some(encoded), ImportResult::Imported(_)) => {
|
||||
match self.decode_and_verify(&encoded, number, hash) {
|
||||
Ok(proof) => {
|
||||
// The proof is valid and the block is imported and final, we can import.
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 import justif {} for block number {:?}.", proof, number
|
||||
);
|
||||
// Send the justification to the BEEFY voter for processing.
|
||||
self.justification_sender
|
||||
.notify(|| Ok::<_, ()>(proof))
|
||||
.expect("the closure always returns Ok; qed.");
|
||||
metric_inc!(self.metrics, beefy_good_justification_imports);
|
||||
},
|
||||
Err(err) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 error importing BEEFY justification for block {:?}: {:?}",
|
||||
number,
|
||||
err,
|
||||
);
|
||||
metric_inc!(self.metrics, beefy_bad_justification_imports);
|
||||
},
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(inner_import_result)
|
||||
}
|
||||
|
||||
async fn check_block(
|
||||
&self,
|
||||
block: BlockCheckParams<Block>,
|
||||
) -> Result<ImportResult, Self::Error> {
|
||||
self.inner.check_block(block).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use codec::DecodeAll;
|
||||
use pezsp_application_crypto::RuntimeAppPublic;
|
||||
use pezsp_consensus::Error as ConsensusError;
|
||||
use pezsp_consensus_beefy::{
|
||||
AuthorityIdBound, BeefySignatureHasher, KnownSignature, ValidatorSet, ValidatorSetId,
|
||||
VersionedFinalityProof,
|
||||
};
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
|
||||
|
||||
/// A finality proof with matching BEEFY authorities' signatures.
|
||||
pub type BeefyVersionedFinalityProof<Block, AuthorityId> =
|
||||
VersionedFinalityProof<NumberFor<Block>, <AuthorityId as RuntimeAppPublic>::Signature>;
|
||||
|
||||
pub(crate) fn proof_block_num_and_set_id<Block: BlockT, AuthorityId: AuthorityIdBound>(
|
||||
proof: &BeefyVersionedFinalityProof<Block, AuthorityId>,
|
||||
) -> (NumberFor<Block>, ValidatorSetId) {
|
||||
match proof {
|
||||
VersionedFinalityProof::V1(sc) =>
|
||||
(sc.commitment.block_number, sc.commitment.validator_set_id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode and verify a Beefy FinalityProof.
|
||||
pub(crate) fn decode_and_verify_finality_proof<Block: BlockT, AuthorityId: AuthorityIdBound>(
|
||||
encoded: &[u8],
|
||||
target_number: NumberFor<Block>,
|
||||
validator_set: &ValidatorSet<AuthorityId>,
|
||||
) -> Result<BeefyVersionedFinalityProof<Block, AuthorityId>, (ConsensusError, u32)> {
|
||||
let proof = <BeefyVersionedFinalityProof<Block, AuthorityId>>::decode_all(&mut &*encoded)
|
||||
.map_err(|_| (ConsensusError::InvalidJustification, 0))?;
|
||||
verify_with_validator_set::<Block, AuthorityId>(target_number, validator_set, &proof)?;
|
||||
Ok(proof)
|
||||
}
|
||||
|
||||
/// Verify the Beefy finality proof against the validator set at the block it was generated.
|
||||
pub(crate) fn verify_with_validator_set<'a, Block: BlockT, AuthorityId: AuthorityIdBound>(
|
||||
target_number: NumberFor<Block>,
|
||||
validator_set: &'a ValidatorSet<AuthorityId>,
|
||||
proof: &'a BeefyVersionedFinalityProof<Block, AuthorityId>,
|
||||
) -> Result<
|
||||
Vec<KnownSignature<&'a AuthorityId, &'a <AuthorityId as RuntimeAppPublic>::Signature>>,
|
||||
(ConsensusError, u32),
|
||||
> {
|
||||
match proof {
|
||||
VersionedFinalityProof::V1(signed_commitment) => {
|
||||
let signatories = signed_commitment
|
||||
.verify_signatures::<_, BeefySignatureHasher>(target_number, validator_set)
|
||||
.map_err(|checked_signatures| {
|
||||
(ConsensusError::InvalidJustification, checked_signatures)
|
||||
})?;
|
||||
|
||||
if signatories.len() >= crate::round::threshold(validator_set.len()) {
|
||||
Ok(signatories)
|
||||
} else {
|
||||
Err((
|
||||
ConsensusError::InvalidJustification,
|
||||
signed_commitment.signature_count() as u32,
|
||||
))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use codec::Encode;
|
||||
use pezsp_consensus_beefy::{
|
||||
ecdsa_crypto, known_payloads, test_utils::Keyring, Commitment, Payload, SignedCommitment,
|
||||
VersionedFinalityProof,
|
||||
};
|
||||
use bizinikiwi_test_runtime_client::runtime::Block;
|
||||
|
||||
use super::*;
|
||||
use crate::tests::make_beefy_ids;
|
||||
|
||||
pub(crate) fn new_finality_proof(
|
||||
block_num: NumberFor<Block>,
|
||||
validator_set: &ValidatorSet<ecdsa_crypto::AuthorityId>,
|
||||
keys: &[Keyring<ecdsa_crypto::AuthorityId>],
|
||||
) -> BeefyVersionedFinalityProof<Block, ecdsa_crypto::AuthorityId> {
|
||||
let commitment = Commitment {
|
||||
payload: Payload::from_single_entry(known_payloads::MMR_ROOT_ID, vec![]),
|
||||
block_number: block_num,
|
||||
validator_set_id: validator_set.id(),
|
||||
};
|
||||
let message = commitment.encode();
|
||||
let signatures = keys.iter().map(|key| Some(key.sign(&message))).collect();
|
||||
VersionedFinalityProof::V1(SignedCommitment { commitment, signatures })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_verify_with_validator_set() {
|
||||
let keys = &[Keyring::Alice, Keyring::Bob, Keyring::Charlie];
|
||||
let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap();
|
||||
|
||||
// build valid justification
|
||||
let block_num = 42;
|
||||
let proof = new_finality_proof(block_num, &validator_set, keys);
|
||||
|
||||
let good_proof = proof.clone().into();
|
||||
// should verify successfully
|
||||
verify_with_validator_set::<Block, ecdsa_crypto::AuthorityId>(
|
||||
block_num,
|
||||
&validator_set,
|
||||
&good_proof,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// wrong block number -> should fail verification
|
||||
let good_proof = proof.clone().into();
|
||||
match verify_with_validator_set::<Block, ecdsa_crypto::AuthorityId>(
|
||||
block_num + 1,
|
||||
&validator_set,
|
||||
&good_proof,
|
||||
) {
|
||||
Err((ConsensusError::InvalidJustification, 0)) => (),
|
||||
e => assert!(false, "Got unexpected {:?}", e),
|
||||
};
|
||||
|
||||
// wrong validator set id -> should fail verification
|
||||
let good_proof = proof.clone().into();
|
||||
let other = ValidatorSet::new(make_beefy_ids(keys), 1).unwrap();
|
||||
match verify_with_validator_set::<Block, ecdsa_crypto::AuthorityId>(
|
||||
block_num,
|
||||
&other,
|
||||
&good_proof,
|
||||
) {
|
||||
Err((ConsensusError::InvalidJustification, 0)) => (),
|
||||
e => assert!(false, "Got unexpected {:?}", e),
|
||||
};
|
||||
|
||||
// wrong signatures length -> should fail verification
|
||||
let mut bad_proof = proof.clone();
|
||||
// change length of signatures
|
||||
let bad_signed_commitment = match bad_proof {
|
||||
VersionedFinalityProof::V1(ref mut sc) => sc,
|
||||
};
|
||||
bad_signed_commitment.signatures.pop().flatten().unwrap();
|
||||
match verify_with_validator_set::<Block, ecdsa_crypto::AuthorityId>(
|
||||
block_num + 1,
|
||||
&validator_set,
|
||||
&bad_proof.into(),
|
||||
) {
|
||||
Err((ConsensusError::InvalidJustification, 0)) => (),
|
||||
e => assert!(false, "Got unexpected {:?}", e),
|
||||
};
|
||||
|
||||
// not enough signatures -> should fail verification
|
||||
let mut bad_proof = proof.clone();
|
||||
let bad_signed_commitment = match bad_proof {
|
||||
VersionedFinalityProof::V1(ref mut sc) => sc,
|
||||
};
|
||||
// remove a signature (but same length)
|
||||
*bad_signed_commitment.signatures.first_mut().unwrap() = None;
|
||||
match verify_with_validator_set::<Block, ecdsa_crypto::AuthorityId>(
|
||||
block_num,
|
||||
&validator_set,
|
||||
&bad_proof.into(),
|
||||
) {
|
||||
Err((ConsensusError::InvalidJustification, 2)) => (),
|
||||
e => assert!(false, "Got unexpected {:?}", e),
|
||||
};
|
||||
|
||||
// not enough _correct_ signatures -> should fail verification
|
||||
let mut bad_proof = proof.clone();
|
||||
let bad_signed_commitment = match bad_proof {
|
||||
VersionedFinalityProof::V1(ref mut sc) => sc,
|
||||
};
|
||||
// change a signature to a different key
|
||||
*bad_signed_commitment.signatures.first_mut().unwrap() = Some(
|
||||
Keyring::<ecdsa_crypto::AuthorityId>::Dave
|
||||
.sign(&bad_signed_commitment.commitment.encode()),
|
||||
);
|
||||
match verify_with_validator_set::<Block, ecdsa_crypto::AuthorityId>(
|
||||
block_num,
|
||||
&validator_set,
|
||||
&bad_proof.into(),
|
||||
) {
|
||||
Err((ConsensusError::InvalidJustification, 3)) => (),
|
||||
e => assert!(false, "Got unexpected {:?}", e),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_decode_and_verify_finality_proof() {
|
||||
let keys = &[Keyring::Alice, Keyring::Bob];
|
||||
let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap();
|
||||
let block_num = 1;
|
||||
|
||||
// build valid justification
|
||||
let proof = new_finality_proof(block_num, &validator_set, keys);
|
||||
let versioned_proof: BeefyVersionedFinalityProof<Block, ecdsa_crypto::AuthorityId> =
|
||||
proof.into();
|
||||
let encoded = versioned_proof.encode();
|
||||
|
||||
// should successfully decode and verify
|
||||
let verified = decode_and_verify_finality_proof::<Block, ecdsa_crypto::AuthorityId>(
|
||||
&encoded,
|
||||
block_num,
|
||||
&validator_set,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(verified, versioned_proof);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use codec::Decode;
|
||||
use log::warn;
|
||||
|
||||
use pezsp_application_crypto::{key_types::BEEFY as BEEFY_KEY_TYPE, AppCrypto, RuntimeAppPublic};
|
||||
#[cfg(feature = "bls-experimental")]
|
||||
use pezsp_core::ecdsa_bls381;
|
||||
use pezsp_core::{ecdsa, keccak_256};
|
||||
|
||||
use pezsp_keystore::KeystorePtr;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use pezsp_consensus_beefy::{AuthorityIdBound, BeefyAuthorityId, BeefySignatureHasher};
|
||||
|
||||
use crate::{error, LOG_TARGET};
|
||||
|
||||
/// A BEEFY specific keystore implemented as a `Newtype`. This is basically a
|
||||
/// wrapper around [`pezsp_keystore::Keystore`] and allows to customize
|
||||
/// common cryptographic functionality.
|
||||
pub(crate) struct BeefyKeystore<AuthorityId: AuthorityIdBound>(
|
||||
Option<KeystorePtr>,
|
||||
PhantomData<fn() -> AuthorityId>,
|
||||
);
|
||||
|
||||
impl<AuthorityId: AuthorityIdBound> BeefyKeystore<AuthorityId> {
|
||||
/// Check if the keystore contains a private key for one of the public keys
|
||||
/// contained in `keys`. A public key with a matching private key is known
|
||||
/// as a local authority id.
|
||||
///
|
||||
/// Return the public key for which we also do have a private key. If no
|
||||
/// matching private key is found, `None` will be returned.
|
||||
pub fn authority_id(&self, keys: &[AuthorityId]) -> Option<AuthorityId> {
|
||||
let store = self.0.clone()?;
|
||||
|
||||
// we do check for multiple private keys as a key store sanity check.
|
||||
let public: Vec<AuthorityId> = keys
|
||||
.iter()
|
||||
.filter(|k| {
|
||||
store
|
||||
.has_keys(&[(<AuthorityId as RuntimeAppPublic>::to_raw_vec(k), BEEFY_KEY_TYPE)])
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if public.len() > 1 {
|
||||
warn!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Multiple private keys found for: {:?} ({})",
|
||||
public,
|
||||
public.len()
|
||||
);
|
||||
}
|
||||
|
||||
public.get(0).cloned()
|
||||
}
|
||||
|
||||
/// Sign `message` with the `public` key.
|
||||
///
|
||||
/// Note that `message` usually will be pre-hashed before being signed.
|
||||
///
|
||||
/// Return the message signature or an error in case of failure.
|
||||
pub fn sign(
|
||||
&self,
|
||||
public: &AuthorityId,
|
||||
message: &[u8],
|
||||
) -> Result<<AuthorityId as RuntimeAppPublic>::Signature, error::Error> {
|
||||
let store = self.0.clone().ok_or_else(|| error::Error::Keystore("no Keystore".into()))?;
|
||||
|
||||
// ECDSA should use ecdsa_sign_prehashed since it needs to be hashed by keccak_256 instead
|
||||
// of blake2. As such we need to deal with producing the signatures case-by-case
|
||||
let signature_byte_array: Vec<u8> = match <AuthorityId as AppCrypto>::CRYPTO_ID {
|
||||
ecdsa::CRYPTO_ID => {
|
||||
let msg_hash = keccak_256(message);
|
||||
let public: ecdsa::Public = ecdsa::Public::try_from(public.as_slice()).unwrap();
|
||||
|
||||
let sig = store
|
||||
.ecdsa_sign_prehashed(BEEFY_KEY_TYPE, &public, &msg_hash)
|
||||
.map_err(|e| error::Error::Keystore(e.to_string()))?
|
||||
.ok_or_else(|| {
|
||||
error::Error::Signature("ecdsa_sign_prehashed() failed".to_string())
|
||||
})?;
|
||||
let sig_ref: &[u8] = sig.as_ref();
|
||||
sig_ref.to_vec()
|
||||
},
|
||||
|
||||
#[cfg(feature = "bls-experimental")]
|
||||
ecdsa_bls381::CRYPTO_ID => {
|
||||
let public: ecdsa_bls381::Public =
|
||||
ecdsa_bls381::Public::try_from(public.as_slice()).unwrap();
|
||||
let sig = store
|
||||
.ecdsa_bls381_sign_with_keccak256(BEEFY_KEY_TYPE, &public, &message)
|
||||
.map_err(|e| error::Error::Keystore(e.to_string()))?
|
||||
.ok_or_else(|| error::Error::Signature("bls381_sign() failed".to_string()))?;
|
||||
let sig_ref: &[u8] = sig.as_ref();
|
||||
sig_ref.to_vec()
|
||||
},
|
||||
|
||||
_ => Err(error::Error::Keystore("key type is not supported by BEEFY Keystore".into()))?,
|
||||
};
|
||||
|
||||
//check that `sig` has the expected result type
|
||||
let signature = <AuthorityId as RuntimeAppPublic>::Signature::decode(
|
||||
&mut signature_byte_array.as_slice(),
|
||||
)
|
||||
.map_err(|_| {
|
||||
error::Error::Signature(format!(
|
||||
"invalid signature {:?} for key {:?}",
|
||||
signature_byte_array, public
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
/// Returns a vector of [`pezsp_consensus_beefy::crypto::Public`] keys which are currently
|
||||
/// supported (i.e. found in the keystore).
|
||||
pub fn public_keys(&self) -> Result<Vec<AuthorityId>, error::Error> {
|
||||
let store = self.0.clone().ok_or_else(|| error::Error::Keystore("no Keystore".into()))?;
|
||||
|
||||
let pk = match <AuthorityId as AppCrypto>::CRYPTO_ID {
|
||||
ecdsa::CRYPTO_ID => store
|
||||
.ecdsa_public_keys(BEEFY_KEY_TYPE)
|
||||
.drain(..)
|
||||
.map(|pk| AuthorityId::try_from(pk.as_ref()))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.or_else(|_| {
|
||||
Err(error::Error::Keystore(
|
||||
"unable to convert public key into authority id".into(),
|
||||
))
|
||||
}),
|
||||
|
||||
#[cfg(feature = "bls-experimental")]
|
||||
ecdsa_bls381::CRYPTO_ID => store
|
||||
.ecdsa_bls381_public_keys(BEEFY_KEY_TYPE)
|
||||
.drain(..)
|
||||
.map(|pk| AuthorityId::try_from(pk.as_ref()))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.or_else(|_| {
|
||||
Err(error::Error::Keystore(
|
||||
"unable to convert public key into authority id".into(),
|
||||
))
|
||||
}),
|
||||
|
||||
_ => Err(error::Error::Keystore("key type is not supported by BEEFY Keystore".into())),
|
||||
};
|
||||
|
||||
pk
|
||||
}
|
||||
|
||||
/// Use the `public` key to verify that `sig` is a valid signature for `message`.
|
||||
///
|
||||
/// Return `true` if the signature is authentic, `false` otherwise.
|
||||
pub fn verify(
|
||||
public: &AuthorityId,
|
||||
sig: &<AuthorityId as RuntimeAppPublic>::Signature,
|
||||
message: &[u8],
|
||||
) -> bool {
|
||||
BeefyAuthorityId::<BeefySignatureHasher>::verify(public, sig, message)
|
||||
}
|
||||
}
|
||||
|
||||
impl<AuthorityId: AuthorityIdBound> From<Option<KeystorePtr>> for BeefyKeystore<AuthorityId> {
|
||||
fn from(store: Option<KeystorePtr>) -> BeefyKeystore<AuthorityId> {
|
||||
BeefyKeystore(store, PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
#[cfg(feature = "bls-experimental")]
|
||||
use pezsp_consensus_beefy::ecdsa_bls_crypto;
|
||||
use pezsp_consensus_beefy::{
|
||||
ecdsa_crypto,
|
||||
test_utils::{BeefySignerAuthority, Keyring},
|
||||
};
|
||||
use pezsp_core::Pair as PairT;
|
||||
use pezsp_keystore::{testing::MemoryKeystore, Keystore};
|
||||
|
||||
use super::*;
|
||||
use crate::error::Error;
|
||||
|
||||
fn keystore() -> KeystorePtr {
|
||||
MemoryKeystore::new().into()
|
||||
}
|
||||
|
||||
fn pair_verify_should_work<
|
||||
AuthorityId: AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
|
||||
>()
|
||||
where
|
||||
<AuthorityId as pezsp_runtime::RuntimeAppPublic>::Signature:
|
||||
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
|
||||
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<pezsp_runtime::traits::Keccak256>,
|
||||
{
|
||||
let msg = b"I am Alice!";
|
||||
let sig = Keyring::<AuthorityId>::Alice.sign(b"I am Alice!");
|
||||
|
||||
assert!(<AuthorityId as BeefyAuthorityId<BeefySignatureHasher>>::verify(
|
||||
&Keyring::Alice.public(),
|
||||
&sig,
|
||||
&msg.as_slice(),
|
||||
));
|
||||
|
||||
// different public key -> fail
|
||||
assert!(!<AuthorityId as BeefyAuthorityId<BeefySignatureHasher>>::verify(
|
||||
&Keyring::Bob.public(),
|
||||
&sig,
|
||||
&msg.as_slice(),
|
||||
));
|
||||
|
||||
let msg = b"I am not Alice!";
|
||||
|
||||
// different msg -> fail
|
||||
assert!(!<AuthorityId as BeefyAuthorityId<BeefySignatureHasher>>::verify(
|
||||
&Keyring::Alice.public(),
|
||||
&sig,
|
||||
&msg.as_slice(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Generate key pair in the given store using the provided seed
|
||||
fn generate_in_store<AuthorityId>(
|
||||
store: KeystorePtr,
|
||||
key_type: pezsp_application_crypto::KeyTypeId,
|
||||
owner: Option<Keyring<AuthorityId>>,
|
||||
) -> AuthorityId
|
||||
where
|
||||
AuthorityId:
|
||||
AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
|
||||
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<BeefySignatureHasher>,
|
||||
<AuthorityId as RuntimeAppPublic>::Signature:
|
||||
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
|
||||
{
|
||||
let optional_seed: Option<String> = owner.map(|owner| owner.to_seed());
|
||||
|
||||
match <AuthorityId as AppCrypto>::CRYPTO_ID {
|
||||
ecdsa::CRYPTO_ID => {
|
||||
let pk = store.ecdsa_generate_new(key_type, optional_seed.as_deref()).ok().unwrap();
|
||||
AuthorityId::decode(&mut pk.as_ref()).unwrap()
|
||||
},
|
||||
#[cfg(feature = "bls-experimental")]
|
||||
ecdsa_bls381::CRYPTO_ID => {
|
||||
let pk = store
|
||||
.ecdsa_bls381_generate_new(key_type, optional_seed.as_deref())
|
||||
.ok()
|
||||
.unwrap();
|
||||
AuthorityId::decode(&mut pk.as_ref()).unwrap()
|
||||
},
|
||||
_ => panic!("Requested CRYPTO_ID is not supported by the BEEFY Keyring"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pair_verify_should_work_ecdsa() {
|
||||
pair_verify_should_work::<ecdsa_crypto::AuthorityId>();
|
||||
}
|
||||
|
||||
#[cfg(feature = "bls-experimental")]
|
||||
#[test]
|
||||
fn pair_verify_should_work_ecdsa_n_bls() {
|
||||
pair_verify_should_work::<ecdsa_bls_crypto::AuthorityId>();
|
||||
}
|
||||
|
||||
fn pair_works<
|
||||
AuthorityId: AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
|
||||
>()
|
||||
where
|
||||
<AuthorityId as pezsp_runtime::RuntimeAppPublic>::Signature:
|
||||
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
|
||||
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<pezsp_runtime::traits::Keccak256>,
|
||||
{
|
||||
let want = <AuthorityId as AppCrypto>::Pair::from_string("//Alice", None)
|
||||
.expect("Pair failed")
|
||||
.to_raw_vec();
|
||||
let got = Keyring::<AuthorityId>::Alice.pair().to_raw_vec();
|
||||
assert_eq!(want, got);
|
||||
|
||||
let want = <AuthorityId as AppCrypto>::Pair::from_string("//Bob", None)
|
||||
.expect("Pair failed")
|
||||
.to_raw_vec();
|
||||
let got = Keyring::<AuthorityId>::Bob.pair().to_raw_vec();
|
||||
assert_eq!(want, got);
|
||||
|
||||
let want = <AuthorityId as AppCrypto>::Pair::from_string("//Charlie", None)
|
||||
.expect("Pair failed")
|
||||
.to_raw_vec();
|
||||
let got = Keyring::<AuthorityId>::Charlie.pair().to_raw_vec();
|
||||
assert_eq!(want, got);
|
||||
|
||||
let want = <AuthorityId as AppCrypto>::Pair::from_string("//Dave", None)
|
||||
.expect("Pair failed")
|
||||
.to_raw_vec();
|
||||
let got = Keyring::<AuthorityId>::Dave.pair().to_raw_vec();
|
||||
assert_eq!(want, got);
|
||||
|
||||
let want = <AuthorityId as AppCrypto>::Pair::from_string("//Eve", None)
|
||||
.expect("Pair failed")
|
||||
.to_raw_vec();
|
||||
let got = Keyring::<AuthorityId>::Eve.pair().to_raw_vec();
|
||||
assert_eq!(want, got);
|
||||
|
||||
let want = <AuthorityId as AppCrypto>::Pair::from_string("//Ferdie", None)
|
||||
.expect("Pair failed")
|
||||
.to_raw_vec();
|
||||
let got = Keyring::<AuthorityId>::Ferdie.pair().to_raw_vec();
|
||||
assert_eq!(want, got);
|
||||
|
||||
let want = <AuthorityId as AppCrypto>::Pair::from_string("//One", None)
|
||||
.expect("Pair failed")
|
||||
.to_raw_vec();
|
||||
let got = Keyring::<AuthorityId>::One.pair().to_raw_vec();
|
||||
assert_eq!(want, got);
|
||||
|
||||
let want = <AuthorityId as AppCrypto>::Pair::from_string("//Two", None)
|
||||
.expect("Pair failed")
|
||||
.to_raw_vec();
|
||||
let got = Keyring::<AuthorityId>::Two.pair().to_raw_vec();
|
||||
assert_eq!(want, got);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecdsa_pair_works() {
|
||||
pair_works::<ecdsa_crypto::AuthorityId>();
|
||||
}
|
||||
|
||||
#[cfg(feature = "bls-experimental")]
|
||||
#[test]
|
||||
fn ecdsa_n_bls_pair_works() {
|
||||
pair_works::<ecdsa_bls_crypto::AuthorityId>();
|
||||
}
|
||||
|
||||
fn authority_id_works<
|
||||
AuthorityId: AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
|
||||
>()
|
||||
where
|
||||
<AuthorityId as pezsp_runtime::RuntimeAppPublic>::Signature:
|
||||
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
|
||||
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<pezsp_runtime::traits::Keccak256>,
|
||||
{
|
||||
let store = keystore();
|
||||
|
||||
generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, Some(Keyring::Alice));
|
||||
|
||||
let alice = Keyring::<AuthorityId>::Alice.public();
|
||||
|
||||
let bob = Keyring::Bob.public();
|
||||
let charlie = Keyring::Charlie.public();
|
||||
|
||||
let beefy_store: BeefyKeystore<AuthorityId> = Some(store).into();
|
||||
|
||||
let mut keys = vec![bob, charlie];
|
||||
|
||||
let id = beefy_store.authority_id(keys.as_slice());
|
||||
assert!(id.is_none());
|
||||
|
||||
keys.push(alice.clone());
|
||||
|
||||
let id = beefy_store.authority_id(keys.as_slice()).unwrap();
|
||||
assert_eq!(id, alice);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authority_id_works_for_ecdsa() {
|
||||
authority_id_works::<ecdsa_crypto::AuthorityId>();
|
||||
}
|
||||
|
||||
#[cfg(feature = "bls-experimental")]
|
||||
#[test]
|
||||
fn authority_id_works_for_ecdsa_n_bls() {
|
||||
authority_id_works::<ecdsa_bls_crypto::AuthorityId>();
|
||||
}
|
||||
|
||||
fn sign_works<
|
||||
AuthorityId: AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
|
||||
>()
|
||||
where
|
||||
<AuthorityId as pezsp_runtime::RuntimeAppPublic>::Signature:
|
||||
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
|
||||
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<pezsp_runtime::traits::Keccak256>,
|
||||
{
|
||||
let store = keystore();
|
||||
|
||||
generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, Some(Keyring::Alice));
|
||||
|
||||
let alice = Keyring::Alice.public();
|
||||
|
||||
let store: BeefyKeystore<AuthorityId> = Some(store).into();
|
||||
|
||||
let msg = b"are you involved or committed?";
|
||||
|
||||
let sig1 = store.sign(&alice, msg).unwrap();
|
||||
let sig2 = Keyring::<AuthorityId>::Alice.sign(msg);
|
||||
|
||||
assert_eq!(sig1, sig2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_works_for_ecdsa() {
|
||||
sign_works::<ecdsa_crypto::AuthorityId>();
|
||||
}
|
||||
|
||||
#[cfg(feature = "bls-experimental")]
|
||||
#[test]
|
||||
fn sign_works_for_ecdsa_n_bls() {
|
||||
sign_works::<ecdsa_bls_crypto::AuthorityId>();
|
||||
}
|
||||
|
||||
fn sign_error<
|
||||
AuthorityId: AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
|
||||
>(
|
||||
expected_error_message: &str,
|
||||
) where
|
||||
<AuthorityId as pezsp_runtime::RuntimeAppPublic>::Signature:
|
||||
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
|
||||
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<pezsp_runtime::traits::Keccak256>,
|
||||
{
|
||||
let store = keystore();
|
||||
|
||||
generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, Some(Keyring::Bob));
|
||||
|
||||
let store: BeefyKeystore<AuthorityId> = Some(store).into();
|
||||
|
||||
let alice = Keyring::Alice.public();
|
||||
|
||||
let msg = b"are you involved or committed?";
|
||||
let sig = store.sign(&alice, msg).err().unwrap();
|
||||
let err = Error::Signature(expected_error_message.to_string());
|
||||
|
||||
assert_eq!(sig, err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_error_for_ecdsa() {
|
||||
sign_error::<ecdsa_crypto::AuthorityId>("ecdsa_sign_prehashed() failed");
|
||||
}
|
||||
|
||||
#[cfg(feature = "bls-experimental")]
|
||||
#[test]
|
||||
fn sign_error_for_ecdsa_n_bls() {
|
||||
sign_error::<ecdsa_bls_crypto::AuthorityId>("bls381_sign() failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_no_keystore() {
|
||||
let store: BeefyKeystore<ecdsa_crypto::Public> = None.into();
|
||||
|
||||
let alice = Keyring::Alice.public();
|
||||
let msg = b"are you involved or committed";
|
||||
|
||||
let sig = store.sign(&alice, msg).err().unwrap();
|
||||
let err = Error::Keystore("no Keystore".to_string());
|
||||
assert_eq!(sig, err);
|
||||
}
|
||||
|
||||
fn verify_works<
|
||||
AuthorityId: AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
|
||||
>()
|
||||
where
|
||||
<AuthorityId as pezsp_runtime::RuntimeAppPublic>::Signature:
|
||||
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
|
||||
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<pezsp_runtime::traits::Keccak256>,
|
||||
{
|
||||
let store = keystore();
|
||||
|
||||
generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, Some(Keyring::Alice));
|
||||
|
||||
let store: BeefyKeystore<AuthorityId> = Some(store).into();
|
||||
|
||||
let alice = Keyring::Alice.public();
|
||||
|
||||
// `msg` and `sig` match
|
||||
let msg = b"are you involved or committed?";
|
||||
let sig = store.sign(&alice, msg).unwrap();
|
||||
assert!(BeefyKeystore::verify(&alice, &sig, msg));
|
||||
|
||||
// `msg and `sig` don't match
|
||||
let msg = b"you are just involved";
|
||||
assert!(!BeefyKeystore::verify(&alice, &sig, msg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_works_for_ecdsa() {
|
||||
verify_works::<ecdsa_crypto::AuthorityId>();
|
||||
}
|
||||
|
||||
#[cfg(feature = "bls-experimental")]
|
||||
#[test]
|
||||
|
||||
fn verify_works_for_ecdsa_n_bls() {
|
||||
verify_works::<ecdsa_bls_crypto::AuthorityId>();
|
||||
}
|
||||
|
||||
// Note that we use keys with and without a seed for this test.
|
||||
fn public_keys_works<
|
||||
AuthorityId: AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
|
||||
>()
|
||||
where
|
||||
<AuthorityId as pezsp_runtime::RuntimeAppPublic>::Signature:
|
||||
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
|
||||
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<pezsp_runtime::traits::Keccak256>,
|
||||
{
|
||||
const TEST_TYPE: pezsp_application_crypto::KeyTypeId =
|
||||
pezsp_application_crypto::KeyTypeId(*b"test");
|
||||
|
||||
let store = keystore();
|
||||
|
||||
// test keys
|
||||
let _ = generate_in_store::<AuthorityId>(store.clone(), TEST_TYPE, Some(Keyring::Alice));
|
||||
let _ = generate_in_store::<AuthorityId>(store.clone(), TEST_TYPE, Some(Keyring::Bob));
|
||||
|
||||
// BEEFY keys
|
||||
let _ =
|
||||
generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, Some(Keyring::Dave));
|
||||
let _ = generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, Some(Keyring::Eve));
|
||||
|
||||
let _ = generate_in_store::<AuthorityId>(store.clone(), TEST_TYPE, None);
|
||||
let _ = generate_in_store::<AuthorityId>(store.clone(), TEST_TYPE, None);
|
||||
|
||||
let key1 = generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, None);
|
||||
let key2 = generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, None);
|
||||
|
||||
let store: BeefyKeystore<AuthorityId> = Some(store).into();
|
||||
|
||||
let keys = store.public_keys().ok().unwrap();
|
||||
|
||||
assert!(keys.len() == 4);
|
||||
assert!(keys.contains(&Keyring::Dave.public()));
|
||||
assert!(keys.contains(&Keyring::Eve.public()));
|
||||
assert!(keys.contains(&key1));
|
||||
assert!(keys.contains(&key2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_keys_works_for_ecdsa_keystore() {
|
||||
public_keys_works::<ecdsa_crypto::AuthorityId>();
|
||||
}
|
||||
|
||||
#[cfg(feature = "bls-experimental")]
|
||||
#[test]
|
||||
|
||||
fn public_keys_works_for_ecdsa_n_bls() {
|
||||
public_keys_works::<ecdsa_bls_crypto::AuthorityId>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,814 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{
|
||||
communication::{
|
||||
notification::{
|
||||
BeefyBestBlockSender, BeefyBestBlockStream, BeefyVersionedFinalityProofSender,
|
||||
BeefyVersionedFinalityProofStream,
|
||||
},
|
||||
peers::KnownPeers,
|
||||
request_response::{
|
||||
outgoing_requests_engine::OnDemandJustificationsEngine, BeefyJustifsRequestHandler,
|
||||
},
|
||||
},
|
||||
error::Error,
|
||||
import::BeefyBlockImport,
|
||||
metrics::register_metrics,
|
||||
};
|
||||
use futures::{stream::Fuse, FutureExt, StreamExt};
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use parking_lot::Mutex;
|
||||
use prometheus_endpoint::Registry;
|
||||
use pezsc_client_api::{Backend, BlockBackend, BlockchainEvents, FinalityNotification, Finalizer};
|
||||
use pezsc_consensus::BlockImport;
|
||||
use pezsc_network::{NetworkRequest, NotificationService, ProtocolName};
|
||||
use pezsc_network_gossip::{GossipEngine, Network as GossipNetwork, Syncing as GossipSyncing};
|
||||
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver};
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_blockchain::{Backend as BlockchainBackend, HeaderBackend};
|
||||
use pezsp_consensus::{Error as ConsensusError, SyncOracle};
|
||||
use pezsp_consensus_beefy::{
|
||||
AuthorityIdBound, BeefyApi, ConsensusLog, PayloadProvider, ValidatorSet, BEEFY_ENGINE_ID,
|
||||
};
|
||||
use pezsp_keystore::KeystorePtr;
|
||||
use pezsp_runtime::traits::{Block, Header as HeaderT, NumberFor, Zero};
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
mod aux_schema;
|
||||
mod error;
|
||||
mod keystore;
|
||||
mod metrics;
|
||||
mod round;
|
||||
mod worker;
|
||||
|
||||
pub mod communication;
|
||||
pub mod import;
|
||||
pub mod justification;
|
||||
|
||||
use crate::{
|
||||
communication::gossip::GossipValidator,
|
||||
fisherman::Fisherman,
|
||||
justification::BeefyVersionedFinalityProof,
|
||||
keystore::BeefyKeystore,
|
||||
metrics::VoterMetrics,
|
||||
round::Rounds,
|
||||
worker::{BeefyWorker, PersistedState},
|
||||
};
|
||||
pub use communication::beefy_protocol_name::{
|
||||
gossip_protocol_name, justifications_protocol_name as justifs_protocol_name,
|
||||
};
|
||||
use pezsp_runtime::generic::OpaqueDigestItemId;
|
||||
|
||||
mod fisherman;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
const LOG_TARGET: &str = "beefy";
|
||||
|
||||
const HEADER_SYNC_DELAY: Duration = Duration::from_secs(60);
|
||||
|
||||
type FinalityNotifications<Block> =
|
||||
pezsc_utils::mpsc::TracingUnboundedReceiver<UnpinnedFinalityNotification<Block>>;
|
||||
/// A convenience BEEFY client trait that defines all the type bounds a BEEFY client
|
||||
/// has to satisfy. Ideally that should actually be a trait alias. Unfortunately as
|
||||
/// of today, Rust does not allow a type alias to be used as a trait bound. Tracking
|
||||
/// issue is <https://github.com/rust-lang/rust/issues/41517>.
|
||||
pub trait Client<B, BE>:
|
||||
BlockchainEvents<B> + HeaderBackend<B> + Finalizer<B, BE> + Send + Sync
|
||||
where
|
||||
B: Block,
|
||||
BE: Backend<B>,
|
||||
{
|
||||
// empty
|
||||
}
|
||||
|
||||
impl<B, BE, T> Client<B, BE> for T
|
||||
where
|
||||
B: Block,
|
||||
BE: Backend<B>,
|
||||
T: BlockchainEvents<B>
|
||||
+ HeaderBackend<B>
|
||||
+ Finalizer<B, BE>
|
||||
+ ProvideRuntimeApi<B>
|
||||
+ Send
|
||||
+ Sync,
|
||||
{
|
||||
// empty
|
||||
}
|
||||
|
||||
/// Links between the block importer, the background voter and the RPC layer,
|
||||
/// to be used by the voter.
|
||||
#[derive(Clone)]
|
||||
pub struct BeefyVoterLinks<B: Block, AuthorityId: AuthorityIdBound> {
|
||||
// BlockImport -> Voter links
|
||||
/// Stream of BEEFY signed commitments from block import to voter.
|
||||
pub from_block_import_justif_stream: BeefyVersionedFinalityProofStream<B, AuthorityId>,
|
||||
|
||||
// Voter -> RPC links
|
||||
/// Sends BEEFY signed commitments from voter to RPC.
|
||||
pub to_rpc_justif_sender: BeefyVersionedFinalityProofSender<B, AuthorityId>,
|
||||
/// Sends BEEFY best block hashes from voter to RPC.
|
||||
pub to_rpc_best_block_sender: BeefyBestBlockSender<B>,
|
||||
}
|
||||
|
||||
/// Links used by the BEEFY RPC layer, from the BEEFY background voter.
|
||||
#[derive(Clone)]
|
||||
pub struct BeefyRPCLinks<B: Block, AuthorityId: AuthorityIdBound> {
|
||||
/// Stream of signed commitments coming from the voter.
|
||||
pub from_voter_justif_stream: BeefyVersionedFinalityProofStream<B, AuthorityId>,
|
||||
/// Stream of BEEFY best block hashes coming from the voter.
|
||||
pub from_voter_best_beefy_stream: BeefyBestBlockStream<B>,
|
||||
}
|
||||
|
||||
/// Make block importer and link half necessary to tie the background voter to it.
|
||||
pub fn beefy_block_import_and_links<B, BE, RuntimeApi, I, AuthorityId: AuthorityIdBound>(
|
||||
wrapped_block_import: I,
|
||||
backend: Arc<BE>,
|
||||
runtime: Arc<RuntimeApi>,
|
||||
prometheus_registry: Option<Registry>,
|
||||
) -> (
|
||||
BeefyBlockImport<B, BE, RuntimeApi, I, AuthorityId>,
|
||||
BeefyVoterLinks<B, AuthorityId>,
|
||||
BeefyRPCLinks<B, AuthorityId>,
|
||||
)
|
||||
where
|
||||
B: Block,
|
||||
BE: Backend<B>,
|
||||
I: BlockImport<B, Error = ConsensusError> + Send + Sync,
|
||||
RuntimeApi: ProvideRuntimeApi<B> + Send + Sync,
|
||||
RuntimeApi::Api: BeefyApi<B, AuthorityId>,
|
||||
AuthorityId: AuthorityIdBound,
|
||||
{
|
||||
// Voter -> RPC links
|
||||
let (to_rpc_justif_sender, from_voter_justif_stream) =
|
||||
BeefyVersionedFinalityProofStream::<B, AuthorityId>::channel();
|
||||
let (to_rpc_best_block_sender, from_voter_best_beefy_stream) =
|
||||
BeefyBestBlockStream::<B>::channel();
|
||||
|
||||
// BlockImport -> Voter links
|
||||
let (to_voter_justif_sender, from_block_import_justif_stream) =
|
||||
BeefyVersionedFinalityProofStream::<B, AuthorityId>::channel();
|
||||
let metrics = register_metrics(prometheus_registry);
|
||||
|
||||
// BlockImport
|
||||
let import = BeefyBlockImport::new(
|
||||
backend,
|
||||
runtime,
|
||||
wrapped_block_import,
|
||||
to_voter_justif_sender,
|
||||
metrics,
|
||||
);
|
||||
let voter_links = BeefyVoterLinks {
|
||||
from_block_import_justif_stream,
|
||||
to_rpc_justif_sender,
|
||||
to_rpc_best_block_sender,
|
||||
};
|
||||
let rpc_links = BeefyRPCLinks { from_voter_best_beefy_stream, from_voter_justif_stream };
|
||||
|
||||
(import, voter_links, rpc_links)
|
||||
}
|
||||
|
||||
/// BEEFY gadget network parameters.
|
||||
pub struct BeefyNetworkParams<B: Block, N, S> {
|
||||
/// Network implementing gossip, requests and sync-oracle.
|
||||
pub network: Arc<N>,
|
||||
/// Syncing service implementing a sync oracle and an event stream for peers.
|
||||
pub sync: Arc<S>,
|
||||
/// Handle for receiving notification events.
|
||||
pub notification_service: Box<dyn NotificationService>,
|
||||
/// Chain specific BEEFY gossip protocol name. See
|
||||
/// [`communication::beefy_protocol_name::gossip_protocol_name`].
|
||||
pub gossip_protocol_name: ProtocolName,
|
||||
/// Chain specific BEEFY on-demand justifications protocol name. See
|
||||
/// [`communication::beefy_protocol_name::justifications_protocol_name`].
|
||||
pub justifications_protocol_name: ProtocolName,
|
||||
|
||||
pub _phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
/// BEEFY gadget initialization parameters.
|
||||
pub struct BeefyParams<B: Block, BE, C, N, P, R, S, AuthorityId: AuthorityIdBound> {
|
||||
/// BEEFY client
|
||||
pub client: Arc<C>,
|
||||
/// Client Backend
|
||||
pub backend: Arc<BE>,
|
||||
/// BEEFY Payload provider
|
||||
pub payload_provider: P,
|
||||
/// Runtime Api Provider
|
||||
pub runtime: Arc<R>,
|
||||
/// Local key store
|
||||
pub key_store: Option<KeystorePtr>,
|
||||
/// BEEFY voter network params
|
||||
pub network_params: BeefyNetworkParams<B, N, S>,
|
||||
/// Minimal delta between blocks, BEEFY should vote for
|
||||
pub min_block_delta: u32,
|
||||
/// Prometheus metric registry
|
||||
pub prometheus_registry: Option<Registry>,
|
||||
/// Links between the block importer, the background voter and the RPC layer.
|
||||
pub links: BeefyVoterLinks<B, AuthorityId>,
|
||||
/// Handler for incoming BEEFY justifications requests from a remote peer.
|
||||
pub on_demand_justifications_handler: BeefyJustifsRequestHandler<B, C>,
|
||||
/// Whether running under "Authority" role.
|
||||
pub is_authority: bool,
|
||||
}
|
||||
/// Helper object holding BEEFY worker communication/gossip components.
|
||||
///
|
||||
/// These are created once, but will be reused if worker is restarted/reinitialized.
|
||||
pub(crate) struct BeefyComms<B: Block, N, AuthorityId: AuthorityIdBound> {
|
||||
pub gossip_engine: GossipEngine<B>,
|
||||
pub gossip_validator: Arc<GossipValidator<B, N, AuthorityId>>,
|
||||
pub on_demand_justifications: OnDemandJustificationsEngine<B, AuthorityId>,
|
||||
}
|
||||
|
||||
/// Helper builder object for building [worker::BeefyWorker].
|
||||
///
|
||||
/// It has to do it in two steps: initialization and build, because the first step can sleep waiting
|
||||
/// for certain chain and backend conditions, and while sleeping we still need to pump the
|
||||
/// GossipEngine. Once initialization is done, the GossipEngine (and other pieces) are added to get
|
||||
/// the complete [worker::BeefyWorker] object.
|
||||
pub(crate) struct BeefyWorkerBuilder<B: Block, BE, RuntimeApi, AuthorityId: AuthorityIdBound> {
|
||||
// utilities
|
||||
backend: Arc<BE>,
|
||||
runtime: Arc<RuntimeApi>,
|
||||
key_store: BeefyKeystore<AuthorityId>,
|
||||
// voter metrics
|
||||
metrics: Option<VoterMetrics>,
|
||||
persisted_state: PersistedState<B, AuthorityId>,
|
||||
}
|
||||
|
||||
impl<B, BE, R, AuthorityId> BeefyWorkerBuilder<B, BE, R, AuthorityId>
|
||||
where
|
||||
B: Block + codec::Codec,
|
||||
BE: Backend<B>,
|
||||
R: ProvideRuntimeApi<B>,
|
||||
R::Api: BeefyApi<B, AuthorityId>,
|
||||
AuthorityId: AuthorityIdBound,
|
||||
{
|
||||
/// This will wait for the chain to enable BEEFY (if not yet enabled) and also wait for the
|
||||
/// backend to sync all headers required by the voter to build a contiguous chain of mandatory
|
||||
/// justifications. Then it builds the initial voter state using a combination of previously
|
||||
/// persisted state in AUX DB and latest chain information/progress.
|
||||
///
|
||||
/// Returns a sane `BeefyWorkerBuilder` that can build the `BeefyWorker`.
|
||||
pub async fn async_initialize<N>(
|
||||
backend: Arc<BE>,
|
||||
runtime: Arc<R>,
|
||||
key_store: BeefyKeystore<AuthorityId>,
|
||||
metrics: Option<VoterMetrics>,
|
||||
min_block_delta: u32,
|
||||
gossip_validator: Arc<GossipValidator<B, N, AuthorityId>>,
|
||||
finality_notifications: &mut Fuse<FinalityNotifications<B>>,
|
||||
is_authority: bool,
|
||||
) -> Result<Self, Error> {
|
||||
// Wait for BEEFY pallet to be active before starting voter.
|
||||
let (beefy_genesis, best_grandpa) =
|
||||
wait_for_runtime_pallet(&*runtime, finality_notifications).await?;
|
||||
|
||||
let persisted_state = Self::load_or_init_state(
|
||||
beefy_genesis,
|
||||
best_grandpa,
|
||||
min_block_delta,
|
||||
backend.clone(),
|
||||
runtime.clone(),
|
||||
&key_store,
|
||||
&metrics,
|
||||
is_authority,
|
||||
)
|
||||
.await?;
|
||||
// Update the gossip validator with the right starting round and set id.
|
||||
persisted_state
|
||||
.gossip_filter_config()
|
||||
.map(|f| gossip_validator.update_filter(f))?;
|
||||
|
||||
Ok(BeefyWorkerBuilder { backend, runtime, key_store, metrics, persisted_state })
|
||||
}
|
||||
|
||||
/// Takes rest of missing pieces as params and builds the `BeefyWorker`.
|
||||
pub fn build<P, S, N>(
|
||||
self,
|
||||
payload_provider: P,
|
||||
sync: Arc<S>,
|
||||
comms: BeefyComms<B, N, AuthorityId>,
|
||||
links: BeefyVoterLinks<B, AuthorityId>,
|
||||
pending_justifications: BTreeMap<NumberFor<B>, BeefyVersionedFinalityProof<B, AuthorityId>>,
|
||||
is_authority: bool,
|
||||
) -> BeefyWorker<B, BE, P, R, S, N, AuthorityId> {
|
||||
let key_store = Arc::new(self.key_store);
|
||||
BeefyWorker {
|
||||
backend: self.backend.clone(),
|
||||
runtime: self.runtime.clone(),
|
||||
key_store: key_store.clone(),
|
||||
payload_provider,
|
||||
sync,
|
||||
fisherman: Arc::new(Fisherman::new(self.backend, self.runtime, key_store)),
|
||||
metrics: self.metrics,
|
||||
persisted_state: self.persisted_state,
|
||||
comms,
|
||||
links,
|
||||
pending_justifications,
|
||||
is_authority,
|
||||
}
|
||||
}
|
||||
|
||||
// If no persisted state present, walk back the chain from first GRANDPA notification to either:
|
||||
// - latest BEEFY finalized block, or if none found on the way,
|
||||
// - BEEFY pallet genesis;
|
||||
// Enqueue any BEEFY mandatory blocks (session boundaries) found on the way, for voter to
|
||||
// finalize.
|
||||
async fn init_state(
|
||||
beefy_genesis: NumberFor<B>,
|
||||
best_grandpa: <B as Block>::Header,
|
||||
min_block_delta: u32,
|
||||
backend: Arc<BE>,
|
||||
runtime: Arc<R>,
|
||||
) -> Result<PersistedState<B, AuthorityId>, Error> {
|
||||
let blockchain = backend.blockchain();
|
||||
|
||||
let beefy_genesis = runtime
|
||||
.runtime_api()
|
||||
.beefy_genesis(best_grandpa.hash())
|
||||
.ok()
|
||||
.flatten()
|
||||
.filter(|genesis| *genesis == beefy_genesis)
|
||||
.ok_or_else(|| Error::Backend("BEEFY pallet expected to be active.".into()))?;
|
||||
// Walk back the imported blocks and initialize voter either, at the last block with
|
||||
// a BEEFY justification, or at pallet genesis block; voter will resume from there.
|
||||
let mut sessions = VecDeque::new();
|
||||
let mut header = best_grandpa.clone();
|
||||
let state = loop {
|
||||
if let Some(true) = blockchain
|
||||
.justifications(header.hash())
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|justifs| justifs.get(BEEFY_ENGINE_ID).is_some())
|
||||
{
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Initialize BEEFY voter at last BEEFY finalized block: {:?}.",
|
||||
*header.number()
|
||||
);
|
||||
let best_beefy = *header.number();
|
||||
// If no session boundaries detected so far, just initialize new rounds here.
|
||||
if sessions.is_empty() {
|
||||
let active_set =
|
||||
expect_validator_set(runtime.as_ref(), backend.as_ref(), &header).await?;
|
||||
let mut rounds = Rounds::new(best_beefy, active_set);
|
||||
// Mark the round as already finalized.
|
||||
rounds.conclude(best_beefy);
|
||||
sessions.push_front(rounds);
|
||||
}
|
||||
let state = PersistedState::checked_new(
|
||||
best_grandpa,
|
||||
best_beefy,
|
||||
sessions,
|
||||
min_block_delta,
|
||||
beefy_genesis,
|
||||
)
|
||||
.ok_or_else(|| Error::Backend("Invalid BEEFY chain".into()))?;
|
||||
break state;
|
||||
}
|
||||
|
||||
if *header.number() == beefy_genesis {
|
||||
// We've reached BEEFY genesis, initialize voter here.
|
||||
let genesis_set =
|
||||
expect_validator_set(runtime.as_ref(), backend.as_ref(), &header).await?;
|
||||
info!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Loading BEEFY voter state from genesis on what appears to be first startup. \
|
||||
Starting voting rounds at block {:?}, genesis validator set {:?}.",
|
||||
beefy_genesis,
|
||||
genesis_set,
|
||||
);
|
||||
|
||||
sessions.push_front(Rounds::new(beefy_genesis, genesis_set));
|
||||
break PersistedState::checked_new(
|
||||
best_grandpa,
|
||||
Zero::zero(),
|
||||
sessions,
|
||||
min_block_delta,
|
||||
beefy_genesis,
|
||||
)
|
||||
.ok_or_else(|| Error::Backend("Invalid BEEFY chain".into()))?;
|
||||
}
|
||||
|
||||
if let Some(active) = find_authorities_change::<B, AuthorityId>(&header) {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Marking block {:?} as BEEFY Mandatory.",
|
||||
*header.number()
|
||||
);
|
||||
sessions.push_front(Rounds::new(*header.number(), active));
|
||||
}
|
||||
|
||||
// Move up the chain.
|
||||
header = wait_for_parent_header(blockchain, header, HEADER_SYNC_DELAY).await?;
|
||||
};
|
||||
|
||||
aux_schema::write_current_version(backend.as_ref())?;
|
||||
aux_schema::write_voter_state(backend.as_ref(), &state)?;
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
async fn load_or_init_state(
|
||||
beefy_genesis: NumberFor<B>,
|
||||
best_grandpa: <B as Block>::Header,
|
||||
min_block_delta: u32,
|
||||
backend: Arc<BE>,
|
||||
runtime: Arc<R>,
|
||||
key_store: &BeefyKeystore<AuthorityId>,
|
||||
metrics: &Option<VoterMetrics>,
|
||||
is_authority: bool,
|
||||
) -> Result<PersistedState<B, AuthorityId>, Error> {
|
||||
// Initialize voter state from AUX DB if compatible.
|
||||
if let Some(mut state) = crate::aux_schema::load_persistent(backend.as_ref())?
|
||||
// Verify state pallet genesis matches runtime.
|
||||
.filter(|state| state.pezpallet_genesis() == beefy_genesis)
|
||||
{
|
||||
// Overwrite persisted state with current best GRANDPA block.
|
||||
state.set_best_grandpa(best_grandpa.clone());
|
||||
// Overwrite persisted data with newly provided `min_block_delta`.
|
||||
state.set_min_block_delta(min_block_delta);
|
||||
debug!(target: LOG_TARGET, "🥩 Loading BEEFY voter state from db.");
|
||||
trace!(target: LOG_TARGET, "🥩 Loaded state: {:?}.", state);
|
||||
|
||||
// Make sure that all the headers that we need have been synced.
|
||||
let mut new_sessions = vec![];
|
||||
let mut header = best_grandpa.clone();
|
||||
while *header.number() > state.best_beefy() {
|
||||
if state.voting_oracle().can_add_session(*header.number()) {
|
||||
if let Some(active) = find_authorities_change::<B, AuthorityId>(&header) {
|
||||
new_sessions.push((active, *header.number()));
|
||||
}
|
||||
}
|
||||
header =
|
||||
wait_for_parent_header(backend.blockchain(), header, HEADER_SYNC_DELAY).await?;
|
||||
}
|
||||
|
||||
// Make sure we didn't miss any sessions during node restart.
|
||||
for (validator_set, new_session_start) in new_sessions.drain(..).rev() {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Handling missed BEEFY session after node restart: {:?}.",
|
||||
new_session_start
|
||||
);
|
||||
state.init_session_at(
|
||||
new_session_start,
|
||||
validator_set,
|
||||
key_store,
|
||||
metrics,
|
||||
is_authority,
|
||||
);
|
||||
}
|
||||
return Ok(state);
|
||||
}
|
||||
|
||||
// No valid voter-state persisted, re-initialize from pallet genesis.
|
||||
Self::init_state(beefy_genesis, best_grandpa, min_block_delta, backend, runtime).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Finality notification for consumption by BEEFY worker.
|
||||
/// This is a stripped down version of `pezsc_client_api::FinalityNotification` which does not keep
|
||||
/// blocks pinned.
|
||||
struct UnpinnedFinalityNotification<B: Block> {
|
||||
/// Finalized block header hash.
|
||||
pub hash: B::Hash,
|
||||
/// Finalized block header.
|
||||
pub header: B::Header,
|
||||
/// Path from the old finalized to new finalized parent (implicitly finalized blocks).
|
||||
///
|
||||
/// This maps to the range `(old_finalized, new_finalized)`.
|
||||
pub tree_route: Arc<[B::Hash]>,
|
||||
}
|
||||
|
||||
impl<B: Block> From<FinalityNotification<B>> for UnpinnedFinalityNotification<B> {
|
||||
fn from(value: FinalityNotification<B>) -> Self {
|
||||
UnpinnedFinalityNotification {
|
||||
hash: value.hash,
|
||||
header: value.header,
|
||||
tree_route: value.tree_route,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the BEEFY gadget.
|
||||
///
|
||||
/// This is a thin shim around running and awaiting a BEEFY worker.
|
||||
pub async fn start_beefy_gadget<B, BE, C, N, P, R, S, AuthorityId>(
|
||||
beefy_params: BeefyParams<B, BE, C, N, P, R, S, AuthorityId>,
|
||||
) where
|
||||
B: Block,
|
||||
BE: Backend<B>,
|
||||
C: Client<B, BE> + BlockBackend<B>,
|
||||
P: PayloadProvider<B> + Clone,
|
||||
R: ProvideRuntimeApi<B>,
|
||||
R::Api: BeefyApi<B, AuthorityId>,
|
||||
N: GossipNetwork<B> + NetworkRequest + Send + Sync + 'static,
|
||||
S: GossipSyncing<B> + SyncOracle + 'static,
|
||||
AuthorityId: AuthorityIdBound,
|
||||
{
|
||||
let BeefyParams {
|
||||
client,
|
||||
backend,
|
||||
payload_provider,
|
||||
runtime,
|
||||
key_store,
|
||||
network_params,
|
||||
min_block_delta,
|
||||
prometheus_registry,
|
||||
links,
|
||||
mut on_demand_justifications_handler,
|
||||
is_authority,
|
||||
} = beefy_params;
|
||||
|
||||
let BeefyNetworkParams {
|
||||
network,
|
||||
sync,
|
||||
notification_service,
|
||||
gossip_protocol_name,
|
||||
justifications_protocol_name,
|
||||
..
|
||||
} = network_params;
|
||||
|
||||
let metrics = register_metrics(prometheus_registry.clone());
|
||||
|
||||
let mut block_import_justif = links.from_block_import_justif_stream.subscribe(100_000).fuse();
|
||||
|
||||
// Subscribe to finality notifications and justifications before waiting for runtime pallet and
|
||||
// reuse the streams, so we don't miss notifications while waiting for pallet to be available.
|
||||
let finality_notifications = client.finality_notification_stream();
|
||||
let (mut transformer, mut finality_notifications) =
|
||||
finality_notification_transformer_future(finality_notifications);
|
||||
|
||||
let known_peers = Arc::new(Mutex::new(KnownPeers::new()));
|
||||
// Default votes filter is to discard everything.
|
||||
// Validator is updated later with correct starting round and set id.
|
||||
let gossip_validator =
|
||||
communication::gossip::GossipValidator::new(known_peers.clone(), network.clone());
|
||||
let gossip_validator = Arc::new(gossip_validator);
|
||||
let gossip_engine = GossipEngine::new(
|
||||
network.clone(),
|
||||
sync.clone(),
|
||||
notification_service,
|
||||
gossip_protocol_name.clone(),
|
||||
gossip_validator.clone(),
|
||||
None,
|
||||
);
|
||||
|
||||
// The `GossipValidator` adds and removes known peers based on valid votes and network
|
||||
// events.
|
||||
let on_demand_justifications = OnDemandJustificationsEngine::new(
|
||||
network.clone(),
|
||||
justifications_protocol_name.clone(),
|
||||
known_peers,
|
||||
prometheus_registry.clone(),
|
||||
);
|
||||
let mut beefy_comms = BeefyComms { gossip_engine, gossip_validator, on_demand_justifications };
|
||||
|
||||
// We re-create and re-run the worker in this loop in order to quickly reinit and resume after
|
||||
// select recoverable errors.
|
||||
loop {
|
||||
// Make sure to pump gossip engine while waiting for initialization conditions.
|
||||
let worker_builder = futures::select! {
|
||||
builder_init_result = BeefyWorkerBuilder::async_initialize(
|
||||
backend.clone(),
|
||||
runtime.clone(),
|
||||
key_store.clone().into(),
|
||||
metrics.clone(),
|
||||
min_block_delta,
|
||||
beefy_comms.gossip_validator.clone(),
|
||||
&mut finality_notifications,
|
||||
is_authority,
|
||||
).fuse() => {
|
||||
match builder_init_result {
|
||||
Ok(builder) => builder,
|
||||
Err(e) => {
|
||||
error!(target: LOG_TARGET, "🥩 Error: {:?}. Terminating.", e);
|
||||
return
|
||||
},
|
||||
}
|
||||
},
|
||||
// Pump gossip engine.
|
||||
_ = &mut beefy_comms.gossip_engine => {
|
||||
error!(target: LOG_TARGET, "🥩 Gossip engine has unexpectedly terminated.");
|
||||
return
|
||||
},
|
||||
_ = &mut transformer => {
|
||||
error!(target: LOG_TARGET, "🥩 Finality notification transformer task has unexpectedly terminated.");
|
||||
return
|
||||
},
|
||||
};
|
||||
|
||||
let worker = worker_builder.build(
|
||||
payload_provider.clone(),
|
||||
sync.clone(),
|
||||
beefy_comms,
|
||||
links.clone(),
|
||||
BTreeMap::new(),
|
||||
is_authority,
|
||||
);
|
||||
|
||||
futures::select! {
|
||||
result = worker.run(&mut block_import_justif, &mut finality_notifications).fuse() => {
|
||||
match result {
|
||||
(error::Error::ConsensusReset, reuse_comms) => {
|
||||
error!(target: LOG_TARGET, "🥩 Error: {:?}. Restarting voter.", error::Error::ConsensusReset);
|
||||
beefy_comms = reuse_comms;
|
||||
continue;
|
||||
},
|
||||
(err, _) => {
|
||||
error!(target: LOG_TARGET, "🥩 Error: {:?}. Terminating.", err)
|
||||
}
|
||||
}
|
||||
},
|
||||
odj_handler_error = on_demand_justifications_handler.run().fuse() => {
|
||||
error!(target: LOG_TARGET, "🥩 Error: {:?}. Terminating.", odj_handler_error)
|
||||
},
|
||||
_ = &mut transformer => {
|
||||
error!(target: LOG_TARGET, "🥩 Finality notification transformer task has unexpectedly terminated.");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// Produce a future that transformes finality notifications into a struct that does not keep blocks
|
||||
/// pinned.
|
||||
fn finality_notification_transformer_future<B>(
|
||||
mut finality_notifications: pezsc_client_api::FinalityNotifications<B>,
|
||||
) -> (
|
||||
Pin<Box<futures::future::Fuse<impl Future<Output = ()> + Sized>>>,
|
||||
Fuse<TracingUnboundedReceiver<UnpinnedFinalityNotification<B>>>,
|
||||
)
|
||||
where
|
||||
B: Block,
|
||||
{
|
||||
let (tx, rx) = tracing_unbounded("beefy-notification-transformer-channel", 10000);
|
||||
let transformer_fut = async move {
|
||||
while let Some(notification) = finality_notifications.next().await {
|
||||
debug!(target: LOG_TARGET, "🥩 Transforming grandpa notification. #{}({:?})", notification.header.number(), notification.hash);
|
||||
if let Err(err) = tx.unbounded_send(UnpinnedFinalityNotification::from(notification)) {
|
||||
error!(target: LOG_TARGET, "🥩 Unable to send transformed notification. Shutting down. err = {}", err);
|
||||
return;
|
||||
};
|
||||
}
|
||||
};
|
||||
(Box::pin(transformer_fut.fuse()), rx.fuse())
|
||||
}
|
||||
|
||||
/// Waits until the parent header of `current` is available and returns it.
|
||||
///
|
||||
/// When the node uses GRANDPA warp sync it initially downloads only the mandatory GRANDPA headers.
|
||||
/// The rest of the headers (gap sync) are lazily downloaded later. But the BEEFY voter also needs
|
||||
/// the headers in range `[beefy_genesis..=best_grandpa]` to be available. This helper method
|
||||
/// enables us to wait until these headers have been synced.
|
||||
async fn wait_for_parent_header<B, BC>(
|
||||
blockchain: &BC,
|
||||
current: <B as Block>::Header,
|
||||
delay: Duration,
|
||||
) -> Result<<B as Block>::Header, Error>
|
||||
where
|
||||
B: Block,
|
||||
BC: BlockchainBackend<B>,
|
||||
{
|
||||
if *current.number() == Zero::zero() {
|
||||
let msg = format!("header {} is Genesis, there is no parent for it", current.hash());
|
||||
warn!(target: LOG_TARGET, "{}", msg);
|
||||
return Err(Error::Backend(msg));
|
||||
}
|
||||
loop {
|
||||
match blockchain
|
||||
.header(*current.parent_hash())
|
||||
.map_err(|e| Error::Backend(e.to_string()))?
|
||||
{
|
||||
Some(parent) => return Ok(parent),
|
||||
None => {
|
||||
info!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Parent of header number {} not found. \
|
||||
BEEFY gadget waiting for header sync to finish ...",
|
||||
current.number()
|
||||
);
|
||||
tokio::time::sleep(delay).await;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for BEEFY runtime pallet to be available, return active validator set.
|
||||
/// Should be called only once during worker initialization.
|
||||
async fn wait_for_runtime_pallet<B, R, AuthorityId: AuthorityIdBound>(
|
||||
runtime: &R,
|
||||
finality: &mut Fuse<FinalityNotifications<B>>,
|
||||
) -> Result<(NumberFor<B>, <B as Block>::Header), Error>
|
||||
where
|
||||
B: Block,
|
||||
R: ProvideRuntimeApi<B>,
|
||||
R::Api: BeefyApi<B, AuthorityId>,
|
||||
{
|
||||
info!(target: LOG_TARGET, "🥩 BEEFY gadget waiting for BEEFY pallet to become available...");
|
||||
loop {
|
||||
let notif = finality.next().await.ok_or_else(|| {
|
||||
let err_msg = "🥩 Finality stream has unexpectedly terminated.".into();
|
||||
error!(target: LOG_TARGET, "{}", err_msg);
|
||||
Error::Backend(err_msg)
|
||||
})?;
|
||||
let at = notif.header.hash();
|
||||
if let Some(start) = runtime.runtime_api().beefy_genesis(at).ok().flatten() {
|
||||
if *notif.header.number() >= start {
|
||||
// Beefy pallet available, return header for best grandpa at the time.
|
||||
info!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 BEEFY pallet available: block {:?} beefy genesis {:?}",
|
||||
notif.header.number(), start
|
||||
);
|
||||
return Ok((start, notif.header));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides validator set active `at_header`. It tries to get it from state, otherwise falls
|
||||
/// back to walk up the chain looking the validator set enactment in header digests.
|
||||
///
|
||||
/// Note: function will `async::sleep()` when walking back the chain if some needed header hasn't
|
||||
/// been synced yet (as it happens when warp syncing when headers are synced in the background).
|
||||
async fn expect_validator_set<B, BE, R, AuthorityId: AuthorityIdBound>(
|
||||
runtime: &R,
|
||||
backend: &BE,
|
||||
at_header: &B::Header,
|
||||
) -> Result<ValidatorSet<AuthorityId>, Error>
|
||||
where
|
||||
B: Block,
|
||||
BE: Backend<B>,
|
||||
R: ProvideRuntimeApi<B>,
|
||||
R::Api: BeefyApi<B, AuthorityId>,
|
||||
{
|
||||
let blockchain = backend.blockchain();
|
||||
// Walk up the chain looking for the validator set active at 'at_header'. Process both state and
|
||||
// header digests.
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Trying to find validator set active at header(number {:?}, hash {:?})",
|
||||
at_header.number(),
|
||||
at_header.hash()
|
||||
);
|
||||
let mut header = at_header.clone();
|
||||
loop {
|
||||
debug!(target: LOG_TARGET, "🥩 Looking for auth set change at block number: {:?}", *header.number());
|
||||
if let Ok(Some(active)) = runtime.runtime_api().validator_set(header.hash()) {
|
||||
return Ok(active);
|
||||
} else {
|
||||
match find_authorities_change::<B, AuthorityId>(&header) {
|
||||
Some(active) => return Ok(active),
|
||||
// Move up the chain. Ultimately we'll get it from chain genesis state, or error out
|
||||
// there.
|
||||
None =>
|
||||
header = wait_for_parent_header(blockchain, header, HEADER_SYNC_DELAY)
|
||||
.await
|
||||
.map_err(|e| Error::Backend(e.to_string()))?,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan the `header` digest log for a BEEFY validator set change. Return either the new
|
||||
/// validator set or `None` in case no validator set change has been signaled.
|
||||
pub(crate) fn find_authorities_change<B, AuthorityId>(
|
||||
header: &B::Header,
|
||||
) -> Option<ValidatorSet<AuthorityId>>
|
||||
where
|
||||
B: Block,
|
||||
AuthorityId: AuthorityIdBound,
|
||||
{
|
||||
let id = OpaqueDigestItemId::Consensus(&BEEFY_ENGINE_ID);
|
||||
|
||||
let filter = |log: ConsensusLog<AuthorityId>| match log {
|
||||
ConsensusLog::AuthoritiesChange(validator_set) => Some(validator_set),
|
||||
_ => None,
|
||||
};
|
||||
header.digest().convert_first(|l| l.try_to(id).and_then(filter))
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! BEEFY Prometheus metrics definition
|
||||
|
||||
use crate::LOG_TARGET;
|
||||
use log::{debug, error};
|
||||
use prometheus_endpoint::{register, Counter, Gauge, PrometheusError, Registry, U64};
|
||||
|
||||
/// Helper trait for registering BEEFY metrics to Prometheus registry.
|
||||
pub(crate) trait PrometheusRegister<T: Sized = Self>: Sized {
|
||||
const DESCRIPTION: &'static str;
|
||||
fn register(registry: &Registry) -> Result<Self, PrometheusError>;
|
||||
}
|
||||
|
||||
/// BEEFY voting-related metrics exposed through Prometheus
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VoterMetrics {
|
||||
/// Current active validator set id
|
||||
pub beefy_validator_set_id: Gauge<U64>,
|
||||
/// Total number of votes sent by this node
|
||||
pub beefy_votes_sent: Counter<U64>,
|
||||
/// Best block finalized by BEEFY
|
||||
pub beefy_best_block: Gauge<U64>,
|
||||
/// Best block BEEFY voted on
|
||||
pub beefy_best_voted: Gauge<U64>,
|
||||
/// Next block BEEFY should vote on
|
||||
pub beefy_should_vote_on: Gauge<U64>,
|
||||
/// Number of sessions with lagging signed commitment on mandatory block
|
||||
pub beefy_lagging_sessions: Counter<U64>,
|
||||
/// Number of times no Authority public key found in store
|
||||
pub beefy_no_authority_found_in_store: Counter<U64>,
|
||||
/// Number of good votes successfully handled
|
||||
pub beefy_good_votes_processed: Counter<U64>,
|
||||
/// Number of equivocation votes received
|
||||
pub beefy_equivocation_votes: Counter<U64>,
|
||||
/// Number of invalid votes received
|
||||
pub beefy_invalid_votes: Counter<U64>,
|
||||
/// Number of valid but stale votes received
|
||||
pub beefy_stale_votes: Counter<U64>,
|
||||
/// Number of currently buffered justifications
|
||||
pub beefy_buffered_justifications: Gauge<U64>,
|
||||
/// Number of valid but stale justifications received
|
||||
pub beefy_stale_justifications: Counter<U64>,
|
||||
/// Number of valid justifications successfully imported
|
||||
pub beefy_imported_justifications: Counter<U64>,
|
||||
/// Number of justifications dropped due to full buffers
|
||||
pub beefy_buffered_justifications_dropped: Counter<U64>,
|
||||
}
|
||||
|
||||
impl PrometheusRegister for VoterMetrics {
|
||||
const DESCRIPTION: &'static str = "voter";
|
||||
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
|
||||
Ok(Self {
|
||||
beefy_validator_set_id: register(
|
||||
Gauge::new(
|
||||
"bizinikiwi_beefy_validator_set_id",
|
||||
"Current BEEFY active validator set id.",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_votes_sent: register(
|
||||
Counter::new("bizinikiwi_beefy_votes_sent", "Number of votes sent by this node")?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_best_block: register(
|
||||
Gauge::new("bizinikiwi_beefy_best_block", "Best block finalized by BEEFY")?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_best_voted: register(
|
||||
Gauge::new("bizinikiwi_beefy_best_voted", "Best block voted on by BEEFY")?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_should_vote_on: register(
|
||||
Gauge::new("bizinikiwi_beefy_should_vote_on", "Next block, BEEFY should vote on")?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_lagging_sessions: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_lagging_sessions",
|
||||
"Number of sessions with lagging signed commitment on mandatory block",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_no_authority_found_in_store: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_no_authority_found_in_store",
|
||||
"Number of times no Authority public key found in store",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_good_votes_processed: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_successful_handled_votes",
|
||||
"Number of good votes successfully handled",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_equivocation_votes: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_equivocation_votes",
|
||||
"Number of equivocation votes received",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_invalid_votes: register(
|
||||
Counter::new("bizinikiwi_beefy_invalid_votes", "Number of invalid votes received")?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_stale_votes: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_stale_votes",
|
||||
"Number of valid but stale votes received",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_buffered_justifications: register(
|
||||
Gauge::new(
|
||||
"bizinikiwi_beefy_buffered_justifications",
|
||||
"Number of currently buffered justifications",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_stale_justifications: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_stale_justifications",
|
||||
"Number of valid but stale justifications received",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_imported_justifications: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_imported_justifications",
|
||||
"Number of valid justifications successfully imported",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_buffered_justifications_dropped: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_buffered_justifications_dropped",
|
||||
"Number of justifications dropped due to full buffers",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// BEEFY block-import-related metrics exposed through Prometheus
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BlockImportMetrics {
|
||||
/// Number of Good Justification imports
|
||||
pub beefy_good_justification_imports: Counter<U64>,
|
||||
/// Number of Bad Justification imports
|
||||
pub beefy_bad_justification_imports: Counter<U64>,
|
||||
}
|
||||
|
||||
impl PrometheusRegister for BlockImportMetrics {
|
||||
const DESCRIPTION: &'static str = "block-import";
|
||||
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
|
||||
Ok(Self {
|
||||
beefy_good_justification_imports: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_good_justification_imports",
|
||||
"Number of good justifications on block-import",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_bad_justification_imports: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_bad_justification_imports",
|
||||
"Number of bad justifications on block-import",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// BEEFY on-demand-justifications-related metrics exposed through Prometheus
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OnDemandIncomingRequestsMetrics {
|
||||
/// Number of Successful Justification responses
|
||||
pub beefy_successful_justification_responses: Counter<U64>,
|
||||
/// Number of Failed Justification responses
|
||||
pub beefy_failed_justification_responses: Counter<U64>,
|
||||
}
|
||||
|
||||
impl PrometheusRegister for OnDemandIncomingRequestsMetrics {
|
||||
const DESCRIPTION: &'static str = "on-demand incoming justification requests";
|
||||
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
|
||||
Ok(Self {
|
||||
beefy_successful_justification_responses: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_successful_justification_responses",
|
||||
"Number of Successful Justification responses",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_failed_justification_responses: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_failed_justification_responses",
|
||||
"Number of Failed Justification responses",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// BEEFY on-demand-justifications-related metrics exposed through Prometheus
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OnDemandOutgoingRequestsMetrics {
|
||||
/// Number of times there was no good peer to request justification from
|
||||
pub beefy_on_demand_justification_no_peer_to_request_from: Counter<U64>,
|
||||
/// Number of on-demand justification peer refused valid requests
|
||||
pub beefy_on_demand_justification_peer_refused: Counter<U64>,
|
||||
/// Number of on-demand justification peer error
|
||||
pub beefy_on_demand_justification_peer_error: Counter<U64>,
|
||||
/// Number of on-demand justification invalid proof
|
||||
pub beefy_on_demand_justification_invalid_proof: Counter<U64>,
|
||||
/// Number of on-demand justification good proof
|
||||
pub beefy_on_demand_justification_good_proof: Counter<U64>,
|
||||
/// Number of live beefy peers available for requests.
|
||||
pub beefy_on_demand_live_peers: Gauge<U64>,
|
||||
}
|
||||
|
||||
impl PrometheusRegister for OnDemandOutgoingRequestsMetrics {
|
||||
const DESCRIPTION: &'static str = "on-demand outgoing justification requests";
|
||||
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
|
||||
Ok(Self {
|
||||
beefy_on_demand_justification_no_peer_to_request_from: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_on_demand_justification_no_peer_to_request_from",
|
||||
"Number of times there was no good peer to request justification from",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_on_demand_justification_peer_refused: register(
|
||||
Counter::new(
|
||||
"beefy_on_demand_justification_peer_refused",
|
||||
"Number of on-demand justification peer refused valid requests",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_on_demand_justification_peer_error: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_on_demand_justification_peer_error",
|
||||
"Number of on-demand justification peer error",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_on_demand_justification_invalid_proof: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_on_demand_justification_invalid_proof",
|
||||
"Number of on-demand justification invalid proof",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_on_demand_justification_good_proof: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_beefy_on_demand_justification_good_proof",
|
||||
"Number of on-demand justification good proof",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_on_demand_live_peers: register(
|
||||
Gauge::new(
|
||||
"bizinikiwi_beefy_on_demand_live_peers",
|
||||
"Number of live beefy peers available for requests.",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register_metrics<T: PrometheusRegister>(
|
||||
prometheus_registry: Option<prometheus_endpoint::Registry>,
|
||||
) -> Option<T> {
|
||||
prometheus_registry.as_ref().map(T::register).and_then(|result| match result {
|
||||
Ok(metrics) => {
|
||||
debug!(target: LOG_TARGET, "🥩 Registered {} metrics", T::DESCRIPTION);
|
||||
Some(metrics)
|
||||
},
|
||||
Err(err) => {
|
||||
error!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Failed to register {} metrics: {:?}",
|
||||
T::DESCRIPTION,
|
||||
err
|
||||
);
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Note: we use the `format` macro to convert an expr into a `u64`. This will fail,
|
||||
// if expr does not derive `Display`.
|
||||
#[macro_export]
|
||||
macro_rules! metric_set {
|
||||
($metrics:expr, $m:ident, $v:expr) => {{
|
||||
let val: u64 = format!("{}", $v).parse().unwrap();
|
||||
|
||||
if let Some(metrics) = $metrics.as_ref() {
|
||||
metrics.$m.set(val);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! metric_inc {
|
||||
($metrics:expr, $m:ident) => {{
|
||||
if let Some(metrics) = $metrics.as_ref() {
|
||||
metrics.$m.inc();
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! metric_get {
|
||||
($metrics:expr, $m:ident) => {{
|
||||
$metrics.as_ref().map(|metrics| metrics.$m.clone())
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_register_metrics() {
|
||||
let registry = Some(Registry::new());
|
||||
assert!(register_metrics::<VoterMetrics>(registry.clone()).is_some());
|
||||
assert!(register_metrics::<BlockImportMetrics>(registry.clone()).is_some());
|
||||
assert!(register_metrics::<OnDemandIncomingRequestsMetrics>(registry.clone()).is_some());
|
||||
assert!(register_metrics::<OnDemandOutgoingRequestsMetrics>(registry.clone()).is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::LOG_TARGET;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use log::{debug, info};
|
||||
use pezsp_application_crypto::RuntimeAppPublic;
|
||||
use pezsp_consensus_beefy::{
|
||||
AuthorityIdBound, Commitment, DoubleVotingProof, SignedCommitment, ValidatorSet,
|
||||
ValidatorSetId, VoteMessage,
|
||||
};
|
||||
use pezsp_runtime::traits::{Block, NumberFor};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Tracks for each round which validators have voted/signed and
|
||||
/// whether the local `self` validator has voted/signed.
|
||||
///
|
||||
/// Does not do any validation on votes or signatures, layers above need to handle that (gossip).
|
||||
#[derive(Debug, Decode, Encode, PartialEq)]
|
||||
pub(crate) struct RoundTracker<AuthorityId: AuthorityIdBound> {
|
||||
votes: BTreeMap<AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature>,
|
||||
}
|
||||
|
||||
impl<AuthorityId: AuthorityIdBound> Default for RoundTracker<AuthorityId> {
|
||||
fn default() -> Self {
|
||||
Self { votes: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<AuthorityId: AuthorityIdBound> RoundTracker<AuthorityId> {
|
||||
fn add_vote(
|
||||
&mut self,
|
||||
vote: (AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature),
|
||||
) -> bool {
|
||||
if self.votes.contains_key(&vote.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.votes.insert(vote.0, vote.1);
|
||||
true
|
||||
}
|
||||
|
||||
fn is_done(&self, threshold: usize) -> bool {
|
||||
self.votes.len() >= threshold
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimum size of `authorities` subset that produced valid signatures for a block to finalize.
|
||||
pub fn threshold(authorities: usize) -> usize {
|
||||
let faulty = authorities.saturating_sub(1) / 3;
|
||||
authorities - faulty
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum VoteImportResult<B: Block, AuthorityId: AuthorityIdBound> {
|
||||
Ok,
|
||||
RoundConcluded(SignedCommitment<NumberFor<B>, <AuthorityId as RuntimeAppPublic>::Signature>),
|
||||
DoubleVoting(
|
||||
DoubleVotingProof<NumberFor<B>, AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature>,
|
||||
),
|
||||
Invalid,
|
||||
Stale,
|
||||
}
|
||||
|
||||
/// Keeps track of all voting rounds (block numbers) within a session.
|
||||
/// Only round numbers > `best_done` are of interest, all others are considered stale.
|
||||
///
|
||||
/// Does not do any validation on votes or signatures, layers above need to handle that (gossip).
|
||||
#[derive(Debug, Decode, Encode, PartialEq)]
|
||||
pub(crate) struct Rounds<B: Block, AuthorityId: AuthorityIdBound> {
|
||||
rounds: BTreeMap<Commitment<NumberFor<B>>, RoundTracker<AuthorityId>>,
|
||||
previous_votes: BTreeMap<
|
||||
(AuthorityId, NumberFor<B>),
|
||||
VoteMessage<NumberFor<B>, AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature>,
|
||||
>,
|
||||
session_start: NumberFor<B>,
|
||||
validator_set: ValidatorSet<AuthorityId>,
|
||||
mandatory_done: bool,
|
||||
best_done: Option<NumberFor<B>>,
|
||||
}
|
||||
|
||||
impl<B, AuthorityId> Rounds<B, AuthorityId>
|
||||
where
|
||||
B: Block,
|
||||
AuthorityId: AuthorityIdBound,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
session_start: NumberFor<B>,
|
||||
validator_set: ValidatorSet<AuthorityId>,
|
||||
) -> Self {
|
||||
Rounds {
|
||||
rounds: BTreeMap::new(),
|
||||
previous_votes: BTreeMap::new(),
|
||||
session_start,
|
||||
validator_set,
|
||||
mandatory_done: false,
|
||||
best_done: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validator_set(&self) -> &ValidatorSet<AuthorityId> {
|
||||
&self.validator_set
|
||||
}
|
||||
|
||||
pub(crate) fn validator_set_id(&self) -> ValidatorSetId {
|
||||
self.validator_set.id()
|
||||
}
|
||||
|
||||
pub(crate) fn validators(&self) -> &[AuthorityId] {
|
||||
self.validator_set.validators()
|
||||
}
|
||||
|
||||
pub(crate) fn session_start(&self) -> NumberFor<B> {
|
||||
self.session_start
|
||||
}
|
||||
|
||||
pub(crate) fn mandatory_done(&self) -> bool {
|
||||
self.mandatory_done
|
||||
}
|
||||
|
||||
pub(crate) fn add_vote(
|
||||
&mut self,
|
||||
vote: VoteMessage<NumberFor<B>, AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature>,
|
||||
) -> VoteImportResult<B, AuthorityId> {
|
||||
let num = vote.commitment.block_number;
|
||||
let vote_key = (vote.id.clone(), num);
|
||||
|
||||
if num < self.session_start || Some(num) <= self.best_done {
|
||||
debug!(target: LOG_TARGET, "🥩 received vote for old stale round {:?}, ignoring", num);
|
||||
return VoteImportResult::Stale;
|
||||
} else if vote.commitment.validator_set_id != self.validator_set_id() {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 expected set_id {:?}, ignoring vote {:?}.",
|
||||
self.validator_set_id(),
|
||||
vote,
|
||||
);
|
||||
return VoteImportResult::Invalid;
|
||||
} else if !self.validators().iter().any(|id| &vote.id == id) {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 received vote {:?} from validator that is not in the validator set, ignoring",
|
||||
vote
|
||||
);
|
||||
return VoteImportResult::Invalid;
|
||||
}
|
||||
|
||||
if let Some(previous_vote) = self.previous_votes.get(&vote_key) {
|
||||
// is the same public key voting for a different payload?
|
||||
if previous_vote.commitment.payload != vote.commitment.payload {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 detected equivocated vote: 1st: {:?}, 2nd: {:?}", previous_vote, vote
|
||||
);
|
||||
return VoteImportResult::DoubleVoting(DoubleVotingProof {
|
||||
first: previous_vote.clone(),
|
||||
second: vote,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// this is the first vote sent by `id` for `num`, all good
|
||||
self.previous_votes.insert(vote_key, vote.clone());
|
||||
}
|
||||
|
||||
// add valid vote
|
||||
let round = self.rounds.entry(vote.commitment.clone()).or_default();
|
||||
if round.add_vote((vote.id, vote.signature)) &&
|
||||
round.is_done(threshold(self.validator_set.len()))
|
||||
{
|
||||
if let Some(round) = self.rounds.remove_entry(&vote.commitment) {
|
||||
return VoteImportResult::RoundConcluded(self.signed_commitment(round));
|
||||
}
|
||||
}
|
||||
VoteImportResult::Ok
|
||||
}
|
||||
|
||||
fn signed_commitment(
|
||||
&mut self,
|
||||
round: (Commitment<NumberFor<B>>, RoundTracker<AuthorityId>),
|
||||
) -> SignedCommitment<NumberFor<B>, <AuthorityId as RuntimeAppPublic>::Signature> {
|
||||
let votes = round.1.votes;
|
||||
let signatures = self
|
||||
.validators()
|
||||
.iter()
|
||||
.map(|authority_id| votes.get(authority_id).cloned())
|
||||
.collect();
|
||||
SignedCommitment { commitment: round.0, signatures }
|
||||
}
|
||||
|
||||
pub(crate) fn conclude(&mut self, round_num: NumberFor<B>) {
|
||||
// Remove this and older (now stale) rounds.
|
||||
self.rounds.retain(|commitment, _| commitment.block_number > round_num);
|
||||
self.previous_votes.retain(|&(_, number), _| number > round_num);
|
||||
self.mandatory_done = self.mandatory_done || round_num == self.session_start;
|
||||
self.best_done = self.best_done.max(Some(round_num));
|
||||
if round_num == self.session_start {
|
||||
info!(target: LOG_TARGET, "🥩 Concluded mandatory round #{}", round_num);
|
||||
} else {
|
||||
debug!(target: LOG_TARGET, "🥩 Concluded optional round #{}", round_num);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pezsc_network_test::Block;
|
||||
|
||||
use pezsp_consensus_beefy::{
|
||||
ecdsa_crypto, known_payloads::MMR_ROOT_ID, test_utils::Keyring, Commitment,
|
||||
DoubleVotingProof, Payload, SignedCommitment, ValidatorSet, VoteMessage,
|
||||
};
|
||||
|
||||
use super::{threshold, Block as BlockT, RoundTracker, Rounds};
|
||||
use crate::round::VoteImportResult;
|
||||
|
||||
impl<B> Rounds<B, ecdsa_crypto::AuthorityId>
|
||||
where
|
||||
B: BlockT,
|
||||
{
|
||||
pub(crate) fn test_set_mandatory_done(&mut self, done: bool) {
|
||||
self.mandatory_done = done;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_tracker() {
|
||||
let mut rt = RoundTracker::<ecdsa_crypto::AuthorityId>::default();
|
||||
let bob_vote = (
|
||||
Keyring::<ecdsa_crypto::AuthorityId>::Bob.public(),
|
||||
Keyring::<ecdsa_crypto::AuthorityId>::Bob.sign(b"I am committed"),
|
||||
);
|
||||
let threshold = 2;
|
||||
|
||||
// adding new vote allowed
|
||||
assert!(rt.add_vote(bob_vote.clone()));
|
||||
// adding existing vote not allowed
|
||||
assert!(!rt.add_vote(bob_vote));
|
||||
|
||||
// vote is not done
|
||||
assert!(!rt.is_done(threshold));
|
||||
|
||||
let alice_vote = (
|
||||
Keyring::<ecdsa_crypto::AuthorityId>::Alice.public(),
|
||||
Keyring::<ecdsa_crypto::AuthorityId>::Alice.sign(b"I am committed"),
|
||||
);
|
||||
// adding new vote (self vote this time) allowed
|
||||
assert!(rt.add_vote(alice_vote));
|
||||
|
||||
// vote is now done
|
||||
assert!(rt.is_done(threshold));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vote_threshold() {
|
||||
assert_eq!(threshold(1), 1);
|
||||
assert_eq!(threshold(2), 2);
|
||||
assert_eq!(threshold(3), 3);
|
||||
assert_eq!(threshold(4), 3);
|
||||
assert_eq!(threshold(100), 67);
|
||||
assert_eq!(threshold(300), 201);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rounds() {
|
||||
pezsp_tracing::try_init_simple();
|
||||
|
||||
let validators = ValidatorSet::<ecdsa_crypto::AuthorityId>::new(
|
||||
vec![Keyring::Alice.public(), Keyring::Bob.public(), Keyring::Charlie.public()],
|
||||
42,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let session_start = 1u64.into();
|
||||
let rounds = Rounds::<Block, ecdsa_crypto::AuthorityId>::new(session_start, validators);
|
||||
|
||||
assert_eq!(42, rounds.validator_set_id());
|
||||
assert_eq!(1, rounds.session_start());
|
||||
assert_eq!(
|
||||
&vec![
|
||||
Keyring::<ecdsa_crypto::AuthorityId>::Alice.public(),
|
||||
Keyring::<ecdsa_crypto::AuthorityId>::Bob.public(),
|
||||
Keyring::<ecdsa_crypto::AuthorityId>::Charlie.public()
|
||||
],
|
||||
rounds.validators()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_and_conclude_votes() {
|
||||
pezsp_tracing::try_init_simple();
|
||||
|
||||
let validators = ValidatorSet::<ecdsa_crypto::AuthorityId>::new(
|
||||
vec![
|
||||
Keyring::Alice.public(),
|
||||
Keyring::Bob.public(),
|
||||
Keyring::Charlie.public(),
|
||||
Keyring::Eve.public(),
|
||||
],
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let validator_set_id = validators.id();
|
||||
|
||||
let session_start = 1u64.into();
|
||||
let mut rounds = Rounds::<Block, ecdsa_crypto::AuthorityId>::new(session_start, validators);
|
||||
|
||||
let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![]);
|
||||
let block_number = 1;
|
||||
let commitment = Commitment { block_number, payload, validator_set_id };
|
||||
let mut vote = VoteMessage {
|
||||
id: Keyring::Alice.public(),
|
||||
commitment: commitment.clone(),
|
||||
signature: Keyring::<ecdsa_crypto::AuthorityId>::Alice.sign(b"I am committed"),
|
||||
};
|
||||
// add 1st good vote
|
||||
assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Ok);
|
||||
|
||||
// double voting (same vote), ok, no effect
|
||||
assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Ok);
|
||||
|
||||
vote.id = Keyring::Dave.public();
|
||||
vote.signature = Keyring::<ecdsa_crypto::AuthorityId>::Dave.sign(b"I am committed");
|
||||
// invalid vote (Dave is not a validator)
|
||||
assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Invalid);
|
||||
|
||||
vote.id = Keyring::Bob.public();
|
||||
vote.signature = Keyring::<ecdsa_crypto::AuthorityId>::Bob.sign(b"I am committed");
|
||||
// add 2nd good vote
|
||||
assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Ok);
|
||||
|
||||
vote.id = Keyring::Charlie.public();
|
||||
vote.signature = Keyring::<ecdsa_crypto::AuthorityId>::Charlie.sign(b"I am committed");
|
||||
// add 3rd good vote -> round concluded -> signatures present
|
||||
assert_eq!(
|
||||
rounds.add_vote(vote.clone()),
|
||||
VoteImportResult::RoundConcluded(SignedCommitment {
|
||||
commitment,
|
||||
signatures: vec![
|
||||
Some(Keyring::<ecdsa_crypto::AuthorityId>::Alice.sign(b"I am committed")),
|
||||
Some(Keyring::<ecdsa_crypto::AuthorityId>::Bob.sign(b"I am committed")),
|
||||
Some(Keyring::<ecdsa_crypto::AuthorityId>::Charlie.sign(b"I am committed")),
|
||||
None,
|
||||
]
|
||||
})
|
||||
);
|
||||
rounds.conclude(block_number);
|
||||
|
||||
vote.id = Keyring::Eve.public();
|
||||
vote.signature = Keyring::<ecdsa_crypto::AuthorityId>::Eve.sign(b"I am committed");
|
||||
// Eve is a validator, but round was concluded, adding vote disallowed
|
||||
assert_eq!(rounds.add_vote(vote), VoteImportResult::Stale);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_rounds_not_accepted() {
|
||||
pezsp_tracing::try_init_simple();
|
||||
|
||||
let validators = ValidatorSet::<ecdsa_crypto::AuthorityId>::new(
|
||||
vec![Keyring::Alice.public(), Keyring::Bob.public(), Keyring::Charlie.public()],
|
||||
42,
|
||||
)
|
||||
.unwrap();
|
||||
let validator_set_id = validators.id();
|
||||
|
||||
// active rounds starts at block 10
|
||||
let session_start = 10u64.into();
|
||||
let mut rounds = Rounds::<Block, ecdsa_crypto::AuthorityId>::new(session_start, validators);
|
||||
|
||||
// vote on round 9
|
||||
let block_number = 9;
|
||||
let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![]);
|
||||
let commitment = Commitment { block_number, payload, validator_set_id };
|
||||
let mut vote = VoteMessage {
|
||||
id: Keyring::Alice.public(),
|
||||
commitment,
|
||||
signature: Keyring::<ecdsa_crypto::AuthorityId>::Alice.sign(b"I am committed"),
|
||||
};
|
||||
// add vote for previous session, should fail
|
||||
assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Stale);
|
||||
// no votes present
|
||||
assert!(rounds.rounds.is_empty());
|
||||
|
||||
// simulate 11 was concluded
|
||||
rounds.best_done = Some(11);
|
||||
// add votes for current session, but already concluded rounds, should fail
|
||||
vote.commitment.block_number = 10;
|
||||
assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Stale);
|
||||
vote.commitment.block_number = 11;
|
||||
assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Stale);
|
||||
// no votes present
|
||||
assert!(rounds.rounds.is_empty());
|
||||
|
||||
// add vote for active round 12
|
||||
vote.commitment.block_number = 12;
|
||||
assert_eq!(rounds.add_vote(vote), VoteImportResult::Ok);
|
||||
// good vote present
|
||||
assert_eq!(rounds.rounds.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_rounds() {
|
||||
pezsp_tracing::try_init_simple();
|
||||
|
||||
let validators = ValidatorSet::<ecdsa_crypto::AuthorityId>::new(
|
||||
vec![Keyring::Alice.public(), Keyring::Bob.public(), Keyring::Charlie.public()],
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let validator_set_id = validators.id();
|
||||
|
||||
let session_start = 1u64.into();
|
||||
let mut rounds = Rounds::<Block, ecdsa_crypto::AuthorityId>::new(session_start, validators);
|
||||
|
||||
let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![]);
|
||||
let commitment = Commitment { block_number: 1, payload, validator_set_id };
|
||||
let mut alice_vote = VoteMessage {
|
||||
id: Keyring::Alice.public(),
|
||||
commitment: commitment.clone(),
|
||||
signature: Keyring::<ecdsa_crypto::AuthorityId>::Alice.sign(b"I am committed"),
|
||||
};
|
||||
let mut bob_vote = VoteMessage {
|
||||
id: Keyring::Bob.public(),
|
||||
commitment: commitment.clone(),
|
||||
signature: Keyring::<ecdsa_crypto::AuthorityId>::Bob.sign(b"I am committed"),
|
||||
};
|
||||
let mut charlie_vote = VoteMessage {
|
||||
id: Keyring::Charlie.public(),
|
||||
commitment,
|
||||
signature: Keyring::<ecdsa_crypto::AuthorityId>::Charlie.sign(b"I am committed"),
|
||||
};
|
||||
let expected_signatures = vec![
|
||||
Some(Keyring::<ecdsa_crypto::AuthorityId>::Alice.sign(b"I am committed")),
|
||||
Some(Keyring::<ecdsa_crypto::AuthorityId>::Bob.sign(b"I am committed")),
|
||||
Some(Keyring::<ecdsa_crypto::AuthorityId>::Charlie.sign(b"I am committed")),
|
||||
];
|
||||
|
||||
// round 1 - only 2 out of 3 vote
|
||||
assert_eq!(rounds.add_vote(alice_vote.clone()), VoteImportResult::Ok);
|
||||
assert_eq!(rounds.add_vote(charlie_vote.clone()), VoteImportResult::Ok);
|
||||
// should be 1 active round
|
||||
assert_eq!(1, rounds.rounds.len());
|
||||
|
||||
// round 2 - only Charlie votes
|
||||
charlie_vote.commitment.block_number = 2;
|
||||
assert_eq!(rounds.add_vote(charlie_vote.clone()), VoteImportResult::Ok);
|
||||
// should be 2 active rounds
|
||||
assert_eq!(2, rounds.rounds.len());
|
||||
|
||||
// round 3 - all validators vote -> round is concluded
|
||||
alice_vote.commitment.block_number = 3;
|
||||
bob_vote.commitment.block_number = 3;
|
||||
charlie_vote.commitment.block_number = 3;
|
||||
assert_eq!(rounds.add_vote(alice_vote.clone()), VoteImportResult::Ok);
|
||||
assert_eq!(rounds.add_vote(bob_vote.clone()), VoteImportResult::Ok);
|
||||
assert_eq!(
|
||||
rounds.add_vote(charlie_vote.clone()),
|
||||
VoteImportResult::RoundConcluded(SignedCommitment {
|
||||
commitment: charlie_vote.commitment,
|
||||
signatures: expected_signatures
|
||||
})
|
||||
);
|
||||
// should be only 2 active since this one auto-concluded
|
||||
assert_eq!(2, rounds.rounds.len());
|
||||
|
||||
// conclude round 2
|
||||
rounds.conclude(2);
|
||||
// should be no more active rounds since 2 was officially concluded and round "1" is stale
|
||||
assert!(rounds.rounds.is_empty());
|
||||
|
||||
// conclude round 3
|
||||
rounds.conclude(3);
|
||||
assert!(rounds.previous_votes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_provide_equivocation_proof() {
|
||||
pezsp_tracing::try_init_simple();
|
||||
|
||||
let validators = ValidatorSet::<ecdsa_crypto::AuthorityId>::new(
|
||||
vec![Keyring::Alice.public(), Keyring::Bob.public()],
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let validator_set_id = validators.id();
|
||||
let session_start = 1u64.into();
|
||||
let mut rounds = Rounds::<Block, ecdsa_crypto::AuthorityId>::new(session_start, validators);
|
||||
|
||||
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![1, 1, 1, 1]);
|
||||
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![2, 2, 2, 2]);
|
||||
let commitment1 = Commitment { block_number: 1, payload: payload1, validator_set_id };
|
||||
let commitment2 = Commitment { block_number: 1, payload: payload2, validator_set_id };
|
||||
|
||||
let alice_vote1 = VoteMessage {
|
||||
id: Keyring::Alice.public(),
|
||||
commitment: commitment1,
|
||||
signature: Keyring::<ecdsa_crypto::AuthorityId>::Alice.sign(b"I am committed"),
|
||||
};
|
||||
let mut alice_vote2 = alice_vote1.clone();
|
||||
alice_vote2.commitment = commitment2;
|
||||
|
||||
let expected_result = VoteImportResult::DoubleVoting(DoubleVotingProof {
|
||||
first: alice_vote1.clone(),
|
||||
second: alice_vote2.clone(),
|
||||
});
|
||||
|
||||
// vote on one payload - ok
|
||||
assert_eq!(rounds.add_vote(alice_vote1), VoteImportResult::Ok);
|
||||
|
||||
// vote on _another_ commitment/payload -> expected equivocation proof
|
||||
assert_eq!(rounds.add_vote(alice_vote2), expected_result);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
[package]
|
||||
name = "pezsc-consensus"
|
||||
version = "0.33.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Collection of common consensus specific implementations for Bizinikiwi (client)"
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
futures = { features = ["thread-pool"], workspace = true }
|
||||
log = { workspace = true, default-features = true }
|
||||
mockall = { workspace = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
prometheus-endpoint = { workspace = true, default-features = true }
|
||||
pezsc-client-api = { workspace = true, default-features = true }
|
||||
pezsc-network-types = { workspace = true, default-features = true }
|
||||
pezsc-utils = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive"], 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-runtime = { workspace = true, default-features = true }
|
||||
pezsp-state-machine = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezsp-test-primitives = { workspace = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezsc-client-api/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"pezsp-state-machine/runtime-benchmarks",
|
||||
"pezsp-test-primitives/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
Collection of common consensus specific implementations
|
||||
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,410 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Block import helpers.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use pezsp_runtime::{
|
||||
traits::{Block as BlockT, HashingFor, Header as HeaderT, NumberFor},
|
||||
DigestItem, Justification, Justifications,
|
||||
};
|
||||
use std::{any::Any, borrow::Cow, collections::HashMap, sync::Arc};
|
||||
|
||||
use pezsp_consensus::{BlockOrigin, Error};
|
||||
|
||||
/// Block import result.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum ImportResult {
|
||||
/// Block imported.
|
||||
Imported(ImportedAux),
|
||||
/// Already in the blockchain.
|
||||
AlreadyInChain,
|
||||
/// Block or parent is known to be bad.
|
||||
KnownBad,
|
||||
/// Block parent is not in the chain.
|
||||
UnknownParent,
|
||||
/// Parent state is missing.
|
||||
MissingState,
|
||||
}
|
||||
|
||||
/// Auxiliary data associated with an imported block result.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ImportedAux {
|
||||
/// Only the header has been imported. Block body verification was skipped.
|
||||
pub header_only: bool,
|
||||
/// Clear all pending justification requests.
|
||||
pub clear_justification_requests: bool,
|
||||
/// Request a justification for the given block.
|
||||
pub needs_justification: bool,
|
||||
/// Received a bad justification.
|
||||
pub bad_justification: bool,
|
||||
/// Whether the block that was imported is the new best block.
|
||||
pub is_new_best: bool,
|
||||
}
|
||||
|
||||
impl ImportResult {
|
||||
/// Returns default value for `ImportResult::Imported` with
|
||||
/// `clear_justification_requests`, `needs_justification`,
|
||||
/// `bad_justification` set to false.
|
||||
pub fn imported(is_new_best: bool) -> ImportResult {
|
||||
let aux = ImportedAux { is_new_best, ..Default::default() };
|
||||
|
||||
ImportResult::Imported(aux)
|
||||
}
|
||||
|
||||
/// Handles any necessary request for justifications (or clearing of pending requests) based on
|
||||
/// the outcome of this block import.
|
||||
pub fn handle_justification<B>(
|
||||
&self,
|
||||
hash: &B::Hash,
|
||||
number: NumberFor<B>,
|
||||
justification_sync_link: &dyn JustificationSyncLink<B>,
|
||||
) where
|
||||
B: BlockT,
|
||||
{
|
||||
match self {
|
||||
ImportResult::Imported(aux) => {
|
||||
if aux.clear_justification_requests {
|
||||
justification_sync_link.clear_justification_requests();
|
||||
}
|
||||
|
||||
if aux.needs_justification {
|
||||
justification_sync_link.request_justification(hash, number);
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fork choice strategy.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum ForkChoiceStrategy {
|
||||
/// Longest chain fork choice.
|
||||
LongestChain,
|
||||
/// Custom fork choice rule, where true indicates the new block should be the best block.
|
||||
Custom(bool),
|
||||
}
|
||||
|
||||
/// Data required to check validity of a Block.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct BlockCheckParams<Block: BlockT> {
|
||||
/// Hash of the block that we verify.
|
||||
pub hash: Block::Hash,
|
||||
/// Block number of the block that we verify.
|
||||
pub number: NumberFor<Block>,
|
||||
/// Parent hash of the block that we verify.
|
||||
pub parent_hash: Block::Hash,
|
||||
/// Allow importing the block skipping state verification if parent state is missing.
|
||||
pub allow_missing_state: bool,
|
||||
/// Allow importing the block if parent block is missing.
|
||||
pub allow_missing_parent: bool,
|
||||
/// Re-validate existing block.
|
||||
pub import_existing: bool,
|
||||
}
|
||||
|
||||
/// Precomputed storage.
|
||||
pub enum StorageChanges<Block: BlockT> {
|
||||
/// Changes coming from block execution.
|
||||
Changes(pezsp_state_machine::StorageChanges<HashingFor<Block>>),
|
||||
/// Whole new state.
|
||||
Import(ImportedState<Block>),
|
||||
}
|
||||
|
||||
/// Imported state data. A vector of key-value pairs that should form a trie.
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
pub struct ImportedState<B: BlockT> {
|
||||
/// Target block hash.
|
||||
pub block: B::Hash,
|
||||
/// State keys and values.
|
||||
pub state: pezsp_state_machine::KeyValueStates,
|
||||
}
|
||||
|
||||
impl<B: BlockT> std::fmt::Debug for ImportedState<B> {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.debug_struct("ImportedState").field("block", &self.block).finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines how a new state is computed for a given imported block.
|
||||
pub enum StateAction<Block: BlockT> {
|
||||
/// Apply precomputed changes coming from block execution or state sync.
|
||||
ApplyChanges(StorageChanges<Block>),
|
||||
/// Execute block body (required) and compute state.
|
||||
Execute,
|
||||
/// Execute block body if parent state is available and compute state.
|
||||
ExecuteIfPossible,
|
||||
/// Don't execute or import state.
|
||||
Skip,
|
||||
}
|
||||
|
||||
impl<Block: BlockT> StateAction<Block> {
|
||||
/// Check if execution checks that require runtime calls should be skipped.
|
||||
pub fn skip_execution_checks(&self) -> bool {
|
||||
match self {
|
||||
StateAction::ApplyChanges(_) |
|
||||
StateAction::Execute |
|
||||
StateAction::ExecuteIfPossible => false,
|
||||
StateAction::Skip => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block: BlockT> From<StorageChanges<Block>> for StateAction<Block> {
|
||||
fn from(value: StorageChanges<Block>) -> Self {
|
||||
Self::ApplyChanges(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block: BlockT> From<pezsp_state_machine::StorageChanges<HashingFor<Block>>>
|
||||
for StateAction<Block>
|
||||
{
|
||||
fn from(value: pezsp_state_machine::StorageChanges<HashingFor<Block>>) -> Self {
|
||||
Self::ApplyChanges(StorageChanges::Changes(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// Data required to import a Block.
|
||||
#[non_exhaustive]
|
||||
pub struct BlockImportParams<Block: BlockT> {
|
||||
/// Origin of the Block
|
||||
pub origin: BlockOrigin,
|
||||
/// The header, without consensus post-digests applied. This should be in the same
|
||||
/// state as it comes out of the runtime.
|
||||
///
|
||||
/// Consensus engines which alter the header (by adding post-runtime digests)
|
||||
/// should strip those off in the initial verification process and pass them
|
||||
/// via the `post_digests` field. During block authorship, they should
|
||||
/// not be pushed to the header directly.
|
||||
///
|
||||
/// The reason for this distinction is so the header can be directly
|
||||
/// re-executed in a runtime that checks digest equivalence -- the
|
||||
/// post-runtime digests are pushed back on after.
|
||||
pub header: Block::Header,
|
||||
/// Justification(s) provided for this block from the outside.
|
||||
pub justifications: Option<Justifications>,
|
||||
/// Digest items that have been added after the runtime for external
|
||||
/// work, like a consensus signature.
|
||||
pub post_digests: Vec<DigestItem>,
|
||||
/// The body of the block.
|
||||
pub body: Option<Vec<Block::Extrinsic>>,
|
||||
/// Indexed transaction body of the block.
|
||||
pub indexed_body: Option<Vec<Vec<u8>>>,
|
||||
/// Specify how the new state is computed.
|
||||
pub state_action: StateAction<Block>,
|
||||
/// Is this block finalized already?
|
||||
/// `true` implies instant finality.
|
||||
pub finalized: bool,
|
||||
/// Intermediate values that are interpreted by block importers. Each block importer,
|
||||
/// upon handling a value, removes it from the intermediate list. The final block importer
|
||||
/// rejects block import if there are still intermediate values that remain unhandled.
|
||||
pub intermediates: HashMap<Cow<'static, [u8]>, Box<dyn Any + Send>>,
|
||||
/// Auxiliary consensus data produced by the block.
|
||||
/// Contains a list of key-value pairs. If values are `None`, the keys will be deleted. These
|
||||
/// changes will be applied to `AuxStore` database all as one batch, which is more efficient
|
||||
/// than updating `AuxStore` directly.
|
||||
pub auxiliary: Vec<(Vec<u8>, Option<Vec<u8>>)>,
|
||||
/// Fork choice strategy of this import. This should only be set by a
|
||||
/// synchronous import, otherwise it may race against other imports.
|
||||
/// `None` indicates that the current verifier or importer cannot yet
|
||||
/// determine the fork choice value, and it expects subsequent importer
|
||||
/// to modify it. If `None` is passed all the way down to bottom block
|
||||
/// importer, the import fails with an `IncompletePipeline` error.
|
||||
pub fork_choice: Option<ForkChoiceStrategy>,
|
||||
/// Re-validate existing block.
|
||||
pub import_existing: bool,
|
||||
/// Whether to create "block gap" in case this block doesn't have parent.
|
||||
pub create_gap: bool,
|
||||
/// Cached full header hash (with post-digests applied).
|
||||
pub post_hash: Option<Block::Hash>,
|
||||
}
|
||||
|
||||
impl<Block: BlockT> BlockImportParams<Block> {
|
||||
/// Create a new block import params.
|
||||
pub fn new(origin: BlockOrigin, header: Block::Header) -> Self {
|
||||
Self {
|
||||
origin,
|
||||
header,
|
||||
justifications: None,
|
||||
post_digests: Vec::new(),
|
||||
body: None,
|
||||
indexed_body: None,
|
||||
state_action: StateAction::Execute,
|
||||
finalized: false,
|
||||
intermediates: HashMap::new(),
|
||||
auxiliary: Vec::new(),
|
||||
fork_choice: None,
|
||||
import_existing: false,
|
||||
create_gap: true,
|
||||
post_hash: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the full header hash (with post-digests applied).
|
||||
pub fn post_hash(&self) -> Block::Hash {
|
||||
if let Some(hash) = self.post_hash {
|
||||
hash
|
||||
} else {
|
||||
self.post_header().hash()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the post header.
|
||||
pub fn post_header(&self) -> Block::Header {
|
||||
if self.post_digests.is_empty() {
|
||||
self.header.clone()
|
||||
} else {
|
||||
let mut hdr = self.header.clone();
|
||||
for digest_item in &self.post_digests {
|
||||
hdr.digest_mut().push(digest_item.clone());
|
||||
}
|
||||
|
||||
hdr
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert intermediate by given key.
|
||||
pub fn insert_intermediate<T: 'static + Send>(&mut self, key: &'static [u8], value: T) {
|
||||
self.intermediates.insert(Cow::from(key), Box::new(value));
|
||||
}
|
||||
|
||||
/// Remove and return intermediate by given key.
|
||||
pub fn remove_intermediate<T: 'static>(&mut self, key: &[u8]) -> Result<T, Error> {
|
||||
let (k, v) = self.intermediates.remove_entry(key).ok_or(Error::NoIntermediate)?;
|
||||
|
||||
v.downcast::<T>().map(|v| *v).map_err(|v| {
|
||||
self.intermediates.insert(k, v);
|
||||
Error::InvalidIntermediate
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a reference to a given intermediate.
|
||||
pub fn get_intermediate<T: 'static>(&self, key: &[u8]) -> Result<&T, Error> {
|
||||
self.intermediates
|
||||
.get(key)
|
||||
.ok_or(Error::NoIntermediate)?
|
||||
.downcast_ref::<T>()
|
||||
.ok_or(Error::InvalidIntermediate)
|
||||
}
|
||||
|
||||
/// Get a mutable reference to a given intermediate.
|
||||
pub fn get_intermediate_mut<T: 'static>(&mut self, key: &[u8]) -> Result<&mut T, Error> {
|
||||
self.intermediates
|
||||
.get_mut(key)
|
||||
.ok_or(Error::NoIntermediate)?
|
||||
.downcast_mut::<T>()
|
||||
.ok_or(Error::InvalidIntermediate)
|
||||
}
|
||||
|
||||
/// Check if this block contains state import action
|
||||
pub fn with_state(&self) -> bool {
|
||||
matches!(self.state_action, StateAction::ApplyChanges(StorageChanges::Import(_)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Block import trait.
|
||||
#[async_trait::async_trait]
|
||||
pub trait BlockImport<B: BlockT> {
|
||||
/// The error type.
|
||||
type Error: std::error::Error + Send + 'static;
|
||||
|
||||
/// Check block preconditions.
|
||||
async fn check_block(&self, block: BlockCheckParams<B>) -> Result<ImportResult, Self::Error>;
|
||||
|
||||
/// Import a block.
|
||||
async fn import_block(&self, block: BlockImportParams<B>) -> Result<ImportResult, Self::Error>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B: BlockT> BlockImport<B> for crate::import_queue::BoxBlockImport<B> {
|
||||
type Error = pezsp_consensus::error::Error;
|
||||
|
||||
/// Check block preconditions.
|
||||
async fn check_block(&self, block: BlockCheckParams<B>) -> Result<ImportResult, Self::Error> {
|
||||
(**self).check_block(block).await
|
||||
}
|
||||
|
||||
/// Import a block.
|
||||
async fn import_block(&self, block: BlockImportParams<B>) -> Result<ImportResult, Self::Error> {
|
||||
(**self).import_block(block).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B: BlockT, T, E: std::error::Error + Send + 'static> BlockImport<B> for Arc<T>
|
||||
where
|
||||
for<'r> &'r T: BlockImport<B, Error = E>,
|
||||
T: Send + Sync,
|
||||
{
|
||||
type Error = E;
|
||||
|
||||
async fn check_block(&self, block: BlockCheckParams<B>) -> Result<ImportResult, Self::Error> {
|
||||
(&**self).check_block(block).await
|
||||
}
|
||||
|
||||
async fn import_block(&self, block: BlockImportParams<B>) -> Result<ImportResult, Self::Error> {
|
||||
(&**self).import_block(block).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Justification import trait
|
||||
#[async_trait::async_trait]
|
||||
pub trait JustificationImport<B: BlockT> {
|
||||
type Error: std::error::Error + Send + 'static;
|
||||
|
||||
/// Called by the import queue when it is started. Returns a list of justifications to request
|
||||
/// from the network.
|
||||
async fn on_start(&mut self) -> Vec<(B::Hash, NumberFor<B>)>;
|
||||
|
||||
/// Import a Block justification and finalize the given block.
|
||||
async fn import_justification(
|
||||
&mut self,
|
||||
hash: B::Hash,
|
||||
number: NumberFor<B>,
|
||||
justification: Justification,
|
||||
) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
/// Control the synchronization process of block justifications.
|
||||
///
|
||||
/// When importing blocks different consensus engines might require that
|
||||
/// additional finality data is provided (i.e. a justification for the block).
|
||||
/// This trait abstracts the required methods to issue those requests
|
||||
pub trait JustificationSyncLink<B: BlockT>: Send + Sync {
|
||||
/// Request a justification for the given block.
|
||||
fn request_justification(&self, hash: &B::Hash, number: NumberFor<B>);
|
||||
|
||||
/// Clear all pending justification requests.
|
||||
fn clear_justification_requests(&self);
|
||||
}
|
||||
|
||||
impl<B: BlockT> JustificationSyncLink<B> for () {
|
||||
fn request_justification(&self, _hash: &B::Hash, _number: NumberFor<B>) {}
|
||||
|
||||
fn clear_justification_requests(&self) {}
|
||||
}
|
||||
|
||||
impl<B: BlockT, L: JustificationSyncLink<B>> JustificationSyncLink<B> for Arc<L> {
|
||||
fn request_justification(&self, hash: &B::Hash, number: NumberFor<B>) {
|
||||
L::request_justification(self, hash, number);
|
||||
}
|
||||
|
||||
fn clear_justification_requests(&self) {
|
||||
L::clear_justification_requests(self);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Import Queue primitive: something which can verify and import blocks.
|
||||
//!
|
||||
//! This serves as an intermediate and abstracted step between synchronization
|
||||
//! and import. Each mode of consensus will have its own requirements for block
|
||||
//! verification. Some algorithms can verify in parallel, while others only
|
||||
//! sequentially.
|
||||
//!
|
||||
//! The `ImportQueue` trait allows such verification strategies to be
|
||||
//! instantiated. The `BasicQueue` and `BasicVerifier` traits allow serial
|
||||
//! queues to be instantiated simply.
|
||||
|
||||
use log::{debug, trace};
|
||||
use std::{
|
||||
fmt,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use pezsp_consensus::{error::Error as ConsensusError, BlockOrigin};
|
||||
use pezsp_runtime::{
|
||||
traits::{Block as BlockT, Header as _, NumberFor},
|
||||
Justifications,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
block_import::{
|
||||
BlockCheckParams, BlockImport, BlockImportParams, ImportResult, ImportedAux, ImportedState,
|
||||
JustificationImport, StateAction,
|
||||
},
|
||||
metrics::Metrics,
|
||||
};
|
||||
|
||||
pub use basic_queue::BasicQueue;
|
||||
|
||||
const LOG_TARGET: &str = "sync::import-queue";
|
||||
|
||||
/// A commonly-used Import Queue type.
|
||||
///
|
||||
/// This defines the transaction type of the `BasicQueue` to be the transaction type for a client.
|
||||
pub type DefaultImportQueue<Block> = BasicQueue<Block>;
|
||||
|
||||
mod basic_queue;
|
||||
pub mod buffered_link;
|
||||
pub mod mock;
|
||||
|
||||
/// Shared block import struct used by the queue.
|
||||
pub type BoxBlockImport<B> = Box<dyn BlockImport<B, Error = ConsensusError> + Send + Sync>;
|
||||
|
||||
/// Shared justification import struct used by the queue.
|
||||
pub type BoxJustificationImport<B> =
|
||||
Box<dyn JustificationImport<B, Error = ConsensusError> + Send + Sync>;
|
||||
|
||||
/// Maps to the RuntimeOrigin used by the network.
|
||||
pub type RuntimeOrigin = pezsc_network_types::PeerId;
|
||||
|
||||
/// Block data used by the queue.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct IncomingBlock<B: BlockT> {
|
||||
/// Block header hash.
|
||||
pub hash: <B as BlockT>::Hash,
|
||||
/// Block header if requested.
|
||||
pub header: Option<<B as BlockT>::Header>,
|
||||
/// Block body if requested.
|
||||
pub body: Option<Vec<<B as BlockT>::Extrinsic>>,
|
||||
/// Indexed block body if requested.
|
||||
pub indexed_body: Option<Vec<Vec<u8>>>,
|
||||
/// Justification(s) if requested.
|
||||
pub justifications: Option<Justifications>,
|
||||
/// The peer, we received this from
|
||||
pub origin: Option<RuntimeOrigin>,
|
||||
/// Allow importing the block skipping state verification if parent state is missing.
|
||||
pub allow_missing_state: bool,
|
||||
/// Skip block execution and state verification.
|
||||
pub skip_execution: bool,
|
||||
/// Re-validate existing block.
|
||||
pub import_existing: bool,
|
||||
/// Do not compute new state, but rather set it to the given set.
|
||||
pub state: Option<ImportedState<B>>,
|
||||
}
|
||||
|
||||
/// Verify a justification of a block
|
||||
#[async_trait::async_trait]
|
||||
pub trait Verifier<B: BlockT>: Send + Sync {
|
||||
/// Verify the given block data and return the `BlockImportParams` to
|
||||
/// continue the block import process.
|
||||
async fn verify(&self, block: BlockImportParams<B>) -> Result<BlockImportParams<B>, String>;
|
||||
}
|
||||
|
||||
/// Blocks import queue API.
|
||||
///
|
||||
/// The `import_*` methods can be called in order to send elements for the import queue to verify.
|
||||
pub trait ImportQueueService<B: BlockT>: Send {
|
||||
/// Import a bunch of blocks, every next block must be an ancestor of the previous block in the
|
||||
/// list.
|
||||
fn import_blocks(&mut self, origin: BlockOrigin, blocks: Vec<IncomingBlock<B>>);
|
||||
|
||||
/// Import block justifications.
|
||||
fn import_justifications(
|
||||
&mut self,
|
||||
who: RuntimeOrigin,
|
||||
hash: B::Hash,
|
||||
number: NumberFor<B>,
|
||||
justifications: Justifications,
|
||||
);
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ImportQueue<B: BlockT>: Send {
|
||||
/// Get a copy of the handle to [`ImportQueueService`].
|
||||
fn service(&self) -> Box<dyn ImportQueueService<B>>;
|
||||
|
||||
/// Get a reference to the handle to [`ImportQueueService`].
|
||||
fn service_ref(&mut self) -> &mut dyn ImportQueueService<B>;
|
||||
|
||||
/// This method should behave in a way similar to `Future::poll`. It can register the current
|
||||
/// task and notify later when more actions are ready to be polled. To continue the comparison,
|
||||
/// it is as if this method always returned `Poll::Pending`.
|
||||
fn poll_actions(&mut self, cx: &mut futures::task::Context, link: &dyn Link<B>);
|
||||
|
||||
/// Start asynchronous runner for import queue.
|
||||
///
|
||||
/// Takes an object implementing [`Link`] which allows the import queue to
|
||||
/// influence the synchronization process.
|
||||
async fn run(self, link: &dyn Link<B>);
|
||||
}
|
||||
|
||||
/// The result of importing a justification.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum JustificationImportResult {
|
||||
/// Justification was imported successfully.
|
||||
Success,
|
||||
|
||||
/// Justification was not imported successfully.
|
||||
Failure,
|
||||
|
||||
/// Justification was not imported successfully, because it is outdated.
|
||||
OutdatedJustification,
|
||||
}
|
||||
|
||||
/// Hooks that the verification queue can use to influence the synchronization
|
||||
/// algorithm.
|
||||
pub trait Link<B: BlockT>: Send + Sync {
|
||||
/// Batch of blocks imported, with or without error.
|
||||
fn blocks_processed(
|
||||
&self,
|
||||
_imported: usize,
|
||||
_count: usize,
|
||||
_results: Vec<(BlockImportResult<B>, B::Hash)>,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Justification import result.
|
||||
fn justification_imported(
|
||||
&self,
|
||||
_who: RuntimeOrigin,
|
||||
_hash: &B::Hash,
|
||||
_number: NumberFor<B>,
|
||||
_import_result: JustificationImportResult,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Request a justification for the given block.
|
||||
fn request_justification(&self, _hash: &B::Hash, _number: NumberFor<B>) {}
|
||||
}
|
||||
|
||||
/// Block import successful result.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum BlockImportStatus<BlockNumber: fmt::Debug + PartialEq> {
|
||||
/// Imported known block.
|
||||
ImportedKnown(BlockNumber, Option<RuntimeOrigin>),
|
||||
/// Imported unknown block.
|
||||
ImportedUnknown(BlockNumber, ImportedAux, Option<RuntimeOrigin>),
|
||||
}
|
||||
|
||||
impl<BlockNumber: fmt::Debug + PartialEq> BlockImportStatus<BlockNumber> {
|
||||
/// Returns the imported block number.
|
||||
pub fn number(&self) -> &BlockNumber {
|
||||
match self {
|
||||
BlockImportStatus::ImportedKnown(n, _) |
|
||||
BlockImportStatus::ImportedUnknown(n, _, _) => n,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Block import error.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum BlockImportError {
|
||||
/// Block missed header, can't be imported
|
||||
#[error("block is missing a header (origin = {0:?})")]
|
||||
IncompleteHeader(Option<RuntimeOrigin>),
|
||||
|
||||
/// Block verification failed, can't be imported
|
||||
#[error("block verification failed (origin = {0:?}): {1}")]
|
||||
VerificationFailed(Option<RuntimeOrigin>, String),
|
||||
|
||||
/// Block is known to be Bad
|
||||
#[error("bad block (origin = {0:?})")]
|
||||
BadBlock(Option<RuntimeOrigin>),
|
||||
|
||||
/// Parent state is missing.
|
||||
#[error("block is missing parent state")]
|
||||
MissingState,
|
||||
|
||||
/// Block has an unknown parent
|
||||
#[error("block has an unknown parent")]
|
||||
UnknownParent,
|
||||
|
||||
/// Block import has been cancelled. This can happen if the parent block fails to be imported.
|
||||
#[error("import has been cancelled")]
|
||||
Cancelled,
|
||||
|
||||
/// Other error.
|
||||
#[error("consensus error: {0}")]
|
||||
Other(ConsensusError),
|
||||
}
|
||||
|
||||
type BlockImportResult<B> = Result<BlockImportStatus<NumberFor<B>>, BlockImportError>;
|
||||
|
||||
/// Single block import function.
|
||||
pub async fn import_single_block<B: BlockT, V: Verifier<B>>(
|
||||
import_handle: &mut impl BlockImport<B, Error = ConsensusError>,
|
||||
block_origin: BlockOrigin,
|
||||
block: IncomingBlock<B>,
|
||||
verifier: &V,
|
||||
) -> BlockImportResult<B> {
|
||||
match verify_single_block_metered(import_handle, block_origin, block, verifier, None).await? {
|
||||
SingleBlockVerificationOutcome::Imported(import_status) => Ok(import_status),
|
||||
SingleBlockVerificationOutcome::Verified(import_parameters) =>
|
||||
import_single_block_metered(import_handle, import_parameters, None).await,
|
||||
}
|
||||
}
|
||||
|
||||
fn import_handler<Block>(
|
||||
number: NumberFor<Block>,
|
||||
hash: Block::Hash,
|
||||
parent_hash: Block::Hash,
|
||||
block_origin: Option<RuntimeOrigin>,
|
||||
import: Result<ImportResult, ConsensusError>,
|
||||
) -> Result<BlockImportStatus<NumberFor<Block>>, BlockImportError>
|
||||
where
|
||||
Block: BlockT,
|
||||
{
|
||||
match import {
|
||||
Ok(ImportResult::AlreadyInChain) => {
|
||||
trace!(target: LOG_TARGET, "Block already in chain {}: {:?}", number, hash);
|
||||
Ok(BlockImportStatus::ImportedKnown(number, block_origin))
|
||||
},
|
||||
Ok(ImportResult::Imported(aux)) =>
|
||||
Ok(BlockImportStatus::ImportedUnknown(number, aux, block_origin)),
|
||||
Ok(ImportResult::MissingState) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Parent state is missing for {}: {:?}, parent: {:?}", number, hash, parent_hash
|
||||
);
|
||||
Err(BlockImportError::MissingState)
|
||||
},
|
||||
Ok(ImportResult::UnknownParent) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Block with unknown parent {}: {:?}, parent: {:?}", number, hash, parent_hash
|
||||
);
|
||||
Err(BlockImportError::UnknownParent)
|
||||
},
|
||||
Ok(ImportResult::KnownBad) => {
|
||||
debug!(target: LOG_TARGET, "Peer gave us a bad block {}: {:?}", number, hash);
|
||||
Err(BlockImportError::BadBlock(block_origin))
|
||||
},
|
||||
Err(e) => {
|
||||
debug!(target: LOG_TARGET, "Error importing block {}: {:?}: {}", number, hash, e);
|
||||
Err(BlockImportError::Other(e))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum SingleBlockVerificationOutcome<Block: BlockT> {
|
||||
/// Block is already imported.
|
||||
Imported(BlockImportStatus<NumberFor<Block>>),
|
||||
/// Block is verified, but needs to be imported.
|
||||
Verified(SingleBlockImportParameters<Block>),
|
||||
}
|
||||
|
||||
pub(crate) struct SingleBlockImportParameters<Block: BlockT> {
|
||||
import_block: BlockImportParams<Block>,
|
||||
hash: Block::Hash,
|
||||
block_origin: Option<RuntimeOrigin>,
|
||||
verification_time: Duration,
|
||||
}
|
||||
|
||||
/// Single block import function with metering.
|
||||
pub(crate) async fn verify_single_block_metered<B: BlockT, V: Verifier<B>>(
|
||||
import_handle: &impl BlockImport<B, Error = ConsensusError>,
|
||||
block_origin: BlockOrigin,
|
||||
block: IncomingBlock<B>,
|
||||
verifier: &V,
|
||||
metrics: Option<&Metrics>,
|
||||
) -> Result<SingleBlockVerificationOutcome<B>, BlockImportError> {
|
||||
let peer = block.origin;
|
||||
let justifications = block.justifications;
|
||||
|
||||
let Some(header) = block.header else {
|
||||
if let Some(ref peer) = peer {
|
||||
debug!(target: LOG_TARGET, "Header {} was not provided by {peer} ", block.hash);
|
||||
} else {
|
||||
debug!(target: LOG_TARGET, "Header {} was not provided ", block.hash);
|
||||
}
|
||||
return Err(BlockImportError::IncompleteHeader(peer));
|
||||
};
|
||||
|
||||
trace!(target: LOG_TARGET, "Header {} has {:?} logs", block.hash, header.digest().logs().len());
|
||||
|
||||
let number = *header.number();
|
||||
let hash = block.hash;
|
||||
let parent_hash = *header.parent_hash();
|
||||
|
||||
match import_handler::<B>(
|
||||
number,
|
||||
hash,
|
||||
parent_hash,
|
||||
peer,
|
||||
import_handle
|
||||
.check_block(BlockCheckParams {
|
||||
hash,
|
||||
number,
|
||||
parent_hash,
|
||||
allow_missing_state: block.allow_missing_state,
|
||||
import_existing: block.import_existing,
|
||||
allow_missing_parent: block.state.is_some(),
|
||||
})
|
||||
.await,
|
||||
)? {
|
||||
BlockImportStatus::ImportedUnknown { .. } => (),
|
||||
r => {
|
||||
// Any other successful result means that the block is already imported.
|
||||
return Ok(SingleBlockVerificationOutcome::Imported(r));
|
||||
},
|
||||
}
|
||||
|
||||
let started = Instant::now();
|
||||
|
||||
let mut import_block = BlockImportParams::new(block_origin, header);
|
||||
import_block.body = block.body;
|
||||
import_block.justifications = justifications;
|
||||
import_block.post_hash = Some(hash);
|
||||
import_block.import_existing = block.import_existing;
|
||||
import_block.indexed_body = block.indexed_body;
|
||||
|
||||
if let Some(state) = block.state {
|
||||
let changes = crate::block_import::StorageChanges::Import(state);
|
||||
import_block.state_action = StateAction::ApplyChanges(changes);
|
||||
} else if block.skip_execution {
|
||||
import_block.state_action = StateAction::Skip;
|
||||
} else if block.allow_missing_state {
|
||||
import_block.state_action = StateAction::ExecuteIfPossible;
|
||||
}
|
||||
|
||||
let import_block = verifier.verify(import_block).await.map_err(|msg| {
|
||||
if let Some(ref peer) = peer {
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"Verifying {}({}) from {} failed: {}",
|
||||
number,
|
||||
hash,
|
||||
peer,
|
||||
msg
|
||||
);
|
||||
} else {
|
||||
trace!(target: LOG_TARGET, "Verifying {}({}) failed: {}", number, hash, msg);
|
||||
}
|
||||
if let Some(metrics) = metrics {
|
||||
metrics.report_verification(false, started.elapsed());
|
||||
}
|
||||
BlockImportError::VerificationFailed(peer, msg)
|
||||
})?;
|
||||
|
||||
let verification_time = started.elapsed();
|
||||
if let Some(metrics) = metrics {
|
||||
metrics.report_verification(true, verification_time);
|
||||
}
|
||||
|
||||
Ok(SingleBlockVerificationOutcome::Verified(SingleBlockImportParameters {
|
||||
import_block,
|
||||
hash,
|
||||
block_origin: peer,
|
||||
verification_time,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn import_single_block_metered<Block: BlockT>(
|
||||
import_handle: &mut impl BlockImport<Block, Error = ConsensusError>,
|
||||
import_parameters: SingleBlockImportParameters<Block>,
|
||||
metrics: Option<&Metrics>,
|
||||
) -> BlockImportResult<Block> {
|
||||
let started = Instant::now();
|
||||
|
||||
let SingleBlockImportParameters { import_block, hash, block_origin, verification_time } =
|
||||
import_parameters;
|
||||
|
||||
let number = *import_block.header.number();
|
||||
let parent_hash = *import_block.header.parent_hash();
|
||||
|
||||
let imported = import_handle.import_block(import_block).await;
|
||||
if let Some(metrics) = metrics {
|
||||
metrics.report_verification_and_import(started.elapsed() + verification_time);
|
||||
}
|
||||
|
||||
import_handler::<Block>(number, hash, parent_hash, block_origin, imported)
|
||||
}
|
||||
@@ -0,0 +1,693 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use futures::{
|
||||
prelude::*,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use log::{debug, trace};
|
||||
use prometheus_endpoint::Registry;
|
||||
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
|
||||
use pezsp_consensus::BlockOrigin;
|
||||
use pezsp_runtime::{
|
||||
traits::{Block as BlockT, Header as HeaderT, NumberFor},
|
||||
Justification, Justifications,
|
||||
};
|
||||
use std::pin::Pin;
|
||||
|
||||
use crate::{
|
||||
import_queue::{
|
||||
buffered_link::{self, BufferedLinkReceiver, BufferedLinkSender},
|
||||
import_single_block_metered, verify_single_block_metered, BlockImportError,
|
||||
BlockImportStatus, BoxBlockImport, BoxJustificationImport, ImportQueue, ImportQueueService,
|
||||
IncomingBlock, JustificationImportResult, Link, RuntimeOrigin,
|
||||
SingleBlockVerificationOutcome, Verifier, LOG_TARGET,
|
||||
},
|
||||
metrics::Metrics,
|
||||
};
|
||||
|
||||
/// Interface to a basic block import queue that is importing blocks sequentially in a separate
|
||||
/// task, with plugable verification.
|
||||
pub struct BasicQueue<B: BlockT> {
|
||||
/// Handle for sending justification and block import messages to the background task.
|
||||
handle: BasicQueueHandle<B>,
|
||||
/// Results coming from the worker task.
|
||||
result_port: BufferedLinkReceiver<B>,
|
||||
}
|
||||
|
||||
impl<B: BlockT> Drop for BasicQueue<B> {
|
||||
fn drop(&mut self) {
|
||||
// Flush the queue and close the receiver to terminate the future.
|
||||
self.handle.close();
|
||||
self.result_port.close();
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT> BasicQueue<B> {
|
||||
/// Instantiate a new basic queue, with given verifier.
|
||||
///
|
||||
/// This creates a background task, and calls `on_start` on the justification importer.
|
||||
pub fn new<V>(
|
||||
verifier: V,
|
||||
block_import: BoxBlockImport<B>,
|
||||
justification_import: Option<BoxJustificationImport<B>>,
|
||||
spawner: &impl pezsp_core::traits::SpawnEssentialNamed,
|
||||
prometheus_registry: Option<&Registry>,
|
||||
) -> Self
|
||||
where
|
||||
V: Verifier<B> + 'static,
|
||||
{
|
||||
let (result_sender, result_port) = buffered_link::buffered_link(100_000);
|
||||
|
||||
let metrics = prometheus_registry.and_then(|r| {
|
||||
Metrics::register(r)
|
||||
.map_err(|err| {
|
||||
log::warn!("Failed to register Prometheus metrics: {}", err);
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
|
||||
let (future, justification_sender, block_import_sender) = BlockImportWorker::new(
|
||||
result_sender,
|
||||
verifier,
|
||||
block_import,
|
||||
justification_import,
|
||||
metrics,
|
||||
);
|
||||
|
||||
spawner.spawn_essential_blocking(
|
||||
"basic-block-import-worker",
|
||||
Some("block-import"),
|
||||
future.boxed(),
|
||||
);
|
||||
|
||||
Self {
|
||||
handle: BasicQueueHandle::new(justification_sender, block_import_sender),
|
||||
result_port,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct BasicQueueHandle<B: BlockT> {
|
||||
/// Channel to send justification import messages to the background task.
|
||||
justification_sender: TracingUnboundedSender<worker_messages::ImportJustification<B>>,
|
||||
/// Channel to send block import messages to the background task.
|
||||
block_import_sender: TracingUnboundedSender<worker_messages::ImportBlocks<B>>,
|
||||
}
|
||||
|
||||
impl<B: BlockT> BasicQueueHandle<B> {
|
||||
pub fn new(
|
||||
justification_sender: TracingUnboundedSender<worker_messages::ImportJustification<B>>,
|
||||
block_import_sender: TracingUnboundedSender<worker_messages::ImportBlocks<B>>,
|
||||
) -> Self {
|
||||
Self { justification_sender, block_import_sender }
|
||||
}
|
||||
|
||||
pub fn close(&mut self) {
|
||||
self.justification_sender.close();
|
||||
self.block_import_sender.close();
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT> ImportQueueService<B> for BasicQueueHandle<B> {
|
||||
fn import_blocks(&mut self, origin: BlockOrigin, blocks: Vec<IncomingBlock<B>>) {
|
||||
if blocks.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
trace!(target: LOG_TARGET, "Scheduling {} blocks for import", blocks.len());
|
||||
let res = self
|
||||
.block_import_sender
|
||||
.unbounded_send(worker_messages::ImportBlocks(origin, blocks));
|
||||
|
||||
if res.is_err() {
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"import_blocks: Background import task is no longer alive"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn import_justifications(
|
||||
&mut self,
|
||||
who: RuntimeOrigin,
|
||||
hash: B::Hash,
|
||||
number: NumberFor<B>,
|
||||
justifications: Justifications,
|
||||
) {
|
||||
for justification in justifications {
|
||||
let res = self.justification_sender.unbounded_send(
|
||||
worker_messages::ImportJustification(who, hash, number, justification),
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"import_justification: Background import task is no longer alive"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B: BlockT> ImportQueue<B> for BasicQueue<B> {
|
||||
/// Get handle to [`ImportQueueService`].
|
||||
fn service(&self) -> Box<dyn ImportQueueService<B>> {
|
||||
Box::new(self.handle.clone())
|
||||
}
|
||||
|
||||
/// Get a reference to the handle to [`ImportQueueService`].
|
||||
fn service_ref(&mut self) -> &mut dyn ImportQueueService<B> {
|
||||
&mut self.handle
|
||||
}
|
||||
|
||||
/// Poll actions from network.
|
||||
fn poll_actions(&mut self, cx: &mut Context, link: &dyn Link<B>) {
|
||||
if self.result_port.poll_actions(cx, link).is_err() {
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"poll_actions: Background import task is no longer alive"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Start asynchronous runner for import queue.
|
||||
///
|
||||
/// Takes an object implementing [`Link`] which allows the import queue to
|
||||
/// influence the synchronization process.
|
||||
async fn run(mut self, link: &dyn Link<B>) {
|
||||
loop {
|
||||
if let Err(_) = self.result_port.next_action(link).await {
|
||||
log::error!(target: "sync", "poll_actions: Background import task is no longer alive");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Messages designated to the background worker.
|
||||
mod worker_messages {
|
||||
use super::*;
|
||||
|
||||
pub struct ImportBlocks<B: BlockT>(pub BlockOrigin, pub Vec<IncomingBlock<B>>);
|
||||
pub struct ImportJustification<B: BlockT>(
|
||||
pub RuntimeOrigin,
|
||||
pub B::Hash,
|
||||
pub NumberFor<B>,
|
||||
pub Justification,
|
||||
);
|
||||
}
|
||||
|
||||
/// The process of importing blocks.
|
||||
///
|
||||
/// This polls the `block_import_receiver` for new blocks to import and than awaits on
|
||||
/// importing these blocks. After each block is imported, this async function yields once
|
||||
/// to give other futures the possibility to be run.
|
||||
///
|
||||
/// Returns when `block_import` ended.
|
||||
async fn block_import_process<B: BlockT>(
|
||||
mut block_import: BoxBlockImport<B>,
|
||||
verifier: impl Verifier<B>,
|
||||
result_sender: BufferedLinkSender<B>,
|
||||
mut block_import_receiver: TracingUnboundedReceiver<worker_messages::ImportBlocks<B>>,
|
||||
metrics: Option<Metrics>,
|
||||
) {
|
||||
loop {
|
||||
let worker_messages::ImportBlocks(origin, blocks) = match block_import_receiver.next().await
|
||||
{
|
||||
Some(blocks) => blocks,
|
||||
None => {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Stopping block import because the import channel was closed!",
|
||||
);
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
let res =
|
||||
import_many_blocks(&mut block_import, origin, blocks, &verifier, metrics.clone()).await;
|
||||
|
||||
result_sender.blocks_processed(res.imported, res.block_count, res.results);
|
||||
}
|
||||
}
|
||||
|
||||
struct BlockImportWorker<B: BlockT> {
|
||||
result_sender: BufferedLinkSender<B>,
|
||||
justification_import: Option<BoxJustificationImport<B>>,
|
||||
metrics: Option<Metrics>,
|
||||
}
|
||||
|
||||
impl<B: BlockT> BlockImportWorker<B> {
|
||||
fn new<V>(
|
||||
result_sender: BufferedLinkSender<B>,
|
||||
verifier: V,
|
||||
block_import: BoxBlockImport<B>,
|
||||
justification_import: Option<BoxJustificationImport<B>>,
|
||||
metrics: Option<Metrics>,
|
||||
) -> (
|
||||
impl Future<Output = ()> + Send,
|
||||
TracingUnboundedSender<worker_messages::ImportJustification<B>>,
|
||||
TracingUnboundedSender<worker_messages::ImportBlocks<B>>,
|
||||
)
|
||||
where
|
||||
V: Verifier<B> + 'static,
|
||||
{
|
||||
use worker_messages::*;
|
||||
|
||||
let (justification_sender, mut justification_port) =
|
||||
tracing_unbounded("mpsc_import_queue_worker_justification", 100_000);
|
||||
|
||||
let (block_import_sender, block_import_receiver) =
|
||||
tracing_unbounded("mpsc_import_queue_worker_blocks", 100_000);
|
||||
|
||||
let mut worker = BlockImportWorker { result_sender, justification_import, metrics };
|
||||
|
||||
let future = async move {
|
||||
// Let's initialize `justification_import`
|
||||
if let Some(justification_import) = worker.justification_import.as_mut() {
|
||||
for (hash, number) in justification_import.on_start().await {
|
||||
worker.result_sender.request_justification(&hash, number);
|
||||
}
|
||||
}
|
||||
|
||||
let block_import_process = block_import_process(
|
||||
block_import,
|
||||
verifier,
|
||||
worker.result_sender.clone(),
|
||||
block_import_receiver,
|
||||
worker.metrics.clone(),
|
||||
);
|
||||
futures::pin_mut!(block_import_process);
|
||||
|
||||
loop {
|
||||
// If the results sender is closed, that means that the import queue is shutting
|
||||
// down and we should end this future.
|
||||
if worker.result_sender.is_closed() {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Stopping block import because result channel was closed!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure to first process all justifications
|
||||
while let Poll::Ready(justification) = futures::poll!(justification_port.next()) {
|
||||
match justification {
|
||||
Some(ImportJustification(who, hash, number, justification)) =>
|
||||
worker.import_justification(who, hash, number, justification).await,
|
||||
None => {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Stopping block import because justification channel was closed!",
|
||||
);
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if let Poll::Ready(()) = futures::poll!(&mut block_import_process) {
|
||||
return;
|
||||
}
|
||||
|
||||
// All futures that we polled are now pending.
|
||||
futures::pending!()
|
||||
}
|
||||
};
|
||||
|
||||
(future, justification_sender, block_import_sender)
|
||||
}
|
||||
|
||||
async fn import_justification(
|
||||
&mut self,
|
||||
who: RuntimeOrigin,
|
||||
hash: B::Hash,
|
||||
number: NumberFor<B>,
|
||||
justification: Justification,
|
||||
) {
|
||||
let started = std::time::Instant::now();
|
||||
|
||||
let import_result = match self.justification_import.as_mut() {
|
||||
Some(justification_import) => {
|
||||
let result = justification_import
|
||||
.import_justification(hash, number, justification)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Justification import failed for hash = {:?} with number = {:?} coming from node = {:?} with error: {}",
|
||||
hash,
|
||||
number,
|
||||
who,
|
||||
e,
|
||||
);
|
||||
e
|
||||
});
|
||||
match result {
|
||||
Ok(()) => JustificationImportResult::Success,
|
||||
Err(pezsp_consensus::Error::OutdatedJustification) =>
|
||||
JustificationImportResult::OutdatedJustification,
|
||||
Err(_) => JustificationImportResult::Failure,
|
||||
}
|
||||
},
|
||||
None => JustificationImportResult::Failure,
|
||||
};
|
||||
|
||||
if let Some(metrics) = self.metrics.as_ref() {
|
||||
metrics.justification_import_time.observe(started.elapsed().as_secs_f64());
|
||||
}
|
||||
|
||||
self.result_sender.justification_imported(who, &hash, number, import_result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of [`import_many_blocks`].
|
||||
struct ImportManyBlocksResult<B: BlockT> {
|
||||
/// The number of blocks imported successfully.
|
||||
imported: usize,
|
||||
/// The total number of blocks processed.
|
||||
block_count: usize,
|
||||
/// The import results for each block.
|
||||
results: Vec<(Result<BlockImportStatus<NumberFor<B>>, BlockImportError>, B::Hash)>,
|
||||
}
|
||||
|
||||
/// Import several blocks at once, returning import result for each block.
|
||||
///
|
||||
/// This will yield after each imported block once, to ensure that other futures can
|
||||
/// be called as well.
|
||||
async fn import_many_blocks<B: BlockT, V: Verifier<B>>(
|
||||
import_handle: &mut BoxBlockImport<B>,
|
||||
blocks_origin: BlockOrigin,
|
||||
blocks: Vec<IncomingBlock<B>>,
|
||||
verifier: &V,
|
||||
metrics: Option<Metrics>,
|
||||
) -> ImportManyBlocksResult<B> {
|
||||
let count = blocks.len();
|
||||
|
||||
let blocks_range = match (
|
||||
blocks.first().and_then(|b| b.header.as_ref().map(|h| h.number())),
|
||||
blocks.last().and_then(|b| b.header.as_ref().map(|h| h.number())),
|
||||
) {
|
||||
(Some(first), Some(last)) if first != last => format!(" ({}..{})", first, last),
|
||||
(Some(first), Some(_)) => format!(" ({})", first),
|
||||
_ => Default::default(),
|
||||
};
|
||||
|
||||
trace!(target: LOG_TARGET, "Starting import of {} blocks {}", count, blocks_range);
|
||||
|
||||
let mut imported = 0;
|
||||
let mut results = vec![];
|
||||
let mut has_error = false;
|
||||
let mut blocks = blocks.into_iter();
|
||||
|
||||
// Blocks in the response/drain should be in ascending order.
|
||||
loop {
|
||||
// Is there any block left to import?
|
||||
let block = match blocks.next() {
|
||||
Some(b) => b,
|
||||
None => {
|
||||
// No block left to import, success!
|
||||
return ImportManyBlocksResult { block_count: count, imported, results };
|
||||
},
|
||||
};
|
||||
|
||||
let block_number = block.header.as_ref().map(|h| *h.number());
|
||||
let block_hash = block.hash;
|
||||
let import_result = if has_error {
|
||||
Err(BlockImportError::Cancelled)
|
||||
} else {
|
||||
let verification_fut = verify_single_block_metered(
|
||||
import_handle,
|
||||
blocks_origin,
|
||||
block,
|
||||
verifier,
|
||||
metrics.as_ref(),
|
||||
);
|
||||
match verification_fut.await {
|
||||
Ok(SingleBlockVerificationOutcome::Imported(import_status)) => Ok(import_status),
|
||||
Ok(SingleBlockVerificationOutcome::Verified(import_parameters)) => {
|
||||
// The actual import.
|
||||
import_single_block_metered(import_handle, import_parameters, metrics.as_ref())
|
||||
.await
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(metrics) = metrics.as_ref() {
|
||||
metrics.report_import::<B>(&import_result);
|
||||
}
|
||||
|
||||
if import_result.is_ok() {
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"Block imported successfully {:?} ({})",
|
||||
block_number,
|
||||
block_hash,
|
||||
);
|
||||
imported += 1;
|
||||
} else {
|
||||
has_error = true;
|
||||
}
|
||||
|
||||
results.push((import_result, block_hash));
|
||||
|
||||
Yield::new().await
|
||||
}
|
||||
}
|
||||
|
||||
/// A future that will always `yield` on the first call of `poll` but schedules the
|
||||
/// current task for re-execution.
|
||||
///
|
||||
/// This is done by getting the waker and calling `wake_by_ref` followed by returning
|
||||
/// `Pending`. The next time the `poll` is called, it will return `Ready`.
|
||||
struct Yield(bool);
|
||||
|
||||
impl Yield {
|
||||
fn new() -> Self {
|
||||
Self(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for Yield {
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
|
||||
if !self.0 {
|
||||
self.0 = true;
|
||||
cx.waker().wake_by_ref();
|
||||
Poll::Pending
|
||||
} else {
|
||||
Poll::Ready(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
block_import::{
|
||||
BlockCheckParams, BlockImport, BlockImportParams, ImportResult, JustificationImport,
|
||||
},
|
||||
import_queue::Verifier,
|
||||
};
|
||||
use futures::{executor::block_on, Future};
|
||||
use parking_lot::Mutex;
|
||||
use pezsp_test_primitives::{Block, BlockNumber, Hash, Header};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Verifier<Block> for () {
|
||||
async fn verify(
|
||||
&self,
|
||||
block: BlockImportParams<Block>,
|
||||
) -> Result<BlockImportParams<Block>, String> {
|
||||
Ok(BlockImportParams::new(block.origin, block.header))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl BlockImport<Block> for () {
|
||||
type Error = pezsp_consensus::Error;
|
||||
|
||||
async fn check_block(
|
||||
&self,
|
||||
_block: BlockCheckParams<Block>,
|
||||
) -> Result<ImportResult, Self::Error> {
|
||||
Ok(ImportResult::imported(false))
|
||||
}
|
||||
|
||||
async fn import_block(
|
||||
&self,
|
||||
_block: BlockImportParams<Block>,
|
||||
) -> Result<ImportResult, Self::Error> {
|
||||
Ok(ImportResult::imported(true))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl JustificationImport<Block> for () {
|
||||
type Error = pezsp_consensus::Error;
|
||||
|
||||
async fn on_start(&mut self) -> Vec<(Hash, BlockNumber)> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
async fn import_justification(
|
||||
&mut self,
|
||||
_hash: Hash,
|
||||
_number: BlockNumber,
|
||||
_justification: Justification,
|
||||
) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Event {
|
||||
JustificationImported(Hash),
|
||||
BlockImported(Hash),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TestLink {
|
||||
events: Mutex<Vec<Event>>,
|
||||
}
|
||||
|
||||
impl Link<Block> for TestLink {
|
||||
fn blocks_processed(
|
||||
&self,
|
||||
_imported: usize,
|
||||
_count: usize,
|
||||
results: Vec<(Result<BlockImportStatus<BlockNumber>, BlockImportError>, Hash)>,
|
||||
) {
|
||||
if let Some(hash) = results.into_iter().find_map(|(r, h)| r.ok().map(|_| h)) {
|
||||
self.events.lock().push(Event::BlockImported(hash));
|
||||
}
|
||||
}
|
||||
|
||||
fn justification_imported(
|
||||
&self,
|
||||
_who: RuntimeOrigin,
|
||||
hash: &Hash,
|
||||
_number: BlockNumber,
|
||||
_import_result: JustificationImportResult,
|
||||
) {
|
||||
self.events.lock().push(Event::JustificationImported(*hash))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prioritizes_finality_work_over_block_import() {
|
||||
let (result_sender, mut result_port) = buffered_link::buffered_link(100_000);
|
||||
|
||||
let (worker, finality_sender, block_import_sender) =
|
||||
BlockImportWorker::new(result_sender, (), Box::new(()), Some(Box::new(())), None);
|
||||
futures::pin_mut!(worker);
|
||||
|
||||
let import_block = |n| {
|
||||
let header = Header {
|
||||
parent_hash: Hash::random(),
|
||||
number: n,
|
||||
extrinsics_root: Hash::random(),
|
||||
state_root: Default::default(),
|
||||
digest: Default::default(),
|
||||
};
|
||||
|
||||
let hash = header.hash();
|
||||
|
||||
block_import_sender
|
||||
.unbounded_send(worker_messages::ImportBlocks(
|
||||
BlockOrigin::Own,
|
||||
vec![IncomingBlock {
|
||||
hash,
|
||||
header: Some(header),
|
||||
body: None,
|
||||
indexed_body: None,
|
||||
justifications: None,
|
||||
origin: None,
|
||||
allow_missing_state: false,
|
||||
import_existing: false,
|
||||
state: None,
|
||||
skip_execution: false,
|
||||
}],
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
hash
|
||||
};
|
||||
|
||||
let import_justification = || {
|
||||
let hash = Hash::random();
|
||||
finality_sender
|
||||
.unbounded_send(worker_messages::ImportJustification(
|
||||
pezsc_network_types::PeerId::random(),
|
||||
hash,
|
||||
1,
|
||||
(*b"TEST", Vec::new()),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
hash
|
||||
};
|
||||
|
||||
let link = TestLink::default();
|
||||
|
||||
// we send a bunch of tasks to the worker
|
||||
let block1 = import_block(1);
|
||||
let block2 = import_block(2);
|
||||
let block3 = import_block(3);
|
||||
let justification1 = import_justification();
|
||||
let justification2 = import_justification();
|
||||
let block4 = import_block(4);
|
||||
let block5 = import_block(5);
|
||||
let block6 = import_block(6);
|
||||
let justification3 = import_justification();
|
||||
|
||||
// we poll the worker until we have processed 9 events
|
||||
block_on(futures::future::poll_fn(|cx| {
|
||||
while link.events.lock().len() < 9 {
|
||||
match Future::poll(Pin::new(&mut worker), cx) {
|
||||
Poll::Pending => {},
|
||||
Poll::Ready(()) => panic!("import queue worker should not conclude."),
|
||||
}
|
||||
|
||||
result_port.poll_actions(cx, &link).unwrap();
|
||||
}
|
||||
|
||||
Poll::Ready(())
|
||||
}));
|
||||
|
||||
// all justification tasks must be done before any block import work
|
||||
assert_eq!(
|
||||
&*link.events.lock(),
|
||||
&[
|
||||
Event::JustificationImported(justification1),
|
||||
Event::JustificationImported(justification2),
|
||||
Event::JustificationImported(justification3),
|
||||
Event::BlockImported(block1),
|
||||
Event::BlockImported(block2),
|
||||
Event::BlockImported(block3),
|
||||
Event::BlockImported(block4),
|
||||
Event::BlockImported(block5),
|
||||
Event::BlockImported(block6),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Provides the `buffered_link` utility.
|
||||
//!
|
||||
//! The buffered link is a channel that allows buffering the method calls on `Link`.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use pezsc_consensus::import_queue::Link;
|
||||
//! # use pezsc_consensus::import_queue::buffered_link::buffered_link;
|
||||
//! # use pezsp_test_primitives::Block;
|
||||
//! # struct DummyLink; impl Link<Block> for DummyLink {}
|
||||
//! # let my_link = DummyLink;
|
||||
//! let (mut tx, mut rx) = buffered_link::<Block>(100_000);
|
||||
//! tx.blocks_processed(0, 0, vec![]);
|
||||
//!
|
||||
//! // Calls `my_link.blocks_processed(0, 0, vec![])` when polled.
|
||||
//! let _fut = futures::future::poll_fn(move |cx| {
|
||||
//! rx.poll_actions(cx, &my_link).unwrap();
|
||||
//! std::task::Poll::Pending::<()>
|
||||
//! });
|
||||
//! ```
|
||||
|
||||
use crate::import_queue::{JustificationImportResult, Link, RuntimeOrigin};
|
||||
use futures::prelude::*;
|
||||
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use super::BlockImportResult;
|
||||
|
||||
/// Wraps around an unbounded channel from the `futures` crate. The sender implements `Link` and
|
||||
/// can be used to buffer commands, and the receiver can be used to poll said commands and transfer
|
||||
/// them to another link. `queue_size_warning` sets the warning threshold of the channel queue size.
|
||||
pub fn buffered_link<B: BlockT>(
|
||||
queue_size_warning: usize,
|
||||
) -> (BufferedLinkSender<B>, BufferedLinkReceiver<B>) {
|
||||
let (tx, rx) = tracing_unbounded("mpsc_buffered_link", queue_size_warning);
|
||||
let tx = BufferedLinkSender { tx };
|
||||
let rx = BufferedLinkReceiver { rx: rx.fuse() };
|
||||
(tx, rx)
|
||||
}
|
||||
|
||||
/// See [`buffered_link`].
|
||||
pub struct BufferedLinkSender<B: BlockT> {
|
||||
tx: TracingUnboundedSender<BlockImportWorkerMsg<B>>,
|
||||
}
|
||||
|
||||
impl<B: BlockT> BufferedLinkSender<B> {
|
||||
/// Returns true if the sender points to nowhere.
|
||||
///
|
||||
/// Once `true` is returned, it is pointless to use the sender anymore.
|
||||
pub fn is_closed(&self) -> bool {
|
||||
self.tx.is_closed()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT> Clone for BufferedLinkSender<B> {
|
||||
fn clone(&self) -> Self {
|
||||
BufferedLinkSender { tx: self.tx.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal buffered message.
|
||||
pub enum BlockImportWorkerMsg<B: BlockT> {
|
||||
BlocksProcessed(usize, usize, Vec<(BlockImportResult<B>, B::Hash)>),
|
||||
JustificationImported(RuntimeOrigin, B::Hash, NumberFor<B>, JustificationImportResult),
|
||||
RequestJustification(B::Hash, NumberFor<B>),
|
||||
}
|
||||
|
||||
impl<B: BlockT> Link<B> for BufferedLinkSender<B> {
|
||||
fn blocks_processed(
|
||||
&self,
|
||||
imported: usize,
|
||||
count: usize,
|
||||
results: Vec<(BlockImportResult<B>, B::Hash)>,
|
||||
) {
|
||||
let _ = self
|
||||
.tx
|
||||
.unbounded_send(BlockImportWorkerMsg::BlocksProcessed(imported, count, results));
|
||||
}
|
||||
|
||||
fn justification_imported(
|
||||
&self,
|
||||
who: RuntimeOrigin,
|
||||
hash: &B::Hash,
|
||||
number: NumberFor<B>,
|
||||
import_result: JustificationImportResult,
|
||||
) {
|
||||
let msg = BlockImportWorkerMsg::JustificationImported(who, *hash, number, import_result);
|
||||
let _ = self.tx.unbounded_send(msg);
|
||||
}
|
||||
|
||||
fn request_justification(&self, hash: &B::Hash, number: NumberFor<B>) {
|
||||
let _ = self
|
||||
.tx
|
||||
.unbounded_send(BlockImportWorkerMsg::RequestJustification(*hash, number));
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`buffered_link`].
|
||||
pub struct BufferedLinkReceiver<B: BlockT> {
|
||||
rx: stream::Fuse<TracingUnboundedReceiver<BlockImportWorkerMsg<B>>>,
|
||||
}
|
||||
|
||||
impl<B: BlockT> BufferedLinkReceiver<B> {
|
||||
/// Send action for the synchronization to perform.
|
||||
pub fn send_actions(&mut self, msg: BlockImportWorkerMsg<B>, link: &dyn Link<B>) {
|
||||
match msg {
|
||||
BlockImportWorkerMsg::BlocksProcessed(imported, count, results) =>
|
||||
link.blocks_processed(imported, count, results),
|
||||
BlockImportWorkerMsg::JustificationImported(who, hash, number, import_result) =>
|
||||
link.justification_imported(who, &hash, number, import_result),
|
||||
BlockImportWorkerMsg::RequestJustification(hash, number) =>
|
||||
link.request_justification(&hash, number),
|
||||
}
|
||||
}
|
||||
|
||||
/// Polls for the buffered link actions. Any enqueued action will be propagated to the link
|
||||
/// passed as parameter.
|
||||
///
|
||||
/// This method should behave in a way similar to `Future::poll`. It can register the current
|
||||
/// task and notify later when more actions are ready to be polled. To continue the comparison,
|
||||
/// it is as if this method always returned `Poll::Pending`.
|
||||
///
|
||||
/// Returns an error if the corresponding [`BufferedLinkSender`] has been closed.
|
||||
pub fn poll_actions(&mut self, cx: &mut Context, link: &dyn Link<B>) -> Result<(), ()> {
|
||||
loop {
|
||||
let msg = match Stream::poll_next(Pin::new(&mut self.rx), cx) {
|
||||
Poll::Ready(Some(msg)) => msg,
|
||||
Poll::Ready(None) => break Err(()),
|
||||
Poll::Pending => break Ok(()),
|
||||
};
|
||||
|
||||
self.send_actions(msg, link);
|
||||
}
|
||||
}
|
||||
|
||||
/// Poll next element from import queue and send the corresponding action command over the link.
|
||||
pub async fn next_action(&mut self, link: &dyn Link<B>) -> Result<(), ()> {
|
||||
if let Some(msg) = self.rx.next().await {
|
||||
self.send_actions(msg, link);
|
||||
return Ok(());
|
||||
}
|
||||
Err(())
|
||||
}
|
||||
|
||||
/// Close the channel.
|
||||
pub fn close(&mut self) -> bool {
|
||||
self.rx.get_mut().close()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pezsp_test_primitives::Block;
|
||||
|
||||
#[test]
|
||||
fn is_closed() {
|
||||
let (tx, rx) = super::buffered_link::<Block>(1);
|
||||
assert!(!tx.is_closed());
|
||||
drop(rx);
|
||||
assert!(tx.is_closed());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use super::*;
|
||||
|
||||
mockall::mock! {
|
||||
pub ImportQueueHandle<B: BlockT> {}
|
||||
|
||||
impl<B: BlockT> ImportQueueService<B> for ImportQueueHandle<B> {
|
||||
fn import_blocks(&mut self, origin: BlockOrigin, blocks: Vec<IncomingBlock<B>>);
|
||||
fn import_justifications(
|
||||
&mut self,
|
||||
who: RuntimeOrigin,
|
||||
hash: B::Hash,
|
||||
number: NumberFor<B>,
|
||||
justifications: Justifications,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mockall::mock! {
|
||||
pub ImportQueue<B: BlockT> {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B: BlockT> ImportQueue<B> for ImportQueue<B> {
|
||||
fn service(&self) -> Box<dyn ImportQueueService<B>>;
|
||||
fn service_ref(&mut self) -> &mut dyn ImportQueueService<B>;
|
||||
fn poll_actions<'a>(&mut self, cx: &mut futures::task::Context<'a>, link: &dyn Link<B>);
|
||||
async fn run(self, link: &'__mockall_link dyn Link<B>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Collection of common consensus specific implementations
|
||||
|
||||
pub mod block_import;
|
||||
pub mod import_queue;
|
||||
pub mod metrics;
|
||||
|
||||
pub use block_import::{
|
||||
BlockCheckParams, BlockImport, BlockImportParams, ForkChoiceStrategy, ImportResult,
|
||||
ImportedAux, ImportedState, JustificationImport, JustificationSyncLink, StateAction,
|
||||
StorageChanges,
|
||||
};
|
||||
pub use import_queue::{
|
||||
import_single_block, BasicQueue, BlockImportError, BlockImportStatus, BoxBlockImport,
|
||||
BoxJustificationImport, DefaultImportQueue, ImportQueue, IncomingBlock,
|
||||
JustificationImportResult, Link, Verifier,
|
||||
};
|
||||
|
||||
mod longest_chain;
|
||||
|
||||
pub mod shared_data;
|
||||
|
||||
pub use longest_chain::LongestChain;
|
||||
@@ -0,0 +1,157 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Longest chain implementation
|
||||
|
||||
use pezsc_client_api::backend;
|
||||
use pezsp_blockchain::{Backend, HeaderBackend};
|
||||
use pezsp_consensus::{Error as ConsensusError, SelectChain};
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header, NumberFor};
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
|
||||
/// Implement Longest Chain Select implementation
|
||||
/// where 'longest' is defined as the highest number of blocks
|
||||
pub struct LongestChain<B, Block> {
|
||||
backend: Arc<B>,
|
||||
_phantom: PhantomData<Block>,
|
||||
}
|
||||
|
||||
impl<B, Block> Clone for LongestChain<B, Block> {
|
||||
fn clone(&self) -> Self {
|
||||
let backend = self.backend.clone();
|
||||
LongestChain { backend, _phantom: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, Block> LongestChain<B, Block>
|
||||
where
|
||||
B: backend::Backend<Block>,
|
||||
Block: BlockT,
|
||||
{
|
||||
/// Instantiate a new LongestChain for Backend B
|
||||
pub fn new(backend: Arc<B>) -> Self {
|
||||
LongestChain { backend, _phantom: Default::default() }
|
||||
}
|
||||
|
||||
fn best_hash(&self) -> pezsp_blockchain::Result<<Block as BlockT>::Hash> {
|
||||
let info = self.backend.blockchain().info();
|
||||
let import_lock = self.backend.get_import_lock();
|
||||
let best_hash = self
|
||||
.backend
|
||||
.blockchain()
|
||||
.longest_containing(info.best_hash, import_lock)?
|
||||
.unwrap_or(info.best_hash);
|
||||
Ok(best_hash)
|
||||
}
|
||||
|
||||
fn best_header(&self) -> pezsp_blockchain::Result<<Block as BlockT>::Header> {
|
||||
let best_hash = self.best_hash()?;
|
||||
Ok(self
|
||||
.backend
|
||||
.blockchain()
|
||||
.header(best_hash)?
|
||||
.expect("given block hash was fetched from block in db; qed"))
|
||||
}
|
||||
|
||||
/// Returns the highest descendant of the given block that is a valid
|
||||
/// candidate to be finalized.
|
||||
///
|
||||
/// In this context, being a valid target means being an ancestor of
|
||||
/// the best chain according to the `best_header` method.
|
||||
///
|
||||
/// If `maybe_max_number` is `Some(max_block_number)` the search is
|
||||
/// limited to block `number <= max_block_number`. In other words
|
||||
/// as if there were no blocks greater than `max_block_number`.
|
||||
fn finality_target(
|
||||
&self,
|
||||
base_hash: Block::Hash,
|
||||
maybe_max_number: Option<NumberFor<Block>>,
|
||||
) -> pezsp_blockchain::Result<Block::Hash> {
|
||||
use pezsp_blockchain::Error::{Application, MissingHeader};
|
||||
let blockchain = self.backend.blockchain();
|
||||
|
||||
let mut current_head = self.best_header()?;
|
||||
let mut best_hash = current_head.hash();
|
||||
|
||||
let base_header = blockchain
|
||||
.header(base_hash)?
|
||||
.ok_or_else(|| MissingHeader(base_hash.to_string()))?;
|
||||
let base_number = *base_header.number();
|
||||
|
||||
if let Some(max_number) = maybe_max_number {
|
||||
if max_number < base_number {
|
||||
let msg = format!(
|
||||
"Requested a finality target using max number {} below the base number {}",
|
||||
max_number, base_number
|
||||
);
|
||||
return Err(Application(msg.into()));
|
||||
}
|
||||
|
||||
while current_head.number() > &max_number {
|
||||
best_hash = *current_head.parent_hash();
|
||||
current_head = blockchain
|
||||
.header(best_hash)?
|
||||
.ok_or_else(|| MissingHeader(format!("{best_hash:?}")))?;
|
||||
}
|
||||
}
|
||||
|
||||
while current_head.hash() != base_hash {
|
||||
if *current_head.number() < base_number {
|
||||
let msg = format!(
|
||||
"Requested a finality target using a base {:?} not in the best chain {:?}",
|
||||
base_hash, best_hash,
|
||||
);
|
||||
return Err(Application(msg.into()));
|
||||
}
|
||||
let current_hash = *current_head.parent_hash();
|
||||
current_head = blockchain
|
||||
.header(current_hash)?
|
||||
.ok_or_else(|| MissingHeader(format!("{best_hash:?}")))?;
|
||||
}
|
||||
|
||||
Ok(best_hash)
|
||||
}
|
||||
|
||||
fn leaves(&self) -> Result<Vec<<Block as BlockT>::Hash>, pezsp_blockchain::Error> {
|
||||
self.backend.blockchain().leaves()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B, Block> SelectChain<Block> for LongestChain<B, Block>
|
||||
where
|
||||
B: backend::Backend<Block>,
|
||||
Block: BlockT,
|
||||
{
|
||||
async fn leaves(&self) -> Result<Vec<<Block as BlockT>::Hash>, ConsensusError> {
|
||||
LongestChain::leaves(self).map_err(|e| ConsensusError::ChainLookup(e.to_string()))
|
||||
}
|
||||
|
||||
async fn best_chain(&self) -> Result<<Block as BlockT>::Header, ConsensusError> {
|
||||
LongestChain::best_header(self).map_err(|e| ConsensusError::ChainLookup(e.to_string()))
|
||||
}
|
||||
|
||||
async fn finality_target(
|
||||
&self,
|
||||
base_hash: Block::Hash,
|
||||
maybe_max_number: Option<NumberFor<Block>>,
|
||||
) -> Result<Block::Hash, ConsensusError> {
|
||||
LongestChain::finality_target(self, base_hash, maybe_max_number)
|
||||
.map_err(|e| ConsensusError::ChainLookup(e.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Metering tools for consensus
|
||||
|
||||
use prometheus_endpoint::{
|
||||
register, CounterVec, Histogram, HistogramOpts, HistogramVec, Opts, PrometheusError, Registry,
|
||||
U64,
|
||||
};
|
||||
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
|
||||
|
||||
use crate::import_queue::{BlockImportError, BlockImportStatus};
|
||||
|
||||
/// Generic Prometheus metrics for common consensus functionality.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Metrics {
|
||||
pub import_queue_processed: CounterVec<U64>,
|
||||
pub block_verification_time: HistogramVec,
|
||||
pub block_verification_and_import_time: Histogram,
|
||||
pub justification_import_time: Histogram,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
pub(crate) fn register(registry: &Registry) -> Result<Self, PrometheusError> {
|
||||
Ok(Self {
|
||||
import_queue_processed: register(
|
||||
CounterVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_import_queue_processed_total",
|
||||
"Blocks processed by import queue",
|
||||
),
|
||||
&["result"], // 'success or failure
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
block_verification_time: register(
|
||||
HistogramVec::new(
|
||||
HistogramOpts::new(
|
||||
"bizinikiwi_block_verification_time",
|
||||
"Time taken to verify blocks",
|
||||
),
|
||||
&["result"],
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
block_verification_and_import_time: register(
|
||||
Histogram::with_opts(HistogramOpts::new(
|
||||
"bizinikiwi_block_verification_and_import_time",
|
||||
"Time taken to verify and import blocks",
|
||||
))?,
|
||||
registry,
|
||||
)?,
|
||||
justification_import_time: register(
|
||||
Histogram::with_opts(HistogramOpts::new(
|
||||
"bizinikiwi_justification_import_time",
|
||||
"Time taken to import justifications",
|
||||
))?,
|
||||
registry,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn report_import<B: BlockT>(
|
||||
&self,
|
||||
result: &Result<BlockImportStatus<NumberFor<B>>, BlockImportError>,
|
||||
) {
|
||||
let label = match result {
|
||||
Ok(_) => "success",
|
||||
Err(BlockImportError::IncompleteHeader(_)) => "incomplete_header",
|
||||
Err(BlockImportError::VerificationFailed(_, _)) => "verification_failed",
|
||||
Err(BlockImportError::BadBlock(_)) => "bad_block",
|
||||
Err(BlockImportError::MissingState) => "missing_state",
|
||||
Err(BlockImportError::UnknownParent) => "unknown_parent",
|
||||
Err(BlockImportError::Cancelled) => "cancelled",
|
||||
Err(BlockImportError::Other(_)) => "failed",
|
||||
};
|
||||
|
||||
self.import_queue_processed.with_label_values(&[label]).inc();
|
||||
}
|
||||
|
||||
pub fn report_verification(&self, success: bool, time: std::time::Duration) {
|
||||
self.block_verification_time
|
||||
.with_label_values(&[if success { "success" } else { "verification_failed" }])
|
||||
.observe(time.as_secs_f64());
|
||||
}
|
||||
|
||||
pub fn report_verification_and_import(&self, time: std::time::Duration) {
|
||||
self.block_verification_and_import_time.observe(time.as_secs_f64());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Provides a generic wrapper around shared data. See [`SharedData`] for more information.
|
||||
|
||||
use parking_lot::{Condvar, MappedMutexGuard, Mutex, MutexGuard};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Created by [`SharedDataLocked::release_mutex`].
|
||||
///
|
||||
/// As long as the object isn't dropped, the shared data is locked. It is advised to drop this
|
||||
/// object when the shared data doesn't need to be locked anymore. To get access to the shared data
|
||||
/// [`Self::upgrade`] is provided.
|
||||
#[must_use = "Shared data will be unlocked on drop!"]
|
||||
pub struct SharedDataLockedUpgradable<T> {
|
||||
shared_data: SharedData<T>,
|
||||
}
|
||||
|
||||
impl<T> SharedDataLockedUpgradable<T> {
|
||||
/// Upgrade to a *real* mutex guard that will give access to the inner data.
|
||||
///
|
||||
/// Every call to this function will reaquire the mutex again.
|
||||
pub fn upgrade(&mut self) -> MappedMutexGuard<'_, T> {
|
||||
MutexGuard::map(self.shared_data.inner.lock(), |i| &mut i.shared_data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for SharedDataLockedUpgradable<T> {
|
||||
fn drop(&mut self) {
|
||||
let mut inner = self.shared_data.inner.lock();
|
||||
// It should not be locked anymore
|
||||
inner.locked = false;
|
||||
|
||||
// Notify all waiting threads.
|
||||
self.shared_data.cond_var.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
/// Created by [`SharedData::shared_data_locked`].
|
||||
///
|
||||
/// As long as this object isn't dropped, the shared data is held in a mutex guard and the shared
|
||||
/// data is tagged as locked. Access to the shared data is provided through
|
||||
/// [`Deref`](std::ops::Deref) and [`DerefMut`](std::ops::DerefMut). The trick is to use
|
||||
/// [`Self::release_mutex`] to release the mutex, but still keep the shared data locked. This means
|
||||
/// every other thread trying to access the shared data in this time will need to wait until this
|
||||
/// lock is freed.
|
||||
///
|
||||
/// If this object is dropped without calling [`Self::release_mutex`], the lock will be dropped
|
||||
/// immediately.
|
||||
#[must_use = "Shared data will be unlocked on drop!"]
|
||||
pub struct SharedDataLocked<'a, T> {
|
||||
/// The current active mutex guard holding the inner data.
|
||||
inner: MutexGuard<'a, SharedDataInner<T>>,
|
||||
/// The [`SharedData`] instance that created this instance.
|
||||
///
|
||||
/// This instance is only taken on drop or when calling [`Self::release_mutex`].
|
||||
shared_data: Option<SharedData<T>>,
|
||||
}
|
||||
|
||||
impl<'a, T> SharedDataLocked<'a, T> {
|
||||
/// Release the mutex, but keep the shared data locked.
|
||||
pub fn release_mutex(mut self) -> SharedDataLockedUpgradable<T> {
|
||||
SharedDataLockedUpgradable {
|
||||
shared_data: self.shared_data.take().expect("`shared_data` is only taken on drop; qed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Drop for SharedDataLocked<'a, T> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(shared_data) = self.shared_data.take() {
|
||||
// If the `shared_data` is still set, it means [`Self::release_mutex`] wasn't
|
||||
// called and the lock should be released.
|
||||
self.inner.locked = false;
|
||||
|
||||
// Notify all waiting threads about the released lock.
|
||||
shared_data.cond_var.notify_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> std::ops::Deref for SharedDataLocked<'a, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner.shared_data
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> std::ops::DerefMut for SharedDataLocked<'a, T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner.shared_data
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds the shared data and if the shared data is currently locked.
|
||||
///
|
||||
/// For more information see [`SharedData`].
|
||||
struct SharedDataInner<T> {
|
||||
/// The actual shared data that is protected here against concurrent access.
|
||||
shared_data: T,
|
||||
/// Is `shared_data` currently locked and can not be accessed?
|
||||
locked: bool,
|
||||
}
|
||||
|
||||
/// Some shared data that provides support for locking this shared data for some time.
|
||||
///
|
||||
/// When working with consensus engines there is often data that needs to be shared between multiple
|
||||
/// parts of the system, like block production and block import. This struct provides an abstraction
|
||||
/// for this shared data in a generic way.
|
||||
///
|
||||
/// The pain point when sharing this data is often the usage of mutex guards in an async context as
|
||||
/// this doesn't work for most of them as these guards don't implement `Send`. This abstraction
|
||||
/// provides a way to lock the shared data, while not having the mutex locked. So, the data stays
|
||||
/// locked and we are still able to hold this lock over an `await` call.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use pezsc_consensus::shared_data::SharedData;
|
||||
///
|
||||
/// let shared_data = SharedData::new(String::from("hello world"));
|
||||
///
|
||||
/// let lock = shared_data.shared_data_locked();
|
||||
///
|
||||
/// let shared_data2 = shared_data.clone();
|
||||
/// let join_handle1 = std::thread::spawn(move || {
|
||||
/// // This will need to wait for the outer lock to be released before it can access the data.
|
||||
/// shared_data2.shared_data().push_str("1");
|
||||
/// });
|
||||
///
|
||||
/// assert_eq!(*lock, "hello world");
|
||||
///
|
||||
/// // Let us release the mutex, but we still keep it locked.
|
||||
/// // Now we could call `await` for example.
|
||||
/// let mut lock = lock.release_mutex();
|
||||
///
|
||||
/// let shared_data2 = shared_data.clone();
|
||||
/// let join_handle2 = std::thread::spawn(move || {
|
||||
/// shared_data2.shared_data().push_str("2");
|
||||
/// });
|
||||
///
|
||||
/// // We still have the lock and can upgrade it to access the data.
|
||||
/// assert_eq!(*lock.upgrade(), "hello world");
|
||||
/// lock.upgrade().push_str("3");
|
||||
///
|
||||
/// drop(lock);
|
||||
/// join_handle1.join().unwrap();
|
||||
/// join_handle2.join().unwrap();
|
||||
///
|
||||
/// let data = shared_data.shared_data();
|
||||
/// // As we don't know the order of the threads, we need to check for both combinations
|
||||
/// assert!(*data == "hello world321" || *data == "hello world312");
|
||||
/// ```
|
||||
///
|
||||
/// # Deadlock
|
||||
///
|
||||
/// Be aware that this data structure doesn't give you any guarantees that you can not create a
|
||||
/// deadlock. If you use [`release_mutex`](SharedDataLocked::release_mutex) followed by a call
|
||||
/// to [`shared_data`](Self::shared_data) in the same thread will make your program dead lock.
|
||||
/// The same applies when you are using a single threaded executor.
|
||||
pub struct SharedData<T> {
|
||||
inner: Arc<Mutex<SharedDataInner<T>>>,
|
||||
cond_var: Arc<Condvar>,
|
||||
}
|
||||
|
||||
impl<T> Clone for SharedData<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self { inner: self.inner.clone(), cond_var: self.cond_var.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SharedData<T> {
|
||||
/// Create a new instance of [`SharedData`] to share the given `shared_data`.
|
||||
pub fn new(shared_data: T) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(SharedDataInner { shared_data, locked: false })),
|
||||
cond_var: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquire access to the shared data.
|
||||
///
|
||||
/// This will give mutable access to the shared data. After the returned mutex guard is dropped,
|
||||
/// the shared data is accessible by other threads. So, this function should be used when
|
||||
/// reading/writing of the shared data in a local context is required.
|
||||
///
|
||||
/// When requiring to lock shared data for some longer time, even with temporarily releasing the
|
||||
/// lock, [`Self::shared_data_locked`] should be used.
|
||||
pub fn shared_data(&self) -> MappedMutexGuard<'_, T> {
|
||||
let mut guard = self.inner.lock();
|
||||
|
||||
while guard.locked {
|
||||
self.cond_var.wait(&mut guard);
|
||||
}
|
||||
|
||||
debug_assert!(!guard.locked);
|
||||
|
||||
MutexGuard::map(guard, |i| &mut i.shared_data)
|
||||
}
|
||||
|
||||
/// Acquire access to the shared data and lock it.
|
||||
///
|
||||
/// This will give mutable access to the shared data. The returned [`SharedDataLocked`]
|
||||
/// provides the function [`SharedDataLocked::release_mutex`] to release the mutex, but
|
||||
/// keeping the data locked. This is useful in async contexts for example where the data needs
|
||||
/// to be locked, but a mutex guard can not be held.
|
||||
///
|
||||
/// For an example see [`SharedData`].
|
||||
pub fn shared_data_locked(&self) -> SharedDataLocked<'_, T> {
|
||||
let mut guard = self.inner.lock();
|
||||
|
||||
while guard.locked {
|
||||
self.cond_var.wait(&mut guard);
|
||||
}
|
||||
|
||||
debug_assert!(!guard.locked);
|
||||
guard.locked = true;
|
||||
|
||||
SharedDataLocked { inner: guard, shared_data: Some(self.clone()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn shared_data_locking_works() {
|
||||
const THREADS: u32 = 100;
|
||||
let shared_data = SharedData::new(0u32);
|
||||
|
||||
let lock = shared_data.shared_data_locked();
|
||||
|
||||
for i in 0..THREADS {
|
||||
let data = shared_data.clone();
|
||||
std::thread::spawn(move || {
|
||||
if i % 2 == 1 {
|
||||
*data.shared_data() += 1;
|
||||
} else {
|
||||
let mut lock = data.shared_data_locked().release_mutex();
|
||||
// Give the other threads some time to wake up
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
*lock.upgrade() += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let lock = lock.release_mutex();
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
drop(lock);
|
||||
|
||||
while *shared_data.shared_data() < THREADS {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "pezsc-consensus-epochs"
|
||||
version = "0.33.0"
|
||||
authors.workspace = true
|
||||
description = "Generic epochs-based utilities for consensus"
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { features = ["derive"], workspace = true, default-features = true }
|
||||
fork-tree = { workspace = true, default-features = true }
|
||||
pezsc-client-api = { workspace = true, default-features = true }
|
||||
pezsc-consensus = { workspace = true, default-features = true }
|
||||
pezsp-blockchain = { workspace = true, default-features = true }
|
||||
pezsp-runtime = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezsc-client-api/runtime-benchmarks",
|
||||
"pezsc-consensus/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
Generic utilities for epoch-based consensus engines.
|
||||
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Migration types for epoch changes.
|
||||
|
||||
use crate::{Epoch, EpochChanges, PersistedEpoch, PersistedEpochHeader};
|
||||
use codec::{Decode, Encode};
|
||||
use fork_tree::ForkTree;
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Legacy definition of epoch changes.
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
pub struct EpochChangesV0<Hash, Number, E: Epoch> {
|
||||
inner: ForkTree<Hash, Number, PersistedEpoch<E>>,
|
||||
}
|
||||
|
||||
/// Legacy definition of epoch changes.
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
pub struct EpochChangesV1<Hash, Number, E: Epoch> {
|
||||
inner: ForkTree<Hash, Number, PersistedEpochHeader<E>>,
|
||||
epochs: BTreeMap<(Hash, Number), PersistedEpoch<E>>,
|
||||
}
|
||||
|
||||
/// Type alias for v0 definition of epoch changes.
|
||||
pub type EpochChangesV0For<Block, Epoch> =
|
||||
EpochChangesV0<<Block as BlockT>::Hash, NumberFor<Block>, Epoch>;
|
||||
/// Type alias for v1 and v2 definition of epoch changes.
|
||||
pub type EpochChangesV1For<Block, Epoch> =
|
||||
EpochChangesV1<<Block as BlockT>::Hash, NumberFor<Block>, Epoch>;
|
||||
|
||||
impl<Hash, Number, E: Epoch> EpochChangesV0<Hash, Number, E>
|
||||
where
|
||||
Hash: PartialEq + Ord + Copy,
|
||||
Number: Ord + Copy,
|
||||
{
|
||||
/// Create a new value of this type from raw.
|
||||
pub fn from_raw(inner: ForkTree<Hash, Number, PersistedEpoch<E>>) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
/// Migrate the type into current epoch changes definition.
|
||||
pub fn migrate(self) -> EpochChanges<Hash, Number, E> {
|
||||
let mut epochs = BTreeMap::new();
|
||||
|
||||
let inner = self.inner.map(&mut |hash, number, data| {
|
||||
let header = PersistedEpochHeader::from(&data);
|
||||
epochs.insert((*hash, *number), data);
|
||||
header
|
||||
});
|
||||
|
||||
EpochChanges { inner, epochs }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Hash, Number, E: Epoch> EpochChangesV1<Hash, Number, E>
|
||||
where
|
||||
Hash: PartialEq + Ord + Copy,
|
||||
Number: Ord + Copy,
|
||||
{
|
||||
/// Migrate the type into current epoch changes definition.
|
||||
pub fn migrate(self) -> EpochChanges<Hash, Number, E> {
|
||||
EpochChanges { inner: self.inner, epochs: self.epochs }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
[package]
|
||||
name = "pezsc-consensus-grandpa"
|
||||
version = "0.19.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Integration of the GRANDPA finality gadget into bizinikiwi."
|
||||
documentation = "https://docs.rs/pezsc-consensus-grandpa"
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
ahash = { workspace = true }
|
||||
array-bytes = { workspace = true, default-features = true }
|
||||
async-trait = { workspace = true }
|
||||
codec = { features = ["derive"], workspace = true, default-features = true }
|
||||
dyn-clone = { workspace = true }
|
||||
finality-grandpa = { features = [
|
||||
"derive-codec",
|
||||
], workspace = true, default-features = true }
|
||||
fork-tree = { workspace = true, default-features = true }
|
||||
futures = { workspace = true }
|
||||
futures-timer = { workspace = true }
|
||||
log = { workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
prometheus-endpoint = { workspace = true, default-features = true }
|
||||
rand = { workspace = true, default-features = true }
|
||||
pezsc-block-builder = { workspace = true, default-features = true }
|
||||
pezsc-chain-spec = { workspace = true, default-features = true }
|
||||
pezsc-client-api = { workspace = true, default-features = true }
|
||||
pezsc-consensus = { workspace = true, default-features = true }
|
||||
pezsc-network = { workspace = true, default-features = true }
|
||||
pezsc-network-common = { workspace = true, default-features = true }
|
||||
pezsc-network-gossip = { workspace = true, default-features = true }
|
||||
pezsc-network-sync = { workspace = true, default-features = true }
|
||||
pezsc-network-types = { workspace = true, default-features = true }
|
||||
pezsc-telemetry = { workspace = true, default-features = true }
|
||||
pezsc-transaction-pool-api = { workspace = true, default-features = true }
|
||||
pezsc-utils = { workspace = true, default-features = true }
|
||||
serde_json = { workspace = true, default-features = true }
|
||||
pezsp-api = { workspace = true, default-features = true }
|
||||
pezsp-application-crypto = { workspace = true, default-features = true }
|
||||
pezsp-arithmetic = { workspace = true, default-features = true }
|
||||
pezsp-blockchain = { workspace = true, default-features = true }
|
||||
pezsp-consensus = { workspace = true, default-features = true }
|
||||
pezsp-consensus-grandpa = { workspace = true, default-features = true }
|
||||
pezsp-core = { workspace = true, default-features = true }
|
||||
pezsp-crypto-hashing = { workspace = true, default-features = true }
|
||||
pezsp-keystore = { workspace = true, default-features = true }
|
||||
pezsp-runtime = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
finality-grandpa = { features = [
|
||||
"derive-codec",
|
||||
"test-helpers",
|
||||
], workspace = true, default-features = true }
|
||||
pezsc-network-test = { workspace = true }
|
||||
pezsp-keyring = { workspace = true, default-features = true }
|
||||
pezsp-tracing = { workspace = true, default-features = true }
|
||||
bizinikiwi-test-runtime-client = { workspace = true }
|
||||
tokio = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezsc-block-builder/runtime-benchmarks",
|
||||
"pezsc-chain-spec/runtime-benchmarks",
|
||||
"pezsc-client-api/runtime-benchmarks",
|
||||
"pezsc-consensus/runtime-benchmarks",
|
||||
"pezsc-network-common/runtime-benchmarks",
|
||||
"pezsc-network-gossip/runtime-benchmarks",
|
||||
"pezsc-network-sync/runtime-benchmarks",
|
||||
"pezsc-network-test/runtime-benchmarks",
|
||||
"pezsc-network/runtime-benchmarks",
|
||||
"pezsc-transaction-pool-api/runtime-benchmarks",
|
||||
"pezsp-api/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus-grandpa/runtime-benchmarks",
|
||||
"pezsp-consensus/runtime-benchmarks",
|
||||
"pezsp-keyring/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"bizinikiwi-test-runtime-client/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
Integration of the GRANDPA finality gadget into Bizinikiwi.
|
||||
|
||||
This crate is unstable and the API and usage may change.
|
||||
|
||||
This crate provides a long-running future that produces finality notifications.
|
||||
|
||||
# Usage
|
||||
|
||||
First, create a block-import wrapper with the `block_import` function. The
|
||||
GRANDPA worker needs to be linked together with this block import object, so
|
||||
a `LinkHalf` is returned as well. All blocks imported (from network or
|
||||
consensus or otherwise) must pass through this wrapper, otherwise consensus
|
||||
is likely to break in unexpected ways.
|
||||
|
||||
Next, use the `LinkHalf` and a local configuration to `run_grandpa_voter`.
|
||||
This requires a `Network` implementation. The returned future should be
|
||||
driven to completion and will finalize blocks in the background.
|
||||
|
||||
# Changing authority sets
|
||||
|
||||
The rough idea behind changing authority sets in GRANDPA is that at some point,
|
||||
we obtain agreement for some maximum block height that the current set can
|
||||
finalize, and once a block with that height is finalized the next set will
|
||||
pick up finalization from there.
|
||||
|
||||
Technically speaking, this would be implemented as a voting rule which says,
|
||||
"if there is a signal for a change in N blocks in block B, only vote on
|
||||
chains with length NUM(B) + N if they contain B". This conditional-inclusion
|
||||
logic is complex to compute because it requires looking arbitrarily far
|
||||
back in the chain.
|
||||
|
||||
Instead, we keep track of a list of all signals we've seen so far (across
|
||||
all forks), sorted ascending by the block number they would be applied at.
|
||||
We never vote on chains with number higher than the earliest handoff block
|
||||
number (this is num(signal) + N). When finalizing a block, we either apply
|
||||
or prune any signaled changes based on whether the signaling block is
|
||||
included in the newly-finalized chain.
|
||||
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,57 @@
|
||||
[package]
|
||||
name = "pezsc-consensus-grandpa-rpc"
|
||||
version = "0.19.0"
|
||||
authors.workspace = true
|
||||
description = "RPC extensions for the GRANDPA finality gadget"
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
readme = "README.md"
|
||||
homepage.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codec = { features = ["derive"], workspace = true, default-features = true }
|
||||
finality-grandpa = { features = [
|
||||
"derive-codec",
|
||||
], workspace = true, default-features = true }
|
||||
futures = { workspace = true }
|
||||
jsonrpsee = { features = [
|
||||
"client-core",
|
||||
"macros",
|
||||
"server-core",
|
||||
], workspace = true }
|
||||
log = { workspace = true, default-features = true }
|
||||
pezsc-client-api = { workspace = true, default-features = true }
|
||||
pezsc-consensus-grandpa = { workspace = true, default-features = true }
|
||||
pezsc-rpc = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive"], workspace = true, default-features = true }
|
||||
pezsp-blockchain = { workspace = true, default-features = true }
|
||||
pezsp-core = { workspace = true, default-features = true }
|
||||
pezsp-runtime = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezsc-block-builder = { workspace = true, default-features = true }
|
||||
pezsc-rpc = { features = [
|
||||
"test-helpers",
|
||||
], workspace = true, default-features = true }
|
||||
pezsp-consensus-grandpa = { workspace = true, default-features = true }
|
||||
pezsp-keyring = { workspace = true, default-features = true }
|
||||
bizinikiwi-test-runtime-client = { workspace = true }
|
||||
tokio = { features = ["macros"], workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezsc-block-builder/runtime-benchmarks",
|
||||
"pezsc-client-api/runtime-benchmarks",
|
||||
"pezsc-consensus-grandpa/runtime-benchmarks",
|
||||
"pezsc-rpc/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus-grandpa/runtime-benchmarks",
|
||||
"pezsp-keyring/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"bizinikiwi-test-runtime-client/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
RPC API for GRANDPA.
|
||||
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,73 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use jsonrpsee::types::error::{ErrorObject, ErrorObjectOwned};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
/// Top-level error type for the RPC handler
|
||||
pub enum Error {
|
||||
/// The GRANDPA RPC endpoint is not ready.
|
||||
#[error("GRANDPA RPC endpoint not ready")]
|
||||
EndpointNotReady,
|
||||
/// GRANDPA reports the authority set id to be larger than 32-bits.
|
||||
#[error("GRANDPA reports authority set id unreasonably large")]
|
||||
AuthoritySetIdReportedAsUnreasonablyLarge,
|
||||
/// GRANDPA reports voter state with round id or weights larger than 32-bits.
|
||||
#[error("GRANDPA reports voter state as unreasonably large")]
|
||||
VoterStateReportsUnreasonablyLargeNumbers,
|
||||
/// GRANDPA prove finality failed.
|
||||
#[error("GRANDPA prove finality rpc failed: {0}")]
|
||||
ProveFinalityFailed(#[from] pezsc_consensus_grandpa::FinalityProofError),
|
||||
}
|
||||
|
||||
/// The error codes returned by jsonrpc.
|
||||
pub enum ErrorCode {
|
||||
/// Returned when Grandpa RPC endpoint is not ready.
|
||||
NotReady = 1,
|
||||
/// Authority set ID is larger than 32-bits.
|
||||
AuthoritySetTooLarge,
|
||||
/// Voter state with round id or weights larger than 32-bits.
|
||||
VoterStateTooLarge,
|
||||
/// Failed to prove finality.
|
||||
ProveFinality,
|
||||
}
|
||||
|
||||
impl From<Error> for ErrorCode {
|
||||
fn from(error: Error) -> Self {
|
||||
match error {
|
||||
Error::EndpointNotReady => ErrorCode::NotReady,
|
||||
Error::AuthoritySetIdReportedAsUnreasonablyLarge => ErrorCode::AuthoritySetTooLarge,
|
||||
Error::VoterStateReportsUnreasonablyLargeNumbers => ErrorCode::VoterStateTooLarge,
|
||||
Error::ProveFinalityFailed(_) => ErrorCode::ProveFinality,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for ErrorObjectOwned {
|
||||
fn from(error: Error) -> Self {
|
||||
let message = error.to_string();
|
||||
let code = ErrorCode::from(error);
|
||||
ErrorObject::owned(code as i32, message, None::<()>)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::num::TryFromIntError> for Error {
|
||||
fn from(_error: std::num::TryFromIntError) -> Self {
|
||||
Error::VoterStateReportsUnreasonablyLargeNumbers
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use pezsc_consensus_grandpa::FinalityProofProvider;
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct EncodedFinalityProof(pub pezsp_core::Bytes);
|
||||
|
||||
/// Local trait mainly to allow mocking in tests.
|
||||
pub trait RpcFinalityProofProvider<Block: BlockT> {
|
||||
/// Prove finality for the given block number by returning a Justification for the last block of
|
||||
/// the authority set.
|
||||
fn rpc_prove_finality(
|
||||
&self,
|
||||
block: NumberFor<Block>,
|
||||
) -> Result<Option<EncodedFinalityProof>, pezsc_consensus_grandpa::FinalityProofError>;
|
||||
}
|
||||
|
||||
impl<B, Block> RpcFinalityProofProvider<Block> for FinalityProofProvider<B, Block>
|
||||
where
|
||||
Block: BlockT,
|
||||
NumberFor<Block>: finality_grandpa::BlockNumberOps,
|
||||
B: pezsc_client_api::backend::Backend<Block> + Send + Sync + 'static,
|
||||
{
|
||||
fn rpc_prove_finality(
|
||||
&self,
|
||||
block: NumberFor<Block>,
|
||||
) -> Result<Option<EncodedFinalityProof>, pezsc_consensus_grandpa::FinalityProofError> {
|
||||
self.prove_finality(block).map(|x| x.map(|y| EncodedFinalityProof(y.into())))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! RPC API for GRANDPA.
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use futures::StreamExt;
|
||||
use log::warn;
|
||||
use std::sync::Arc;
|
||||
|
||||
use jsonrpsee::{
|
||||
core::{async_trait, server::PendingSubscriptionSink},
|
||||
proc_macros::rpc,
|
||||
};
|
||||
|
||||
mod error;
|
||||
mod finality;
|
||||
mod notification;
|
||||
mod report;
|
||||
|
||||
use error::Error;
|
||||
use finality::{EncodedFinalityProof, RpcFinalityProofProvider};
|
||||
use notification::JustificationNotification;
|
||||
use report::{ReportAuthoritySet, ReportVoterState, ReportedRoundStates};
|
||||
use pezsc_consensus_grandpa::GrandpaJustificationStream;
|
||||
use pezsc_rpc::{
|
||||
utils::{BoundedVecDeque, PendingSubscription},
|
||||
SubscriptionTaskExecutor,
|
||||
};
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
|
||||
|
||||
/// Provides RPC methods for interacting with GRANDPA.
|
||||
#[rpc(client, server)]
|
||||
pub trait GrandpaApi<Notification, Hash, Number> {
|
||||
/// Returns the state of the current best round state as well as the
|
||||
/// ongoing background rounds.
|
||||
#[method(name = "grandpa_roundState")]
|
||||
async fn round_state(&self) -> Result<ReportedRoundStates, Error>;
|
||||
|
||||
/// Returns the block most recently finalized by Grandpa, alongside
|
||||
/// side its justification.
|
||||
#[subscription(
|
||||
name = "grandpa_subscribeJustifications" => "grandpa_justifications",
|
||||
unsubscribe = "grandpa_unsubscribeJustifications",
|
||||
item = Notification
|
||||
)]
|
||||
fn subscribe_justifications(&self);
|
||||
|
||||
/// Prove finality for the given block number by returning the Justification for the last block
|
||||
/// in the set and all the intermediary headers to link them together.
|
||||
#[method(name = "grandpa_proveFinality")]
|
||||
async fn prove_finality(&self, block: Number) -> Result<Option<EncodedFinalityProof>, Error>;
|
||||
}
|
||||
|
||||
/// Provides RPC methods for interacting with GRANDPA.
|
||||
pub struct Grandpa<AuthoritySet, VoterState, Block: BlockT, ProofProvider> {
|
||||
executor: SubscriptionTaskExecutor,
|
||||
authority_set: AuthoritySet,
|
||||
voter_state: VoterState,
|
||||
justification_stream: GrandpaJustificationStream<Block>,
|
||||
finality_proof_provider: Arc<ProofProvider>,
|
||||
}
|
||||
impl<AuthoritySet, VoterState, Block: BlockT, ProofProvider>
|
||||
Grandpa<AuthoritySet, VoterState, Block, ProofProvider>
|
||||
{
|
||||
/// Prepare a new [`Grandpa`] Rpc handler.
|
||||
pub fn new(
|
||||
executor: SubscriptionTaskExecutor,
|
||||
authority_set: AuthoritySet,
|
||||
voter_state: VoterState,
|
||||
justification_stream: GrandpaJustificationStream<Block>,
|
||||
finality_proof_provider: Arc<ProofProvider>,
|
||||
) -> Self {
|
||||
Self { executor, authority_set, voter_state, justification_stream, finality_proof_provider }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<AuthoritySet, VoterState, Block, ProofProvider>
|
||||
GrandpaApiServer<JustificationNotification, Block::Hash, NumberFor<Block>>
|
||||
for Grandpa<AuthoritySet, VoterState, Block, ProofProvider>
|
||||
where
|
||||
VoterState: ReportVoterState + Send + Sync + 'static,
|
||||
AuthoritySet: ReportAuthoritySet + Send + Sync + 'static,
|
||||
Block: BlockT,
|
||||
ProofProvider: RpcFinalityProofProvider<Block> + Send + Sync + 'static,
|
||||
{
|
||||
async fn round_state(&self) -> Result<ReportedRoundStates, Error> {
|
||||
ReportedRoundStates::from(&self.authority_set, &self.voter_state)
|
||||
}
|
||||
|
||||
fn subscribe_justifications(&self, pending: PendingSubscriptionSink) {
|
||||
let stream = self.justification_stream.subscribe(100_000).map(
|
||||
|x: pezsc_consensus_grandpa::GrandpaJustification<Block>| {
|
||||
JustificationNotification::from(x)
|
||||
},
|
||||
);
|
||||
|
||||
pezsc_rpc::utils::spawn_subscription_task(
|
||||
&self.executor,
|
||||
PendingSubscription::from(pending).pipe_from_stream(stream, BoundedVecDeque::default()),
|
||||
);
|
||||
}
|
||||
|
||||
async fn prove_finality(
|
||||
&self,
|
||||
block: NumberFor<Block>,
|
||||
) -> Result<Option<EncodedFinalityProof>, Error> {
|
||||
self.finality_proof_provider.rpc_prove_finality(block).map_err(|e| {
|
||||
warn!("Error proving finality: {}", e);
|
||||
error::Error::ProveFinalityFailed(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use jsonrpsee::{core::EmptyServerParams as EmptyParams, types::SubscriptionId, RpcModule};
|
||||
use pezsc_block_builder::BlockBuilderBuilder;
|
||||
use pezsc_consensus_grandpa::{
|
||||
report, AuthorityId, FinalityProof, GrandpaJustification, GrandpaJustificationSender,
|
||||
};
|
||||
use pezsc_rpc::testing::test_executor;
|
||||
use pezsp_blockchain::HeaderBackend;
|
||||
use pezsp_core::crypto::ByteArray;
|
||||
use pezsp_keyring::Ed25519Keyring;
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
|
||||
use bizinikiwi_test_runtime_client::{
|
||||
runtime::{Block, Header, H256},
|
||||
DefaultTestClientBuilderExt, TestClientBuilder, TestClientBuilderExt,
|
||||
};
|
||||
|
||||
struct TestAuthoritySet;
|
||||
struct TestVoterState;
|
||||
struct EmptyVoterState;
|
||||
|
||||
struct TestFinalityProofProvider {
|
||||
finality_proof: Option<FinalityProof<Header>>,
|
||||
}
|
||||
|
||||
fn voters() -> HashSet<AuthorityId> {
|
||||
let voter_id_1 = AuthorityId::from_slice(&[1; 32]).unwrap();
|
||||
let voter_id_2 = AuthorityId::from_slice(&[2; 32]).unwrap();
|
||||
|
||||
vec![voter_id_1, voter_id_2].into_iter().collect()
|
||||
}
|
||||
|
||||
impl ReportAuthoritySet for TestAuthoritySet {
|
||||
fn get(&self) -> (u64, HashSet<AuthorityId>) {
|
||||
(1, voters())
|
||||
}
|
||||
}
|
||||
|
||||
impl ReportVoterState for EmptyVoterState {
|
||||
fn get(&self) -> Option<report::VoterState<AuthorityId>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn header(number: u64) -> Header {
|
||||
let parent_hash = match number {
|
||||
0 => Default::default(),
|
||||
_ => header(number - 1).hash(),
|
||||
};
|
||||
Header::new(
|
||||
number,
|
||||
H256::from_low_u64_be(0),
|
||||
H256::from_low_u64_be(0),
|
||||
parent_hash,
|
||||
Default::default(),
|
||||
)
|
||||
}
|
||||
|
||||
impl<Block: BlockT> RpcFinalityProofProvider<Block> for TestFinalityProofProvider {
|
||||
fn rpc_prove_finality(
|
||||
&self,
|
||||
_block: NumberFor<Block>,
|
||||
) -> Result<Option<EncodedFinalityProof>, pezsc_consensus_grandpa::FinalityProofError> {
|
||||
Ok(Some(EncodedFinalityProof(
|
||||
self.finality_proof
|
||||
.as_ref()
|
||||
.expect("Don't call rpc_prove_finality without setting the FinalityProof")
|
||||
.encode()
|
||||
.into(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl ReportVoterState for TestVoterState {
|
||||
fn get(&self) -> Option<report::VoterState<AuthorityId>> {
|
||||
let voter_id_1 = AuthorityId::from_slice(&[1; 32]).unwrap();
|
||||
let voters_best: HashSet<_> = vec![voter_id_1].into_iter().collect();
|
||||
|
||||
let best_round_state = pezsc_consensus_grandpa::report::RoundState {
|
||||
total_weight: 100_u64.try_into().unwrap(),
|
||||
threshold_weight: 67_u64.try_into().unwrap(),
|
||||
prevote_current_weight: 50.into(),
|
||||
prevote_ids: voters_best,
|
||||
precommit_current_weight: 0.into(),
|
||||
precommit_ids: HashSet::new(),
|
||||
};
|
||||
|
||||
let past_round_state = pezsc_consensus_grandpa::report::RoundState {
|
||||
total_weight: 100_u64.try_into().unwrap(),
|
||||
threshold_weight: 67_u64.try_into().unwrap(),
|
||||
prevote_current_weight: 100.into(),
|
||||
prevote_ids: voters(),
|
||||
precommit_current_weight: 100.into(),
|
||||
precommit_ids: voters(),
|
||||
};
|
||||
|
||||
let background_rounds = vec![(1, past_round_state)].into_iter().collect();
|
||||
|
||||
Some(report::VoterState { background_rounds, best_round: (2, best_round_state) })
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_io_handler<VoterState>(
|
||||
voter_state: VoterState,
|
||||
) -> (
|
||||
RpcModule<Grandpa<TestAuthoritySet, VoterState, Block, TestFinalityProofProvider>>,
|
||||
GrandpaJustificationSender<Block>,
|
||||
)
|
||||
where
|
||||
VoterState: ReportVoterState + Send + Sync + 'static,
|
||||
{
|
||||
setup_io_handler_with_finality_proofs(voter_state, None)
|
||||
}
|
||||
|
||||
fn setup_io_handler_with_finality_proofs<VoterState>(
|
||||
voter_state: VoterState,
|
||||
finality_proof: Option<FinalityProof<Header>>,
|
||||
) -> (
|
||||
RpcModule<Grandpa<TestAuthoritySet, VoterState, Block, TestFinalityProofProvider>>,
|
||||
GrandpaJustificationSender<Block>,
|
||||
)
|
||||
where
|
||||
VoterState: ReportVoterState + Send + Sync + 'static,
|
||||
{
|
||||
let (justification_sender, justification_stream) = GrandpaJustificationStream::channel();
|
||||
let finality_proof_provider = Arc::new(TestFinalityProofProvider { finality_proof });
|
||||
let executor = test_executor();
|
||||
|
||||
let rpc = Grandpa::new(
|
||||
executor,
|
||||
TestAuthoritySet,
|
||||
voter_state,
|
||||
justification_stream,
|
||||
finality_proof_provider,
|
||||
)
|
||||
.into_rpc();
|
||||
|
||||
(rpc, justification_sender)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn uninitialized_rpc_handler() {
|
||||
let (rpc, _) = setup_io_handler(EmptyVoterState);
|
||||
let expected_response = r#"{"jsonrpc":"2.0","id":0,"error":{"code":1,"message":"GRANDPA RPC endpoint not ready"}}"#.to_string();
|
||||
let request = r#"{"jsonrpc":"2.0","method":"grandpa_roundState","params":[],"id":0}"#;
|
||||
let (response, _) = rpc.raw_json_request(&request, 1).await.unwrap();
|
||||
|
||||
assert_eq!(expected_response, response);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn working_rpc_handler() {
|
||||
let (rpc, _) = setup_io_handler(TestVoterState);
|
||||
let expected_response = "{\"jsonrpc\":\"2.0\",\"id\":0,\"result\":{\
|
||||
\"setId\":1,\
|
||||
\"best\":{\
|
||||
\"round\":2,\"totalWeight\":100,\"thresholdWeight\":67,\
|
||||
\"prevotes\":{\"currentWeight\":50,\"missing\":[\"5C7LYpP2ZH3tpKbvVvwiVe54AapxErdPBbvkYhe6y9ZBkqWt\"]},\
|
||||
\"precommits\":{\"currentWeight\":0,\"missing\":[\"5C62Ck4UrFPiBtoCmeSrgF7x9yv9mn38446dhCpsi2mLHiFT\",\"5C7LYpP2ZH3tpKbvVvwiVe54AapxErdPBbvkYhe6y9ZBkqWt\"]}\
|
||||
},\
|
||||
\"background\":[{\
|
||||
\"round\":1,\"totalWeight\":100,\"thresholdWeight\":67,\
|
||||
\"prevotes\":{\"currentWeight\":100,\"missing\":[]},\
|
||||
\"precommits\":{\"currentWeight\":100,\"missing\":[]}\
|
||||
}]\
|
||||
}}".to_string();
|
||||
|
||||
let request = r#"{"jsonrpc":"2.0","method":"grandpa_roundState","params":[],"id":0}"#;
|
||||
let (response, _) = rpc.raw_json_request(&request, 1).await.unwrap();
|
||||
assert_eq!(expected_response, response);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribe_and_unsubscribe_with_wrong_id() {
|
||||
let (rpc, _) = setup_io_handler(TestVoterState);
|
||||
// Subscribe call.
|
||||
let _sub = rpc
|
||||
.subscribe_unbounded("grandpa_subscribeJustifications", EmptyParams::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Unsubscribe with wrong ID
|
||||
let (response, _) = rpc
|
||||
.raw_json_request(
|
||||
r#"{"jsonrpc":"2.0","method":"grandpa_unsubscribeJustifications","params":["FOO"],"id":1}"#,
|
||||
1,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let expected = r#"{"jsonrpc":"2.0","id":1,"result":false}"#;
|
||||
|
||||
assert_eq!(response, expected);
|
||||
}
|
||||
|
||||
fn create_justification() -> GrandpaJustification<Block> {
|
||||
let peers = &[Ed25519Keyring::Alice];
|
||||
|
||||
let builder = TestClientBuilder::new();
|
||||
let client = builder.build();
|
||||
let client = Arc::new(client);
|
||||
|
||||
let built_block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.info().best_hash)
|
||||
.with_parent_block_number(client.info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let block = built_block.block;
|
||||
let block_hash = block.hash();
|
||||
|
||||
let justification = {
|
||||
let round = 1;
|
||||
let set_id = 0;
|
||||
|
||||
let precommit = finality_grandpa::Precommit {
|
||||
target_hash: block_hash,
|
||||
target_number: *block.header.number(),
|
||||
};
|
||||
|
||||
let msg = finality_grandpa::Message::Precommit(precommit.clone());
|
||||
let encoded = pezsp_consensus_grandpa::localized_payload(round, set_id, &msg);
|
||||
let signature = peers[0].sign(&encoded[..]).into();
|
||||
|
||||
let precommit = finality_grandpa::SignedPrecommit {
|
||||
precommit,
|
||||
signature,
|
||||
id: peers[0].public().into(),
|
||||
};
|
||||
|
||||
let commit = finality_grandpa::Commit {
|
||||
target_hash: block_hash,
|
||||
target_number: *block.header.number(),
|
||||
precommits: vec![precommit],
|
||||
};
|
||||
|
||||
GrandpaJustification::from_commit(&client, round, commit).unwrap()
|
||||
};
|
||||
|
||||
justification
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribe_and_listen_to_one_justification() {
|
||||
let (rpc, justification_sender) = setup_io_handler(TestVoterState);
|
||||
|
||||
let mut sub = rpc
|
||||
.subscribe_unbounded("grandpa_subscribeJustifications", EmptyParams::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Notify with a header and justification
|
||||
let justification = create_justification();
|
||||
justification_sender.notify(|| Ok::<_, ()>(justification.clone())).unwrap();
|
||||
|
||||
// Inspect what we received
|
||||
let (recv_justification, recv_sub_id): (pezsp_core::Bytes, SubscriptionId) =
|
||||
sub.next().await.unwrap().unwrap();
|
||||
let recv_justification: GrandpaJustification<Block> =
|
||||
Decode::decode(&mut &recv_justification[..]).unwrap();
|
||||
|
||||
assert_eq!(&recv_sub_id, sub.subscription_id());
|
||||
assert_eq!(recv_justification, justification);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prove_finality_with_test_finality_proof_provider() {
|
||||
let finality_proof = FinalityProof {
|
||||
block: header(42).hash(),
|
||||
justification: create_justification().encode(),
|
||||
unknown_headers: vec![header(2)],
|
||||
};
|
||||
let (rpc, _) =
|
||||
setup_io_handler_with_finality_proofs(TestVoterState, Some(finality_proof.clone()));
|
||||
|
||||
let bytes: pezsp_core::Bytes = rpc.call("grandpa_proveFinality", [42]).await.unwrap();
|
||||
let finality_proof_rpc: FinalityProof<Header> = Decode::decode(&mut &bytes[..]).unwrap();
|
||||
assert_eq!(finality_proof_rpc, finality_proof);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use codec::Encode;
|
||||
use pezsc_consensus_grandpa::GrandpaJustification;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
/// An encoded justification proving that the given header has been finalized
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct JustificationNotification(pezsp_core::Bytes);
|
||||
|
||||
impl<Block: BlockT> From<GrandpaJustification<Block>> for JustificationNotification {
|
||||
fn from(notification: GrandpaJustification<Block>) -> Self {
|
||||
JustificationNotification(notification.encode().into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeSet, HashSet},
|
||||
fmt::Debug,
|
||||
ops::Add,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use pezsc_consensus_grandpa::{report, AuthorityId, SharedAuthoritySet, SharedVoterState};
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
/// Utility trait to get reporting data for the current GRANDPA authority set.
|
||||
pub trait ReportAuthoritySet {
|
||||
fn get(&self) -> (u64, HashSet<AuthorityId>);
|
||||
}
|
||||
|
||||
/// Utility trait to get reporting data for the current GRANDPA voter state.
|
||||
pub trait ReportVoterState {
|
||||
fn get(&self) -> Option<report::VoterState<AuthorityId>>;
|
||||
}
|
||||
|
||||
impl<H, N> ReportAuthoritySet for SharedAuthoritySet<H, N>
|
||||
where
|
||||
N: Add<Output = N> + Ord + Clone + Debug,
|
||||
H: Clone + Debug + Eq,
|
||||
{
|
||||
fn get(&self) -> (u64, HashSet<AuthorityId>) {
|
||||
let current_voters: HashSet<AuthorityId> =
|
||||
self.current_authorities().iter().map(|p| p.0.clone()).collect();
|
||||
|
||||
(self.set_id(), current_voters)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReportVoterState for SharedVoterState {
|
||||
fn get(&self) -> Option<report::VoterState<AuthorityId>> {
|
||||
self.voter_state()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Prevotes {
|
||||
current_weight: u32,
|
||||
missing: BTreeSet<AuthorityId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Precommits {
|
||||
current_weight: u32,
|
||||
missing: BTreeSet<AuthorityId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RoundState {
|
||||
round: u32,
|
||||
total_weight: u32,
|
||||
threshold_weight: u32,
|
||||
prevotes: Prevotes,
|
||||
precommits: Precommits,
|
||||
}
|
||||
|
||||
impl RoundState {
|
||||
fn from(
|
||||
round: u64,
|
||||
round_state: &report::RoundState<AuthorityId>,
|
||||
voters: &HashSet<AuthorityId>,
|
||||
) -> Result<Self, Error> {
|
||||
let prevotes = &round_state.prevote_ids;
|
||||
let missing_prevotes = voters.difference(prevotes).cloned().collect();
|
||||
|
||||
let precommits = &round_state.precommit_ids;
|
||||
let missing_precommits = voters.difference(precommits).cloned().collect();
|
||||
|
||||
Ok(Self {
|
||||
round: round.try_into()?,
|
||||
total_weight: round_state.total_weight.get().try_into()?,
|
||||
threshold_weight: round_state.threshold_weight.get().try_into()?,
|
||||
prevotes: Prevotes {
|
||||
current_weight: round_state.prevote_current_weight.0.try_into()?,
|
||||
missing: missing_prevotes,
|
||||
},
|
||||
precommits: Precommits {
|
||||
current_weight: round_state.precommit_current_weight.0.try_into()?,
|
||||
missing: missing_precommits,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The state of the current best round, as well as the background rounds in a
|
||||
/// form suitable for serialization.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReportedRoundStates {
|
||||
set_id: u32,
|
||||
best: RoundState,
|
||||
background: Vec<RoundState>,
|
||||
}
|
||||
|
||||
impl ReportedRoundStates {
|
||||
pub fn from<AuthoritySet, VoterState>(
|
||||
authority_set: &AuthoritySet,
|
||||
voter_state: &VoterState,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
AuthoritySet: ReportAuthoritySet,
|
||||
VoterState: ReportVoterState,
|
||||
{
|
||||
let voter_state = voter_state.get().ok_or(Error::EndpointNotReady)?;
|
||||
|
||||
let (set_id, current_voters) = authority_set.get();
|
||||
let set_id =
|
||||
u32::try_from(set_id).map_err(|_| Error::AuthoritySetIdReportedAsUnreasonablyLarge)?;
|
||||
|
||||
let best = {
|
||||
let (round, round_state) = voter_state.best_round;
|
||||
RoundState::from(round, &round_state, ¤t_voters)?
|
||||
};
|
||||
|
||||
let background = voter_state
|
||||
.background_rounds
|
||||
.iter()
|
||||
.map(|(round, round_state)| RoundState::from(*round, round_state, ¤t_voters))
|
||||
.collect::<Result<Vec<_>, Error>>()?;
|
||||
|
||||
Ok(Self { set_id, best, background })
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,794 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Schema for stuff in the aux-db.
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use finality_grandpa::round::State as RoundState;
|
||||
use log::{info, warn};
|
||||
|
||||
use fork_tree::ForkTree;
|
||||
use pezsc_client_api::backend::AuxStore;
|
||||
use pezsp_blockchain::{Error as ClientError, Result as ClientResult};
|
||||
use pezsp_consensus_grandpa::{AuthorityList, RoundNumber, SetId};
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
|
||||
|
||||
use crate::{
|
||||
authorities::{
|
||||
AuthoritySet, AuthoritySetChanges, DelayKind, PendingChange, SharedAuthoritySet,
|
||||
},
|
||||
environment::{
|
||||
CompletedRound, CompletedRounds, CurrentRounds, HasVoted, SharedVoterSetState,
|
||||
VoterSetState,
|
||||
},
|
||||
GrandpaJustification, NewAuthoritySet, LOG_TARGET,
|
||||
};
|
||||
|
||||
const VERSION_KEY: &[u8] = b"grandpa_schema_version";
|
||||
const SET_STATE_KEY: &[u8] = b"grandpa_completed_round";
|
||||
const CONCLUDED_ROUNDS: &[u8] = b"grandpa_concluded_rounds";
|
||||
const AUTHORITY_SET_KEY: &[u8] = b"grandpa_voters";
|
||||
const BEST_JUSTIFICATION: &[u8] = b"grandpa_best_justification";
|
||||
|
||||
const CURRENT_VERSION: u32 = 3;
|
||||
|
||||
/// The voter set state.
|
||||
#[derive(Debug, Clone, Encode, Decode)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub enum V1VoterSetState<H, N> {
|
||||
/// The voter set state, currently paused.
|
||||
Paused(RoundNumber, RoundState<H, N>),
|
||||
/// The voter set state, currently live.
|
||||
Live(RoundNumber, RoundState<H, N>),
|
||||
}
|
||||
|
||||
type V0VoterSetState<H, N> = (RoundNumber, RoundState<H, N>);
|
||||
|
||||
#[derive(Debug, Clone, Encode, Decode, PartialEq)]
|
||||
struct V0PendingChange<H, N> {
|
||||
next_authorities: AuthorityList,
|
||||
delay: N,
|
||||
canon_height: N,
|
||||
canon_hash: H,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Encode, Decode, PartialEq)]
|
||||
struct V0AuthoritySet<H, N> {
|
||||
current_authorities: AuthorityList,
|
||||
set_id: SetId,
|
||||
pending_changes: Vec<V0PendingChange<H, N>>,
|
||||
}
|
||||
|
||||
impl<H, N> Into<AuthoritySet<H, N>> for V0AuthoritySet<H, N>
|
||||
where
|
||||
H: Clone + Debug + PartialEq,
|
||||
N: Clone + Debug + Ord,
|
||||
{
|
||||
fn into(self) -> AuthoritySet<H, N> {
|
||||
let mut pending_standard_changes = ForkTree::new();
|
||||
|
||||
for old_change in self.pending_changes {
|
||||
let new_change = PendingChange {
|
||||
next_authorities: old_change.next_authorities,
|
||||
delay: old_change.delay,
|
||||
canon_height: old_change.canon_height,
|
||||
canon_hash: old_change.canon_hash,
|
||||
delay_kind: DelayKind::Finalized,
|
||||
};
|
||||
|
||||
if let Err(err) = pending_standard_changes.import::<_, ClientError>(
|
||||
new_change.canon_hash.clone(),
|
||||
new_change.canon_height.clone(),
|
||||
new_change,
|
||||
// previously we only supported at most one pending change per fork
|
||||
&|_, _| Ok(false),
|
||||
) {
|
||||
warn!(target: LOG_TARGET, "Error migrating pending authority set change: {}", err);
|
||||
warn!(target: LOG_TARGET, "Node is in a potentially inconsistent state.");
|
||||
}
|
||||
}
|
||||
|
||||
let authority_set = AuthoritySet::new(
|
||||
self.current_authorities,
|
||||
self.set_id,
|
||||
pending_standard_changes,
|
||||
Vec::new(),
|
||||
AuthoritySetChanges::empty(),
|
||||
);
|
||||
|
||||
authority_set.expect("current_authorities is non-empty and weights are non-zero; qed.")
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, N> Into<AuthoritySet<H, N>> for V2AuthoritySet<H, N>
|
||||
where
|
||||
H: Clone + Debug + PartialEq,
|
||||
N: Clone + Debug + Ord,
|
||||
{
|
||||
fn into(self) -> AuthoritySet<H, N> {
|
||||
AuthoritySet::new(
|
||||
self.current_authorities,
|
||||
self.set_id,
|
||||
self.pending_standard_changes,
|
||||
self.pending_forced_changes,
|
||||
AuthoritySetChanges::empty(),
|
||||
)
|
||||
.expect("current_authorities is non-empty and weights are non-zero; qed.")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Encode, Decode, PartialEq)]
|
||||
struct V2AuthoritySet<H, N> {
|
||||
current_authorities: AuthorityList,
|
||||
set_id: u64,
|
||||
pending_standard_changes: ForkTree<H, N, PendingChange<H, N>>,
|
||||
pending_forced_changes: Vec<PendingChange<H, N>>,
|
||||
}
|
||||
|
||||
pub(crate) fn load_decode<B: AuxStore, T: Decode>(
|
||||
backend: &B,
|
||||
key: &[u8],
|
||||
) -> ClientResult<Option<T>> {
|
||||
match backend.get_aux(key)? {
|
||||
None => Ok(None),
|
||||
Some(t) => T::decode(&mut &t[..])
|
||||
.map_err(|e| ClientError::Backend(format!("GRANDPA DB is corrupted: {}", e)))
|
||||
.map(Some),
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistent data kept between runs.
|
||||
pub(crate) struct PersistentData<Block: BlockT> {
|
||||
pub(crate) authority_set: SharedAuthoritySet<Block::Hash, NumberFor<Block>>,
|
||||
pub(crate) set_state: SharedVoterSetState<Block>,
|
||||
}
|
||||
|
||||
fn migrate_from_version0<Block: BlockT, B, G>(
|
||||
backend: &B,
|
||||
genesis_round: &G,
|
||||
) -> ClientResult<Option<(AuthoritySet<Block::Hash, NumberFor<Block>>, VoterSetState<Block>)>>
|
||||
where
|
||||
B: AuxStore,
|
||||
G: Fn() -> RoundState<Block::Hash, NumberFor<Block>>,
|
||||
{
|
||||
CURRENT_VERSION.using_encoded(|s| backend.insert_aux(&[(VERSION_KEY, s)], &[]))?;
|
||||
|
||||
if let Some(old_set) =
|
||||
load_decode::<_, V0AuthoritySet<Block::Hash, NumberFor<Block>>>(backend, AUTHORITY_SET_KEY)?
|
||||
{
|
||||
let new_set: AuthoritySet<Block::Hash, NumberFor<Block>> = old_set.into();
|
||||
backend.insert_aux(&[(AUTHORITY_SET_KEY, new_set.encode().as_slice())], &[])?;
|
||||
|
||||
let (last_round_number, last_round_state) = match load_decode::<
|
||||
_,
|
||||
V0VoterSetState<Block::Hash, NumberFor<Block>>,
|
||||
>(backend, SET_STATE_KEY)?
|
||||
{
|
||||
Some((number, state)) => (number, state),
|
||||
None => (0, genesis_round()),
|
||||
};
|
||||
|
||||
let set_id = new_set.set_id;
|
||||
|
||||
let base = last_round_state.prevote_ghost.expect(
|
||||
"state is for completed round; completed rounds must have a prevote ghost; qed.",
|
||||
);
|
||||
|
||||
let mut current_rounds = CurrentRounds::<Block>::new();
|
||||
current_rounds.insert(last_round_number + 1, HasVoted::No);
|
||||
|
||||
let set_state = VoterSetState::Live {
|
||||
completed_rounds: CompletedRounds::new(
|
||||
CompletedRound {
|
||||
number: last_round_number,
|
||||
state: last_round_state,
|
||||
votes: Vec::new(),
|
||||
base,
|
||||
},
|
||||
set_id,
|
||||
&new_set,
|
||||
),
|
||||
current_rounds,
|
||||
};
|
||||
|
||||
backend.insert_aux(&[(SET_STATE_KEY, set_state.encode().as_slice())], &[])?;
|
||||
|
||||
return Ok(Some((new_set, set_state)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn migrate_from_version1<Block: BlockT, B, G>(
|
||||
backend: &B,
|
||||
genesis_round: &G,
|
||||
) -> ClientResult<Option<(AuthoritySet<Block::Hash, NumberFor<Block>>, VoterSetState<Block>)>>
|
||||
where
|
||||
B: AuxStore,
|
||||
G: Fn() -> RoundState<Block::Hash, NumberFor<Block>>,
|
||||
{
|
||||
CURRENT_VERSION.using_encoded(|s| backend.insert_aux(&[(VERSION_KEY, s)], &[]))?;
|
||||
|
||||
if let Some(set) =
|
||||
load_decode::<_, AuthoritySet<Block::Hash, NumberFor<Block>>>(backend, AUTHORITY_SET_KEY)?
|
||||
{
|
||||
let set_id = set.set_id;
|
||||
|
||||
let completed_rounds = |number, state, base| {
|
||||
CompletedRounds::new(
|
||||
CompletedRound { number, state, votes: Vec::new(), base },
|
||||
set_id,
|
||||
&set,
|
||||
)
|
||||
};
|
||||
|
||||
let set_state = match load_decode::<_, V1VoterSetState<Block::Hash, NumberFor<Block>>>(
|
||||
backend,
|
||||
SET_STATE_KEY,
|
||||
)? {
|
||||
Some(V1VoterSetState::Paused(last_round_number, set_state)) => {
|
||||
let base = set_state.prevote_ghost
|
||||
.expect("state is for completed round; completed rounds must have a prevote ghost; qed.");
|
||||
|
||||
VoterSetState::Paused {
|
||||
completed_rounds: completed_rounds(last_round_number, set_state, base),
|
||||
}
|
||||
},
|
||||
Some(V1VoterSetState::Live(last_round_number, set_state)) => {
|
||||
let base = set_state.prevote_ghost
|
||||
.expect("state is for completed round; completed rounds must have a prevote ghost; qed.");
|
||||
|
||||
let mut current_rounds = CurrentRounds::<Block>::new();
|
||||
current_rounds.insert(last_round_number + 1, HasVoted::No);
|
||||
|
||||
VoterSetState::Live {
|
||||
completed_rounds: completed_rounds(last_round_number, set_state, base),
|
||||
current_rounds,
|
||||
}
|
||||
},
|
||||
None => {
|
||||
let set_state = genesis_round();
|
||||
let base = set_state.prevote_ghost
|
||||
.expect("state is for completed round; completed rounds must have a prevote ghost; qed.");
|
||||
|
||||
VoterSetState::live(set_id, &set, base)
|
||||
},
|
||||
};
|
||||
|
||||
backend.insert_aux(&[(SET_STATE_KEY, set_state.encode().as_slice())], &[])?;
|
||||
|
||||
return Ok(Some((set, set_state)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn migrate_from_version2<Block: BlockT, B, G>(
|
||||
backend: &B,
|
||||
genesis_round: &G,
|
||||
) -> ClientResult<Option<(AuthoritySet<Block::Hash, NumberFor<Block>>, VoterSetState<Block>)>>
|
||||
where
|
||||
B: AuxStore,
|
||||
G: Fn() -> RoundState<Block::Hash, NumberFor<Block>>,
|
||||
{
|
||||
CURRENT_VERSION.using_encoded(|s| backend.insert_aux(&[(VERSION_KEY, s)], &[]))?;
|
||||
|
||||
if let Some(old_set) =
|
||||
load_decode::<_, V2AuthoritySet<Block::Hash, NumberFor<Block>>>(backend, AUTHORITY_SET_KEY)?
|
||||
{
|
||||
let new_set: AuthoritySet<Block::Hash, NumberFor<Block>> = old_set.into();
|
||||
backend.insert_aux(&[(AUTHORITY_SET_KEY, new_set.encode().as_slice())], &[])?;
|
||||
|
||||
let set_state = match load_decode::<_, VoterSetState<Block>>(backend, SET_STATE_KEY)? {
|
||||
Some(state) => state,
|
||||
None => {
|
||||
let state = genesis_round();
|
||||
let base = state.prevote_ghost
|
||||
.expect("state is for completed round; completed rounds must have a prevote ghost; qed.");
|
||||
|
||||
VoterSetState::live(new_set.set_id, &new_set, base)
|
||||
},
|
||||
};
|
||||
|
||||
return Ok(Some((new_set, set_state)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Load or initialize persistent data from backend.
|
||||
pub(crate) fn load_persistent<Block: BlockT, B, G>(
|
||||
backend: &B,
|
||||
genesis_hash: Block::Hash,
|
||||
genesis_number: NumberFor<Block>,
|
||||
genesis_authorities: G,
|
||||
) -> ClientResult<PersistentData<Block>>
|
||||
where
|
||||
B: AuxStore,
|
||||
G: FnOnce() -> ClientResult<AuthorityList>,
|
||||
{
|
||||
let version: Option<u32> = load_decode(backend, VERSION_KEY)?;
|
||||
|
||||
let make_genesis_round = move || RoundState::genesis((genesis_hash, genesis_number));
|
||||
|
||||
match version {
|
||||
None => {
|
||||
if let Some((new_set, set_state)) =
|
||||
migrate_from_version0::<Block, _, _>(backend, &make_genesis_round)?
|
||||
{
|
||||
return Ok(PersistentData {
|
||||
authority_set: new_set.into(),
|
||||
set_state: set_state.into(),
|
||||
});
|
||||
}
|
||||
},
|
||||
Some(1) => {
|
||||
if let Some((new_set, set_state)) =
|
||||
migrate_from_version1::<Block, _, _>(backend, &make_genesis_round)?
|
||||
{
|
||||
return Ok(PersistentData {
|
||||
authority_set: new_set.into(),
|
||||
set_state: set_state.into(),
|
||||
});
|
||||
}
|
||||
},
|
||||
Some(2) => {
|
||||
if let Some((new_set, set_state)) =
|
||||
migrate_from_version2::<Block, _, _>(backend, &make_genesis_round)?
|
||||
{
|
||||
return Ok(PersistentData {
|
||||
authority_set: new_set.into(),
|
||||
set_state: set_state.into(),
|
||||
});
|
||||
}
|
||||
},
|
||||
Some(3) => {
|
||||
if let Some(set) = load_decode::<_, AuthoritySet<Block::Hash, NumberFor<Block>>>(
|
||||
backend,
|
||||
AUTHORITY_SET_KEY,
|
||||
)? {
|
||||
let set_state =
|
||||
match load_decode::<_, VoterSetState<Block>>(backend, SET_STATE_KEY)? {
|
||||
Some(state) => state,
|
||||
None => {
|
||||
let state = make_genesis_round();
|
||||
let base = state.prevote_ghost
|
||||
.expect("state is for completed round; completed rounds must have a prevote ghost; qed.");
|
||||
|
||||
VoterSetState::live(set.set_id, &set, base)
|
||||
},
|
||||
};
|
||||
|
||||
return Ok(PersistentData {
|
||||
authority_set: set.into(),
|
||||
set_state: set_state.into(),
|
||||
});
|
||||
}
|
||||
},
|
||||
Some(other) =>
|
||||
return Err(ClientError::Backend(format!("Unsupported GRANDPA DB version: {:?}", other))),
|
||||
}
|
||||
|
||||
// genesis.
|
||||
info!(
|
||||
target: LOG_TARGET,
|
||||
"👴 Loading GRANDPA authority set \
|
||||
from genesis on what appears to be first startup."
|
||||
);
|
||||
|
||||
let genesis_authorities = genesis_authorities()?;
|
||||
let genesis_set = AuthoritySet::genesis(genesis_authorities)
|
||||
.expect("genesis authorities is non-empty; all weights are non-zero; qed.");
|
||||
let state = make_genesis_round();
|
||||
let base = state
|
||||
.prevote_ghost
|
||||
.expect("state is for completed round; completed rounds must have a prevote ghost; qed.");
|
||||
|
||||
let genesis_state = VoterSetState::live(0, &genesis_set, base);
|
||||
|
||||
backend.insert_aux(
|
||||
&[
|
||||
(AUTHORITY_SET_KEY, genesis_set.encode().as_slice()),
|
||||
(SET_STATE_KEY, genesis_state.encode().as_slice()),
|
||||
],
|
||||
&[],
|
||||
)?;
|
||||
|
||||
Ok(PersistentData { authority_set: genesis_set.into(), set_state: genesis_state.into() })
|
||||
}
|
||||
|
||||
/// Update the authority set on disk after a change.
|
||||
///
|
||||
/// If there has just been a handoff, pass a `new_set` parameter that describes the
|
||||
/// handoff. `set` in all cases should reflect the current authority set, with all
|
||||
/// changes and handoffs applied.
|
||||
pub(crate) fn update_authority_set<Block: BlockT, F, R>(
|
||||
set: &AuthoritySet<Block::Hash, NumberFor<Block>>,
|
||||
new_set: Option<&NewAuthoritySet<Block::Hash, NumberFor<Block>>>,
|
||||
write_aux: F,
|
||||
) -> R
|
||||
where
|
||||
F: FnOnce(&[(&'static [u8], &[u8])]) -> R,
|
||||
{
|
||||
// write new authority set state to disk.
|
||||
let encoded_set = set.encode();
|
||||
|
||||
if let Some(new_set) = new_set {
|
||||
// we also overwrite the "last completed round" entry with a blank slate
|
||||
// because from the perspective of the finality gadget, the chain has
|
||||
// reset.
|
||||
let set_state = VoterSetState::<Block>::live(
|
||||
new_set.set_id,
|
||||
set,
|
||||
(new_set.canon_hash, new_set.canon_number),
|
||||
);
|
||||
let encoded = set_state.encode();
|
||||
|
||||
write_aux(&[(AUTHORITY_SET_KEY, &encoded_set[..]), (SET_STATE_KEY, &encoded[..])])
|
||||
} else {
|
||||
write_aux(&[(AUTHORITY_SET_KEY, &encoded_set[..])])
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the justification for the latest finalized block on-disk.
|
||||
///
|
||||
/// We always keep around the justification for the best finalized block and overwrite it
|
||||
/// as we finalize new blocks, this makes sure that we don't store useless justifications
|
||||
/// but can always prove finality of the latest block.
|
||||
pub(crate) fn update_best_justification<Block: BlockT, F, R>(
|
||||
justification: &GrandpaJustification<Block>,
|
||||
write_aux: F,
|
||||
) -> R
|
||||
where
|
||||
F: FnOnce(&[(&'static [u8], &[u8])]) -> R,
|
||||
{
|
||||
let encoded_justification = justification.encode();
|
||||
write_aux(&[(BEST_JUSTIFICATION, &encoded_justification[..])])
|
||||
}
|
||||
|
||||
/// Fetch the justification for the latest block finalized by GRANDPA, if any.
|
||||
pub fn best_justification<B, Block>(
|
||||
backend: &B,
|
||||
) -> ClientResult<Option<GrandpaJustification<Block>>>
|
||||
where
|
||||
B: AuxStore,
|
||||
Block: BlockT,
|
||||
{
|
||||
load_decode::<_, GrandpaJustification<Block>>(backend, BEST_JUSTIFICATION)
|
||||
}
|
||||
|
||||
/// Write voter set state.
|
||||
pub(crate) fn write_voter_set_state<Block: BlockT, B: AuxStore>(
|
||||
backend: &B,
|
||||
state: &VoterSetState<Block>,
|
||||
) -> ClientResult<()> {
|
||||
backend.insert_aux(&[(SET_STATE_KEY, state.encode().as_slice())], &[])
|
||||
}
|
||||
|
||||
/// Write concluded round.
|
||||
pub(crate) fn write_concluded_round<Block: BlockT, B: AuxStore>(
|
||||
backend: &B,
|
||||
round_data: &CompletedRound<Block>,
|
||||
) -> ClientResult<()> {
|
||||
let mut key = CONCLUDED_ROUNDS.to_vec();
|
||||
let round_number = round_data.number;
|
||||
round_number.using_encoded(|n| key.extend(n));
|
||||
|
||||
backend.insert_aux(&[(&key[..], round_data.encode().as_slice())], &[])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn load_authorities<B: AuxStore, H: Decode, N: Decode + Clone + Ord>(
|
||||
backend: &B,
|
||||
) -> Option<AuthoritySet<H, N>> {
|
||||
load_decode::<_, AuthoritySet<H, N>>(backend, AUTHORITY_SET_KEY).expect("backend error")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pezsp_consensus_grandpa::AuthorityId;
|
||||
use pezsp_core::{crypto::UncheckedFrom, H256};
|
||||
use bizinikiwi_test_runtime_client::{self, runtime::Block};
|
||||
|
||||
fn dummy_id() -> AuthorityId {
|
||||
AuthorityId::unchecked_from([1; 32])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_decode_from_v0_migrates_data_format() {
|
||||
let client = bizinikiwi_test_runtime_client::new();
|
||||
|
||||
let authorities = vec![(dummy_id(), 100)];
|
||||
let set_id = 3;
|
||||
let round_number: RoundNumber = 42;
|
||||
let round_state = RoundState::<H256, u64> {
|
||||
prevote_ghost: Some((H256::random(), 32)),
|
||||
finalized: None,
|
||||
estimate: None,
|
||||
completable: false,
|
||||
};
|
||||
|
||||
{
|
||||
let authority_set = V0AuthoritySet::<H256, u64> {
|
||||
current_authorities: authorities.clone(),
|
||||
pending_changes: Vec::new(),
|
||||
set_id,
|
||||
};
|
||||
|
||||
let voter_set_state = (round_number, round_state.clone());
|
||||
|
||||
client
|
||||
.insert_aux(
|
||||
&[
|
||||
(AUTHORITY_SET_KEY, authority_set.encode().as_slice()),
|
||||
(SET_STATE_KEY, voter_set_state.encode().as_slice()),
|
||||
],
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(load_decode::<_, u32>(&client, VERSION_KEY).unwrap(), None);
|
||||
|
||||
// should perform the migration
|
||||
load_persistent::<bizinikiwi_test_runtime_client::runtime::Block, _, _>(
|
||||
&client,
|
||||
H256::random(),
|
||||
0,
|
||||
|| unreachable!(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(load_decode::<_, u32>(&client, VERSION_KEY).unwrap(), Some(3));
|
||||
|
||||
let PersistentData { authority_set, set_state, .. } =
|
||||
load_persistent::<bizinikiwi_test_runtime_client::runtime::Block, _, _>(
|
||||
&client,
|
||||
H256::random(),
|
||||
0,
|
||||
|| unreachable!(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
*authority_set.inner(),
|
||||
AuthoritySet::new(
|
||||
authorities.clone(),
|
||||
set_id,
|
||||
ForkTree::new(),
|
||||
Vec::new(),
|
||||
AuthoritySetChanges::empty(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let mut current_rounds = CurrentRounds::<Block>::new();
|
||||
current_rounds.insert(round_number + 1, HasVoted::No);
|
||||
|
||||
assert_eq!(
|
||||
&*set_state.read(),
|
||||
&VoterSetState::Live {
|
||||
completed_rounds: CompletedRounds::new(
|
||||
CompletedRound {
|
||||
number: round_number,
|
||||
state: round_state.clone(),
|
||||
base: round_state.prevote_ghost.unwrap(),
|
||||
votes: vec![],
|
||||
},
|
||||
set_id,
|
||||
&*authority_set.inner(),
|
||||
),
|
||||
current_rounds,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_decode_from_v1_migrates_data_format() {
|
||||
let client = bizinikiwi_test_runtime_client::new();
|
||||
|
||||
let authorities = vec![(dummy_id(), 100)];
|
||||
let set_id = 3;
|
||||
let round_number: RoundNumber = 42;
|
||||
let round_state = RoundState::<H256, u64> {
|
||||
prevote_ghost: Some((H256::random(), 32)),
|
||||
finalized: None,
|
||||
estimate: None,
|
||||
completable: false,
|
||||
};
|
||||
|
||||
{
|
||||
let authority_set = AuthoritySet::<H256, u64>::new(
|
||||
authorities.clone(),
|
||||
set_id,
|
||||
ForkTree::new(),
|
||||
Vec::new(),
|
||||
AuthoritySetChanges::empty(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let voter_set_state = V1VoterSetState::Live(round_number, round_state.clone());
|
||||
|
||||
client
|
||||
.insert_aux(
|
||||
&[
|
||||
(AUTHORITY_SET_KEY, authority_set.encode().as_slice()),
|
||||
(SET_STATE_KEY, voter_set_state.encode().as_slice()),
|
||||
(VERSION_KEY, 1u32.encode().as_slice()),
|
||||
],
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(load_decode::<_, u32>(&client, VERSION_KEY).unwrap(), Some(1));
|
||||
|
||||
// should perform the migration
|
||||
load_persistent::<bizinikiwi_test_runtime_client::runtime::Block, _, _>(
|
||||
&client,
|
||||
H256::random(),
|
||||
0,
|
||||
|| unreachable!(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(load_decode::<_, u32>(&client, VERSION_KEY).unwrap(), Some(3));
|
||||
|
||||
let PersistentData { authority_set, set_state, .. } =
|
||||
load_persistent::<bizinikiwi_test_runtime_client::runtime::Block, _, _>(
|
||||
&client,
|
||||
H256::random(),
|
||||
0,
|
||||
|| unreachable!(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
*authority_set.inner(),
|
||||
AuthoritySet::new(
|
||||
authorities.clone(),
|
||||
set_id,
|
||||
ForkTree::new(),
|
||||
Vec::new(),
|
||||
AuthoritySetChanges::empty(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let mut current_rounds = CurrentRounds::<Block>::new();
|
||||
current_rounds.insert(round_number + 1, HasVoted::No);
|
||||
|
||||
assert_eq!(
|
||||
&*set_state.read(),
|
||||
&VoterSetState::Live {
|
||||
completed_rounds: CompletedRounds::new(
|
||||
CompletedRound {
|
||||
number: round_number,
|
||||
state: round_state.clone(),
|
||||
base: round_state.prevote_ghost.unwrap(),
|
||||
votes: vec![],
|
||||
},
|
||||
set_id,
|
||||
&*authority_set.inner(),
|
||||
),
|
||||
current_rounds,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_decode_from_v2_migrates_data_format() {
|
||||
let client = bizinikiwi_test_runtime_client::new();
|
||||
|
||||
let authorities = vec![(dummy_id(), 100)];
|
||||
let set_id = 3;
|
||||
|
||||
{
|
||||
let authority_set = V2AuthoritySet::<H256, u64> {
|
||||
current_authorities: authorities.clone(),
|
||||
set_id,
|
||||
pending_standard_changes: ForkTree::new(),
|
||||
pending_forced_changes: Vec::new(),
|
||||
};
|
||||
|
||||
let genesis_state = (H256::random(), 32);
|
||||
let voter_set_state: VoterSetState<bizinikiwi_test_runtime_client::runtime::Block> =
|
||||
VoterSetState::live(
|
||||
set_id,
|
||||
&authority_set.clone().into(), // Note the conversion!
|
||||
genesis_state,
|
||||
);
|
||||
|
||||
client
|
||||
.insert_aux(
|
||||
&[
|
||||
(AUTHORITY_SET_KEY, authority_set.encode().as_slice()),
|
||||
(SET_STATE_KEY, voter_set_state.encode().as_slice()),
|
||||
(VERSION_KEY, 2u32.encode().as_slice()),
|
||||
],
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(load_decode::<_, u32>(&client, VERSION_KEY).unwrap(), Some(2));
|
||||
|
||||
// should perform the migration
|
||||
load_persistent::<bizinikiwi_test_runtime_client::runtime::Block, _, _>(
|
||||
&client,
|
||||
H256::random(),
|
||||
0,
|
||||
|| unreachable!(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(load_decode::<_, u32>(&client, VERSION_KEY).unwrap(), Some(3));
|
||||
|
||||
let PersistentData { authority_set, .. } = load_persistent::<
|
||||
bizinikiwi_test_runtime_client::runtime::Block,
|
||||
_,
|
||||
_,
|
||||
>(
|
||||
&client, H256::random(), 0, || unreachable!()
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
*authority_set.inner(),
|
||||
AuthoritySet::new(
|
||||
authorities.clone(),
|
||||
set_id,
|
||||
ForkTree::new(),
|
||||
Vec::new(),
|
||||
AuthoritySetChanges::empty(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_read_concluded_rounds() {
|
||||
let client = bizinikiwi_test_runtime_client::new();
|
||||
let hash = H256::random();
|
||||
let round_state = RoundState::genesis((hash, 0));
|
||||
|
||||
let completed_round = CompletedRound::<bizinikiwi_test_runtime_client::runtime::Block> {
|
||||
number: 42,
|
||||
state: round_state.clone(),
|
||||
base: round_state.prevote_ghost.unwrap(),
|
||||
votes: vec![],
|
||||
};
|
||||
|
||||
assert!(write_concluded_round(&client, &completed_round).is_ok());
|
||||
|
||||
let round_number = completed_round.number;
|
||||
let mut key = CONCLUDED_ROUNDS.to_vec();
|
||||
round_number.using_encoded(|n| key.extend(n));
|
||||
|
||||
assert_eq!(
|
||||
load_decode::<_, CompletedRound::<bizinikiwi_test_runtime_client::runtime::Block>>(
|
||||
&client, &key
|
||||
)
|
||||
.unwrap(),
|
||||
Some(completed_round),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,119 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Periodic rebroadcast of neighbor packets.
|
||||
|
||||
use futures::{future::FutureExt as _, prelude::*, ready, stream::Stream};
|
||||
use futures_timer::Delay;
|
||||
use log::debug;
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
|
||||
|
||||
use super::gossip::{GossipMessage, NeighborPacket};
|
||||
use crate::LOG_TARGET;
|
||||
|
||||
/// A sender used to send neighbor packets to a background job.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct NeighborPacketSender<B: BlockT>(
|
||||
TracingUnboundedSender<(Vec<PeerId>, NeighborPacket<NumberFor<B>>)>,
|
||||
);
|
||||
|
||||
impl<B: BlockT> NeighborPacketSender<B> {
|
||||
/// Send a neighbor packet for the background worker to gossip to peers.
|
||||
pub fn send(
|
||||
&self,
|
||||
who: Vec<pezsc_network_types::PeerId>,
|
||||
neighbor_packet: NeighborPacket<NumberFor<B>>,
|
||||
) {
|
||||
if let Err(err) = self.0.unbounded_send((who, neighbor_packet)) {
|
||||
debug!(target: LOG_TARGET, "Failed to send neighbor packet: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// NeighborPacketWorker is listening on a channel for new neighbor packets being produced by
|
||||
/// components within `finality-grandpa` and forwards those packets to the underlying
|
||||
/// `NetworkEngine` through the `NetworkBridge` that it is being polled by (see `Stream`
|
||||
/// implementation). Periodically it sends out the last packet in cases where no new ones arrive.
|
||||
pub(super) struct NeighborPacketWorker<B: BlockT> {
|
||||
last: Option<(Vec<PeerId>, NeighborPacket<NumberFor<B>>)>,
|
||||
rebroadcast_period: Duration,
|
||||
delay: Delay,
|
||||
rx: TracingUnboundedReceiver<(Vec<PeerId>, NeighborPacket<NumberFor<B>>)>,
|
||||
}
|
||||
|
||||
impl<B: BlockT> Unpin for NeighborPacketWorker<B> {}
|
||||
|
||||
impl<B: BlockT> NeighborPacketWorker<B> {
|
||||
pub(super) fn new(rebroadcast_period: Duration) -> (Self, NeighborPacketSender<B>) {
|
||||
let (tx, rx) = tracing_unbounded::<(Vec<PeerId>, NeighborPacket<NumberFor<B>>)>(
|
||||
"mpsc_grandpa_neighbor_packet_worker",
|
||||
100_000,
|
||||
);
|
||||
let delay = Delay::new(rebroadcast_period);
|
||||
|
||||
(
|
||||
NeighborPacketWorker { last: None, rebroadcast_period, delay, rx },
|
||||
NeighborPacketSender(tx),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT> Stream for NeighborPacketWorker<B> {
|
||||
type Item = (Vec<PeerId>, GossipMessage<B>);
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
let this = &mut *self;
|
||||
match this.rx.poll_next_unpin(cx) {
|
||||
Poll::Ready(None) => return Poll::Ready(None),
|
||||
Poll::Ready(Some((to, packet))) => {
|
||||
this.delay.reset(this.rebroadcast_period);
|
||||
this.last = Some((to.clone(), packet.clone()));
|
||||
|
||||
return Poll::Ready(Some((to, GossipMessage::<B>::from(packet))));
|
||||
},
|
||||
// Don't return yet, maybe the timer fired.
|
||||
Poll::Pending => {},
|
||||
};
|
||||
|
||||
ready!(this.delay.poll_unpin(cx));
|
||||
|
||||
// Getting this far here implies that the timer fired.
|
||||
|
||||
this.delay.reset(this.rebroadcast_period);
|
||||
|
||||
// Make sure the underlying task is scheduled for wake-up.
|
||||
//
|
||||
// Note: In case poll_unpin is called after the reset delay fires again, this
|
||||
// will drop one tick. Deemed as very unlikely and also not critical.
|
||||
while this.delay.poll_unpin(cx).is_ready() {}
|
||||
|
||||
if let Some((ref to, ref packet)) = this.last {
|
||||
return Poll::Ready(Some((to.clone(), GossipMessage::<B>::from(packet.clone()))));
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,720 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Tests for the communication portion of the GRANDPA crate.
|
||||
|
||||
use super::{
|
||||
gossip::{self, GossipValidator},
|
||||
Round, SetId, VoterSet,
|
||||
};
|
||||
use crate::{communication::grandpa_protocol_name, environment::SharedVoterSetState};
|
||||
use codec::{DecodeAll, Encode};
|
||||
use futures::prelude::*;
|
||||
use pezsc_network::{
|
||||
config::{MultiaddrWithPeerId, Role},
|
||||
event::Event as NetworkEvent,
|
||||
service::traits::{Direction, MessageSink, NotificationEvent, NotificationService},
|
||||
types::ProtocolName,
|
||||
Multiaddr, NetworkBlock, NetworkEventStream, NetworkPeers, NetworkSyncForkRequest,
|
||||
ReputationChange,
|
||||
};
|
||||
use pezsc_network_common::role::{ObservedRole, Roles};
|
||||
use pezsc_network_gossip::Validator;
|
||||
use pezsc_network_sync::{SyncEvent as SyncStreamEvent, SyncEventStream};
|
||||
use pezsc_network_test::{Block, Hash};
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
|
||||
use pezsp_consensus_grandpa::AuthorityList;
|
||||
use pezsp_keyring::Ed25519Keyring;
|
||||
use pezsp_runtime::traits::NumberFor;
|
||||
use std::{collections::HashSet, pin::Pin, sync::Arc, task::Poll};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Event {
|
||||
WriteNotification(PeerId, Vec<u8>),
|
||||
Report(PeerId, ReputationChange),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TestNetwork {
|
||||
sender: TracingUnboundedSender<Event>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NetworkPeers for TestNetwork {
|
||||
fn set_authorized_peers(&self, _peers: HashSet<PeerId>) {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn set_authorized_only(&self, _reserved_only: bool) {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn add_known_address(&self, _peer_id: PeerId, _addr: Multiaddr) {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn report_peer(&self, peer_id: PeerId, cost_benefit: ReputationChange) {
|
||||
let _ = self.sender.unbounded_send(Event::Report(peer_id, cost_benefit));
|
||||
}
|
||||
|
||||
fn peer_reputation(&self, _peer_id: &PeerId) -> i32 {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn disconnect_peer(&self, _peer_id: PeerId, _protocol: ProtocolName) {}
|
||||
|
||||
fn accept_unreserved_peers(&self) {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn deny_unreserved_peers(&self) {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn add_reserved_peer(&self, _peer: MultiaddrWithPeerId) -> Result<(), String> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn remove_reserved_peer(&self, _peer_id: PeerId) {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn set_reserved_peers(
|
||||
&self,
|
||||
_protocol: ProtocolName,
|
||||
_peers: HashSet<Multiaddr>,
|
||||
) -> Result<(), String> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn add_peers_to_reserved_set(
|
||||
&self,
|
||||
_protocol: ProtocolName,
|
||||
_peers: HashSet<Multiaddr>,
|
||||
) -> Result<(), String> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn remove_peers_from_reserved_set(
|
||||
&self,
|
||||
_protocol: ProtocolName,
|
||||
_peers: Vec<PeerId>,
|
||||
) -> Result<(), String> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn sync_num_connected(&self) -> usize {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn peer_role(&self, _peer_id: PeerId, handshake: Vec<u8>) -> Option<ObservedRole> {
|
||||
Roles::decode_all(&mut &handshake[..])
|
||||
.ok()
|
||||
.and_then(|role| Some(ObservedRole::from(role)))
|
||||
}
|
||||
|
||||
async fn reserved_peers(&self) -> Result<Vec<PeerId>, ()> {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkEventStream for TestNetwork {
|
||||
fn event_stream(
|
||||
&self,
|
||||
_name: &'static str,
|
||||
) -> Pin<Box<dyn Stream<Item = NetworkEvent> + Send>> {
|
||||
futures::stream::pending().boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBlock<Hash, NumberFor<Block>> for TestNetwork {
|
||||
fn announce_block(&self, _: Hash, _data: Option<Vec<u8>>) {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn new_best_block_imported(&self, _hash: Hash, _number: NumberFor<Block>) {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkSyncForkRequest<Hash, NumberFor<Block>> for TestNetwork {
|
||||
fn set_sync_fork_request(&self, _peers: Vec<PeerId>, _hash: Hash, _number: NumberFor<Block>) {}
|
||||
}
|
||||
|
||||
impl pezsc_network_gossip::ValidatorContext<Block> for TestNetwork {
|
||||
fn broadcast_topic(&mut self, _: Hash, _: bool) {}
|
||||
|
||||
fn broadcast_message(&mut self, _: Hash, _: Vec<u8>, _: bool) {}
|
||||
|
||||
fn send_message(&mut self, who: &PeerId, data: Vec<u8>) {
|
||||
let _ = self.sender.unbounded_send(Event::WriteNotification(*who, data));
|
||||
}
|
||||
|
||||
fn send_topic(&mut self, _: &PeerId, _: Hash, _: bool) {}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TestSync;
|
||||
|
||||
impl SyncEventStream for TestSync {
|
||||
fn event_stream(
|
||||
&self,
|
||||
_name: &'static str,
|
||||
) -> Pin<Box<dyn Stream<Item = SyncStreamEvent> + Send>> {
|
||||
Box::pin(futures::stream::pending())
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBlock<Hash, NumberFor<Block>> for TestSync {
|
||||
fn announce_block(&self, _hash: Hash, _data: Option<Vec<u8>>) {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn new_best_block_imported(&self, _hash: Hash, _number: NumberFor<Block>) {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkSyncForkRequest<Hash, NumberFor<Block>> for TestSync {
|
||||
fn set_sync_fork_request(&self, _peers: Vec<PeerId>, _hash: Hash, _number: NumberFor<Block>) {}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TestNotificationService {
|
||||
sender: TracingUnboundedSender<Event>,
|
||||
rx: TracingUnboundedReceiver<NotificationEvent>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NotificationService for TestNotificationService {
|
||||
/// Instruct `Notifications` to open a new substream for `peer`.
|
||||
async fn open_substream(&mut self, _peer: PeerId) -> Result<(), ()> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
/// Instruct `Notifications` to close substream for `peer`.
|
||||
async fn close_substream(&mut self, _peer: PeerId) -> Result<(), ()> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
/// Send synchronous `notification` to `peer`.
|
||||
fn send_sync_notification(&mut self, peer: &PeerId, notification: Vec<u8>) {
|
||||
let _ = self.sender.unbounded_send(Event::WriteNotification(*peer, notification));
|
||||
}
|
||||
|
||||
/// Send asynchronous `notification` to `peer`, allowing sender to exercise backpressure.
|
||||
async fn send_async_notification(
|
||||
&mut self,
|
||||
_peer: &PeerId,
|
||||
_notification: Vec<u8>,
|
||||
) -> Result<(), pezsc_network::error::Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
/// Set handshake for the notification protocol replacing the old handshake.
|
||||
async fn set_handshake(&mut self, _handshake: Vec<u8>) -> Result<(), ()> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn try_set_handshake(&mut self, _handshake: Vec<u8>) -> Result<(), ()> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
/// Get next event from the `Notifications` event stream.
|
||||
async fn next_event(&mut self) -> Option<NotificationEvent> {
|
||||
self.rx.next().await
|
||||
}
|
||||
|
||||
fn clone(&mut self) -> Result<Box<dyn NotificationService>, ()> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn protocol(&self) -> &ProtocolName {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn message_sink(&self, _peer: &PeerId) -> Option<Box<dyn MessageSink>> {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Tester {
|
||||
pub(crate) net_handle: super::NetworkBridge<Block, TestNetwork, TestSync>,
|
||||
gossip_validator: Arc<GossipValidator<Block>>,
|
||||
pub(crate) events: TracingUnboundedReceiver<Event>,
|
||||
pub(crate) notification_tx: TracingUnboundedSender<NotificationEvent>,
|
||||
}
|
||||
|
||||
impl Tester {
|
||||
fn filter_network_events<F>(self, mut pred: F) -> impl Future<Output = Self>
|
||||
where
|
||||
F: FnMut(Event) -> bool,
|
||||
{
|
||||
let mut s = Some(self);
|
||||
futures::future::poll_fn(move |cx| loop {
|
||||
match Stream::poll_next(Pin::new(&mut s.as_mut().unwrap().events), cx) {
|
||||
Poll::Ready(None) => panic!("concluded early"),
|
||||
Poll::Ready(Some(item)) =>
|
||||
if pred(item) {
|
||||
return Poll::Ready(s.take().unwrap());
|
||||
},
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn trigger_gossip_validator_reputation_change(&self, p: &PeerId) {
|
||||
self.gossip_validator.validate(
|
||||
&mut crate::communication::tests::NoopContext,
|
||||
p,
|
||||
&vec![1, 2, 3],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// some random config (not really needed)
|
||||
fn config() -> crate::Config {
|
||||
crate::Config {
|
||||
gossip_duration: std::time::Duration::from_millis(10),
|
||||
justification_generation_period: 256,
|
||||
keystore: None,
|
||||
name: None,
|
||||
local_role: Role::Authority,
|
||||
observer_enabled: true,
|
||||
telemetry: None,
|
||||
protocol_name: grandpa_protocol_name::NAME.into(),
|
||||
}
|
||||
}
|
||||
|
||||
// dummy voter set state
|
||||
fn voter_set_state() -> SharedVoterSetState<Block> {
|
||||
use crate::{authorities::AuthoritySet, environment::VoterSetState};
|
||||
use finality_grandpa::round::State as RoundState;
|
||||
use pezsp_consensus_grandpa::AuthorityId;
|
||||
use pezsp_core::{crypto::ByteArray, H256};
|
||||
|
||||
let state = RoundState::genesis((H256::zero(), 0));
|
||||
let base = state.prevote_ghost.unwrap();
|
||||
|
||||
let voters = vec![(AuthorityId::from_slice(&[1; 32]).unwrap(), 1)];
|
||||
let voters = AuthoritySet::genesis(voters).unwrap();
|
||||
|
||||
let set_state = VoterSetState::live(0, &voters, base);
|
||||
|
||||
set_state.into()
|
||||
}
|
||||
|
||||
// needs to run in a tokio runtime.
|
||||
pub(crate) fn make_test_network() -> (impl Future<Output = Tester>, TestNetwork) {
|
||||
let (tx, rx) = tracing_unbounded("test", 100_000);
|
||||
let (notification_tx, notification_rx) = tracing_unbounded("test-notification", 100_000);
|
||||
|
||||
let notification_service = TestNotificationService { rx: notification_rx, sender: tx.clone() };
|
||||
let net = TestNetwork { sender: tx };
|
||||
let sync = TestSync {};
|
||||
|
||||
let bridge = super::NetworkBridge::new(
|
||||
net.clone(),
|
||||
sync,
|
||||
Box::new(notification_service),
|
||||
config(),
|
||||
voter_set_state(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
(
|
||||
futures::future::ready(Tester {
|
||||
gossip_validator: bridge.validator.clone(),
|
||||
net_handle: bridge,
|
||||
events: rx,
|
||||
notification_tx,
|
||||
}),
|
||||
net,
|
||||
)
|
||||
}
|
||||
|
||||
fn make_ids(keys: &[Ed25519Keyring]) -> AuthorityList {
|
||||
keys.iter().map(|&key| key.public().into()).map(|id| (id, 1)).collect()
|
||||
}
|
||||
|
||||
struct NoopContext;
|
||||
|
||||
impl pezsc_network_gossip::ValidatorContext<Block> for NoopContext {
|
||||
fn broadcast_topic(&mut self, _: Hash, _: bool) {}
|
||||
fn broadcast_message(&mut self, _: Hash, _: Vec<u8>, _: bool) {}
|
||||
fn send_message(&mut self, _: &PeerId, _: Vec<u8>) {}
|
||||
fn send_topic(&mut self, _: &PeerId, _: Hash, _: bool) {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn good_commit_leads_to_relay() {
|
||||
let private = [Ed25519Keyring::Alice, Ed25519Keyring::Bob, Ed25519Keyring::Charlie];
|
||||
let public = make_ids(&private[..]);
|
||||
let voter_set = Arc::new(VoterSet::new(public.iter().cloned()).unwrap());
|
||||
|
||||
let round = 1;
|
||||
let set_id = 1;
|
||||
|
||||
let commit = {
|
||||
let target_hash: Hash = [1; 32].into();
|
||||
let target_number = 500;
|
||||
|
||||
let precommit = finality_grandpa::Precommit { target_hash, target_number };
|
||||
let payload = pezsp_consensus_grandpa::localized_payload(
|
||||
round,
|
||||
set_id,
|
||||
&finality_grandpa::Message::Precommit(precommit.clone()),
|
||||
);
|
||||
|
||||
let mut precommits = Vec::new();
|
||||
let mut auth_data = Vec::new();
|
||||
|
||||
for (i, key) in private.iter().enumerate() {
|
||||
precommits.push(precommit.clone());
|
||||
|
||||
let signature = pezsp_consensus_grandpa::AuthoritySignature::from(key.sign(&payload[..]));
|
||||
auth_data.push((signature, public[i].0.clone()))
|
||||
}
|
||||
|
||||
finality_grandpa::CompactCommit { target_hash, target_number, precommits, auth_data }
|
||||
};
|
||||
|
||||
let encoded_commit = gossip::GossipMessage::<Block>::Commit(gossip::FullCommitMessage {
|
||||
round: Round(round),
|
||||
set_id: SetId(set_id),
|
||||
message: commit,
|
||||
})
|
||||
.encode();
|
||||
|
||||
let id = PeerId::random();
|
||||
let global_topic = super::global_topic::<Block>(set_id);
|
||||
|
||||
let test = make_test_network()
|
||||
.0
|
||||
.then(move |tester| {
|
||||
// register a peer.
|
||||
tester.gossip_validator.new_peer(&mut NoopContext, &id, ObservedRole::Full);
|
||||
future::ready((tester, id))
|
||||
})
|
||||
.then(move |(tester, id)| {
|
||||
// start round, dispatch commit, and wait for broadcast.
|
||||
let (commits_in, _) =
|
||||
tester.net_handle.global_communication(SetId(1), voter_set, false);
|
||||
|
||||
{
|
||||
let (action, ..) = tester.gossip_validator.do_validate(&id, &encoded_commit[..]);
|
||||
match action {
|
||||
gossip::Action::ProcessAndDiscard(t, _) => assert_eq!(t, global_topic),
|
||||
_ => panic!("wrong expected outcome from initial commit validation"),
|
||||
}
|
||||
}
|
||||
|
||||
let commit_to_send = encoded_commit.clone();
|
||||
let network_bridge = tester.net_handle.clone();
|
||||
|
||||
// `NetworkBridge` will be operational as soon as it's created and it's
|
||||
// waiting for events from the network. Send it events that inform that
|
||||
// a notification stream was opened and that a notification was received.
|
||||
//
|
||||
// Since each protocol has its own notification stream, events need not be filtered.
|
||||
let sender_id = id;
|
||||
|
||||
let send_message = async move {
|
||||
let _ = tester.notification_tx.unbounded_send(
|
||||
NotificationEvent::NotificationStreamOpened {
|
||||
peer: sender_id,
|
||||
direction: Direction::Inbound,
|
||||
negotiated_fallback: None,
|
||||
handshake: Roles::FULL.encode(),
|
||||
},
|
||||
);
|
||||
let _ = tester.notification_tx.unbounded_send(
|
||||
NotificationEvent::NotificationReceived {
|
||||
peer: sender_id,
|
||||
notification: commit_to_send.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
// Add a random peer which will be the recipient of this message
|
||||
let receiver_id = PeerId::random();
|
||||
let _ = tester.notification_tx.unbounded_send(
|
||||
NotificationEvent::NotificationStreamOpened {
|
||||
peer: receiver_id,
|
||||
direction: Direction::Inbound,
|
||||
negotiated_fallback: None,
|
||||
handshake: Roles::FULL.encode(),
|
||||
},
|
||||
);
|
||||
|
||||
// Announce its local set being on the current set id through a neighbor
|
||||
// packet, otherwise it won't be eligible to receive the commit
|
||||
let _ = {
|
||||
let update = gossip::VersionedNeighborPacket::V1(gossip::NeighborPacket {
|
||||
round: Round(round),
|
||||
set_id: SetId(set_id),
|
||||
commit_finalized_height: 1,
|
||||
});
|
||||
|
||||
let msg = gossip::GossipMessage::<Block>::Neighbor(update);
|
||||
|
||||
let _ = tester.notification_tx.unbounded_send(
|
||||
NotificationEvent::NotificationReceived {
|
||||
peer: receiver_id,
|
||||
notification: msg.encode(),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
tester
|
||||
}
|
||||
.boxed();
|
||||
|
||||
// when the commit comes in, we'll tell the callback it was good.
|
||||
let handle_commit = commits_in.into_future().map(|(item, _)| match item.unwrap() {
|
||||
finality_grandpa::voter::CommunicationIn::Commit(_, _, mut callback) => {
|
||||
callback.run(finality_grandpa::voter::CommitProcessingOutcome::good());
|
||||
},
|
||||
_ => panic!("commit expected"),
|
||||
});
|
||||
|
||||
// once the message is sent and commit is "handled" we should have
|
||||
// a repropagation event coming from the network.
|
||||
let fut = future::join(send_message, handle_commit)
|
||||
.then(move |(tester, ())| {
|
||||
tester.filter_network_events(move |event| match event {
|
||||
Event::WriteNotification(_, data) => data == encoded_commit,
|
||||
_ => false,
|
||||
})
|
||||
})
|
||||
.map(|_| ());
|
||||
|
||||
// Poll both the future sending and handling the commit, as well as the underlying
|
||||
// NetworkBridge. Complete once the former completes.
|
||||
future::select(fut, network_bridge)
|
||||
});
|
||||
|
||||
futures::executor::block_on(test);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_commit_leads_to_report() {
|
||||
pezsp_tracing::try_init_simple();
|
||||
let private = [Ed25519Keyring::Alice, Ed25519Keyring::Bob, Ed25519Keyring::Charlie];
|
||||
let public = make_ids(&private[..]);
|
||||
let voter_set = Arc::new(VoterSet::new(public.iter().cloned()).unwrap());
|
||||
|
||||
let round = 1;
|
||||
let set_id = 1;
|
||||
|
||||
let commit = {
|
||||
let target_hash: Hash = [1; 32].into();
|
||||
let target_number = 500;
|
||||
|
||||
let precommit = finality_grandpa::Precommit { target_hash, target_number };
|
||||
let payload = pezsp_consensus_grandpa::localized_payload(
|
||||
round,
|
||||
set_id,
|
||||
&finality_grandpa::Message::Precommit(precommit.clone()),
|
||||
);
|
||||
|
||||
let mut precommits = Vec::new();
|
||||
let mut auth_data = Vec::new();
|
||||
|
||||
for (i, key) in private.iter().enumerate() {
|
||||
precommits.push(precommit.clone());
|
||||
|
||||
let signature = pezsp_consensus_grandpa::AuthoritySignature::from(key.sign(&payload[..]));
|
||||
auth_data.push((signature, public[i].0.clone()))
|
||||
}
|
||||
|
||||
finality_grandpa::CompactCommit { target_hash, target_number, precommits, auth_data }
|
||||
};
|
||||
|
||||
let encoded_commit = gossip::GossipMessage::<Block>::Commit(gossip::FullCommitMessage {
|
||||
round: Round(round),
|
||||
set_id: SetId(set_id),
|
||||
message: commit,
|
||||
})
|
||||
.encode();
|
||||
|
||||
let id = PeerId::random();
|
||||
let global_topic = super::global_topic::<Block>(set_id);
|
||||
|
||||
let test = make_test_network()
|
||||
.0
|
||||
.map(move |tester| {
|
||||
// register a peer.
|
||||
tester.gossip_validator.new_peer(&mut NoopContext, &id, ObservedRole::Full);
|
||||
(tester, id)
|
||||
})
|
||||
.then(move |(tester, id)| {
|
||||
// start round, dispatch commit, and wait for broadcast.
|
||||
let (commits_in, _) =
|
||||
tester.net_handle.global_communication(SetId(1), voter_set, false);
|
||||
|
||||
{
|
||||
let (action, ..) = tester.gossip_validator.do_validate(&id, &encoded_commit[..]);
|
||||
match action {
|
||||
gossip::Action::ProcessAndDiscard(t, _) => assert_eq!(t, global_topic),
|
||||
_ => panic!("wrong expected outcome from initial commit validation"),
|
||||
}
|
||||
}
|
||||
|
||||
let commit_to_send = encoded_commit.clone();
|
||||
let network_bridge = tester.net_handle.clone();
|
||||
|
||||
// `NetworkBridge` will be operational as soon as it's created and it's
|
||||
// waiting for events from the network. Send it events that inform that
|
||||
// a notification stream was opened and that a notification was received.
|
||||
//
|
||||
// Since each protocol has its own notification stream, events need not be filtered.
|
||||
let sender_id = id;
|
||||
|
||||
let send_message = async move {
|
||||
let _ = tester.notification_tx.unbounded_send(
|
||||
NotificationEvent::NotificationStreamOpened {
|
||||
peer: sender_id,
|
||||
direction: Direction::Inbound,
|
||||
negotiated_fallback: None,
|
||||
handshake: Roles::FULL.encode(),
|
||||
},
|
||||
);
|
||||
let _ = tester.notification_tx.unbounded_send(
|
||||
NotificationEvent::NotificationReceived {
|
||||
peer: sender_id,
|
||||
notification: commit_to_send.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
tester
|
||||
}
|
||||
.boxed();
|
||||
|
||||
// when the commit comes in, we'll tell the callback it was bad.
|
||||
let handle_commit = commits_in.into_future().map(|(item, _)| match item.unwrap() {
|
||||
finality_grandpa::voter::CommunicationIn::Commit(_, _, mut callback) => {
|
||||
callback.run(finality_grandpa::voter::CommitProcessingOutcome::bad());
|
||||
},
|
||||
_ => panic!("commit expected"),
|
||||
});
|
||||
|
||||
// once the message is sent and commit is "handled" we should have
|
||||
// a report event coming from the network.
|
||||
let fut = future::join(send_message, handle_commit)
|
||||
.then(move |(tester, ())| {
|
||||
tester.filter_network_events(move |event| match event {
|
||||
Event::Report(who, cost_benefit) =>
|
||||
who == id && cost_benefit == super::cost::INVALID_COMMIT,
|
||||
_ => false,
|
||||
})
|
||||
})
|
||||
.map(|_| ());
|
||||
|
||||
// Poll both the future sending and handling the commit, as well as the underlying
|
||||
// NetworkBridge. Complete once the former completes.
|
||||
future::select(fut, network_bridge)
|
||||
});
|
||||
|
||||
futures::executor::block_on(test);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_with_higher_view_leads_to_catch_up_request() {
|
||||
let id = PeerId::random();
|
||||
|
||||
let (tester, mut net) = make_test_network();
|
||||
let test = tester
|
||||
.map(move |tester| {
|
||||
// register a peer with authority role.
|
||||
tester.gossip_validator.new_peer(&mut NoopContext, &id, ObservedRole::Authority);
|
||||
(tester, id)
|
||||
})
|
||||
.then(move |(tester, id)| {
|
||||
// send neighbor message at round 10 and height 50
|
||||
let result = tester.gossip_validator.validate(
|
||||
&mut net,
|
||||
&id,
|
||||
&gossip::GossipMessage::<Block>::from(gossip::NeighborPacket {
|
||||
set_id: SetId(0),
|
||||
round: Round(10),
|
||||
commit_finalized_height: 50,
|
||||
})
|
||||
.encode(),
|
||||
);
|
||||
|
||||
// neighbor packets are always discard
|
||||
match result {
|
||||
pezsc_network_gossip::ValidationResult::Discard => {},
|
||||
_ => panic!("wrong expected outcome from neighbor validation"),
|
||||
}
|
||||
|
||||
// a catch up request should be sent to the peer for round - 1
|
||||
tester
|
||||
.filter_network_events(move |event| match event {
|
||||
Event::WriteNotification(peer, message) => {
|
||||
assert_eq!(peer, id);
|
||||
|
||||
assert_eq!(
|
||||
message,
|
||||
gossip::GossipMessage::<Block>::CatchUpRequest(
|
||||
gossip::CatchUpRequestMessage { set_id: SetId(0), round: Round(9) }
|
||||
)
|
||||
.encode(),
|
||||
);
|
||||
|
||||
true
|
||||
},
|
||||
_ => false,
|
||||
})
|
||||
.map(|_| ())
|
||||
});
|
||||
|
||||
futures::executor::block_on(test);
|
||||
}
|
||||
|
||||
fn local_chain_spec() -> Box<dyn pezsc_chain_spec::ChainSpec> {
|
||||
let chain_spec =
|
||||
pezsc_chain_spec::GenericChainSpec::<pezsc_chain_spec::NoExtension, ()>::from_json_bytes(
|
||||
&include_bytes!("../../../../chain-spec/res/chain_spec.json")[..],
|
||||
)
|
||||
.unwrap();
|
||||
pezsc_chain_spec::ChainSpec::cloned_box(&chain_spec)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grandpa_protocol_name() {
|
||||
let chain_spec = local_chain_spec();
|
||||
|
||||
// Create protocol name using random genesis hash.
|
||||
let genesis_hash = pezsp_core::H256::random();
|
||||
let expected = format!("/{}/grandpa/1", array_bytes::bytes2hex("", genesis_hash));
|
||||
let proto_name = grandpa_protocol_name::standard_name(&genesis_hash, &chain_spec);
|
||||
assert_eq!(proto_name.to_string(), expected);
|
||||
|
||||
// Create protocol name using hardcoded genesis hash. Verify exact representation.
|
||||
let genesis_hash = [
|
||||
53, 79, 112, 97, 119, 217, 39, 202, 147, 138, 225, 38, 88, 182, 215, 185, 110, 88, 8, 53,
|
||||
125, 210, 158, 151, 50, 113, 102, 59, 245, 199, 221, 240,
|
||||
];
|
||||
let expected =
|
||||
"/354f706177d927ca938ae12658b6d7b96e5808357dd29e973271663bf5c7ddf0/grandpa/1".to_string();
|
||||
let proto_name = grandpa_protocol_name::standard_name(&genesis_hash, &chain_spec);
|
||||
assert_eq!(proto_name.to_string(), expected);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,598 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! GRANDPA block finality proof generation and check.
|
||||
//!
|
||||
//! Finality of block B is proved by providing:
|
||||
//! 1) the justification for the descendant block F;
|
||||
//! 2) headers sub-chain (B; F] if B != F;
|
||||
//! 3) proof of GRANDPA::authorities() if the set changes at block F.
|
||||
//!
|
||||
//! Since earliest possible justification is returned, the GRANDPA authorities set
|
||||
//! at the block F is guaranteed to be the same as in the block B (this is because block
|
||||
//! that enacts new GRANDPA authorities set always comes with justification). It also
|
||||
//! means that the `set_id` is the same at blocks B and F.
|
||||
//!
|
||||
//! Let U be the last finalized block known to caller. If authorities set has changed several
|
||||
//! times in the (U; F] interval, multiple finality proof fragments are returned (one for each
|
||||
//! authority set change) and they must be verified in-order.
|
||||
//!
|
||||
//! Finality proof provider can choose how to provide finality proof on its own. The incomplete
|
||||
//! finality proof (that finalizes some block C that is ancestor of the B and descendant
|
||||
//! of the U) could be returned.
|
||||
|
||||
use log::{trace, warn};
|
||||
use std::sync::Arc;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use pezsc_client_api::backend::Backend;
|
||||
use pezsp_blockchain::{Backend as BlockchainBackend, HeaderBackend};
|
||||
use pezsp_consensus_grandpa::GRANDPA_ENGINE_ID;
|
||||
use pezsp_runtime::{
|
||||
generic::BlockId,
|
||||
traits::{Block as BlockT, Header as HeaderT, NumberFor, One},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
authorities::{AuthoritySetChangeId, AuthoritySetChanges},
|
||||
best_justification,
|
||||
justification::GrandpaJustification,
|
||||
SharedAuthoritySet, LOG_TARGET,
|
||||
};
|
||||
|
||||
const MAX_UNKNOWN_HEADERS: usize = 100_000;
|
||||
|
||||
/// Finality proof provider for serving network requests.
|
||||
#[derive(Clone)]
|
||||
pub struct FinalityProofProvider<BE, Block: BlockT> {
|
||||
backend: Arc<BE>,
|
||||
shared_authority_set: Option<SharedAuthoritySet<Block::Hash, NumberFor<Block>>>,
|
||||
}
|
||||
|
||||
impl<B, Block> FinalityProofProvider<B, Block>
|
||||
where
|
||||
Block: BlockT,
|
||||
B: Backend<Block>,
|
||||
{
|
||||
/// Create new finality proof provider using:
|
||||
///
|
||||
/// - backend for accessing blockchain data;
|
||||
/// - authority_provider for calling and proving runtime methods.
|
||||
/// - shared_authority_set for accessing authority set data
|
||||
pub fn new(
|
||||
backend: Arc<B>,
|
||||
shared_authority_set: Option<SharedAuthoritySet<Block::Hash, NumberFor<Block>>>,
|
||||
) -> Self {
|
||||
FinalityProofProvider { backend, shared_authority_set }
|
||||
}
|
||||
|
||||
/// Create new finality proof provider for the service using:
|
||||
///
|
||||
/// - backend for accessing blockchain data;
|
||||
/// - storage_provider, which is generally a client.
|
||||
/// - shared_authority_set for accessing authority set data
|
||||
pub fn new_for_service(
|
||||
backend: Arc<B>,
|
||||
shared_authority_set: Option<SharedAuthoritySet<Block::Hash, NumberFor<Block>>>,
|
||||
) -> Arc<Self> {
|
||||
Arc::new(Self::new(backend, shared_authority_set))
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, Block> FinalityProofProvider<B, Block>
|
||||
where
|
||||
Block: BlockT,
|
||||
B: Backend<Block>,
|
||||
{
|
||||
/// Prove finality for the given block number by returning a Justification for the last block of
|
||||
/// the authority set in bytes.
|
||||
pub fn prove_finality(
|
||||
&self,
|
||||
block: NumberFor<Block>,
|
||||
) -> Result<Option<Vec<u8>>, FinalityProofError> {
|
||||
Ok(self.prove_finality_proof(block, true)?.map(|proof| proof.encode()))
|
||||
}
|
||||
|
||||
/// Prove finality for the given block number by returning a Justification for the last block of
|
||||
/// the authority set.
|
||||
///
|
||||
/// If `collect_unknown_headers` is true, the finality proof will include all headers from the
|
||||
/// requested block until the block the justification refers to.
|
||||
pub fn prove_finality_proof(
|
||||
&self,
|
||||
block: NumberFor<Block>,
|
||||
collect_unknown_headers: bool,
|
||||
) -> Result<Option<FinalityProof<Block::Header>>, FinalityProofError> {
|
||||
let authority_set_changes = if let Some(changes) = self
|
||||
.shared_authority_set
|
||||
.as_ref()
|
||||
.map(SharedAuthoritySet::authority_set_changes)
|
||||
{
|
||||
changes
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
prove_finality(&*self.backend, authority_set_changes, block, collect_unknown_headers)
|
||||
}
|
||||
}
|
||||
|
||||
/// Finality for block B is proved by providing:
|
||||
/// 1) the justification for the descendant block F;
|
||||
/// 2) headers sub-chain (B; F] if B != F;
|
||||
#[derive(Debug, PartialEq, Encode, Decode, Clone)]
|
||||
pub struct FinalityProof<Header: HeaderT> {
|
||||
/// The hash of block F for which justification is provided.
|
||||
pub block: Header::Hash,
|
||||
/// Justification of the block F.
|
||||
pub justification: Vec<u8>,
|
||||
/// The set of headers in the range (B; F] that we believe are unknown to the caller. Ordered.
|
||||
pub unknown_headers: Vec<Header>,
|
||||
}
|
||||
|
||||
/// Errors occurring when trying to prove finality
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FinalityProofError {
|
||||
/// The requested block has not yet been finalized.
|
||||
#[error("Block not yet finalized")]
|
||||
BlockNotYetFinalized,
|
||||
/// The requested block is not covered by authority set changes. Likely this means the block is
|
||||
/// in the latest authority set, and the subscription API is more appropriate.
|
||||
#[error("Block not covered by authority set changes")]
|
||||
BlockNotInAuthoritySetChanges,
|
||||
/// Errors originating from the client.
|
||||
#[error(transparent)]
|
||||
Client(#[from] pezsp_blockchain::Error),
|
||||
}
|
||||
|
||||
/// Prove finality for the given block number by returning a justification for the last block of
|
||||
/// the authority set of which the given block is part of, or a justification for the latest
|
||||
/// finalized block if the given block is part of the current authority set.
|
||||
///
|
||||
/// If `collect_unknown_headers` is true, the finality proof will include all headers from the
|
||||
/// requested block until the block the justification refers to.
|
||||
fn prove_finality<Block, B>(
|
||||
backend: &B,
|
||||
authority_set_changes: AuthoritySetChanges<NumberFor<Block>>,
|
||||
block: NumberFor<Block>,
|
||||
collect_unknown_headers: bool,
|
||||
) -> Result<Option<FinalityProof<Block::Header>>, FinalityProofError>
|
||||
where
|
||||
Block: BlockT,
|
||||
B: Backend<Block>,
|
||||
{
|
||||
// Early-return if we are sure that there are no blocks finalized that cover the requested
|
||||
// block.
|
||||
let finalized_number = backend.blockchain().info().finalized_number;
|
||||
if finalized_number < block {
|
||||
let err = format!(
|
||||
"Requested finality proof for descendant of #{} while we only have finalized #{}.",
|
||||
block, finalized_number,
|
||||
);
|
||||
trace!(target: LOG_TARGET, "{}", &err);
|
||||
return Err(FinalityProofError::BlockNotYetFinalized);
|
||||
}
|
||||
|
||||
let (justification, just_block) = match authority_set_changes.get_set_id(block) {
|
||||
AuthoritySetChangeId::Latest => {
|
||||
if let Some(justification) = best_justification(backend)?
|
||||
.map(|j: GrandpaJustification<Block>| (j.encode(), j.target().0))
|
||||
{
|
||||
justification
|
||||
} else {
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"No justification found for the latest finalized block. \
|
||||
Returning empty proof.",
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
},
|
||||
AuthoritySetChangeId::Set(_, last_block_for_set) => {
|
||||
let last_block_for_set_id = backend
|
||||
.blockchain()
|
||||
.expect_block_hash_from_id(&BlockId::Number(last_block_for_set))?;
|
||||
let justification = if let Some(grandpa_justification) = backend
|
||||
.blockchain()
|
||||
.justifications(last_block_for_set_id)?
|
||||
.and_then(|justifications| justifications.into_justification(GRANDPA_ENGINE_ID))
|
||||
{
|
||||
grandpa_justification
|
||||
} else {
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"No justification found when making finality proof for {}. \
|
||||
Returning empty proof.",
|
||||
block,
|
||||
);
|
||||
return Ok(None);
|
||||
};
|
||||
(justification, last_block_for_set)
|
||||
},
|
||||
AuthoritySetChangeId::Unknown => {
|
||||
warn!(
|
||||
target: LOG_TARGET,
|
||||
"AuthoritySetChanges does not cover the requested block #{} due to missing data. \
|
||||
You need to resync to populate AuthoritySetChanges properly.",
|
||||
block,
|
||||
);
|
||||
return Err(FinalityProofError::BlockNotInAuthoritySetChanges);
|
||||
},
|
||||
};
|
||||
|
||||
let mut headers = Vec::new();
|
||||
if collect_unknown_headers {
|
||||
// Collect all headers from the requested block until the last block of the set
|
||||
let mut current = block + One::one();
|
||||
loop {
|
||||
if current > just_block || headers.len() >= MAX_UNKNOWN_HEADERS {
|
||||
break;
|
||||
}
|
||||
let hash = backend.blockchain().expect_block_hash_from_id(&BlockId::Number(current))?;
|
||||
headers.push(backend.blockchain().expect_header(hash)?);
|
||||
current += One::one();
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(FinalityProof {
|
||||
block: backend.blockchain().expect_block_hash_from_id(&BlockId::Number(just_block))?,
|
||||
justification,
|
||||
unknown_headers: headers,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{authorities::AuthoritySetChanges, BlockNumberOps, ClientError, SetId};
|
||||
use futures::executor::block_on;
|
||||
use pezsc_block_builder::BlockBuilderBuilder;
|
||||
use pezsc_client_api::{apply_aux, LockImportRun};
|
||||
use pezsp_consensus::BlockOrigin;
|
||||
use pezsp_consensus_grandpa::GRANDPA_ENGINE_ID as ID;
|
||||
use pezsp_core::crypto::UncheckedFrom;
|
||||
use pezsp_keyring::Ed25519Keyring;
|
||||
use bizinikiwi_test_runtime_client::{
|
||||
runtime::{Block, Header, H256},
|
||||
Backend as TestBackend, ClientBlockImportExt, ClientExt, DefaultTestClientBuilderExt,
|
||||
TestClient, TestClientBuilder, TestClientBuilderExt,
|
||||
};
|
||||
|
||||
/// Check GRANDPA proof-of-finality for the given block.
|
||||
///
|
||||
/// Returns the vector of headers that MUST be validated + imported
|
||||
/// AND if at least one of those headers is invalid, all other MUST be considered invalid.
|
||||
fn check_finality_proof<Block: BlockT>(
|
||||
current_set_id: SetId,
|
||||
current_authorities: pezsp_consensus_grandpa::AuthorityList,
|
||||
remote_proof: Vec<u8>,
|
||||
) -> pezsp_blockchain::Result<super::FinalityProof<Block::Header>>
|
||||
where
|
||||
NumberFor<Block>: BlockNumberOps,
|
||||
{
|
||||
let proof = super::FinalityProof::<Block::Header>::decode(&mut &remote_proof[..])
|
||||
.map_err(|_| ClientError::BadJustification("failed to decode finality proof".into()))?;
|
||||
|
||||
let justification: GrandpaJustification<Block> =
|
||||
Decode::decode(&mut &proof.justification[..])
|
||||
.map_err(|_| ClientError::JustificationDecode)?;
|
||||
|
||||
justification.verify(current_set_id, ¤t_authorities)?;
|
||||
|
||||
Ok(proof)
|
||||
}
|
||||
|
||||
pub(crate) type FinalityProof = super::FinalityProof<Header>;
|
||||
|
||||
fn header(number: u64) -> Header {
|
||||
let parent_hash = match number {
|
||||
0 => Default::default(),
|
||||
_ => header(number - 1).hash(),
|
||||
};
|
||||
Header::new(
|
||||
number,
|
||||
H256::from_low_u64_be(0),
|
||||
H256::from_low_u64_be(0),
|
||||
parent_hash,
|
||||
Default::default(),
|
||||
)
|
||||
}
|
||||
|
||||
fn test_blockchain(
|
||||
number_of_blocks: u64,
|
||||
to_finalize: &[u64],
|
||||
) -> (Arc<TestClient>, Arc<TestBackend>, Vec<Block>) {
|
||||
let builder = TestClientBuilder::new();
|
||||
let backend = builder.backend();
|
||||
let client = Arc::new(builder.build());
|
||||
|
||||
let mut blocks = Vec::new();
|
||||
for _ in 0..number_of_blocks {
|
||||
let block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
block_on(client.import(BlockOrigin::Own, block.clone())).unwrap();
|
||||
blocks.push(block);
|
||||
}
|
||||
|
||||
for block in to_finalize {
|
||||
let hash = blocks[*block as usize - 1].hash();
|
||||
client.finalize_block(hash, None).unwrap();
|
||||
}
|
||||
(client, backend, blocks)
|
||||
}
|
||||
|
||||
fn store_best_justification(client: &TestClient, just: &GrandpaJustification<Block>) {
|
||||
client
|
||||
.lock_import_and_run(|import_op| {
|
||||
crate::aux_schema::update_best_justification(just, |insert| {
|
||||
apply_aux(import_op, insert, &[])
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finality_proof_fails_if_no_more_last_finalized_blocks() {
|
||||
let (_, backend, _) = test_blockchain(6, &[4]);
|
||||
let authority_set_changes = AuthoritySetChanges::empty();
|
||||
|
||||
// The last finalized block is 4, so we cannot provide further justifications.
|
||||
let proof_of_5 = prove_finality(&*backend, authority_set_changes, 5, true);
|
||||
assert!(matches!(proof_of_5, Err(FinalityProofError::BlockNotYetFinalized)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finality_proof_is_none_if_no_justification_known() {
|
||||
let (_, backend, _) = test_blockchain(6, &[4]);
|
||||
|
||||
let mut authority_set_changes = AuthoritySetChanges::empty();
|
||||
authority_set_changes.append(0, 4);
|
||||
|
||||
// Block 4 is finalized without justification
|
||||
// => we can't prove finality of 3
|
||||
let proof_of_3 = prove_finality(&*backend, authority_set_changes, 3, true).unwrap();
|
||||
assert_eq!(proof_of_3, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finality_proof_check_fails_when_proof_decode_fails() {
|
||||
// When we can't decode proof from Vec<u8>
|
||||
check_finality_proof::<Block>(
|
||||
1,
|
||||
vec![(UncheckedFrom::unchecked_from([3u8; 32]), 1u64)],
|
||||
vec![42],
|
||||
)
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finality_proof_check_fails_when_proof_is_empty() {
|
||||
// When decoded proof has zero length
|
||||
check_finality_proof::<Block>(
|
||||
1,
|
||||
vec![(UncheckedFrom::unchecked_from([3u8; 32]), 1u64)],
|
||||
Vec::<GrandpaJustification<Block>>::new().encode(),
|
||||
)
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finality_proof_check_fails_with_incomplete_justification() {
|
||||
let (_, _, blocks) = test_blockchain(8, &[4, 5, 8]);
|
||||
|
||||
// Create a commit without precommits
|
||||
let commit = finality_grandpa::Commit {
|
||||
target_hash: blocks[7].hash(),
|
||||
target_number: *blocks[7].header().number(),
|
||||
precommits: Vec::new(),
|
||||
};
|
||||
|
||||
let grandpa_just: GrandpaJustification<Block> =
|
||||
pezsp_consensus_grandpa::GrandpaJustification::<Header> {
|
||||
round: 8,
|
||||
votes_ancestries: Vec::new(),
|
||||
commit,
|
||||
}
|
||||
.into();
|
||||
|
||||
let finality_proof = FinalityProof {
|
||||
block: header(2).hash(),
|
||||
justification: grandpa_just.encode(),
|
||||
unknown_headers: Vec::new(),
|
||||
};
|
||||
|
||||
check_finality_proof::<Block>(
|
||||
1,
|
||||
vec![(UncheckedFrom::unchecked_from([3u8; 32]), 1u64)],
|
||||
finality_proof.encode(),
|
||||
)
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
fn create_commit<S, Id>(
|
||||
block: Block,
|
||||
round: u64,
|
||||
set_id: SetId,
|
||||
auth: &[Ed25519Keyring],
|
||||
) -> finality_grandpa::Commit<H256, u64, S, Id>
|
||||
where
|
||||
Id: From<pezsp_core::ed25519::Public>,
|
||||
S: From<pezsp_core::ed25519::Signature>,
|
||||
{
|
||||
let mut precommits = Vec::new();
|
||||
|
||||
for voter in auth {
|
||||
let precommit = finality_grandpa::Precommit {
|
||||
target_hash: block.hash(),
|
||||
target_number: *block.header().number(),
|
||||
};
|
||||
|
||||
let msg = finality_grandpa::Message::Precommit(precommit.clone());
|
||||
let encoded = pezsp_consensus_grandpa::localized_payload(round, set_id, &msg);
|
||||
let signature = voter.sign(&encoded[..]).into();
|
||||
|
||||
let signed_precommit = finality_grandpa::SignedPrecommit {
|
||||
precommit,
|
||||
signature,
|
||||
id: voter.public().into(),
|
||||
};
|
||||
precommits.push(signed_precommit);
|
||||
}
|
||||
|
||||
finality_grandpa::Commit {
|
||||
target_hash: block.hash(),
|
||||
target_number: *block.header().number(),
|
||||
precommits,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finality_proof_check_works_with_correct_justification() {
|
||||
let (client, _, blocks) = test_blockchain(8, &[4, 5, 8]);
|
||||
|
||||
let alice = Ed25519Keyring::Alice;
|
||||
let set_id = 1;
|
||||
let round = 8;
|
||||
let commit = create_commit(blocks[7].clone(), round, set_id, &[alice]);
|
||||
let grandpa_just = GrandpaJustification::from_commit(&client, round, commit).unwrap();
|
||||
|
||||
let finality_proof = FinalityProof {
|
||||
block: header(2).hash(),
|
||||
justification: grandpa_just.encode(),
|
||||
unknown_headers: Vec::new(),
|
||||
};
|
||||
assert_eq!(
|
||||
finality_proof,
|
||||
check_finality_proof::<Block>(
|
||||
set_id,
|
||||
vec![(alice.public().into(), 1u64)],
|
||||
finality_proof.encode(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finality_proof_using_authority_set_changes_fails_with_undefined_start() {
|
||||
let (_, backend, _) = test_blockchain(8, &[4, 5, 8]);
|
||||
|
||||
// We have stored the correct block number for the relevant set, but as we are missing the
|
||||
// block for the preceding set the start is not well-defined.
|
||||
let mut authority_set_changes = AuthoritySetChanges::empty();
|
||||
authority_set_changes.append(1, 8);
|
||||
|
||||
let proof_of_6 = prove_finality(&*backend, authority_set_changes, 6, true);
|
||||
assert!(matches!(proof_of_6, Err(FinalityProofError::BlockNotInAuthoritySetChanges)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finality_proof_using_authority_set_changes_works() {
|
||||
let (client, backend, blocks) = test_blockchain(8, &[4, 5]);
|
||||
let block7 = &blocks[6];
|
||||
let block8 = &blocks[7];
|
||||
|
||||
let round = 8;
|
||||
let commit = create_commit(block8.clone(), round, 1, &[Ed25519Keyring::Alice]);
|
||||
let grandpa_just8 = GrandpaJustification::from_commit(&client, round, commit).unwrap();
|
||||
|
||||
client
|
||||
.finalize_block(block8.hash(), Some((ID, grandpa_just8.encode().clone())))
|
||||
.unwrap();
|
||||
|
||||
// Authority set change at block 8, so the justification stored there will be used in the
|
||||
// FinalityProof for block 6
|
||||
let mut authority_set_changes = AuthoritySetChanges::empty();
|
||||
authority_set_changes.append(0, 5);
|
||||
authority_set_changes.append(1, 8);
|
||||
|
||||
let proof_of_6: FinalityProof =
|
||||
prove_finality(&*backend, authority_set_changes.clone(), 6, true)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
proof_of_6,
|
||||
FinalityProof {
|
||||
block: block8.hash(),
|
||||
justification: grandpa_just8.encode(),
|
||||
unknown_headers: vec![block7.header().clone(), block8.header().clone()],
|
||||
},
|
||||
);
|
||||
|
||||
let proof_of_6_without_unknown: FinalityProof =
|
||||
prove_finality(&*backend, authority_set_changes.clone(), 6, false)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
proof_of_6_without_unknown,
|
||||
FinalityProof {
|
||||
block: block8.hash(),
|
||||
justification: grandpa_just8.encode(),
|
||||
unknown_headers: vec![],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finality_proof_in_last_set_fails_without_latest() {
|
||||
let (_, backend, _) = test_blockchain(8, &[4, 5, 8]);
|
||||
|
||||
// No recent authority set change, so we are in the latest set, and we will try to pickup
|
||||
// the best stored justification, for which there is none in this case.
|
||||
let mut authority_set_changes = AuthoritySetChanges::empty();
|
||||
authority_set_changes.append(0, 5);
|
||||
|
||||
assert!(matches!(prove_finality(&*backend, authority_set_changes, 6, true), Ok(None)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finality_proof_in_last_set_using_latest_justification_works() {
|
||||
let (client, backend, blocks) = test_blockchain(8, &[4, 5, 8]);
|
||||
let block7 = &blocks[6];
|
||||
let block8 = &blocks[7];
|
||||
|
||||
let round = 8;
|
||||
let commit = create_commit(block8.clone(), round, 1, &[Ed25519Keyring::Alice]);
|
||||
let grandpa_just8 = GrandpaJustification::from_commit(&client, round, commit).unwrap();
|
||||
store_best_justification(&client, &grandpa_just8);
|
||||
|
||||
// No recent authority set change, so we are in the latest set, and will pickup the best
|
||||
// stored justification
|
||||
let mut authority_set_changes = AuthoritySetChanges::empty();
|
||||
authority_set_changes.append(0, 5);
|
||||
|
||||
let proof_of_6: FinalityProof =
|
||||
prove_finality(&*backend, authority_set_changes, 6, true).unwrap().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
proof_of_6,
|
||||
FinalityProof {
|
||||
block: block8.hash(),
|
||||
justification: grandpa_just8.encode(),
|
||||
unknown_headers: vec![block7.header().clone(), block8.header().clone()],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,854 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{collections::HashMap, marker::PhantomData, sync::Arc};
|
||||
|
||||
use codec::Decode;
|
||||
use log::debug;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use pezsc_client_api::{backend::Backend, utils::is_descendent_of};
|
||||
use pezsc_consensus::{
|
||||
shared_data::{SharedDataLocked, SharedDataLockedUpgradable},
|
||||
BlockCheckParams, BlockImport, BlockImportParams, ImportResult, JustificationImport,
|
||||
};
|
||||
use pezsc_telemetry::TelemetryHandle;
|
||||
use pezsc_utils::mpsc::TracingUnboundedSender;
|
||||
use pezsp_api::{Core, RuntimeApiInfo};
|
||||
use pezsp_blockchain::BlockStatus;
|
||||
use pezsp_consensus::{BlockOrigin, Error as ConsensusError, SelectChain};
|
||||
use pezsp_consensus_grandpa::{ConsensusLog, GrandpaApi, ScheduledChange, SetId, GRANDPA_ENGINE_ID};
|
||||
use pezsp_runtime::{
|
||||
generic::OpaqueDigestItemId,
|
||||
traits::{Block as BlockT, Header as HeaderT, NumberFor, Zero},
|
||||
Justification,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
authorities::{AuthoritySet, DelayKind, PendingChange, SharedAuthoritySet},
|
||||
environment,
|
||||
justification::GrandpaJustification,
|
||||
notification::GrandpaJustificationSender,
|
||||
AuthoritySetChanges, ClientForGrandpa, CommandOrError, Error, NewAuthoritySet, VoterCommand,
|
||||
LOG_TARGET,
|
||||
};
|
||||
|
||||
/// A block-import handler for GRANDPA.
|
||||
///
|
||||
/// This scans each imported block for signals of changing authority set.
|
||||
/// If the block being imported enacts an authority set change then:
|
||||
/// - If the current authority set is still live: we import the block
|
||||
/// - Otherwise, the block must include a valid justification.
|
||||
///
|
||||
/// When using GRANDPA, the block import worker should be using this block import
|
||||
/// object.
|
||||
pub struct GrandpaBlockImport<Backend, Block: BlockT, Client, SC> {
|
||||
inner: Arc<Client>,
|
||||
justification_import_period: u32,
|
||||
select_chain: SC,
|
||||
authority_set: SharedAuthoritySet<Block::Hash, NumberFor<Block>>,
|
||||
send_voter_commands: TracingUnboundedSender<VoterCommand<Block::Hash, NumberFor<Block>>>,
|
||||
authority_set_hard_forks:
|
||||
Mutex<HashMap<Block::Hash, PendingChange<Block::Hash, NumberFor<Block>>>>,
|
||||
justification_sender: GrandpaJustificationSender<Block>,
|
||||
telemetry: Option<TelemetryHandle>,
|
||||
_phantom: PhantomData<Backend>,
|
||||
}
|
||||
|
||||
impl<Backend, Block: BlockT, Client, SC: Clone> Clone
|
||||
for GrandpaBlockImport<Backend, Block, Client, SC>
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
GrandpaBlockImport {
|
||||
inner: self.inner.clone(),
|
||||
justification_import_period: self.justification_import_period,
|
||||
select_chain: self.select_chain.clone(),
|
||||
authority_set: self.authority_set.clone(),
|
||||
send_voter_commands: self.send_voter_commands.clone(),
|
||||
authority_set_hard_forks: Mutex::new(self.authority_set_hard_forks.lock().clone()),
|
||||
justification_sender: self.justification_sender.clone(),
|
||||
telemetry: self.telemetry.clone(),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<BE, Block: BlockT, Client, SC> JustificationImport<Block>
|
||||
for GrandpaBlockImport<BE, Block, Client, SC>
|
||||
where
|
||||
NumberFor<Block>: finality_grandpa::BlockNumberOps,
|
||||
BE: Backend<Block>,
|
||||
Client: ClientForGrandpa<Block, BE>,
|
||||
SC: SelectChain<Block>,
|
||||
{
|
||||
type Error = ConsensusError;
|
||||
|
||||
async fn on_start(&mut self) -> Vec<(Block::Hash, NumberFor<Block>)> {
|
||||
let mut out = Vec::new();
|
||||
let chain_info = self.inner.info();
|
||||
|
||||
// request justifications for all pending changes for which change blocks have already been
|
||||
// imported
|
||||
let pending_changes: Vec<_> =
|
||||
self.authority_set.inner().pending_changes().cloned().collect();
|
||||
|
||||
for pending_change in pending_changes {
|
||||
if pending_change.delay_kind == DelayKind::Finalized &&
|
||||
pending_change.effective_number() > chain_info.finalized_number &&
|
||||
pending_change.effective_number() <= chain_info.best_number
|
||||
{
|
||||
let effective_block_hash = if !pending_change.delay.is_zero() {
|
||||
self.select_chain
|
||||
.finality_target(
|
||||
pending_change.canon_hash,
|
||||
Some(pending_change.effective_number()),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Ok(pending_change.canon_hash)
|
||||
};
|
||||
|
||||
if let Ok(hash) = effective_block_hash {
|
||||
if let Ok(Some(header)) = self.inner.header(hash) {
|
||||
if *header.number() == pending_change.effective_number() {
|
||||
out.push((header.hash(), *header.number()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
async fn import_justification(
|
||||
&mut self,
|
||||
hash: Block::Hash,
|
||||
number: NumberFor<Block>,
|
||||
justification: Justification,
|
||||
) -> Result<(), Self::Error> {
|
||||
// this justification was requested by the sync service, therefore we
|
||||
// are not sure if it should enact a change or not. it could have been a
|
||||
// request made as part of initial sync but that means the justification
|
||||
// wasn't part of the block and was requested asynchronously, probably
|
||||
// makes sense to log in that case.
|
||||
GrandpaBlockImport::import_justification(self, hash, number, justification, false, false)
|
||||
}
|
||||
}
|
||||
|
||||
enum AppliedChanges<H, N> {
|
||||
Standard(bool), // true if the change is ready to be applied (i.e. it's a root)
|
||||
Forced(NewAuthoritySet<H, N>),
|
||||
None,
|
||||
}
|
||||
|
||||
impl<H, N> AppliedChanges<H, N> {
|
||||
fn needs_justification(&self) -> bool {
|
||||
match *self {
|
||||
AppliedChanges::Standard(_) => true,
|
||||
AppliedChanges::Forced(_) | AppliedChanges::None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PendingSetChanges<Block: BlockT> {
|
||||
just_in_case: Option<(
|
||||
AuthoritySet<Block::Hash, NumberFor<Block>>,
|
||||
SharedDataLockedUpgradable<AuthoritySet<Block::Hash, NumberFor<Block>>>,
|
||||
)>,
|
||||
applied_changes: AppliedChanges<Block::Hash, NumberFor<Block>>,
|
||||
do_pause: bool,
|
||||
}
|
||||
|
||||
impl<Block: BlockT> PendingSetChanges<Block> {
|
||||
// revert the pending set change explicitly.
|
||||
fn revert(self) {}
|
||||
|
||||
fn defuse(mut self) -> (AppliedChanges<Block::Hash, NumberFor<Block>>, bool) {
|
||||
self.just_in_case = None;
|
||||
let applied_changes = std::mem::replace(&mut self.applied_changes, AppliedChanges::None);
|
||||
(applied_changes, self.do_pause)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block: BlockT> Drop for PendingSetChanges<Block> {
|
||||
fn drop(&mut self) {
|
||||
if let Some((old_set, mut authorities)) = self.just_in_case.take() {
|
||||
*authorities.upgrade() = old_set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks the given header for a consensus digest signalling a **standard** scheduled change and
|
||||
/// extracts it.
|
||||
pub fn find_scheduled_change<B: BlockT>(
|
||||
header: &B::Header,
|
||||
) -> Option<ScheduledChange<NumberFor<B>>> {
|
||||
let id = OpaqueDigestItemId::Consensus(&GRANDPA_ENGINE_ID);
|
||||
|
||||
let filter_log = |log: ConsensusLog<NumberFor<B>>| match log {
|
||||
ConsensusLog::ScheduledChange(change) => Some(change),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// find the first consensus digest with the right ID which converts to
|
||||
// the right kind of consensus log.
|
||||
header.digest().convert_first(|l| l.try_to(id).and_then(filter_log))
|
||||
}
|
||||
|
||||
/// Checks the given header for a consensus digest signalling a **forced** scheduled change and
|
||||
/// extracts it.
|
||||
pub fn find_forced_change<B: BlockT>(
|
||||
header: &B::Header,
|
||||
) -> Option<(NumberFor<B>, ScheduledChange<NumberFor<B>>)> {
|
||||
let id = OpaqueDigestItemId::Consensus(&GRANDPA_ENGINE_ID);
|
||||
|
||||
let filter_log = |log: ConsensusLog<NumberFor<B>>| match log {
|
||||
ConsensusLog::ForcedChange(delay, change) => Some((delay, change)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// find the first consensus digest with the right ID which converts to
|
||||
// the right kind of consensus log.
|
||||
header.digest().convert_first(|l| l.try_to(id).and_then(filter_log))
|
||||
}
|
||||
|
||||
impl<BE, Block: BlockT, Client, SC> GrandpaBlockImport<BE, Block, Client, SC>
|
||||
where
|
||||
NumberFor<Block>: finality_grandpa::BlockNumberOps,
|
||||
BE: Backend<Block>,
|
||||
Client: ClientForGrandpa<Block, BE>,
|
||||
Client::Api: GrandpaApi<Block>,
|
||||
for<'a> &'a Client: BlockImport<Block, Error = ConsensusError>,
|
||||
{
|
||||
// check for a new authority set change.
|
||||
fn check_new_change(
|
||||
&self,
|
||||
header: &Block::Header,
|
||||
hash: Block::Hash,
|
||||
) -> Option<PendingChange<Block::Hash, NumberFor<Block>>> {
|
||||
// check for forced authority set hard forks
|
||||
if let Some(change) = self.authority_set_hard_forks.lock().get(&hash) {
|
||||
return Some(change.clone());
|
||||
}
|
||||
|
||||
// check for forced change.
|
||||
if let Some((median_last_finalized, change)) = find_forced_change::<Block>(header) {
|
||||
return Some(PendingChange {
|
||||
next_authorities: change.next_authorities,
|
||||
delay: change.delay,
|
||||
canon_height: *header.number(),
|
||||
canon_hash: hash,
|
||||
delay_kind: DelayKind::Best { median_last_finalized },
|
||||
});
|
||||
}
|
||||
|
||||
// check normal scheduled change.
|
||||
let change = find_scheduled_change::<Block>(header)?;
|
||||
Some(PendingChange {
|
||||
next_authorities: change.next_authorities,
|
||||
delay: change.delay,
|
||||
canon_height: *header.number(),
|
||||
canon_hash: hash,
|
||||
delay_kind: DelayKind::Finalized,
|
||||
})
|
||||
}
|
||||
|
||||
fn make_authorities_changes(
|
||||
&self,
|
||||
block: &mut BlockImportParams<Block>,
|
||||
hash: Block::Hash,
|
||||
initial_sync: bool,
|
||||
) -> Result<PendingSetChanges<Block>, ConsensusError> {
|
||||
// when we update the authorities, we need to hold the lock
|
||||
// until the block is written to prevent a race if we need to restore
|
||||
// the old authority set on error or panic.
|
||||
struct InnerGuard<'a, H, N> {
|
||||
old: Option<AuthoritySet<H, N>>,
|
||||
guard: Option<SharedDataLocked<'a, AuthoritySet<H, N>>>,
|
||||
}
|
||||
|
||||
impl<'a, H, N> InnerGuard<'a, H, N> {
|
||||
fn as_mut(&mut self) -> &mut AuthoritySet<H, N> {
|
||||
self.guard.as_mut().expect("only taken on deconstruction; qed")
|
||||
}
|
||||
|
||||
fn set_old(&mut self, old: AuthoritySet<H, N>) {
|
||||
if self.old.is_none() {
|
||||
// ignore "newer" old changes.
|
||||
self.old = Some(old);
|
||||
}
|
||||
}
|
||||
|
||||
fn consume(
|
||||
mut self,
|
||||
) -> Option<(AuthoritySet<H, N>, SharedDataLocked<'a, AuthoritySet<H, N>>)> {
|
||||
self.old
|
||||
.take()
|
||||
.map(|old| (old, self.guard.take().expect("only taken on deconstruction; qed")))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, H, N> Drop for InnerGuard<'a, H, N> {
|
||||
fn drop(&mut self) {
|
||||
if let (Some(mut guard), Some(old)) = (self.guard.take(), self.old.take()) {
|
||||
*guard = old;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let number = *(block.header.number());
|
||||
let maybe_change = self.check_new_change(&block.header, hash);
|
||||
|
||||
// returns a function for checking whether a block is a descendent of another
|
||||
// consistent with querying client directly after importing the block.
|
||||
let parent_hash = *block.header.parent_hash();
|
||||
let is_descendent_of = is_descendent_of(&*self.inner, Some((hash, parent_hash)));
|
||||
|
||||
let mut guard = InnerGuard { guard: Some(self.authority_set.inner_locked()), old: None };
|
||||
|
||||
// whether to pause the old authority set -- happens after import
|
||||
// of a forced change block.
|
||||
let mut do_pause = false;
|
||||
|
||||
// add any pending changes.
|
||||
if let Some(change) = maybe_change {
|
||||
let old = guard.as_mut().clone();
|
||||
guard.set_old(old);
|
||||
|
||||
if let DelayKind::Best { .. } = change.delay_kind {
|
||||
do_pause = true;
|
||||
}
|
||||
|
||||
guard
|
||||
.as_mut()
|
||||
.add_pending_change(change, &is_descendent_of)
|
||||
.map_err(|e| ConsensusError::ClientImport(e.to_string()))?;
|
||||
}
|
||||
|
||||
let applied_changes = {
|
||||
let forced_change_set = guard
|
||||
.as_mut()
|
||||
.apply_forced_changes(
|
||||
hash,
|
||||
number,
|
||||
&is_descendent_of,
|
||||
initial_sync,
|
||||
self.telemetry.clone(),
|
||||
)
|
||||
.map_err(|e| ConsensusError::ClientImport(e.to_string()))
|
||||
.map_err(ConsensusError::from)?;
|
||||
|
||||
if let Some((median_last_finalized_number, new_set)) = forced_change_set {
|
||||
let new_authorities = {
|
||||
let (set_id, new_authorities) = new_set.current();
|
||||
|
||||
// we will use the median last finalized number as a hint
|
||||
// for the canon block the new authority set should start
|
||||
// with. we use the minimum between the median and the local
|
||||
// best finalized block.
|
||||
let best_finalized_number = self.inner.info().finalized_number;
|
||||
let canon_number = best_finalized_number.min(median_last_finalized_number);
|
||||
let canon_hash = self.inner.hash(canon_number)
|
||||
.map_err(|e| ConsensusError::ClientImport(e.to_string()))?
|
||||
.expect(
|
||||
"the given block number is less or equal than the current best finalized number; \
|
||||
current best finalized number must exist in chain; qed."
|
||||
);
|
||||
|
||||
NewAuthoritySet {
|
||||
canon_number,
|
||||
canon_hash,
|
||||
set_id,
|
||||
authorities: new_authorities.to_vec(),
|
||||
}
|
||||
};
|
||||
let old = ::std::mem::replace(guard.as_mut(), new_set);
|
||||
guard.set_old(old);
|
||||
|
||||
AppliedChanges::Forced(new_authorities)
|
||||
} else {
|
||||
let did_standard = guard
|
||||
.as_mut()
|
||||
.enacts_standard_change(hash, number, &is_descendent_of)
|
||||
.map_err(|e| ConsensusError::ClientImport(e.to_string()))
|
||||
.map_err(ConsensusError::from)?;
|
||||
|
||||
if let Some(root) = did_standard {
|
||||
AppliedChanges::Standard(root)
|
||||
} else {
|
||||
AppliedChanges::None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// consume the guard safely and write necessary changes.
|
||||
let just_in_case = guard.consume();
|
||||
if let Some((_, ref authorities)) = just_in_case {
|
||||
let authorities_change = match applied_changes {
|
||||
AppliedChanges::Forced(ref new) => Some(new),
|
||||
AppliedChanges::Standard(_) => None, // the change isn't actually applied yet.
|
||||
AppliedChanges::None => None,
|
||||
};
|
||||
|
||||
crate::aux_schema::update_authority_set::<Block, _, _>(
|
||||
authorities,
|
||||
authorities_change,
|
||||
|insert| {
|
||||
block
|
||||
.auxiliary
|
||||
.extend(insert.iter().map(|(k, v)| (k.to_vec(), Some(v.to_vec()))))
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let just_in_case = just_in_case.map(|(o, i)| (o, i.release_mutex()));
|
||||
|
||||
Ok(PendingSetChanges { just_in_case, applied_changes, do_pause })
|
||||
}
|
||||
|
||||
/// Read current set id form a given state.
|
||||
fn current_set_id(&self, hash: Block::Hash) -> Result<SetId, ConsensusError> {
|
||||
let runtime_version = self.inner.runtime_api().version(hash).map_err(|e| {
|
||||
ConsensusError::ClientImport(format!(
|
||||
"Unable to retrieve current runtime version. {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
if runtime_version
|
||||
.api_version(&<dyn GrandpaApi<Block>>::ID)
|
||||
.map_or(false, |v| v < 3)
|
||||
{
|
||||
// The new API is not supported in this runtime. Try reading directly from storage.
|
||||
// This code may be removed once warp sync to an old runtime is no longer needed.
|
||||
for prefix in ["GrandpaFinality", "Grandpa"] {
|
||||
let k = [
|
||||
pezsp_crypto_hashing::twox_128(prefix.as_bytes()),
|
||||
pezsp_crypto_hashing::twox_128(b"CurrentSetId"),
|
||||
]
|
||||
.concat();
|
||||
if let Ok(Some(id)) =
|
||||
self.inner.storage(hash, &pezsc_client_api::StorageKey(k.to_vec()))
|
||||
{
|
||||
if let Ok(id) = SetId::decode(&mut id.0.as_ref()) {
|
||||
return Ok(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(ConsensusError::ClientImport("Unable to retrieve current set id.".into()))
|
||||
} else {
|
||||
self.inner
|
||||
.runtime_api()
|
||||
.current_set_id(hash)
|
||||
.map_err(|e| ConsensusError::ClientImport(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Import whole new state and reset authority set.
|
||||
async fn import_state(
|
||||
&self,
|
||||
mut block: BlockImportParams<Block>,
|
||||
) -> Result<ImportResult, ConsensusError> {
|
||||
let hash = block.post_hash();
|
||||
let number = *block.header.number();
|
||||
// Force imported state finality.
|
||||
block.finalized = true;
|
||||
let import_result = (&*self.inner).import_block(block).await;
|
||||
match import_result {
|
||||
Ok(ImportResult::Imported(aux)) => {
|
||||
// We've just imported a new state. We trust the sync module has verified
|
||||
// finality proofs and that the state is correct and final.
|
||||
// So we can read the authority list and set id from the state.
|
||||
self.authority_set_hard_forks.lock().clear();
|
||||
let authorities = self
|
||||
.inner
|
||||
.runtime_api()
|
||||
.grandpa_authorities(hash)
|
||||
.map_err(|e| ConsensusError::ClientImport(e.to_string()))?;
|
||||
let set_id = self.current_set_id(hash)?;
|
||||
let authority_set = AuthoritySet::new(
|
||||
authorities.clone(),
|
||||
set_id,
|
||||
fork_tree::ForkTree::new(),
|
||||
Vec::new(),
|
||||
AuthoritySetChanges::empty(),
|
||||
)
|
||||
.ok_or_else(|| ConsensusError::ClientImport("Invalid authority list".into()))?;
|
||||
*self.authority_set.inner_locked() = authority_set.clone();
|
||||
|
||||
crate::aux_schema::update_authority_set::<Block, _, _>(
|
||||
&authority_set,
|
||||
None,
|
||||
|insert| self.inner.insert_aux(insert, []),
|
||||
)
|
||||
.map_err(|e| ConsensusError::ClientImport(e.to_string()))?;
|
||||
let new_set =
|
||||
NewAuthoritySet { canon_number: number, canon_hash: hash, set_id, authorities };
|
||||
let _ = self
|
||||
.send_voter_commands
|
||||
.unbounded_send(VoterCommand::ChangeAuthorities(new_set));
|
||||
Ok(ImportResult::Imported(aux))
|
||||
},
|
||||
Ok(r) => Ok(r),
|
||||
Err(e) => Err(ConsensusError::ClientImport(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<BE, Block: BlockT, Client, SC> BlockImport<Block> for GrandpaBlockImport<BE, Block, Client, SC>
|
||||
where
|
||||
NumberFor<Block>: finality_grandpa::BlockNumberOps,
|
||||
BE: Backend<Block>,
|
||||
Client: ClientForGrandpa<Block, BE>,
|
||||
Client::Api: GrandpaApi<Block>,
|
||||
for<'a> &'a Client: BlockImport<Block, Error = ConsensusError>,
|
||||
SC: Send + Sync,
|
||||
{
|
||||
type Error = ConsensusError;
|
||||
|
||||
async fn import_block(
|
||||
&self,
|
||||
mut block: BlockImportParams<Block>,
|
||||
) -> Result<ImportResult, Self::Error> {
|
||||
let hash = block.post_hash();
|
||||
let number = *block.header.number();
|
||||
|
||||
// early exit if block already in chain, otherwise the check for
|
||||
// authority changes will error when trying to re-import a change block
|
||||
match self.inner.status(hash) {
|
||||
Ok(BlockStatus::InChain) => {
|
||||
// Strip justifications when re-importing an existing block.
|
||||
let _justifications = block.justifications.take();
|
||||
return (&*self.inner).import_block(block).await;
|
||||
},
|
||||
Ok(BlockStatus::Unknown) => {},
|
||||
Err(e) => return Err(ConsensusError::ClientImport(e.to_string())),
|
||||
}
|
||||
|
||||
if block.with_state() {
|
||||
return self.import_state(block).await;
|
||||
}
|
||||
|
||||
if number <= self.inner.info().finalized_number {
|
||||
// Importing an old block. Just save justifications and authority set changes
|
||||
if self.check_new_change(&block.header, hash).is_some() {
|
||||
if block.justifications.is_none() {
|
||||
return Err(ConsensusError::ClientImport(
|
||||
"Justification required when importing \
|
||||
an old block with authority set change."
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
let mut authority_set = self.authority_set.inner_locked();
|
||||
authority_set.authority_set_changes.insert(number);
|
||||
crate::aux_schema::update_authority_set::<Block, _, _>(
|
||||
&authority_set,
|
||||
None,
|
||||
|insert| {
|
||||
block
|
||||
.auxiliary
|
||||
.extend(insert.iter().map(|(k, v)| (k.to_vec(), Some(v.to_vec()))))
|
||||
},
|
||||
);
|
||||
}
|
||||
return (&*self.inner).import_block(block).await;
|
||||
}
|
||||
|
||||
// on initial sync we will restrict logging under info to avoid spam.
|
||||
let initial_sync = block.origin == BlockOrigin::NetworkInitialSync;
|
||||
|
||||
let pending_changes = self.make_authorities_changes(&mut block, hash, initial_sync)?;
|
||||
|
||||
// we don't want to finalize on `inner.import_block`
|
||||
let mut justifications = block.justifications.take();
|
||||
let import_result = (&*self.inner).import_block(block).await;
|
||||
|
||||
let mut imported_aux = {
|
||||
match import_result {
|
||||
Ok(ImportResult::Imported(aux)) => aux,
|
||||
Ok(r) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Restoring old authority set after block import result: {:?}", r,
|
||||
);
|
||||
pending_changes.revert();
|
||||
return Ok(r);
|
||||
},
|
||||
Err(e) => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Restoring old authority set after block import error: {}", e,
|
||||
);
|
||||
pending_changes.revert();
|
||||
return Err(ConsensusError::ClientImport(e.to_string()));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let (applied_changes, do_pause) = pending_changes.defuse();
|
||||
|
||||
// Send the pause signal after import but BEFORE sending a `ChangeAuthorities` message.
|
||||
if do_pause {
|
||||
let _ = self.send_voter_commands.unbounded_send(VoterCommand::Pause(
|
||||
"Forced change scheduled after inactivity".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let needs_justification = applied_changes.needs_justification();
|
||||
|
||||
match applied_changes {
|
||||
AppliedChanges::Forced(new) => {
|
||||
// NOTE: when we do a force change we are "discrediting" the old set so we
|
||||
// ignore any justifications from them. this block may contain a justification
|
||||
// which should be checked and imported below against the new authority
|
||||
// triggered by this forced change. the new grandpa voter will start at the
|
||||
// last median finalized block (which is before the block that enacts the
|
||||
// change), full nodes syncing the chain will not be able to successfully
|
||||
// import justifications for those blocks since their local authority set view
|
||||
// is still of the set before the forced change was enacted, still after #1867
|
||||
// they should import the block and discard the justification, and they will
|
||||
// then request a justification from sync if it's necessary (which they should
|
||||
// then be able to successfully validate).
|
||||
let _ =
|
||||
self.send_voter_commands.unbounded_send(VoterCommand::ChangeAuthorities(new));
|
||||
|
||||
// we must clear all pending justifications requests, presumably they won't be
|
||||
// finalized hence why this forced changes was triggered
|
||||
imported_aux.clear_justification_requests = true;
|
||||
},
|
||||
AppliedChanges::Standard(false) => {
|
||||
// we can't apply this change yet since there are other dependent changes that we
|
||||
// need to apply first, drop any justification that might have been provided with
|
||||
// the block to make sure we request them from `sync` which will ensure they'll be
|
||||
// applied in-order.
|
||||
justifications.take();
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
let grandpa_justification =
|
||||
justifications.and_then(|just| just.into_justification(GRANDPA_ENGINE_ID));
|
||||
|
||||
match grandpa_justification {
|
||||
Some(justification) => {
|
||||
if environment::should_process_justification(
|
||||
&*self.inner,
|
||||
self.justification_import_period,
|
||||
number,
|
||||
needs_justification,
|
||||
) {
|
||||
let import_res = self.import_justification(
|
||||
hash,
|
||||
number,
|
||||
(GRANDPA_ENGINE_ID, justification),
|
||||
needs_justification,
|
||||
initial_sync,
|
||||
);
|
||||
|
||||
import_res.unwrap_or_else(|err| {
|
||||
if needs_justification {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Requesting justification from peers due to imported block #{} that enacts authority set change with invalid justification: {}",
|
||||
number,
|
||||
err
|
||||
);
|
||||
imported_aux.bad_justification = true;
|
||||
imported_aux.needs_justification = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Ignoring unnecessary justification for block #{}",
|
||||
number,
|
||||
);
|
||||
}
|
||||
},
|
||||
None =>
|
||||
if needs_justification {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Imported unjustified block #{} that enacts authority set change, waiting for finality for enactment.",
|
||||
number,
|
||||
);
|
||||
|
||||
imported_aux.needs_justification = true;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(ImportResult::Imported(imported_aux))
|
||||
}
|
||||
|
||||
async fn check_block(
|
||||
&self,
|
||||
block: BlockCheckParams<Block>,
|
||||
) -> Result<ImportResult, Self::Error> {
|
||||
self.inner.check_block(block).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<Backend, Block: BlockT, Client, SC> GrandpaBlockImport<Backend, Block, Client, SC> {
|
||||
pub(crate) fn new(
|
||||
inner: Arc<Client>,
|
||||
justification_import_period: u32,
|
||||
select_chain: SC,
|
||||
authority_set: SharedAuthoritySet<Block::Hash, NumberFor<Block>>,
|
||||
send_voter_commands: TracingUnboundedSender<VoterCommand<Block::Hash, NumberFor<Block>>>,
|
||||
authority_set_hard_forks: Vec<(SetId, PendingChange<Block::Hash, NumberFor<Block>>)>,
|
||||
justification_sender: GrandpaJustificationSender<Block>,
|
||||
telemetry: Option<TelemetryHandle>,
|
||||
) -> GrandpaBlockImport<Backend, Block, Client, SC> {
|
||||
// check for and apply any forced authority set hard fork that applies
|
||||
// to the *current* authority set.
|
||||
if let Some((_, change)) = authority_set_hard_forks
|
||||
.iter()
|
||||
.find(|(set_id, _)| *set_id == authority_set.set_id())
|
||||
{
|
||||
authority_set.inner().current_authorities = change.next_authorities.clone();
|
||||
}
|
||||
|
||||
// index authority set hard forks by block hash so that they can be used
|
||||
// by any node syncing the chain and importing a block hard fork
|
||||
// authority set changes.
|
||||
let authority_set_hard_forks = authority_set_hard_forks
|
||||
.into_iter()
|
||||
.map(|(_, change)| (change.canon_hash, change))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
// check for and apply any forced authority set hard fork that apply to
|
||||
// any *pending* standard changes, checking by the block hash at which
|
||||
// they were announced.
|
||||
{
|
||||
let mut authority_set = authority_set.inner();
|
||||
|
||||
authority_set.pending_standard_changes =
|
||||
authority_set.pending_standard_changes.clone().map(&mut |hash, _, original| {
|
||||
authority_set_hard_forks.get(hash).cloned().unwrap_or(original)
|
||||
});
|
||||
}
|
||||
|
||||
GrandpaBlockImport {
|
||||
inner,
|
||||
justification_import_period,
|
||||
select_chain,
|
||||
authority_set,
|
||||
send_voter_commands,
|
||||
authority_set_hard_forks: Mutex::new(authority_set_hard_forks),
|
||||
justification_sender,
|
||||
telemetry,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<BE, Block: BlockT, Client, SC> GrandpaBlockImport<BE, Block, Client, SC>
|
||||
where
|
||||
BE: Backend<Block>,
|
||||
Client: ClientForGrandpa<Block, BE>,
|
||||
NumberFor<Block>: finality_grandpa::BlockNumberOps,
|
||||
{
|
||||
/// Import a block justification and finalize the block.
|
||||
///
|
||||
/// If `enacts_change` is set to true, then finalizing this block *must*
|
||||
/// enact an authority set change, the function will panic otherwise.
|
||||
fn import_justification(
|
||||
&self,
|
||||
hash: Block::Hash,
|
||||
number: NumberFor<Block>,
|
||||
justification: Justification,
|
||||
enacts_change: bool,
|
||||
initial_sync: bool,
|
||||
) -> Result<(), ConsensusError> {
|
||||
if justification.0 != GRANDPA_ENGINE_ID {
|
||||
// TODO: the import queue needs to be refactored to be able dispatch to the correct
|
||||
// `JustificationImport` instance based on `ConsensusEngineId`, or we need to build a
|
||||
// justification import pipeline similar to what we do for `BlockImport`. In the
|
||||
// meantime we'll just drop the justification, since this is only used for BEEFY which
|
||||
// is still WIP.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let justification = GrandpaJustification::decode_and_verify_finalizes(
|
||||
&justification.1,
|
||||
(hash, number),
|
||||
self.authority_set.set_id(),
|
||||
&self.authority_set.current_authorities(),
|
||||
);
|
||||
|
||||
let justification = match justification {
|
||||
Err(e) => {
|
||||
return match e {
|
||||
pezsp_blockchain::Error::OutdatedJustification =>
|
||||
Err(ConsensusError::OutdatedJustification),
|
||||
_ => Err(ConsensusError::ClientImport(e.to_string())),
|
||||
};
|
||||
},
|
||||
Ok(justification) => justification,
|
||||
};
|
||||
|
||||
let result = environment::finalize_block(
|
||||
self.inner.clone(),
|
||||
&self.authority_set,
|
||||
None,
|
||||
hash,
|
||||
number,
|
||||
justification.into(),
|
||||
initial_sync,
|
||||
Some(&self.justification_sender),
|
||||
self.telemetry.clone(),
|
||||
);
|
||||
|
||||
match result {
|
||||
Err(CommandOrError::VoterCommand(command)) => {
|
||||
grandpa_log!(
|
||||
initial_sync,
|
||||
"👴 Imported justification for block #{} that triggers \
|
||||
command {}, signaling voter.",
|
||||
number,
|
||||
command,
|
||||
);
|
||||
|
||||
// send the command to the voter
|
||||
let _ = self.send_voter_commands.unbounded_send(command);
|
||||
},
|
||||
Err(CommandOrError::Error(e)) =>
|
||||
return Err(match e {
|
||||
Error::Grandpa(error) => ConsensusError::ClientImport(error.to_string()),
|
||||
Error::Network(error) => ConsensusError::ClientImport(error),
|
||||
Error::Blockchain(error) => ConsensusError::ClientImport(error),
|
||||
Error::Client(error) => ConsensusError::ClientImport(error.to_string()),
|
||||
Error::Safety(error) => ConsensusError::ClientImport(error),
|
||||
Error::Signing(error) => ConsensusError::ClientImport(error),
|
||||
Error::Timer(error) => ConsensusError::ClientImport(error.to_string()),
|
||||
Error::RuntimeApi(error) => ConsensusError::ClientImport(error.to_string()),
|
||||
}),
|
||||
Ok(_) => {
|
||||
assert!(
|
||||
!enacts_change,
|
||||
"returns Ok when no authority set change should be enacted; qed;"
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
marker::PhantomData,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use codec::{Decode, DecodeAll, Encode};
|
||||
use finality_grandpa::{voter_set::VoterSet, Error as GrandpaError};
|
||||
use pezsp_blockchain::{Error as ClientError, HeaderBackend};
|
||||
use pezsp_consensus_grandpa::AuthorityId;
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
|
||||
|
||||
use crate::{AuthorityList, Commit, Error};
|
||||
|
||||
/// A GRANDPA justification for block finality, it includes a commit message and
|
||||
/// an ancestry proof including all headers routing all precommit target blocks
|
||||
/// to the commit target block. Due to the current voting strategy the precommit
|
||||
/// targets should be the same as the commit target, since honest voters don't
|
||||
/// vote past authority set change blocks.
|
||||
///
|
||||
/// This is meant to be stored in the db and passed around the network to other
|
||||
/// nodes, and are used by syncing nodes to prove authority set handoffs.
|
||||
#[derive(Clone, Encode, Decode, PartialEq, Eq, Debug)]
|
||||
pub struct GrandpaJustification<Block: BlockT> {
|
||||
/// The GRANDPA justification for block finality.
|
||||
pub justification: pezsp_consensus_grandpa::GrandpaJustification<Block::Header>,
|
||||
_block: PhantomData<Block>,
|
||||
}
|
||||
|
||||
impl<Block: BlockT> From<pezsp_consensus_grandpa::GrandpaJustification<Block::Header>>
|
||||
for GrandpaJustification<Block>
|
||||
{
|
||||
fn from(justification: pezsp_consensus_grandpa::GrandpaJustification<Block::Header>) -> Self {
|
||||
Self { justification, _block: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block: BlockT> Into<pezsp_consensus_grandpa::GrandpaJustification<Block::Header>>
|
||||
for GrandpaJustification<Block>
|
||||
{
|
||||
fn into(self) -> pezsp_consensus_grandpa::GrandpaJustification<Block::Header> {
|
||||
self.justification
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block: BlockT> GrandpaJustification<Block> {
|
||||
/// Create a GRANDPA justification from the given commit. This method
|
||||
/// assumes the commit is valid and well-formed.
|
||||
pub fn from_commit<C>(
|
||||
client: &Arc<C>,
|
||||
round: u64,
|
||||
commit: Commit<Block::Header>,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
C: HeaderBackend<Block>,
|
||||
{
|
||||
let mut votes_ancestries_hashes = HashSet::new();
|
||||
let mut votes_ancestries = Vec::new();
|
||||
|
||||
let error = || {
|
||||
let msg = "invalid precommits for target commit".to_string();
|
||||
Err(Error::Client(ClientError::BadJustification(msg)))
|
||||
};
|
||||
|
||||
// we pick the precommit for the lowest block as the base that
|
||||
// should serve as the root block for populating ancestry (i.e.
|
||||
// collect all headers from all precommit blocks to the base)
|
||||
let (base_hash, base_number) = match commit
|
||||
.precommits
|
||||
.iter()
|
||||
.map(|signed| &signed.precommit)
|
||||
.min_by_key(|precommit| precommit.target_number)
|
||||
.map(|precommit| (precommit.target_hash, precommit.target_number))
|
||||
{
|
||||
None => return error(),
|
||||
Some(base) => base,
|
||||
};
|
||||
|
||||
for signed in commit.precommits.iter() {
|
||||
let mut current_hash = signed.precommit.target_hash;
|
||||
loop {
|
||||
if current_hash == base_hash {
|
||||
break;
|
||||
}
|
||||
|
||||
match client.header(current_hash)? {
|
||||
Some(current_header) => {
|
||||
// NOTE: this should never happen as we pick the lowest block
|
||||
// as base and only traverse backwards from the other blocks
|
||||
// in the commit. but better be safe to avoid an unbound loop.
|
||||
if *current_header.number() <= base_number {
|
||||
return error();
|
||||
}
|
||||
|
||||
let parent_hash = *current_header.parent_hash();
|
||||
if votes_ancestries_hashes.insert(current_hash) {
|
||||
votes_ancestries.push(current_header);
|
||||
}
|
||||
|
||||
current_hash = parent_hash;
|
||||
},
|
||||
_ => return error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pezsp_consensus_grandpa::GrandpaJustification { round, commit, votes_ancestries }.into())
|
||||
}
|
||||
|
||||
/// Decode a GRANDPA justification and validate the commit and the votes'
|
||||
/// ancestry proofs finalize the given block.
|
||||
pub fn decode_and_verify_finalizes(
|
||||
encoded: &[u8],
|
||||
finalized_target: (Block::Hash, NumberFor<Block>),
|
||||
set_id: u64,
|
||||
voters: &VoterSet<AuthorityId>,
|
||||
) -> Result<Self, ClientError>
|
||||
where
|
||||
NumberFor<Block>: finality_grandpa::BlockNumberOps,
|
||||
{
|
||||
let justification = GrandpaJustification::<Block>::decode_all(&mut &*encoded)
|
||||
.map_err(|_| ClientError::JustificationDecode)?;
|
||||
|
||||
if (
|
||||
justification.justification.commit.target_hash,
|
||||
justification.justification.commit.target_number,
|
||||
) != finalized_target
|
||||
{
|
||||
let msg = "invalid commit target in grandpa justification".to_string();
|
||||
Err(ClientError::BadJustification(msg))
|
||||
} else {
|
||||
justification.verify_with_voter_set(set_id, voters).map(|_| justification)
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate the commit and the votes' ancestry proofs.
|
||||
pub fn verify(&self, set_id: u64, authorities: &AuthorityList) -> Result<(), ClientError>
|
||||
where
|
||||
NumberFor<Block>: finality_grandpa::BlockNumberOps,
|
||||
{
|
||||
let voters = VoterSet::new(authorities.iter().cloned())
|
||||
.ok_or(ClientError::Consensus(pezsp_consensus::Error::InvalidAuthoritiesSet))?;
|
||||
|
||||
self.verify_with_voter_set(set_id, &voters)
|
||||
}
|
||||
|
||||
/// Validate the commit and the votes' ancestry proofs.
|
||||
pub(crate) fn verify_with_voter_set(
|
||||
&self,
|
||||
set_id: u64,
|
||||
voters: &VoterSet<AuthorityId>,
|
||||
) -> Result<(), ClientError>
|
||||
where
|
||||
NumberFor<Block>: finality_grandpa::BlockNumberOps,
|
||||
{
|
||||
use finality_grandpa::Chain;
|
||||
|
||||
let ancestry_chain = AncestryChain::<Block>::new(&self.justification.votes_ancestries);
|
||||
|
||||
match finality_grandpa::validate_commit(&self.justification.commit, voters, &ancestry_chain)
|
||||
{
|
||||
Ok(ref result) if result.is_valid() => {},
|
||||
_ => {
|
||||
let msg = "invalid commit in grandpa justification".to_string();
|
||||
return Err(ClientError::BadJustification(msg));
|
||||
},
|
||||
}
|
||||
|
||||
// we pick the precommit for the lowest block as the base that
|
||||
// should serve as the root block for populating ancestry (i.e.
|
||||
// collect all headers from all precommit blocks to the base)
|
||||
let base_hash = self
|
||||
.justification
|
||||
.commit
|
||||
.precommits
|
||||
.iter()
|
||||
.map(|signed| &signed.precommit)
|
||||
.min_by_key(|precommit| precommit.target_number)
|
||||
.map(|precommit| precommit.target_hash)
|
||||
.expect(
|
||||
"can only fail if precommits is empty; \
|
||||
commit has been validated above; \
|
||||
valid commits must include precommits; \
|
||||
qed.",
|
||||
);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut visited_hashes = HashSet::new();
|
||||
for signed in self.justification.commit.precommits.iter() {
|
||||
let signature_result = pezsp_consensus_grandpa::check_message_signature_with_buffer(
|
||||
&finality_grandpa::Message::Precommit(signed.precommit.clone()),
|
||||
&signed.id,
|
||||
&signed.signature,
|
||||
self.justification.round,
|
||||
set_id,
|
||||
&mut buf,
|
||||
);
|
||||
match signature_result {
|
||||
pezsp_consensus_grandpa::SignatureResult::Invalid =>
|
||||
return Err(ClientError::BadJustification(
|
||||
"invalid signature for precommit in grandpa justification".to_string(),
|
||||
)),
|
||||
pezsp_consensus_grandpa::SignatureResult::OutdatedSet =>
|
||||
return Err(ClientError::OutdatedJustification),
|
||||
pezsp_consensus_grandpa::SignatureResult::Valid => {},
|
||||
}
|
||||
|
||||
if base_hash == signed.precommit.target_hash {
|
||||
continue;
|
||||
}
|
||||
|
||||
match ancestry_chain.ancestry(base_hash, signed.precommit.target_hash) {
|
||||
Ok(route) => {
|
||||
// ancestry starts from parent hash but the precommit target hash has been
|
||||
// visited
|
||||
visited_hashes.insert(signed.precommit.target_hash);
|
||||
for hash in route {
|
||||
visited_hashes.insert(hash);
|
||||
}
|
||||
},
|
||||
_ =>
|
||||
return Err(ClientError::BadJustification(
|
||||
"invalid precommit ancestry proof in grandpa justification".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
let ancestry_hashes: HashSet<_> = self
|
||||
.justification
|
||||
.votes_ancestries
|
||||
.iter()
|
||||
.map(|h: &Block::Header| h.hash())
|
||||
.collect();
|
||||
|
||||
if visited_hashes != ancestry_hashes {
|
||||
return Err(ClientError::BadJustification(
|
||||
"invalid precommit ancestries in grandpa justification with unused headers"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The target block number and hash that this justifications proves finality for.
|
||||
pub fn target(&self) -> (NumberFor<Block>, Block::Hash) {
|
||||
(self.justification.commit.target_number, self.justification.commit.target_hash)
|
||||
}
|
||||
}
|
||||
|
||||
/// A utility trait implementing `finality_grandpa::Chain` using a given set of headers.
|
||||
/// This is useful when validating commits, using the given set of headers to
|
||||
/// verify a valid ancestry route to the target commit block.
|
||||
struct AncestryChain<Block: BlockT> {
|
||||
ancestry: HashMap<Block::Hash, Block::Header>,
|
||||
}
|
||||
|
||||
impl<Block: BlockT> AncestryChain<Block> {
|
||||
fn new(ancestry: &[Block::Header]) -> AncestryChain<Block> {
|
||||
let ancestry: HashMap<_, _> =
|
||||
ancestry.iter().cloned().map(|h: Block::Header| (h.hash(), h)).collect();
|
||||
|
||||
AncestryChain { ancestry }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block: BlockT> finality_grandpa::Chain<Block::Hash, NumberFor<Block>> for AncestryChain<Block>
|
||||
where
|
||||
NumberFor<Block>: finality_grandpa::BlockNumberOps,
|
||||
{
|
||||
fn ancestry(
|
||||
&self,
|
||||
base: Block::Hash,
|
||||
block: Block::Hash,
|
||||
) -> Result<Vec<Block::Hash>, GrandpaError> {
|
||||
let mut route = Vec::new();
|
||||
let mut current_hash = block;
|
||||
loop {
|
||||
if current_hash == base {
|
||||
break;
|
||||
}
|
||||
match self.ancestry.get(¤t_hash) {
|
||||
Some(current_header) => {
|
||||
current_hash = *current_header.parent_hash();
|
||||
route.push(current_hash);
|
||||
},
|
||||
_ => return Err(GrandpaError::NotDescendent),
|
||||
}
|
||||
}
|
||||
route.pop(); // remove the base
|
||||
|
||||
Ok(route)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use pezsc_utils::notification::{NotificationSender, NotificationStream, TracingKeyStr};
|
||||
|
||||
use crate::justification::GrandpaJustification;
|
||||
|
||||
/// The sending half of the Grandpa justification channel(s).
|
||||
///
|
||||
/// Used to send notifications about justifications generated
|
||||
/// at the end of a Grandpa round.
|
||||
pub type GrandpaJustificationSender<Block> = NotificationSender<GrandpaJustification<Block>>;
|
||||
|
||||
/// The receiving half of the Grandpa justification channel.
|
||||
///
|
||||
/// Used to receive notifications about justifications generated
|
||||
/// at the end of a Grandpa round.
|
||||
/// The `GrandpaJustificationStream` entity stores the `SharedJustificationSenders`
|
||||
/// so it can be used to add more subscriptions.
|
||||
pub type GrandpaJustificationStream<Block> =
|
||||
NotificationStream<GrandpaJustification<Block>, GrandpaJustificationsTracingKey>;
|
||||
|
||||
/// Provides tracing key for GRANDPA justifications stream.
|
||||
#[derive(Clone)]
|
||||
pub struct GrandpaJustificationsTracingKey;
|
||||
impl TracingKeyStr for GrandpaJustificationsTracingKey {
|
||||
const TRACING_KEY: &'static str = "mpsc_grandpa_justification_notification_stream";
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
marker::{PhantomData, Unpin},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use finality_grandpa::{voter, voter_set::VoterSet, BlockNumberOps, Error as GrandpaError};
|
||||
use futures::prelude::*;
|
||||
use log::{debug, info, warn};
|
||||
|
||||
use pezsc_client_api::backend::Backend;
|
||||
use pezsc_network::NotificationService;
|
||||
use pezsc_telemetry::TelemetryHandle;
|
||||
use pezsc_utils::mpsc::TracingUnboundedReceiver;
|
||||
use pezsp_blockchain::HeaderMetadata;
|
||||
use pezsp_consensus::SelectChain;
|
||||
use pezsp_consensus_grandpa::AuthorityId;
|
||||
use pezsp_keystore::KeystorePtr;
|
||||
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
|
||||
|
||||
use crate::{
|
||||
authorities::SharedAuthoritySet,
|
||||
aux_schema::PersistentData,
|
||||
communication::{Network as NetworkT, NetworkBridge, Syncing as SyncingT},
|
||||
environment, global_communication,
|
||||
notification::GrandpaJustificationSender,
|
||||
ClientForGrandpa, CommandOrError, CommunicationIn, Config, Error, LinkHalf, VoterCommand,
|
||||
VoterSetState, LOG_TARGET,
|
||||
};
|
||||
|
||||
struct ObserverChain<'a, Block: BlockT, Client> {
|
||||
client: &'a Arc<Client>,
|
||||
_phantom: PhantomData<Block>,
|
||||
}
|
||||
|
||||
impl<'a, Block, Client> finality_grandpa::Chain<Block::Hash, NumberFor<Block>>
|
||||
for ObserverChain<'a, Block, Client>
|
||||
where
|
||||
Block: BlockT,
|
||||
Client: HeaderMetadata<Block, Error = pezsp_blockchain::Error>,
|
||||
NumberFor<Block>: BlockNumberOps,
|
||||
{
|
||||
fn ancestry(
|
||||
&self,
|
||||
base: Block::Hash,
|
||||
block: Block::Hash,
|
||||
) -> Result<Vec<Block::Hash>, GrandpaError> {
|
||||
environment::ancestry(self.client, base, block)
|
||||
}
|
||||
}
|
||||
|
||||
fn grandpa_observer<BE, Block: BlockT, Client, S, F>(
|
||||
client: &Arc<Client>,
|
||||
authority_set: &SharedAuthoritySet<Block::Hash, NumberFor<Block>>,
|
||||
voters: &Arc<VoterSet<AuthorityId>>,
|
||||
justification_sender: &Option<GrandpaJustificationSender<Block>>,
|
||||
last_finalized_number: NumberFor<Block>,
|
||||
commits: S,
|
||||
note_round: F,
|
||||
telemetry: Option<TelemetryHandle>,
|
||||
) -> impl Future<Output = Result<(), CommandOrError<Block::Hash, NumberFor<Block>>>>
|
||||
where
|
||||
NumberFor<Block>: BlockNumberOps,
|
||||
S: Stream<Item = Result<CommunicationIn<Block>, CommandOrError<Block::Hash, NumberFor<Block>>>>,
|
||||
F: Fn(u64),
|
||||
BE: Backend<Block>,
|
||||
Client: ClientForGrandpa<Block, BE>,
|
||||
{
|
||||
let authority_set = authority_set.clone();
|
||||
let client = client.clone();
|
||||
let voters = voters.clone();
|
||||
let justification_sender = justification_sender.clone();
|
||||
|
||||
let observer = commits.try_fold(last_finalized_number, move |last_finalized_number, global| {
|
||||
let (round, commit, callback) = match global {
|
||||
voter::CommunicationIn::Commit(round, commit, callback) => {
|
||||
let commit = finality_grandpa::Commit::from(commit);
|
||||
(round, commit, callback)
|
||||
},
|
||||
voter::CommunicationIn::CatchUp(..) => {
|
||||
// ignore catch up messages
|
||||
return future::ok(last_finalized_number);
|
||||
},
|
||||
};
|
||||
|
||||
// if the commit we've received targets a block lower or equal to the last
|
||||
// finalized, ignore it and continue with the current state
|
||||
if commit.target_number <= last_finalized_number {
|
||||
return future::ok(last_finalized_number);
|
||||
}
|
||||
|
||||
let validation_result = match finality_grandpa::validate_commit(
|
||||
&commit,
|
||||
&voters,
|
||||
&ObserverChain { client: &client, _phantom: PhantomData },
|
||||
) {
|
||||
Ok(r) => r,
|
||||
Err(e) => return future::err(e.into()),
|
||||
};
|
||||
|
||||
if validation_result.is_valid() {
|
||||
let finalized_hash = commit.target_hash;
|
||||
let finalized_number = commit.target_number;
|
||||
|
||||
// commit is valid, finalize the block it targets
|
||||
match environment::finalize_block(
|
||||
client.clone(),
|
||||
&authority_set,
|
||||
None,
|
||||
finalized_hash,
|
||||
finalized_number,
|
||||
(round, commit).into(),
|
||||
false,
|
||||
justification_sender.as_ref(),
|
||||
telemetry.clone(),
|
||||
) {
|
||||
Ok(_) => {},
|
||||
Err(e) => return future::err(e),
|
||||
};
|
||||
|
||||
// note that we've observed completion of this round through the commit,
|
||||
// and that implies that the next round has started.
|
||||
note_round(round + 1);
|
||||
|
||||
finality_grandpa::process_commit_validation_result(validation_result, callback);
|
||||
|
||||
// proceed processing with new finalized block number
|
||||
future::ok(finalized_number)
|
||||
} else {
|
||||
debug!(target: LOG_TARGET, "Received invalid commit: ({:?}, {:?})", round, commit);
|
||||
|
||||
finality_grandpa::process_commit_validation_result(validation_result, callback);
|
||||
|
||||
// commit is invalid, continue processing commits with the current state
|
||||
future::ok(last_finalized_number)
|
||||
}
|
||||
});
|
||||
|
||||
observer.map_ok(|_| ())
|
||||
}
|
||||
|
||||
/// Run a GRANDPA observer as a task, the observer will finalize blocks only by
|
||||
/// listening for and validating GRANDPA commits instead of following the full
|
||||
/// protocol. Provide configuration and a link to a block import worker that has
|
||||
/// already been instantiated with `block_import`.
|
||||
/// NOTE: this is currently not part of the crate's public API since we don't consider
|
||||
/// it stable enough to use on a live network.
|
||||
pub fn run_grandpa_observer<BE, Block: BlockT, Client, N, S, SC>(
|
||||
config: Config,
|
||||
link: LinkHalf<Block, Client, SC>,
|
||||
network: N,
|
||||
sync: S,
|
||||
notification_service: Box<dyn NotificationService>,
|
||||
) -> pezsp_blockchain::Result<impl Future<Output = ()> + Send>
|
||||
where
|
||||
BE: Backend<Block> + Unpin + 'static,
|
||||
N: NetworkT<Block>,
|
||||
S: SyncingT<Block>,
|
||||
SC: SelectChain<Block>,
|
||||
NumberFor<Block>: BlockNumberOps,
|
||||
Client: ClientForGrandpa<Block, BE> + 'static,
|
||||
{
|
||||
let LinkHalf {
|
||||
client,
|
||||
persistent_data,
|
||||
voter_commands_rx,
|
||||
justification_sender,
|
||||
telemetry,
|
||||
..
|
||||
} = link;
|
||||
|
||||
let network = NetworkBridge::new(
|
||||
network,
|
||||
sync,
|
||||
notification_service,
|
||||
config.clone(),
|
||||
persistent_data.set_state.clone(),
|
||||
None,
|
||||
telemetry.clone(),
|
||||
);
|
||||
|
||||
let observer_work = ObserverWork::new(
|
||||
client,
|
||||
network,
|
||||
persistent_data,
|
||||
config.keystore,
|
||||
voter_commands_rx,
|
||||
Some(justification_sender),
|
||||
telemetry,
|
||||
);
|
||||
|
||||
let observer_work = observer_work.map_ok(|_| ()).map_err(|e| {
|
||||
warn!("GRANDPA Observer failed: {}", e);
|
||||
});
|
||||
|
||||
Ok(observer_work.map(drop))
|
||||
}
|
||||
|
||||
/// Future that powers the observer.
|
||||
#[must_use]
|
||||
struct ObserverWork<B: BlockT, BE, Client, N: NetworkT<B>, S: SyncingT<B>> {
|
||||
observer:
|
||||
Pin<Box<dyn Future<Output = Result<(), CommandOrError<B::Hash, NumberFor<B>>>> + Send>>,
|
||||
client: Arc<Client>,
|
||||
network: NetworkBridge<B, N, S>,
|
||||
persistent_data: PersistentData<B>,
|
||||
keystore: Option<KeystorePtr>,
|
||||
voter_commands_rx: TracingUnboundedReceiver<VoterCommand<B::Hash, NumberFor<B>>>,
|
||||
justification_sender: Option<GrandpaJustificationSender<B>>,
|
||||
telemetry: Option<TelemetryHandle>,
|
||||
_phantom: PhantomData<BE>,
|
||||
}
|
||||
|
||||
impl<B, BE, Client, Network, Syncing> ObserverWork<B, BE, Client, Network, Syncing>
|
||||
where
|
||||
B: BlockT,
|
||||
BE: Backend<B> + 'static,
|
||||
Client: ClientForGrandpa<B, BE> + 'static,
|
||||
Network: NetworkT<B>,
|
||||
Syncing: SyncingT<B>,
|
||||
NumberFor<B>: BlockNumberOps,
|
||||
{
|
||||
fn new(
|
||||
client: Arc<Client>,
|
||||
network: NetworkBridge<B, Network, Syncing>,
|
||||
persistent_data: PersistentData<B>,
|
||||
keystore: Option<KeystorePtr>,
|
||||
voter_commands_rx: TracingUnboundedReceiver<VoterCommand<B::Hash, NumberFor<B>>>,
|
||||
justification_sender: Option<GrandpaJustificationSender<B>>,
|
||||
telemetry: Option<TelemetryHandle>,
|
||||
) -> Self {
|
||||
let mut work = ObserverWork {
|
||||
// `observer` is set to a temporary value and replaced below when
|
||||
// calling `rebuild_observer`.
|
||||
observer: Box::pin(future::pending()) as Pin<Box<_>>,
|
||||
client,
|
||||
network,
|
||||
persistent_data,
|
||||
keystore: keystore.clone(),
|
||||
voter_commands_rx,
|
||||
justification_sender,
|
||||
telemetry,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
work.rebuild_observer();
|
||||
work
|
||||
}
|
||||
|
||||
/// Rebuilds the `self.observer` field using the current authority set
|
||||
/// state. This method should be called when we know that the authority set
|
||||
/// has changed (e.g. as signalled by a voter command).
|
||||
fn rebuild_observer(&mut self) {
|
||||
let set_id = self.persistent_data.authority_set.set_id();
|
||||
let voters = Arc::new(self.persistent_data.authority_set.current_authorities());
|
||||
|
||||
// start global communication stream for the current set
|
||||
let (global_in, _) = global_communication(
|
||||
set_id,
|
||||
&voters,
|
||||
self.client.clone(),
|
||||
&self.network,
|
||||
self.keystore.as_ref(),
|
||||
None,
|
||||
);
|
||||
|
||||
let last_finalized_number = self.client.info().finalized_number;
|
||||
|
||||
// NOTE: since we are not using `round_communication` we have to
|
||||
// manually note the round with the gossip validator, otherwise we won't
|
||||
// relay round messages. we want all full nodes to contribute to vote
|
||||
// availability.
|
||||
let note_round = {
|
||||
let network = self.network.clone();
|
||||
let voters = voters.clone();
|
||||
|
||||
move |round| {
|
||||
network.note_round(
|
||||
crate::communication::Round(round),
|
||||
crate::communication::SetId(set_id),
|
||||
&voters,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// create observer for the current set
|
||||
let observer = grandpa_observer(
|
||||
&self.client,
|
||||
&self.persistent_data.authority_set,
|
||||
&voters,
|
||||
&self.justification_sender,
|
||||
last_finalized_number,
|
||||
global_in,
|
||||
note_round,
|
||||
self.telemetry.clone(),
|
||||
);
|
||||
|
||||
self.observer = Box::pin(observer);
|
||||
}
|
||||
|
||||
fn handle_voter_command(
|
||||
&mut self,
|
||||
command: VoterCommand<B::Hash, NumberFor<B>>,
|
||||
) -> Result<(), Error> {
|
||||
// the observer doesn't use the voter set state, but we need to
|
||||
// update it on-disk in case we restart as validator in the future.
|
||||
self.persistent_data.set_state = match command {
|
||||
VoterCommand::Pause(reason) => {
|
||||
info!(target: LOG_TARGET, "Pausing old validator set: {}", reason);
|
||||
|
||||
let completed_rounds = self.persistent_data.set_state.read().completed_rounds();
|
||||
let set_state = VoterSetState::Paused { completed_rounds };
|
||||
|
||||
crate::aux_schema::write_voter_set_state(&*self.client, &set_state)?;
|
||||
|
||||
set_state
|
||||
},
|
||||
VoterCommand::ChangeAuthorities(new) => {
|
||||
// start the new authority set using the block where the
|
||||
// set changed (not where the signal happened!) as the base.
|
||||
let set_state = VoterSetState::live(
|
||||
new.set_id,
|
||||
&*self.persistent_data.authority_set.inner(),
|
||||
(new.canon_hash, new.canon_number),
|
||||
);
|
||||
|
||||
crate::aux_schema::write_voter_set_state(&*self.client, &set_state)?;
|
||||
|
||||
set_state
|
||||
},
|
||||
}
|
||||
.into();
|
||||
|
||||
self.rebuild_observer();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, BE, C, N, S> Future for ObserverWork<B, BE, C, N, S>
|
||||
where
|
||||
B: BlockT,
|
||||
BE: Backend<B> + Unpin + 'static,
|
||||
C: ClientForGrandpa<B, BE> + 'static,
|
||||
N: NetworkT<B>,
|
||||
S: SyncingT<B>,
|
||||
NumberFor<B>: BlockNumberOps,
|
||||
{
|
||||
type Output = Result<(), Error>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
match Future::poll(Pin::new(&mut self.observer), cx) {
|
||||
Poll::Pending => {},
|
||||
Poll::Ready(Ok(())) => {
|
||||
// observer commit stream doesn't conclude naturally; this could reasonably be an
|
||||
// error.
|
||||
return Poll::Ready(Ok(()));
|
||||
},
|
||||
Poll::Ready(Err(CommandOrError::Error(e))) => {
|
||||
// return inner observer error
|
||||
return Poll::Ready(Err(e));
|
||||
},
|
||||
Poll::Ready(Err(CommandOrError::VoterCommand(command))) => {
|
||||
// some command issued internally
|
||||
self.handle_voter_command(command)?;
|
||||
cx.waker().wake_by_ref();
|
||||
},
|
||||
}
|
||||
|
||||
match Stream::poll_next(Pin::new(&mut self.voter_commands_rx), cx) {
|
||||
Poll::Pending => {},
|
||||
Poll::Ready(None) => {
|
||||
// the `voter_commands_rx` stream should never conclude since it's never closed.
|
||||
return Poll::Ready(Ok(()));
|
||||
},
|
||||
Poll::Ready(Some(command)) => {
|
||||
// some command issued externally
|
||||
self.handle_voter_command(command)?;
|
||||
cx.waker().wake_by_ref();
|
||||
},
|
||||
}
|
||||
|
||||
Future::poll(Pin::new(&mut self.network), cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::{
|
||||
aux_schema,
|
||||
communication::tests::{make_test_network, Event},
|
||||
};
|
||||
use assert_matches::assert_matches;
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsc_utils::mpsc::tracing_unbounded;
|
||||
use pezsp_blockchain::HeaderBackend as _;
|
||||
use bizinikiwi_test_runtime_client::{TestClientBuilder, TestClientBuilderExt};
|
||||
|
||||
use futures::executor;
|
||||
|
||||
/// Ensure `Future` implementation of `ObserverWork` is polling its `NetworkBridge`.
|
||||
/// Regression test for bug introduced in d4fbb897c and fixed in b7af8b339.
|
||||
///
|
||||
/// When polled, `NetworkBridge` forwards reputation change requests from the
|
||||
/// `GossipValidator` to the underlying `dyn Network`. This test triggers a reputation change
|
||||
/// by calling `GossipValidator::validate` with an invalid gossip message. After polling the
|
||||
/// `ObserverWork` which should poll the `NetworkBridge`, the reputation change should be
|
||||
/// forwarded to the test network.
|
||||
#[test]
|
||||
fn observer_work_polls_underlying_network_bridge() {
|
||||
// Create a test network.
|
||||
let (tester_fut, _network) = make_test_network();
|
||||
let mut tester = executor::block_on(tester_fut);
|
||||
|
||||
// Create an observer.
|
||||
let (client, backend) = {
|
||||
let builder = TestClientBuilder::with_default_backend();
|
||||
let backend = builder.backend();
|
||||
let (client, _) = builder.build_with_longest_chain();
|
||||
(Arc::new(client), backend)
|
||||
};
|
||||
|
||||
let voters = vec![(pezsp_keyring::Ed25519Keyring::Alice.public().into(), 1)];
|
||||
|
||||
let persistent_data =
|
||||
aux_schema::load_persistent(&*backend, client.info().genesis_hash, 0, || Ok(voters))
|
||||
.unwrap();
|
||||
|
||||
let (_tx, voter_command_rx) = tracing_unbounded("test_mpsc_voter_command", 100_000);
|
||||
|
||||
let observer = ObserverWork::new(
|
||||
client,
|
||||
tester.net_handle.clone(),
|
||||
persistent_data,
|
||||
None,
|
||||
voter_command_rx,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// Trigger a reputation change through the gossip validator.
|
||||
let peer_id = PeerId::random();
|
||||
tester.trigger_gossip_validator_reputation_change(&peer_id);
|
||||
|
||||
executor::block_on(async move {
|
||||
// Poll the observer once and have it forward the reputation change from the gossip
|
||||
// validator to the test network.
|
||||
assert!(observer.now_or_never().is_none());
|
||||
|
||||
assert_matches!(tester.events.next().now_or_never(), Some(Some(Event::Report(_, _))));
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,454 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Handling custom voting rules for GRANDPA.
|
||||
//!
|
||||
//! This exposes the `VotingRule` trait used to implement arbitrary voting
|
||||
//! restrictions that are taken into account by the GRANDPA environment when
|
||||
//! selecting a finality target to vote on.
|
||||
|
||||
use std::{future::Future, pin::Pin, sync::Arc};
|
||||
|
||||
use dyn_clone::DynClone;
|
||||
|
||||
use pezsc_client_api::blockchain::HeaderBackend;
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header, NumberFor, One, Zero};
|
||||
|
||||
/// A future returned by a `VotingRule` to restrict a given vote, if any restriction is necessary.
|
||||
pub type VotingRuleResult<Block> =
|
||||
Pin<Box<dyn Future<Output = Option<(<Block as BlockT>::Hash, NumberFor<Block>)>> + Send>>;
|
||||
|
||||
/// A trait for custom voting rules in GRANDPA.
|
||||
pub trait VotingRule<Block, B>: DynClone + Send + Sync
|
||||
where
|
||||
Block: BlockT,
|
||||
B: HeaderBackend<Block>,
|
||||
{
|
||||
/// Restrict the given `current_target` vote, returning the block hash and
|
||||
/// number of the block to vote on, and `None` in case the vote should not
|
||||
/// be restricted. `base` is the block that we're basing our votes on in
|
||||
/// order to pick our target (e.g. last round estimate), and `best_target`
|
||||
/// is the initial best vote target before any vote rules were applied. When
|
||||
/// applying multiple `VotingRule`s both `base` and `best_target` should
|
||||
/// remain unchanged.
|
||||
///
|
||||
/// The contract of this interface requires that when restricting a vote, the
|
||||
/// returned value **must** be an ancestor of the given `current_target`,
|
||||
/// this also means that a variant must be maintained throughout the
|
||||
/// execution of voting rules wherein `current_target <= best_target`.
|
||||
fn restrict_vote(
|
||||
&self,
|
||||
backend: Arc<B>,
|
||||
base: &Block::Header,
|
||||
best_target: &Block::Header,
|
||||
current_target: &Block::Header,
|
||||
) -> VotingRuleResult<Block>;
|
||||
}
|
||||
|
||||
impl<Block, B> VotingRule<Block, B> for ()
|
||||
where
|
||||
Block: BlockT,
|
||||
B: HeaderBackend<Block>,
|
||||
{
|
||||
fn restrict_vote(
|
||||
&self,
|
||||
_backend: Arc<B>,
|
||||
_base: &Block::Header,
|
||||
_best_target: &Block::Header,
|
||||
_current_target: &Block::Header,
|
||||
) -> VotingRuleResult<Block> {
|
||||
Box::pin(async { None })
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom voting rule that guarantees that our vote is always behind the best
|
||||
/// block by at least N blocks, unless the base number is < N blocks behind the
|
||||
/// best, in which case it votes for the base.
|
||||
///
|
||||
/// In the best case our vote is exactly N blocks
|
||||
/// behind the best block, but if there is a scenario where either
|
||||
/// \>34% of validators run without this rule or the fork-choice rule
|
||||
/// can prioritize shorter chains over longer ones, the vote may be
|
||||
/// closer to the best block than N.
|
||||
#[derive(Clone)]
|
||||
pub struct BeforeBestBlockBy<N>(pub N);
|
||||
impl<Block, B> VotingRule<Block, B> for BeforeBestBlockBy<NumberFor<Block>>
|
||||
where
|
||||
Block: BlockT,
|
||||
B: HeaderBackend<Block>,
|
||||
{
|
||||
fn restrict_vote(
|
||||
&self,
|
||||
backend: Arc<B>,
|
||||
base: &Block::Header,
|
||||
best_target: &Block::Header,
|
||||
current_target: &Block::Header,
|
||||
) -> VotingRuleResult<Block> {
|
||||
use pezsp_arithmetic::traits::Saturating;
|
||||
|
||||
if current_target.number().is_zero() {
|
||||
return Box::pin(async { None });
|
||||
}
|
||||
|
||||
// Constrain to the base number, if that's the minimal
|
||||
// vote that can be placed.
|
||||
if *base.number() + self.0 > *best_target.number() {
|
||||
return Box::pin(std::future::ready(Some((base.hash(), *base.number()))));
|
||||
}
|
||||
|
||||
// find the target number restricted by this rule
|
||||
let target_number = best_target.number().saturating_sub(self.0);
|
||||
|
||||
// our current target is already lower than this rule would restrict
|
||||
if target_number >= *current_target.number() {
|
||||
return Box::pin(async { None });
|
||||
}
|
||||
|
||||
let current_target = current_target.clone();
|
||||
|
||||
// find the block at the given target height
|
||||
Box::pin(std::future::ready(find_target(&*backend, target_number, ¤t_target)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom voting rule that limits votes towards 3/4 of the unfinalized chain,
|
||||
/// using the given `base` and `best_target` to figure where the 3/4 target
|
||||
/// should fall.
|
||||
#[derive(Clone)]
|
||||
pub struct ThreeQuartersOfTheUnfinalizedChain;
|
||||
|
||||
impl<Block, B> VotingRule<Block, B> for ThreeQuartersOfTheUnfinalizedChain
|
||||
where
|
||||
Block: BlockT,
|
||||
B: HeaderBackend<Block>,
|
||||
{
|
||||
fn restrict_vote(
|
||||
&self,
|
||||
backend: Arc<B>,
|
||||
base: &Block::Header,
|
||||
best_target: &Block::Header,
|
||||
current_target: &Block::Header,
|
||||
) -> VotingRuleResult<Block> {
|
||||
// target a vote towards 3/4 of the unfinalized chain (rounding up)
|
||||
let target_number = {
|
||||
let two = NumberFor::<Block>::one() + One::one();
|
||||
let three = two + One::one();
|
||||
let four = three + One::one();
|
||||
|
||||
let diff = *best_target.number() - *base.number();
|
||||
let diff = ((diff * three) + two) / four;
|
||||
|
||||
*base.number() + diff
|
||||
};
|
||||
|
||||
// our current target is already lower than this rule would restrict
|
||||
if target_number >= *current_target.number() {
|
||||
return Box::pin(async { None });
|
||||
}
|
||||
|
||||
// find the block at the given target height
|
||||
Box::pin(std::future::ready(find_target(&*backend, target_number, current_target)))
|
||||
}
|
||||
}
|
||||
|
||||
// walk backwards until we find the target block
|
||||
fn find_target<Block, B>(
|
||||
backend: &B,
|
||||
target_number: NumberFor<Block>,
|
||||
current_header: &Block::Header,
|
||||
) -> Option<(Block::Hash, NumberFor<Block>)>
|
||||
where
|
||||
Block: BlockT,
|
||||
B: HeaderBackend<Block>,
|
||||
{
|
||||
let mut target_hash = current_header.hash();
|
||||
let mut target_header = current_header.clone();
|
||||
|
||||
loop {
|
||||
if *target_header.number() < target_number {
|
||||
unreachable!(
|
||||
"we are traversing backwards from a known block; \
|
||||
blocks are stored contiguously; \
|
||||
qed"
|
||||
);
|
||||
}
|
||||
|
||||
if *target_header.number() == target_number {
|
||||
return Some((target_hash, target_number));
|
||||
}
|
||||
|
||||
target_hash = *target_header.parent_hash();
|
||||
target_header = backend
|
||||
.header(target_hash)
|
||||
.ok()?
|
||||
.expect("Header known to exist due to the existence of one of its descendants; qed");
|
||||
}
|
||||
}
|
||||
|
||||
struct VotingRules<Block, B> {
|
||||
rules: Arc<Vec<Box<dyn VotingRule<Block, B>>>>,
|
||||
}
|
||||
|
||||
impl<B, Block> Clone for VotingRules<B, Block> {
|
||||
fn clone(&self) -> Self {
|
||||
VotingRules { rules: self.rules.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block, B> VotingRule<Block, B> for VotingRules<Block, B>
|
||||
where
|
||||
Block: BlockT,
|
||||
B: HeaderBackend<Block> + 'static,
|
||||
{
|
||||
fn restrict_vote(
|
||||
&self,
|
||||
backend: Arc<B>,
|
||||
base: &Block::Header,
|
||||
best_target: &Block::Header,
|
||||
current_target: &Block::Header,
|
||||
) -> VotingRuleResult<Block> {
|
||||
let rules = self.rules.clone();
|
||||
let base = base.clone();
|
||||
let best_target = best_target.clone();
|
||||
let current_target = current_target.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
let mut restricted_target = current_target.clone();
|
||||
|
||||
for rule in rules.iter() {
|
||||
if let Some(header) = rule
|
||||
.restrict_vote(backend.clone(), &base, &best_target, &restricted_target)
|
||||
.await
|
||||
.filter(|(_, restricted_number)| {
|
||||
// NOTE: we can only restrict votes within the interval [base, target)
|
||||
restricted_number >= base.number() &&
|
||||
restricted_number < restricted_target.number()
|
||||
})
|
||||
.and_then(|(hash, _)| backend.header(hash).ok())
|
||||
.and_then(std::convert::identity)
|
||||
{
|
||||
restricted_target = header;
|
||||
}
|
||||
}
|
||||
|
||||
let restricted_hash = restricted_target.hash();
|
||||
|
||||
if restricted_hash != current_target.hash() {
|
||||
Some((restricted_hash, *restricted_target.number()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder of a composite voting rule that applies a set of rules to
|
||||
/// progressively restrict the vote.
|
||||
pub struct VotingRulesBuilder<Block, B> {
|
||||
rules: Vec<Box<dyn VotingRule<Block, B>>>,
|
||||
}
|
||||
|
||||
impl<Block, B> Default for VotingRulesBuilder<Block, B>
|
||||
where
|
||||
Block: BlockT,
|
||||
B: HeaderBackend<Block> + 'static,
|
||||
{
|
||||
fn default() -> Self {
|
||||
VotingRulesBuilder::new()
|
||||
.add(BeforeBestBlockBy(2u32.into()))
|
||||
.add(ThreeQuartersOfTheUnfinalizedChain)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block, B> VotingRulesBuilder<Block, B>
|
||||
where
|
||||
Block: BlockT,
|
||||
B: HeaderBackend<Block> + 'static,
|
||||
{
|
||||
/// Return a new voting rule builder using the given backend.
|
||||
pub fn new() -> Self {
|
||||
VotingRulesBuilder { rules: Vec::new() }
|
||||
}
|
||||
|
||||
/// Add a new voting rule to the builder.
|
||||
pub fn add<R>(mut self, rule: R) -> Self
|
||||
where
|
||||
R: VotingRule<Block, B> + 'static,
|
||||
{
|
||||
self.rules.push(Box::new(rule));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add all given voting rules to the builder.
|
||||
pub fn add_all<I>(mut self, rules: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = Box<dyn VotingRule<Block, B>>>,
|
||||
{
|
||||
self.rules.extend(rules);
|
||||
self
|
||||
}
|
||||
|
||||
/// Return a new `VotingRule` that applies all of the previously added
|
||||
/// voting rules in-order.
|
||||
pub fn build(self) -> impl VotingRule<Block, B> + Clone {
|
||||
VotingRules { rules: Arc::new(self.rules) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block, B> VotingRule<Block, B> for Box<dyn VotingRule<Block, B>>
|
||||
where
|
||||
Block: BlockT,
|
||||
B: HeaderBackend<Block>,
|
||||
Self: Clone,
|
||||
{
|
||||
fn restrict_vote(
|
||||
&self,
|
||||
backend: Arc<B>,
|
||||
base: &Block::Header,
|
||||
best_target: &Block::Header,
|
||||
current_target: &Block::Header,
|
||||
) -> VotingRuleResult<Block> {
|
||||
(**self).restrict_vote(backend, base, best_target, current_target)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pezsc_block_builder::BlockBuilderBuilder;
|
||||
use pezsp_consensus::BlockOrigin;
|
||||
use pezsp_runtime::traits::Header as _;
|
||||
|
||||
use bizinikiwi_test_runtime_client::{
|
||||
runtime::{Block, Header},
|
||||
Backend, Client, ClientBlockImportExt, DefaultTestClientBuilderExt, TestClientBuilder,
|
||||
TestClientBuilderExt,
|
||||
};
|
||||
|
||||
/// A mock voting rule that subtracts a static number of block from the `current_target`.
|
||||
#[derive(Clone)]
|
||||
struct Subtract(u64);
|
||||
impl VotingRule<Block, Client<Backend>> for Subtract {
|
||||
fn restrict_vote(
|
||||
&self,
|
||||
backend: Arc<Client<Backend>>,
|
||||
_base: &Header,
|
||||
_best_target: &Header,
|
||||
current_target: &Header,
|
||||
) -> VotingRuleResult<Block> {
|
||||
let target_number = current_target.number() - self.0;
|
||||
let res = backend
|
||||
.hash(target_number)
|
||||
.unwrap()
|
||||
.map(|target_hash| (target_hash, target_number));
|
||||
|
||||
Box::pin(std::future::ready(res))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_voting_rules_cannot_restrict_past_base() {
|
||||
// setup an aggregate voting rule composed of two voting rules
|
||||
// where each subtracts 50 blocks from the current target
|
||||
let rule = VotingRulesBuilder::new().add(Subtract(50)).add(Subtract(50)).build();
|
||||
|
||||
let client = Arc::new(TestClientBuilder::new().build());
|
||||
let mut hashes = Vec::with_capacity(200);
|
||||
|
||||
for _ in 0..200 {
|
||||
let block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
hashes.push(block.hash());
|
||||
|
||||
futures::executor::block_on(client.import(BlockOrigin::Own, block)).unwrap();
|
||||
}
|
||||
|
||||
let genesis = client.header(client.info().genesis_hash).unwrap().unwrap();
|
||||
|
||||
let best = client.header(client.info().best_hash).unwrap().unwrap();
|
||||
|
||||
let (_, number) =
|
||||
futures::executor::block_on(rule.restrict_vote(client.clone(), &genesis, &best, &best))
|
||||
.unwrap();
|
||||
|
||||
// we apply both rules which should subtract 100 blocks from best block (#200)
|
||||
// which means that we should be voting for block #100
|
||||
assert_eq!(number, 100);
|
||||
|
||||
let block110 = client.header(hashes[109]).unwrap().unwrap();
|
||||
|
||||
let (_, number) = futures::executor::block_on(rule.restrict_vote(
|
||||
client.clone(),
|
||||
&block110,
|
||||
&best,
|
||||
&best,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
// base block is #110 while best block is #200, applying both rules would make
|
||||
// would make the target block (#100) be lower than the base block, therefore
|
||||
// only one of the rules is applied.
|
||||
assert_eq!(number, 150);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn before_best_by_has_cutoff_at_base() {
|
||||
let rule = BeforeBestBlockBy(2);
|
||||
|
||||
let client = Arc::new(TestClientBuilder::new().build());
|
||||
|
||||
let n = 5;
|
||||
let mut hashes = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
let block = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block;
|
||||
hashes.push(block.hash());
|
||||
|
||||
futures::executor::block_on(client.import(BlockOrigin::Own, block)).unwrap();
|
||||
}
|
||||
|
||||
let best = client.header(client.info().best_hash).unwrap().unwrap();
|
||||
let best_number = *best.number();
|
||||
|
||||
for i in 0..n {
|
||||
let base = client.header(hashes[i]).unwrap().unwrap();
|
||||
let (_, number) = futures::executor::block_on(rule.restrict_vote(
|
||||
client.clone(),
|
||||
&base,
|
||||
&best,
|
||||
&best,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let expected = std::cmp::max(best_number - 2, *base.number());
|
||||
assert_eq!(number, expected, "best = {}, lag = 2, base = {}", best_number, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Bizinikiwi.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// Bizinikiwi 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.
|
||||
|
||||
// Bizinikiwi 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 Bizinikiwi. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Utilities for generating and verifying GRANDPA warp sync proofs.
|
||||
|
||||
use codec::{Decode, DecodeAll, Encode};
|
||||
|
||||
use crate::{
|
||||
best_justification, find_scheduled_change, AuthoritySetChanges, AuthoritySetHardFork,
|
||||
BlockNumberOps, GrandpaJustification, SharedAuthoritySet,
|
||||
};
|
||||
use pezsc_client_api::Backend as ClientBackend;
|
||||
use pezsc_network_sync::strategy::warp::{EncodedProof, VerificationResult, WarpSyncProvider};
|
||||
use pezsp_blockchain::{Backend as BlockchainBackend, HeaderBackend};
|
||||
use pezsp_consensus_grandpa::{AuthorityList, SetId, GRANDPA_ENGINE_ID};
|
||||
use pezsp_runtime::{
|
||||
generic::BlockId,
|
||||
traits::{Block as BlockT, Header as HeaderT, NumberFor, One},
|
||||
Justifications,
|
||||
};
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
/// Warp proof processing error.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Decoding error.
|
||||
#[error("Failed to decode block hash: {0}.")]
|
||||
DecodeScale(#[from] codec::Error),
|
||||
/// Client backend error.
|
||||
#[error("{0}")]
|
||||
Client(#[from] pezsp_blockchain::Error),
|
||||
/// Invalid request data.
|
||||
#[error("{0}")]
|
||||
InvalidRequest(String),
|
||||
/// Invalid warp proof.
|
||||
#[error("{0}")]
|
||||
InvalidProof(String),
|
||||
/// Missing header or authority set change data.
|
||||
#[error("Missing required data to be able to answer request.")]
|
||||
MissingData,
|
||||
}
|
||||
|
||||
/// The maximum size in bytes of the `WarpSyncProof`.
|
||||
pub(super) const MAX_WARP_SYNC_PROOF_SIZE: usize = 8 * 1024 * 1024;
|
||||
|
||||
/// A proof of an authority set change.
|
||||
#[derive(Decode, Encode, Debug)]
|
||||
pub struct WarpSyncFragment<Block: BlockT> {
|
||||
/// The last block that the given authority set finalized. This block should contain a digest
|
||||
/// signaling an authority set change from which we can fetch the next authority set.
|
||||
pub header: Block::Header,
|
||||
/// A justification for the header above which proves its finality. In order to validate it the
|
||||
/// verifier must be aware of the authorities and set id for which the justification refers to.
|
||||
pub justification: GrandpaJustification<Block>,
|
||||
}
|
||||
|
||||
/// An accumulated proof of multiple authority set changes.
|
||||
#[derive(Decode, Encode)]
|
||||
pub struct WarpSyncProof<Block: BlockT> {
|
||||
proofs: Vec<WarpSyncFragment<Block>>,
|
||||
is_finished: bool,
|
||||
}
|
||||
|
||||
impl<Block: BlockT> WarpSyncProof<Block> {
|
||||
/// Generates a warp sync proof starting at the given block. It will generate authority set
|
||||
/// change proofs for all changes that happened from `begin` until the current authority set
|
||||
/// (capped by MAX_WARP_SYNC_PROOF_SIZE).
|
||||
fn generate<Backend>(
|
||||
backend: &Backend,
|
||||
begin: Block::Hash,
|
||||
set_changes: &AuthoritySetChanges<NumberFor<Block>>,
|
||||
) -> Result<WarpSyncProof<Block>, Error>
|
||||
where
|
||||
Backend: ClientBackend<Block>,
|
||||
{
|
||||
// TODO: cache best response (i.e. the one with lowest begin_number)
|
||||
let blockchain = backend.blockchain();
|
||||
|
||||
let begin_number = blockchain
|
||||
.block_number_from_id(&BlockId::Hash(begin))?
|
||||
.ok_or_else(|| Error::InvalidRequest("Missing start block".to_string()))?;
|
||||
|
||||
if begin_number > blockchain.info().finalized_number {
|
||||
return Err(Error::InvalidRequest("Start block is not finalized".to_string()));
|
||||
}
|
||||
|
||||
let canon_hash = blockchain.hash(begin_number)?.expect(
|
||||
"begin number is lower than finalized number; \
|
||||
all blocks below finalized number must have been imported; \
|
||||
qed.",
|
||||
);
|
||||
|
||||
if canon_hash != begin {
|
||||
return Err(Error::InvalidRequest(
|
||||
"Start block is not in the finalized chain".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut proofs = Vec::new();
|
||||
let mut proofs_encoded_len = 0;
|
||||
let mut proof_limit_reached = false;
|
||||
|
||||
let set_changes = set_changes.iter_from(begin_number).ok_or(Error::MissingData)?;
|
||||
|
||||
for (_, last_block) in set_changes {
|
||||
let hash = blockchain.block_hash_from_id(&BlockId::Number(*last_block))?
|
||||
.expect("header number comes from previously applied set changes; corresponding hash must exist in db; qed.");
|
||||
|
||||
let header = blockchain
|
||||
.header(hash)?
|
||||
.expect("header hash obtained from header number exists in db; corresponding header must exist in db too; qed.");
|
||||
|
||||
// the last block in a set is the one that triggers a change to the next set,
|
||||
// therefore the block must have a digest that signals the authority set change
|
||||
if find_scheduled_change::<Block>(&header).is_none() {
|
||||
// if it doesn't contain a signal for standard change then the set must have changed
|
||||
// through a forced changed, in which case we stop collecting proofs as the chain of
|
||||
// trust in authority handoffs was broken.
|
||||
break;
|
||||
}
|
||||
|
||||
let justification = blockchain
|
||||
.justifications(header.hash())?
|
||||
.and_then(|just| just.into_justification(GRANDPA_ENGINE_ID))
|
||||
.ok_or_else(|| Error::MissingData)?;
|
||||
|
||||
let justification = GrandpaJustification::<Block>::decode_all(&mut &justification[..])?;
|
||||
|
||||
let proof = WarpSyncFragment { header: header.clone(), justification };
|
||||
let proof_size = proof.encoded_size();
|
||||
|
||||
// Check for the limit. We remove some bytes from the maximum size, because we're only
|
||||
// counting the size of the `WarpSyncFragment`s. The extra margin is here to leave
|
||||
// room for rest of the data (the size of the `Vec` and the boolean).
|
||||
if proofs_encoded_len + proof_size >= MAX_WARP_SYNC_PROOF_SIZE - 50 {
|
||||
proof_limit_reached = true;
|
||||
break;
|
||||
}
|
||||
|
||||
proofs_encoded_len += proof_size;
|
||||
proofs.push(proof);
|
||||
}
|
||||
|
||||
let is_finished = if proof_limit_reached {
|
||||
false
|
||||
} else {
|
||||
let latest_justification = best_justification(backend)?.filter(|justification| {
|
||||
// the existing best justification must be for a block higher than the
|
||||
// last authority set change. if we didn't prove any authority set
|
||||
// change then we fallback to make sure it's higher or equal to the
|
||||
// initial warp sync block.
|
||||
let limit = proofs
|
||||
.last()
|
||||
.map(|proof| proof.justification.target().0 + One::one())
|
||||
.unwrap_or(begin_number);
|
||||
|
||||
justification.target().0 >= limit
|
||||
});
|
||||
|
||||
if let Some(latest_justification) = latest_justification {
|
||||
let header = blockchain.header(latest_justification.target().1)?
|
||||
.expect("header hash corresponds to a justification in db; must exist in db as well; qed.");
|
||||
|
||||
let proof = WarpSyncFragment { header, justification: latest_justification };
|
||||
|
||||
// Check for the limit. We remove some bytes from the maximum size, because we're
|
||||
// only counting the size of the `WarpSyncFragment`s. The extra margin is here
|
||||
// to leave room for rest of the data (the size of the `Vec` and the boolean).
|
||||
if proofs_encoded_len + proof.encoded_size() >= MAX_WARP_SYNC_PROOF_SIZE - 50 {
|
||||
false
|
||||
} else {
|
||||
proofs.push(proof);
|
||||
true
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
let final_outcome = WarpSyncProof { proofs, is_finished };
|
||||
debug_assert!(final_outcome.encoded_size() <= MAX_WARP_SYNC_PROOF_SIZE);
|
||||
Ok(final_outcome)
|
||||
}
|
||||
|
||||
/// Verifies the warp sync proof starting at the given set id and with the given authorities.
|
||||
/// Verification stops when either the proof is exhausted or finality for the target header can
|
||||
/// be proven. If the proof is valid the new set id and authorities is returned.
|
||||
fn verify(
|
||||
&self,
|
||||
set_id: SetId,
|
||||
authorities: AuthorityList,
|
||||
hard_forks: &HashMap<(Block::Hash, NumberFor<Block>), (SetId, AuthorityList)>,
|
||||
) -> Result<(SetId, AuthorityList), Error>
|
||||
where
|
||||
NumberFor<Block>: BlockNumberOps,
|
||||
{
|
||||
let mut current_set_id = set_id;
|
||||
let mut current_authorities = authorities;
|
||||
|
||||
for (fragment_num, proof) in self.proofs.iter().enumerate() {
|
||||
let hash = proof.header.hash();
|
||||
let number = *proof.header.number();
|
||||
|
||||
if let Some((set_id, list)) = hard_forks.get(&(hash, number)) {
|
||||
current_set_id = *set_id;
|
||||
current_authorities = list.clone();
|
||||
} else {
|
||||
proof
|
||||
.justification
|
||||
.verify(current_set_id, ¤t_authorities)
|
||||
.map_err(|err| Error::InvalidProof(err.to_string()))?;
|
||||
|
||||
if proof.justification.target().1 != hash {
|
||||
return Err(Error::InvalidProof(
|
||||
"Mismatch between header and justification".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(scheduled_change) = find_scheduled_change::<Block>(&proof.header) {
|
||||
current_authorities = scheduled_change.next_authorities;
|
||||
current_set_id += 1;
|
||||
} else if fragment_num != self.proofs.len() - 1 || !self.is_finished {
|
||||
// Only the last fragment of the last proof message is allowed to be missing the
|
||||
// authority set change.
|
||||
return Err(Error::InvalidProof(
|
||||
"Header is missing authority set change digest".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((current_set_id, current_authorities))
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements network API for warp sync.
|
||||
pub struct NetworkProvider<Block: BlockT, Backend: ClientBackend<Block>>
|
||||
where
|
||||
NumberFor<Block>: BlockNumberOps,
|
||||
{
|
||||
backend: Arc<Backend>,
|
||||
authority_set: SharedAuthoritySet<Block::Hash, NumberFor<Block>>,
|
||||
hard_forks: HashMap<(Block::Hash, NumberFor<Block>), (SetId, AuthorityList)>,
|
||||
}
|
||||
|
||||
impl<Block: BlockT, Backend: ClientBackend<Block>> NetworkProvider<Block, Backend>
|
||||
where
|
||||
NumberFor<Block>: BlockNumberOps,
|
||||
{
|
||||
/// Create a new instance for a given backend and authority set.
|
||||
pub fn new(
|
||||
backend: Arc<Backend>,
|
||||
authority_set: SharedAuthoritySet<Block::Hash, NumberFor<Block>>,
|
||||
hard_forks: Vec<AuthoritySetHardFork<Block>>,
|
||||
) -> Self {
|
||||
NetworkProvider {
|
||||
backend,
|
||||
authority_set,
|
||||
hard_forks: hard_forks
|
||||
.into_iter()
|
||||
.map(|fork| (fork.block, (fork.set_id, fork.authorities)))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block: BlockT, Backend: ClientBackend<Block>> WarpSyncProvider<Block>
|
||||
for NetworkProvider<Block, Backend>
|
||||
where
|
||||
NumberFor<Block>: BlockNumberOps,
|
||||
{
|
||||
fn generate(
|
||||
&self,
|
||||
start: Block::Hash,
|
||||
) -> Result<EncodedProof, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let proof = WarpSyncProof::<Block>::generate(
|
||||
&*self.backend,
|
||||
start,
|
||||
&self.authority_set.authority_set_changes(),
|
||||
)
|
||||
.map_err(Box::new)?;
|
||||
Ok(EncodedProof(proof.encode()))
|
||||
}
|
||||
|
||||
fn verify(
|
||||
&self,
|
||||
proof: &EncodedProof,
|
||||
set_id: SetId,
|
||||
authorities: AuthorityList,
|
||||
) -> Result<VerificationResult<Block>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let EncodedProof(proof) = proof;
|
||||
let proof = WarpSyncProof::<Block>::decode_all(&mut proof.as_slice())
|
||||
.map_err(|e| format!("Proof decoding error: {:?}", e))?;
|
||||
let last_header = proof
|
||||
.proofs
|
||||
.last()
|
||||
.map(|p| p.header.clone())
|
||||
.ok_or_else(|| "Empty proof".to_string())?;
|
||||
let (next_set_id, next_authorities) =
|
||||
proof.verify(set_id, authorities, &self.hard_forks).map_err(Box::new)?;
|
||||
let justifications = proof
|
||||
.proofs
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
let justifications =
|
||||
Justifications::new(vec![(GRANDPA_ENGINE_ID, p.justification.encode())]);
|
||||
(p.header, justifications)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if proof.is_finished {
|
||||
Ok(VerificationResult::<Block>::Complete(
|
||||
next_set_id,
|
||||
next_authorities,
|
||||
last_header,
|
||||
justifications,
|
||||
))
|
||||
} else {
|
||||
Ok(VerificationResult::<Block>::Partial(
|
||||
next_set_id,
|
||||
next_authorities,
|
||||
last_header.hash(),
|
||||
justifications,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn current_authorities(&self) -> AuthorityList {
|
||||
self.authority_set.inner().current_authorities.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::WarpSyncProof;
|
||||
use crate::{AuthoritySetChanges, GrandpaJustification};
|
||||
use codec::Encode;
|
||||
use rand::prelude::*;
|
||||
use pezsc_block_builder::BlockBuilderBuilder;
|
||||
use pezsp_blockchain::HeaderBackend;
|
||||
use pezsp_consensus::BlockOrigin;
|
||||
use pezsp_consensus_grandpa::GRANDPA_ENGINE_ID;
|
||||
use pezsp_keyring::Ed25519Keyring;
|
||||
use std::sync::Arc;
|
||||
use bizinikiwi_test_runtime_client::{
|
||||
BlockBuilderExt, ClientBlockImportExt, ClientExt, DefaultTestClientBuilderExt,
|
||||
TestClientBuilder, TestClientBuilderExt,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn warp_sync_proof_generate_verify() {
|
||||
let mut rng = rand::rngs::StdRng::from_seed([0; 32]);
|
||||
let builder = TestClientBuilder::new();
|
||||
let backend = builder.backend();
|
||||
let client = Arc::new(builder.build());
|
||||
|
||||
let available_authorities = Ed25519Keyring::iter().collect::<Vec<_>>();
|
||||
let genesis_authorities = vec![(Ed25519Keyring::Alice.public().into(), 1)];
|
||||
|
||||
let mut current_authorities = vec![Ed25519Keyring::Alice];
|
||||
let mut current_set_id = 0;
|
||||
let mut authority_set_changes = Vec::new();
|
||||
|
||||
for n in 1..=100 {
|
||||
let mut builder = BlockBuilderBuilder::new(&*client)
|
||||
.on_parent_block(client.chain_info().best_hash)
|
||||
.with_parent_block_number(client.chain_info().best_number)
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut new_authorities = None;
|
||||
|
||||
// we will trigger an authority set change every 10 blocks
|
||||
if n != 0 && n % 10 == 0 {
|
||||
// pick next authorities and add digest for the set change
|
||||
let n_authorities = rng.gen_range(1..available_authorities.len());
|
||||
let next_authorities = available_authorities
|
||||
.choose_multiple(&mut rng, n_authorities)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
new_authorities = Some(next_authorities.clone());
|
||||
|
||||
let next_authorities = next_authorities
|
||||
.iter()
|
||||
.map(|keyring| (keyring.public().into(), 1))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let digest = pezsp_runtime::generic::DigestItem::Consensus(
|
||||
pezsp_consensus_grandpa::GRANDPA_ENGINE_ID,
|
||||
pezsp_consensus_grandpa::ConsensusLog::ScheduledChange(
|
||||
pezsp_consensus_grandpa::ScheduledChange { delay: 0u64, next_authorities },
|
||||
)
|
||||
.encode(),
|
||||
);
|
||||
|
||||
builder.push_deposit_log_digest_item(digest).unwrap();
|
||||
}
|
||||
|
||||
let block = builder.build().unwrap().block;
|
||||
|
||||
futures::executor::block_on(client.import(BlockOrigin::Own, block)).unwrap();
|
||||
|
||||
if let Some(new_authorities) = new_authorities {
|
||||
// generate a justification for this block, finalize it and note the authority set
|
||||
// change
|
||||
let (target_hash, target_number) = {
|
||||
let info = client.info();
|
||||
(info.best_hash, info.best_number)
|
||||
};
|
||||
|
||||
let mut precommits = Vec::new();
|
||||
for keyring in ¤t_authorities {
|
||||
let precommit = finality_grandpa::Precommit { target_hash, target_number };
|
||||
|
||||
let msg = finality_grandpa::Message::Precommit(precommit.clone());
|
||||
let encoded = pezsp_consensus_grandpa::localized_payload(42, current_set_id, &msg);
|
||||
let signature = keyring.sign(&encoded[..]).into();
|
||||
|
||||
let precommit = finality_grandpa::SignedPrecommit {
|
||||
precommit,
|
||||
signature,
|
||||
id: keyring.public().into(),
|
||||
};
|
||||
|
||||
precommits.push(precommit);
|
||||
}
|
||||
|
||||
let commit = finality_grandpa::Commit { target_hash, target_number, precommits };
|
||||
|
||||
let justification = GrandpaJustification::from_commit(&client, 42, commit).unwrap();
|
||||
|
||||
client
|
||||
.finalize_block(target_hash, Some((GRANDPA_ENGINE_ID, justification.encode())))
|
||||
.unwrap();
|
||||
|
||||
authority_set_changes.push((current_set_id, n));
|
||||
|
||||
current_set_id += 1;
|
||||
current_authorities = new_authorities;
|
||||
}
|
||||
}
|
||||
|
||||
let authority_set_changes = AuthoritySetChanges::from(authority_set_changes);
|
||||
|
||||
// generate a warp sync proof
|
||||
let genesis_hash = client.hash(0).unwrap().unwrap();
|
||||
|
||||
let warp_sync_proof =
|
||||
WarpSyncProof::generate(&*backend, genesis_hash, &authority_set_changes).unwrap();
|
||||
|
||||
// verifying the proof should yield the last set id and authorities
|
||||
let (new_set_id, new_authorities) =
|
||||
warp_sync_proof.verify(0, genesis_authorities, &Default::default()).unwrap();
|
||||
|
||||
let expected_authorities = current_authorities
|
||||
.iter()
|
||||
.map(|keyring| (keyring.public().into(), 1))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(new_set_id, current_set_id);
|
||||
assert_eq!(new_authorities, expected_authorities);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
[package]
|
||||
name = "pezsc-consensus-manual-seal"
|
||||
version = "0.35.0"
|
||||
authors.workspace = true
|
||||
description = "Manual sealing engine for Bizinikiwi"
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
codec = { workspace = true, default-features = true }
|
||||
futures = { workspace = true }
|
||||
futures-timer = { workspace = true }
|
||||
jsonrpsee = { features = [
|
||||
"client-core",
|
||||
"macros",
|
||||
"server-core",
|
||||
], workspace = true }
|
||||
log = { workspace = true, default-features = true }
|
||||
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-epochs = { workspace = true, default-features = true }
|
||||
pezsc-transaction-pool = { workspace = true, default-features = true }
|
||||
pezsc-transaction-pool-api = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive"], workspace = true, default-features = true }
|
||||
pezsp-api = { 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-consensus-babe = { workspace = true, default-features = true }
|
||||
pezsp-consensus-slots = { 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-timestamp = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezsc-basic-authorship = { workspace = true, default-features = true }
|
||||
bizinikiwi-test-runtime-client = { workspace = true }
|
||||
bizinikiwi-test-runtime-transaction-pool = { workspace = true }
|
||||
tokio = { features = [
|
||||
"macros",
|
||||
"rt-multi-thread",
|
||||
], workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezsc-basic-authorship/runtime-benchmarks",
|
||||
"pezsc-client-api/runtime-benchmarks",
|
||||
"pezsc-consensus-aura/runtime-benchmarks",
|
||||
"pezsc-consensus-babe/runtime-benchmarks",
|
||||
"pezsc-consensus-epochs/runtime-benchmarks",
|
||||
"pezsc-consensus/runtime-benchmarks",
|
||||
"pezsc-transaction-pool-api/runtime-benchmarks",
|
||||
"pezsc-transaction-pool/runtime-benchmarks",
|
||||
"pezsp-api/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus-aura/runtime-benchmarks",
|
||||
"pezsp-consensus-babe/runtime-benchmarks",
|
||||
"pezsp-consensus-slots/runtime-benchmarks",
|
||||
"pezsp-consensus/runtime-benchmarks",
|
||||
"pezsp-inherents/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"pezsp-timestamp/runtime-benchmarks",
|
||||
"bizinikiwi-test-runtime-client/runtime-benchmarks",
|
||||
"bizinikiwi-test-runtime-transaction-pool/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
A manual sealing engine: the engine listens for rpc calls to seal blocks and create forks.
|
||||
This is suitable for a testing environment.
|
||||
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,47 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Extensions for manual seal to produce blocks valid for any runtime.
|
||||
use super::Error;
|
||||
|
||||
use pezsc_consensus::BlockImportParams;
|
||||
use pezsp_inherents::InherentData;
|
||||
use pezsp_runtime::{traits::Block as BlockT, Digest};
|
||||
|
||||
pub mod aura;
|
||||
pub mod babe;
|
||||
pub mod timestamp;
|
||||
|
||||
/// Consensus data provider, manual seal uses this trait object for authoring blocks valid
|
||||
/// for any runtime.
|
||||
pub trait ConsensusDataProvider<B: BlockT>: Send + Sync {
|
||||
/// The proof type.
|
||||
type Proof;
|
||||
|
||||
/// Attempt to create a consensus digest.
|
||||
fn create_digest(&self, parent: &B::Header, inherents: &InherentData) -> Result<Digest, Error>;
|
||||
|
||||
/// Set up the necessary import params.
|
||||
fn append_block_import(
|
||||
&self,
|
||||
parent: &B::Header,
|
||||
params: &mut BlockImportParams<B>,
|
||||
inherents: &InherentData,
|
||||
proof: Self::Proof,
|
||||
) -> Result<(), Error>;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Aura consensus data provider, This allows manual seal author blocks that are valid for
|
||||
//! runtimes that expect the aura-specific digests.
|
||||
|
||||
use crate::{ConsensusDataProvider, Error};
|
||||
use pezsc_client_api::{AuxStore, UsageProvider};
|
||||
use pezsc_consensus::BlockImportParams;
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_consensus_aura::{
|
||||
digests::CompatibleDigestItem,
|
||||
sr25519::{AuthorityId, AuthoritySignature},
|
||||
AuraApi, Slot, SlotDuration,
|
||||
};
|
||||
use pezsp_inherents::InherentData;
|
||||
use pezsp_runtime::{traits::Block as BlockT, Digest, DigestItem};
|
||||
use pezsp_timestamp::TimestampInherentData;
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
|
||||
/// Consensus data provider for Aura. This allows to use manual-seal driven nodes to author valid
|
||||
/// AURA blocks. It will inspect incoming [`InherentData`] and look for included timestamps. Based
|
||||
/// on these timestamps, the [`AuraConsensusDataProvider`] will emit fitting digest items.
|
||||
pub struct AuraConsensusDataProvider<B, P> {
|
||||
// slot duration
|
||||
slot_duration: SlotDuration,
|
||||
// phantom data for required generics
|
||||
_phantom: PhantomData<(B, P)>,
|
||||
}
|
||||
|
||||
impl<B, P> AuraConsensusDataProvider<B, P>
|
||||
where
|
||||
B: BlockT,
|
||||
{
|
||||
/// Creates a new instance of the [`AuraConsensusDataProvider`], requires that `client`
|
||||
/// implements [`pezsp_consensus_aura::AuraApi`]
|
||||
pub fn new<C>(client: Arc<C>) -> Self
|
||||
where
|
||||
C: AuxStore + ProvideRuntimeApi<B> + UsageProvider<B>,
|
||||
C::Api: AuraApi<B, AuthorityId>,
|
||||
{
|
||||
let slot_duration = pezsc_consensus_aura::slot_duration(&*client)
|
||||
.expect("slot_duration is always present; qed.");
|
||||
|
||||
Self { slot_duration, _phantom: PhantomData }
|
||||
}
|
||||
|
||||
/// Creates a new instance of the [`AuraConsensusDataProvider`]
|
||||
pub fn new_with_slot_duration(slot_duration: SlotDuration) -> Self {
|
||||
Self { slot_duration, _phantom: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, P> ConsensusDataProvider<B> for AuraConsensusDataProvider<B, P>
|
||||
where
|
||||
B: BlockT,
|
||||
P: Send + Sync,
|
||||
{
|
||||
type Proof = P;
|
||||
|
||||
fn create_digest(
|
||||
&self,
|
||||
_parent: &B::Header,
|
||||
inherents: &InherentData,
|
||||
) -> Result<Digest, Error> {
|
||||
let timestamp =
|
||||
inherents.timestamp_inherent_data()?.expect("Timestamp is always present; qed");
|
||||
|
||||
// we always calculate the new slot number based on the current time-stamp and the slot
|
||||
// duration.
|
||||
let digest_item = <DigestItem as CompatibleDigestItem<AuthoritySignature>>::aura_pre_digest(
|
||||
Slot::from_timestamp(timestamp, self.slot_duration),
|
||||
);
|
||||
|
||||
Ok(Digest { logs: vec![digest_item] })
|
||||
}
|
||||
|
||||
fn append_block_import(
|
||||
&self,
|
||||
_parent: &B::Header,
|
||||
_params: &mut BlockImportParams<B>,
|
||||
_inherents: &InherentData,
|
||||
_proof: Self::Proof,
|
||||
) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! BABE consensus data provider, This allows manual seal author blocks that are valid for runtimes
|
||||
//! that expect babe-specific digests.
|
||||
|
||||
use super::ConsensusDataProvider;
|
||||
use crate::{Error, LOG_TARGET};
|
||||
use codec::Encode;
|
||||
use pezsc_client_api::{AuxStore, UsageProvider};
|
||||
use pezsc_consensus_babe::{
|
||||
authorship, find_pre_digest, BabeIntermediate, CompatibleDigestItem, Epoch, INTERMEDIATE_KEY,
|
||||
};
|
||||
use pezsc_consensus_epochs::{
|
||||
descendent_query, EpochHeader, SharedEpochChanges, ViableEpochDescriptor,
|
||||
};
|
||||
use pezsp_keystore::KeystorePtr;
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
|
||||
use pezsc_consensus::{BlockImportParams, ForkChoiceStrategy, Verifier};
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_blockchain::{HeaderBackend, HeaderMetadata};
|
||||
use pezsp_consensus_babe::{
|
||||
digests::{NextEpochDescriptor, PreDigest, SecondaryPlainPreDigest},
|
||||
inherents::BabeInherentData,
|
||||
AuthorityId, BabeApi, BabeAuthorityWeight, BabeConfiguration, ConsensusLog, BABE_ENGINE_ID,
|
||||
};
|
||||
use pezsp_consensus_slots::Slot;
|
||||
use pezsp_inherents::InherentData;
|
||||
use pezsp_runtime::{
|
||||
generic::Digest,
|
||||
traits::{Block as BlockT, Header},
|
||||
DigestItem,
|
||||
};
|
||||
use pezsp_timestamp::TimestampInherentData;
|
||||
|
||||
/// Provides BABE-compatible predigests and BlockImportParams.
|
||||
/// Intended for use with BABE runtimes.
|
||||
pub struct BabeConsensusDataProvider<B: BlockT, C, P> {
|
||||
/// shared reference to keystore
|
||||
keystore: KeystorePtr,
|
||||
|
||||
/// Shared reference to the client.
|
||||
client: Arc<C>,
|
||||
|
||||
/// Shared epoch changes
|
||||
epoch_changes: SharedEpochChanges<B, Epoch>,
|
||||
|
||||
/// BABE config, gotten from the runtime.
|
||||
/// NOTE: This is used to fetch `slot_duration` and `epoch_length` in the
|
||||
/// `ConsensusDataProvider` implementation. Correct as far as these values
|
||||
/// are not changed during an epoch change.
|
||||
config: BabeConfiguration,
|
||||
|
||||
/// Authorities to be used for this babe chain.
|
||||
authorities: Vec<(AuthorityId, BabeAuthorityWeight)>,
|
||||
_phantom: PhantomData<P>,
|
||||
}
|
||||
|
||||
/// Verifier to be used for babe chains
|
||||
pub struct BabeVerifier<B: BlockT, C> {
|
||||
/// Shared epoch changes
|
||||
epoch_changes: SharedEpochChanges<B, Epoch>,
|
||||
|
||||
/// Shared reference to the client.
|
||||
client: Arc<C>,
|
||||
}
|
||||
|
||||
impl<B: BlockT, C> BabeVerifier<B, C> {
|
||||
/// create a new verifier
|
||||
pub fn new(epoch_changes: SharedEpochChanges<B, Epoch>, client: Arc<C>) -> BabeVerifier<B, C> {
|
||||
BabeVerifier { epoch_changes, client }
|
||||
}
|
||||
}
|
||||
|
||||
/// The verifier for the manual seal engine; instantly finalizes.
|
||||
#[async_trait::async_trait]
|
||||
impl<B, C> Verifier<B> for BabeVerifier<B, C>
|
||||
where
|
||||
B: BlockT,
|
||||
C: HeaderBackend<B> + HeaderMetadata<B, Error = pezsp_blockchain::Error>,
|
||||
{
|
||||
async fn verify(
|
||||
&self,
|
||||
mut import_params: BlockImportParams<B>,
|
||||
) -> Result<BlockImportParams<B>, String> {
|
||||
import_params.finalized = false;
|
||||
import_params.fork_choice = Some(ForkChoiceStrategy::LongestChain);
|
||||
|
||||
let pre_digest = find_pre_digest::<B>(&import_params.header)?;
|
||||
|
||||
let parent_hash = import_params.header.parent_hash();
|
||||
let parent = self
|
||||
.client
|
||||
.header(*parent_hash)
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or_else(|| format!("header for block {} not found", parent_hash))?;
|
||||
let epoch_changes = self.epoch_changes.shared_data();
|
||||
let epoch_descriptor = epoch_changes
|
||||
.epoch_descriptor_for_child_of(
|
||||
descendent_query(&*self.client),
|
||||
&parent.hash(),
|
||||
*parent.number(),
|
||||
pre_digest.slot(),
|
||||
)
|
||||
.map_err(|e| format!("failed to fetch epoch_descriptor: {}", e))?
|
||||
.ok_or_else(|| format!("{}", pezsp_consensus::Error::InvalidAuthoritiesSet))?;
|
||||
// drop the lock
|
||||
drop(epoch_changes);
|
||||
|
||||
import_params
|
||||
.insert_intermediate(INTERMEDIATE_KEY, BabeIntermediate::<B> { epoch_descriptor });
|
||||
|
||||
Ok(import_params)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, C, P> BabeConsensusDataProvider<B, C, P>
|
||||
where
|
||||
B: BlockT,
|
||||
C: AuxStore
|
||||
+ HeaderBackend<B>
|
||||
+ ProvideRuntimeApi<B>
|
||||
+ HeaderMetadata<B, Error = pezsp_blockchain::Error>
|
||||
+ UsageProvider<B>,
|
||||
C::Api: BabeApi<B>,
|
||||
{
|
||||
pub fn new(
|
||||
client: Arc<C>,
|
||||
keystore: KeystorePtr,
|
||||
epoch_changes: SharedEpochChanges<B, Epoch>,
|
||||
authorities: Vec<(AuthorityId, BabeAuthorityWeight)>,
|
||||
) -> Result<Self, Error> {
|
||||
if authorities.is_empty() {
|
||||
return Err(Error::StringError("Cannot supply empty authority set!".into()));
|
||||
}
|
||||
|
||||
let config = pezsc_consensus_babe::configuration(&*client)?;
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
client,
|
||||
keystore,
|
||||
epoch_changes,
|
||||
authorities,
|
||||
_phantom: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn epoch(&self, parent: &B::Header, slot: Slot) -> Result<Epoch, Error> {
|
||||
let epoch_changes = self.epoch_changes.shared_data();
|
||||
let epoch_descriptor = epoch_changes
|
||||
.epoch_descriptor_for_child_of(
|
||||
descendent_query(&*self.client),
|
||||
&parent.hash(),
|
||||
*parent.number(),
|
||||
slot,
|
||||
)
|
||||
.map_err(|e| Error::StringError(format!("failed to fetch epoch_descriptor: {}", e)))?
|
||||
.ok_or(pezsp_consensus::Error::InvalidAuthoritiesSet)?;
|
||||
|
||||
let epoch = epoch_changes
|
||||
.viable_epoch(&epoch_descriptor, |slot| Epoch::genesis(&self.config, slot))
|
||||
.ok_or_else(|| {
|
||||
log::info!(target: LOG_TARGET, "create_digest: no viable_epoch :(");
|
||||
pezsp_consensus::Error::InvalidAuthoritiesSet
|
||||
})?;
|
||||
|
||||
Ok(epoch.as_ref().clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, C, P> ConsensusDataProvider<B> for BabeConsensusDataProvider<B, C, P>
|
||||
where
|
||||
B: BlockT,
|
||||
C: AuxStore
|
||||
+ HeaderBackend<B>
|
||||
+ HeaderMetadata<B, Error = pezsp_blockchain::Error>
|
||||
+ UsageProvider<B>
|
||||
+ ProvideRuntimeApi<B>,
|
||||
C::Api: BabeApi<B>,
|
||||
P: Send + Sync,
|
||||
{
|
||||
type Proof = P;
|
||||
|
||||
fn create_digest(&self, parent: &B::Header, inherents: &InherentData) -> Result<Digest, Error> {
|
||||
let slot = inherents
|
||||
.babe_inherent_data()?
|
||||
.ok_or_else(|| Error::StringError("No babe inherent data".into()))?;
|
||||
let epoch = self.epoch(parent, slot)?;
|
||||
|
||||
// this is a dev node environment, we should always be able to claim a slot.
|
||||
let logs = if let Some((predigest, _)) =
|
||||
authorship::claim_slot(slot, &epoch, &self.keystore)
|
||||
{
|
||||
vec![<DigestItem as CompatibleDigestItem>::babe_pre_digest(predigest)]
|
||||
} else {
|
||||
// well we couldn't claim a slot because this is an existing chain and we're not in the
|
||||
// authorities. we need to tell BabeBlockImport that the epoch has changed, and we put
|
||||
// ourselves in the authorities.
|
||||
let predigest =
|
||||
PreDigest::SecondaryPlain(SecondaryPlainPreDigest { slot, authority_index: 0_u32 });
|
||||
|
||||
let mut epoch_changes = self.epoch_changes.shared_data();
|
||||
let epoch_descriptor = epoch_changes
|
||||
.epoch_descriptor_for_child_of(
|
||||
descendent_query(&*self.client),
|
||||
&parent.hash(),
|
||||
*parent.number(),
|
||||
slot,
|
||||
)
|
||||
.map_err(|e| {
|
||||
Error::StringError(format!("failed to fetch epoch_descriptor: {}", e))
|
||||
})?
|
||||
.ok_or(pezsp_consensus::Error::InvalidAuthoritiesSet)?;
|
||||
|
||||
match epoch_descriptor {
|
||||
ViableEpochDescriptor::Signaled(identifier, _epoch_header) => {
|
||||
let epoch_mut = epoch_changes
|
||||
.epoch_mut(&identifier)
|
||||
.ok_or(pezsp_consensus::Error::InvalidAuthoritiesSet)?;
|
||||
|
||||
// mutate the current epoch
|
||||
epoch_mut.authorities = self.authorities.clone();
|
||||
|
||||
let next_epoch = ConsensusLog::NextEpochData(NextEpochDescriptor {
|
||||
authorities: self.authorities.clone(),
|
||||
// copy the old randomness
|
||||
randomness: epoch_mut.randomness,
|
||||
});
|
||||
|
||||
vec![
|
||||
DigestItem::PreRuntime(BABE_ENGINE_ID, predigest.encode()),
|
||||
DigestItem::Consensus(BABE_ENGINE_ID, next_epoch.encode()),
|
||||
]
|
||||
},
|
||||
ViableEpochDescriptor::UnimportedGenesis(_) => {
|
||||
// since this is the genesis, secondary predigest works for now.
|
||||
vec![DigestItem::PreRuntime(BABE_ENGINE_ID, predigest.encode())]
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Digest { logs })
|
||||
}
|
||||
|
||||
fn append_block_import(
|
||||
&self,
|
||||
parent: &B::Header,
|
||||
params: &mut BlockImportParams<B>,
|
||||
inherents: &InherentData,
|
||||
_proof: Self::Proof,
|
||||
) -> Result<(), Error> {
|
||||
let slot = inherents
|
||||
.babe_inherent_data()?
|
||||
.ok_or_else(|| Error::StringError("No babe inherent data".into()))?;
|
||||
let epoch_changes = self.epoch_changes.shared_data();
|
||||
let mut epoch_descriptor = epoch_changes
|
||||
.epoch_descriptor_for_child_of(
|
||||
descendent_query(&*self.client),
|
||||
&parent.hash(),
|
||||
*parent.number(),
|
||||
slot,
|
||||
)
|
||||
.map_err(|e| Error::StringError(format!("failed to fetch epoch_descriptor: {}", e)))?
|
||||
.ok_or(pezsp_consensus::Error::InvalidAuthoritiesSet)?;
|
||||
// drop the lock
|
||||
drop(epoch_changes);
|
||||
// a quick check to see if we're in the authorities
|
||||
let epoch = self.epoch(parent, slot)?;
|
||||
let (authority, _) = self.authorities.first().expect("authorities is non-emptyp; qed");
|
||||
let has_authority = epoch.authorities.iter().any(|(id, _)| *id == *authority);
|
||||
|
||||
if !has_authority {
|
||||
log::info!(target: LOG_TARGET, "authority not found");
|
||||
let timestamp = inherents
|
||||
.timestamp_inherent_data()?
|
||||
.ok_or_else(|| Error::StringError("No timestamp inherent data".into()))?;
|
||||
|
||||
let slot = Slot::from_timestamp(timestamp, self.config.slot_duration());
|
||||
|
||||
// manually hard code epoch descriptor
|
||||
epoch_descriptor = match epoch_descriptor {
|
||||
ViableEpochDescriptor::Signaled(identifier, _header) =>
|
||||
ViableEpochDescriptor::Signaled(
|
||||
identifier,
|
||||
EpochHeader {
|
||||
start_slot: slot,
|
||||
end_slot: (*slot * self.config.epoch_length).into(),
|
||||
},
|
||||
),
|
||||
_ => unreachable!(
|
||||
"we're not in the authorities, so this isn't the genesis epoch; qed"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
params.insert_intermediate(INTERMEDIATE_KEY, BabeIntermediate::<B> { epoch_descriptor });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Mocked timestamp inherent, allows for manual seal to create blocks for runtimes
|
||||
//! that expect this inherent.
|
||||
|
||||
use crate::Error;
|
||||
use pezsc_client_api::{AuxStore, UsageProvider};
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_blockchain::HeaderBackend;
|
||||
use pezsp_consensus_aura::{
|
||||
sr25519::{AuthorityId, AuthoritySignature},
|
||||
AuraApi,
|
||||
};
|
||||
use pezsp_consensus_babe::BabeApi;
|
||||
use pezsp_consensus_slots::{Slot, SlotDuration};
|
||||
use pezsp_inherents::{InherentData, InherentDataProvider, InherentIdentifier};
|
||||
use pezsp_runtime::traits::{Block as BlockT, Zero};
|
||||
use pezsp_timestamp::{InherentType, INHERENT_IDENTIFIER};
|
||||
use std::{
|
||||
sync::{atomic, Arc},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
/// Provide duration since unix epoch in millisecond for timestamp inherent.
|
||||
/// Mocks the timestamp inherent to always produce a valid timestamp for the next slot.
|
||||
///
|
||||
/// This works by either fetching the `slot_number` from the most recent header and dividing
|
||||
/// that value by `slot_duration` in order to fork chains that expect this inherent.
|
||||
///
|
||||
/// It produces timestamp inherents that are increased by `slot_duration` whenever
|
||||
/// `provide_inherent_data` is called.
|
||||
pub struct SlotTimestampProvider {
|
||||
// holds the unix millisecond timestamp for the most recent block
|
||||
unix_millis: atomic::AtomicU64,
|
||||
// configured slot_duration in the runtime
|
||||
slot_duration: SlotDuration,
|
||||
}
|
||||
|
||||
impl SlotTimestampProvider {
|
||||
/// Create a new mocked time stamp provider, for babe.
|
||||
pub fn new_babe<B, C>(client: Arc<C>) -> Result<Self, Error>
|
||||
where
|
||||
B: BlockT,
|
||||
C: AuxStore + HeaderBackend<B> + ProvideRuntimeApi<B> + UsageProvider<B>,
|
||||
C::Api: BabeApi<B>,
|
||||
{
|
||||
let slot_duration = pezsc_consensus_babe::configuration(&*client)?.slot_duration();
|
||||
|
||||
let time = Self::with_header(&client, slot_duration, |header| {
|
||||
let slot_number = *pezsc_consensus_babe::find_pre_digest::<B>(&header)
|
||||
.map_err(|err| format!("{}", err))?
|
||||
.slot();
|
||||
Ok(slot_number)
|
||||
})?;
|
||||
|
||||
Ok(Self { unix_millis: atomic::AtomicU64::new(time), slot_duration })
|
||||
}
|
||||
|
||||
/// Create a new mocked time stamp provider, for aura
|
||||
pub fn new_aura<B, C>(client: Arc<C>) -> Result<Self, Error>
|
||||
where
|
||||
B: BlockT,
|
||||
C: AuxStore + HeaderBackend<B> + ProvideRuntimeApi<B> + UsageProvider<B>,
|
||||
C::Api: AuraApi<B, AuthorityId>,
|
||||
{
|
||||
let slot_duration = pezsc_consensus_aura::slot_duration(&*client)?;
|
||||
|
||||
let time = Self::with_header(&client, slot_duration, |header| {
|
||||
let slot_number = *pezsc_consensus_aura::find_pre_digest::<B, AuthoritySignature>(&header)
|
||||
.map_err(|err| format!("{}", err))?;
|
||||
Ok(slot_number)
|
||||
})?;
|
||||
|
||||
Ok(Self { unix_millis: atomic::AtomicU64::new(time), slot_duration })
|
||||
}
|
||||
|
||||
fn with_header<F, C, B>(
|
||||
client: &Arc<C>,
|
||||
slot_duration: SlotDuration,
|
||||
func: F,
|
||||
) -> Result<u64, Error>
|
||||
where
|
||||
B: BlockT,
|
||||
C: AuxStore + HeaderBackend<B> + UsageProvider<B>,
|
||||
F: Fn(B::Header) -> Result<u64, Error>,
|
||||
{
|
||||
let info = client.info();
|
||||
|
||||
// looks like this isn't the first block, rehydrate the fake time.
|
||||
// otherwise we'd be producing blocks for older slots.
|
||||
let time = if info.best_number != Zero::zero() {
|
||||
let header = client
|
||||
.header(info.best_hash)?
|
||||
.ok_or_else(|| "best header not found in the db!".to_string())?;
|
||||
let slot = func(header)?;
|
||||
// add the slot duration so there's no collision of slots
|
||||
(slot * slot_duration.as_millis() as u64) + slot_duration.as_millis() as u64
|
||||
} else {
|
||||
// this is the first block, use the correct time.
|
||||
let now = SystemTime::now();
|
||||
now.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map_err(|err| Error::StringError(format!("{}", err)))?
|
||||
.as_millis() as u64
|
||||
};
|
||||
|
||||
Ok(time)
|
||||
}
|
||||
|
||||
/// Get the current slot number
|
||||
pub fn slot(&self) -> Slot {
|
||||
Slot::from_timestamp(
|
||||
self.unix_millis.load(atomic::Ordering::SeqCst).into(),
|
||||
self.slot_duration,
|
||||
)
|
||||
}
|
||||
|
||||
/// Gets the current time stamp.
|
||||
pub fn timestamp(&self) -> pezsp_timestamp::Timestamp {
|
||||
pezsp_timestamp::Timestamp::new(self.unix_millis.load(atomic::Ordering::SeqCst))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl InherentDataProvider for SlotTimestampProvider {
|
||||
async fn provide_inherent_data(
|
||||
&self,
|
||||
inherent_data: &mut InherentData,
|
||||
) -> Result<(), pezsp_inherents::Error> {
|
||||
// we update the time here.
|
||||
let new_time: InherentType = self
|
||||
.unix_millis
|
||||
.fetch_add(self.slot_duration.as_millis() as u64, atomic::Ordering::SeqCst)
|
||||
.into();
|
||||
inherent_data.put_data(INHERENT_IDENTIFIER, &new_time)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_handle_error(
|
||||
&self,
|
||||
_: &InherentIdentifier,
|
||||
_: &[u8],
|
||||
) -> Option<Result<(), pezsp_inherents::Error>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! A manual sealing engine: the engine listens for rpc calls to seal blocks and create forks.
|
||||
//! This is suitable for a testing environment.
|
||||
|
||||
use futures::channel::{mpsc::SendError, oneshot};
|
||||
use jsonrpsee::types::error::{ErrorObject, ErrorObjectOwned};
|
||||
use pezsc_consensus::ImportResult;
|
||||
use pezsp_blockchain::Error as BlockchainError;
|
||||
use pezsp_consensus::Error as ConsensusError;
|
||||
use pezsp_inherents::Error as InherentsError;
|
||||
|
||||
/// Error code for rpc
|
||||
mod codes {
|
||||
pub const SERVER_SHUTTING_DOWN: i32 = 10_000;
|
||||
pub const BLOCK_IMPORT_FAILED: i32 = 11_000;
|
||||
pub const EMPTY_TRANSACTION_POOL: i32 = 12_000;
|
||||
pub const BLOCK_NOT_FOUND: i32 = 13_000;
|
||||
pub const CONSENSUS_ERROR: i32 = 14_000;
|
||||
pub const INHERENTS_ERROR: i32 = 15_000;
|
||||
pub const BLOCKCHAIN_ERROR: i32 = 16_000;
|
||||
pub const UNKNOWN_ERROR: i32 = 20_000;
|
||||
}
|
||||
|
||||
/// errors encountered by background block authorship task
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// An error occurred while importing the block
|
||||
#[error("Block import failed: {0:?}")]
|
||||
BlockImportError(ImportResult),
|
||||
/// Transaction pool is empty, cannot create a block
|
||||
#[error(
|
||||
"Transaction pool is empty, set create_empty to true, if you want to create empty blocks"
|
||||
)]
|
||||
EmptyTransactionPool,
|
||||
/// encountered during creation of Proposer.
|
||||
#[error("Consensus Error: {0}")]
|
||||
ConsensusError(#[from] ConsensusError),
|
||||
/// Failed to create Inherents data
|
||||
#[error("Inherents Error: {0}")]
|
||||
InherentError(#[from] InherentsError),
|
||||
/// error encountered during finalization
|
||||
#[error("Finalization Error: {0}")]
|
||||
BlockchainError(#[from] BlockchainError),
|
||||
/// Supplied parent_hash doesn't exist in chain
|
||||
#[error("Supplied parent_hash: {0} doesn't exist in chain")]
|
||||
BlockNotFound(String),
|
||||
/// Some string error
|
||||
#[error("{0}")]
|
||||
StringError(String),
|
||||
/// send error
|
||||
#[error("Consensus process is terminating")]
|
||||
Canceled(#[from] oneshot::Canceled),
|
||||
/// send error
|
||||
#[error("Consensus process is terminating")]
|
||||
SendError(#[from] SendError),
|
||||
/// Some other error.
|
||||
#[error("Other error: {0}")]
|
||||
Other(Box<dyn std::error::Error + Send + Sync>),
|
||||
}
|
||||
|
||||
impl From<ImportResult> for Error {
|
||||
fn from(err: ImportResult) -> Self {
|
||||
Error::BlockImportError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(s: String) -> Self {
|
||||
Error::StringError(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
fn to_code(&self) -> i32 {
|
||||
use Error::*;
|
||||
match self {
|
||||
BlockImportError(_) => codes::BLOCK_IMPORT_FAILED,
|
||||
BlockNotFound(_) => codes::BLOCK_NOT_FOUND,
|
||||
EmptyTransactionPool => codes::EMPTY_TRANSACTION_POOL,
|
||||
ConsensusError(_) => codes::CONSENSUS_ERROR,
|
||||
InherentError(_) => codes::INHERENTS_ERROR,
|
||||
BlockchainError(_) => codes::BLOCKCHAIN_ERROR,
|
||||
SendError(_) | Canceled(_) => codes::SERVER_SHUTTING_DOWN,
|
||||
_ => codes::UNKNOWN_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for ErrorObjectOwned {
|
||||
fn from(err: Error) -> Self {
|
||||
ErrorObject::owned(err.to_code(), err.to_string(), None::<()>)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Block finalization utilities
|
||||
|
||||
use crate::rpc;
|
||||
use pezsc_client_api::backend::{Backend as ClientBackend, Finalizer};
|
||||
use pezsp_runtime::{traits::Block as BlockT, Justification};
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
|
||||
/// params for block finalization.
|
||||
pub struct FinalizeBlockParams<B: BlockT, F, CB> {
|
||||
/// hash of the block
|
||||
pub hash: <B as BlockT>::Hash,
|
||||
/// sender to report errors/success to the rpc.
|
||||
pub sender: rpc::Sender<()>,
|
||||
/// finalization justification
|
||||
pub justification: Option<Justification>,
|
||||
/// Finalizer trait object.
|
||||
pub finalizer: Arc<F>,
|
||||
/// phantom type to pin the Backend type
|
||||
pub _phantom: PhantomData<CB>,
|
||||
}
|
||||
|
||||
/// finalizes a block in the backend with the given params.
|
||||
pub async fn finalize_block<B, F, CB>(params: FinalizeBlockParams<B, F, CB>)
|
||||
where
|
||||
B: BlockT,
|
||||
F: Finalizer<B, CB>,
|
||||
CB: ClientBackend<B>,
|
||||
{
|
||||
let FinalizeBlockParams { hash, mut sender, justification, finalizer, .. } = params;
|
||||
|
||||
match finalizer.finalize_block(hash, justification, true) {
|
||||
Err(e) => {
|
||||
log::warn!("Failed to finalize block {}", e);
|
||||
rpc::send_result(&mut sender, Err(e.into()))
|
||||
},
|
||||
Ok(()) => {
|
||||
log::info!("✅ Successfully finalized block: {}", hash);
|
||||
rpc::send_result(&mut sender, Ok(()))
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,803 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! A manual sealing engine: the engine listens for rpc calls to seal blocks and create forks.
|
||||
//! This is suitable for a testing environment.
|
||||
|
||||
use futures::prelude::*;
|
||||
use futures_timer::Delay;
|
||||
use prometheus_endpoint::Registry;
|
||||
use pezsc_client_api::{
|
||||
backend::{Backend as ClientBackend, Finalizer},
|
||||
client::BlockchainEvents,
|
||||
};
|
||||
use pezsc_consensus::{
|
||||
block_import::{BlockImport, BlockImportParams, ForkChoiceStrategy},
|
||||
import_queue::{BasicQueue, BoxBlockImport, Verifier},
|
||||
};
|
||||
use pezsp_blockchain::HeaderBackend;
|
||||
use pezsp_consensus::{Environment, Proposer, SelectChain};
|
||||
use pezsp_core::traits::SpawnNamed;
|
||||
use pezsp_inherents::CreateInherentDataProviders;
|
||||
use pezsp_runtime::{traits::Block as BlockT, ConsensusEngineId};
|
||||
use std::{marker::PhantomData, sync::Arc, time::Duration};
|
||||
|
||||
mod error;
|
||||
mod finalize_block;
|
||||
mod seal_block;
|
||||
|
||||
pub mod consensus;
|
||||
pub mod rpc;
|
||||
|
||||
pub use self::{
|
||||
consensus::ConsensusDataProvider,
|
||||
error::Error,
|
||||
finalize_block::{finalize_block, FinalizeBlockParams},
|
||||
rpc::{CreatedBlock, EngineCommand},
|
||||
seal_block::{seal_block, SealBlockParams, MAX_PROPOSAL_DURATION},
|
||||
};
|
||||
use pezsc_transaction_pool_api::TransactionPool;
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
|
||||
const LOG_TARGET: &str = "manual-seal";
|
||||
|
||||
/// The `ConsensusEngineId` of Manual Seal.
|
||||
pub const MANUAL_SEAL_ENGINE_ID: ConsensusEngineId = [b'm', b'a', b'n', b'l'];
|
||||
|
||||
/// The verifier for the manual seal engine; instantly finalizes.
|
||||
struct ManualSealVerifier;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B: BlockT> Verifier<B> for ManualSealVerifier {
|
||||
async fn verify(
|
||||
&self,
|
||||
mut block: BlockImportParams<B>,
|
||||
) -> Result<BlockImportParams<B>, String> {
|
||||
block.finalized = false;
|
||||
block.fork_choice = Some(ForkChoiceStrategy::LongestChain);
|
||||
Ok(block)
|
||||
}
|
||||
}
|
||||
|
||||
/// Instantiate the import queue for the manual seal consensus engine.
|
||||
pub fn import_queue<Block>(
|
||||
block_import: BoxBlockImport<Block>,
|
||||
spawner: &impl pezsp_core::traits::SpawnEssentialNamed,
|
||||
registry: Option<&Registry>,
|
||||
) -> BasicQueue<Block>
|
||||
where
|
||||
Block: BlockT,
|
||||
{
|
||||
BasicQueue::new(ManualSealVerifier, block_import, None, spawner, registry)
|
||||
}
|
||||
|
||||
/// Params required to start the manual sealing authorship task.
|
||||
pub struct ManualSealParams<B: BlockT, BI, E, C: ProvideRuntimeApi<B>, TP, SC, CS, CIDP, P> {
|
||||
/// Block import instance.
|
||||
pub block_import: BI,
|
||||
|
||||
/// The environment we are producing blocks for.
|
||||
pub env: E,
|
||||
|
||||
/// Client instance
|
||||
pub client: Arc<C>,
|
||||
|
||||
/// Shared reference to the transaction pool.
|
||||
pub pool: Arc<TP>,
|
||||
|
||||
/// Stream<Item = EngineCommands>, Basically the receiving end of a channel for sending
|
||||
/// commands to the authorship task.
|
||||
pub commands_stream: CS,
|
||||
|
||||
/// SelectChain strategy.
|
||||
pub select_chain: SC,
|
||||
|
||||
/// Digest provider for inclusion in blocks.
|
||||
pub consensus_data_provider: Option<Box<dyn ConsensusDataProvider<B, Proof = P>>>,
|
||||
|
||||
/// Something that can create the inherent data providers.
|
||||
pub create_inherent_data_providers: CIDP,
|
||||
}
|
||||
|
||||
/// Params required to start the instant sealing authorship task.
|
||||
pub struct InstantSealParams<B: BlockT, BI, E, C: ProvideRuntimeApi<B>, TP, SC, CIDP, P> {
|
||||
/// Block import instance for well. importing blocks.
|
||||
pub block_import: BI,
|
||||
|
||||
/// The environment we are producing blocks for.
|
||||
pub env: E,
|
||||
|
||||
/// Client instance
|
||||
pub client: Arc<C>,
|
||||
|
||||
/// Shared reference to the transaction pool.
|
||||
pub pool: Arc<TP>,
|
||||
|
||||
/// SelectChain strategy.
|
||||
pub select_chain: SC,
|
||||
|
||||
/// Digest provider for inclusion in blocks.
|
||||
pub consensus_data_provider: Option<Box<dyn ConsensusDataProvider<B, Proof = P>>>,
|
||||
|
||||
/// Something that can create the inherent data providers.
|
||||
pub create_inherent_data_providers: CIDP,
|
||||
}
|
||||
|
||||
/// Params required to start the delayed finalization task.
|
||||
pub struct DelayedFinalizeParams<C, S> {
|
||||
/// Block import instance.
|
||||
pub client: Arc<C>,
|
||||
|
||||
/// Handle for spawning delayed finalization tasks.
|
||||
pub spawn_handle: S,
|
||||
|
||||
/// The delay in seconds before a block is finalized.
|
||||
pub delay_sec: u64,
|
||||
}
|
||||
|
||||
/// Creates the background authorship task for the manually seal engine.
|
||||
pub async fn run_manual_seal<B, BI, CB, E, C, TP, SC, CS, CIDP, P>(
|
||||
ManualSealParams {
|
||||
mut block_import,
|
||||
mut env,
|
||||
client,
|
||||
pool,
|
||||
mut commands_stream,
|
||||
select_chain,
|
||||
consensus_data_provider,
|
||||
create_inherent_data_providers,
|
||||
}: ManualSealParams<B, BI, E, C, TP, SC, CS, CIDP, P>,
|
||||
) where
|
||||
B: BlockT + 'static,
|
||||
BI: BlockImport<B, Error = pezsp_consensus::Error> + Send + Sync + 'static,
|
||||
C: HeaderBackend<B> + Finalizer<B, CB> + ProvideRuntimeApi<B> + 'static,
|
||||
CB: ClientBackend<B> + 'static,
|
||||
E: Environment<B> + 'static,
|
||||
E::Proposer: Proposer<B, Proof = P>,
|
||||
CS: Stream<Item = EngineCommand<<B as BlockT>::Hash>> + Unpin + 'static,
|
||||
SC: SelectChain<B> + 'static,
|
||||
TP: TransactionPool<Block = B>,
|
||||
CIDP: CreateInherentDataProviders<B, ()>,
|
||||
P: codec::Encode + Send + Sync + 'static,
|
||||
{
|
||||
while let Some(command) = commands_stream.next().await {
|
||||
match command {
|
||||
EngineCommand::SealNewBlock { create_empty, finalize, parent_hash, sender } => {
|
||||
seal_block(SealBlockParams {
|
||||
sender,
|
||||
parent_hash,
|
||||
finalize,
|
||||
create_empty,
|
||||
env: &mut env,
|
||||
select_chain: &select_chain,
|
||||
block_import: &mut block_import,
|
||||
consensus_data_provider: consensus_data_provider.as_deref(),
|
||||
pool: pool.clone(),
|
||||
client: client.clone(),
|
||||
create_inherent_data_providers: &create_inherent_data_providers,
|
||||
})
|
||||
.await;
|
||||
},
|
||||
EngineCommand::FinalizeBlock { hash, sender, justification } => {
|
||||
let justification = justification.map(|j| (MANUAL_SEAL_ENGINE_ID, j));
|
||||
finalize_block(FinalizeBlockParams {
|
||||
hash,
|
||||
sender,
|
||||
justification,
|
||||
finalizer: client.clone(),
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
.await
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// runs the background authorship task for the instant seal engine.
|
||||
/// instant-seal creates a new block for every transaction imported into
|
||||
/// the transaction pool.
|
||||
pub async fn run_instant_seal<B, BI, CB, E, C, TP, SC, CIDP, P>(
|
||||
InstantSealParams {
|
||||
block_import,
|
||||
env,
|
||||
client,
|
||||
pool,
|
||||
select_chain,
|
||||
consensus_data_provider,
|
||||
create_inherent_data_providers,
|
||||
}: InstantSealParams<B, BI, E, C, TP, SC, CIDP, P>,
|
||||
) where
|
||||
B: BlockT + 'static,
|
||||
BI: BlockImport<B, Error = pezsp_consensus::Error> + Send + Sync + 'static,
|
||||
C: HeaderBackend<B> + Finalizer<B, CB> + ProvideRuntimeApi<B> + 'static,
|
||||
CB: ClientBackend<B> + 'static,
|
||||
E: Environment<B> + 'static,
|
||||
E::Proposer: Proposer<B, Proof = P>,
|
||||
SC: SelectChain<B> + 'static,
|
||||
TP: TransactionPool<Block = B>,
|
||||
CIDP: CreateInherentDataProviders<B, ()>,
|
||||
P: codec::Encode + Send + Sync + 'static,
|
||||
{
|
||||
// instant-seal creates blocks as soon as transactions are imported
|
||||
// into the transaction pool.
|
||||
let commands_stream = pool.import_notification_stream().map(|_| EngineCommand::SealNewBlock {
|
||||
create_empty: true,
|
||||
finalize: false,
|
||||
parent_hash: None,
|
||||
sender: None,
|
||||
});
|
||||
|
||||
run_manual_seal(ManualSealParams {
|
||||
block_import,
|
||||
env,
|
||||
client,
|
||||
pool,
|
||||
commands_stream,
|
||||
select_chain,
|
||||
consensus_data_provider,
|
||||
create_inherent_data_providers,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Runs the background authorship task for the instant seal engine.
|
||||
/// instant-seal creates a new block for every transaction imported into
|
||||
/// the transaction pool.
|
||||
///
|
||||
/// This function will finalize the block immediately as well. If you don't
|
||||
/// want this behavior use `run_instant_seal` instead.
|
||||
pub async fn run_instant_seal_and_finalize<B, BI, CB, E, C, TP, SC, CIDP, P>(
|
||||
InstantSealParams {
|
||||
block_import,
|
||||
env,
|
||||
client,
|
||||
pool,
|
||||
select_chain,
|
||||
consensus_data_provider,
|
||||
create_inherent_data_providers,
|
||||
}: InstantSealParams<B, BI, E, C, TP, SC, CIDP, P>,
|
||||
) where
|
||||
B: BlockT + 'static,
|
||||
BI: BlockImport<B, Error = pezsp_consensus::Error> + Send + Sync + 'static,
|
||||
C: HeaderBackend<B> + Finalizer<B, CB> + ProvideRuntimeApi<B> + 'static,
|
||||
CB: ClientBackend<B> + 'static,
|
||||
E: Environment<B> + 'static,
|
||||
E::Proposer: Proposer<B, Proof = P>,
|
||||
SC: SelectChain<B> + 'static,
|
||||
TP: TransactionPool<Block = B>,
|
||||
CIDP: CreateInherentDataProviders<B, ()>,
|
||||
P: codec::Encode + Send + Sync + 'static,
|
||||
{
|
||||
// Creates and finalizes blocks as soon as transactions are imported
|
||||
// into the transaction pool.
|
||||
let commands_stream = pool.import_notification_stream().map(|_| EngineCommand::SealNewBlock {
|
||||
create_empty: false,
|
||||
finalize: true,
|
||||
parent_hash: None,
|
||||
sender: None,
|
||||
});
|
||||
|
||||
run_manual_seal(ManualSealParams {
|
||||
block_import,
|
||||
env,
|
||||
client,
|
||||
pool,
|
||||
commands_stream,
|
||||
select_chain,
|
||||
consensus_data_provider,
|
||||
create_inherent_data_providers,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates a future for delayed finalization of manual sealed blocks.
|
||||
///
|
||||
/// The future needs to be spawned in the background alongside the
|
||||
/// [`run_manual_seal`]/[`run_instant_seal`] future. It is required that
|
||||
/// [`EngineCommand::SealNewBlock`] is send with `finalize = false` to not finalize blocks directly
|
||||
/// after building them. This also means that delayed finality can not be used with
|
||||
/// [`run_instant_seal_and_finalize`].
|
||||
pub async fn run_delayed_finalize<B, CB, C, S>(
|
||||
DelayedFinalizeParams { client, spawn_handle, delay_sec }: DelayedFinalizeParams<C, S>,
|
||||
) where
|
||||
B: BlockT + 'static,
|
||||
CB: ClientBackend<B> + 'static,
|
||||
C: HeaderBackend<B> + Finalizer<B, CB> + ProvideRuntimeApi<B> + BlockchainEvents<B> + 'static,
|
||||
S: SpawnNamed,
|
||||
{
|
||||
let mut block_import_stream = client.import_notification_stream();
|
||||
|
||||
while let Some(notification) = block_import_stream.next().await {
|
||||
let delay = Delay::new(Duration::from_secs(delay_sec));
|
||||
let cloned_client = client.clone();
|
||||
spawn_handle.spawn(
|
||||
"delayed-finalize",
|
||||
None,
|
||||
Box::pin(async move {
|
||||
delay.await;
|
||||
finalize_block(FinalizeBlockParams {
|
||||
hash: notification.hash,
|
||||
sender: None,
|
||||
justification: None,
|
||||
finalizer: cloned_client,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
.await
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pezsc_basic_authorship::ProposerFactory;
|
||||
use pezsc_consensus::ImportedAux;
|
||||
use pezsc_transaction_pool::{BasicPool, FullChainApi, Options, RevalidationType};
|
||||
use pezsc_transaction_pool_api::{MaintainedTransactionPool, TransactionPool, TransactionSource};
|
||||
use pezsp_inherents::InherentData;
|
||||
use pezsp_runtime::generic::{Digest, DigestItem};
|
||||
use bizinikiwi_test_runtime_client::{
|
||||
DefaultTestClientBuilderExt, Sr25519Keyring::*, TestClientBuilder, TestClientBuilderExt,
|
||||
};
|
||||
use bizinikiwi_test_runtime_transaction_pool::{uxt, TestApi};
|
||||
|
||||
fn api() -> Arc<TestApi> {
|
||||
Arc::new(TestApi::empty())
|
||||
}
|
||||
|
||||
const SOURCE: TransactionSource = TransactionSource::External;
|
||||
|
||||
struct TestDigestProvider<C> {
|
||||
_client: Arc<C>,
|
||||
}
|
||||
impl<B, C> ConsensusDataProvider<B> for TestDigestProvider<C>
|
||||
where
|
||||
B: BlockT,
|
||||
C: ProvideRuntimeApi<B> + Send + Sync,
|
||||
{
|
||||
type Proof = ();
|
||||
|
||||
fn create_digest(
|
||||
&self,
|
||||
_parent: &B::Header,
|
||||
_inherents: &InherentData,
|
||||
) -> Result<Digest, Error> {
|
||||
Ok(Digest { logs: vec![] })
|
||||
}
|
||||
|
||||
fn append_block_import(
|
||||
&self,
|
||||
_parent: &B::Header,
|
||||
params: &mut BlockImportParams<B>,
|
||||
_inherents: &InherentData,
|
||||
_proof: Self::Proof,
|
||||
) -> Result<(), Error> {
|
||||
params.post_digests.push(DigestItem::Other(vec![1]));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn instant_seal() {
|
||||
let builder = TestClientBuilder::new();
|
||||
let (client, select_chain) = builder.build_with_longest_chain();
|
||||
let client = Arc::new(client);
|
||||
let spawner = pezsp_core::testing::TaskExecutor::new();
|
||||
let genesis_hash = client.info().genesis_hash;
|
||||
let pool_api = Arc::new(FullChainApi::new(client.clone(), None, &spawner.clone()));
|
||||
let pool = Arc::new(BasicPool::with_revalidation_type(
|
||||
Options::default(),
|
||||
true.into(),
|
||||
pool_api,
|
||||
None,
|
||||
RevalidationType::Full,
|
||||
spawner.clone(),
|
||||
0,
|
||||
genesis_hash,
|
||||
genesis_hash,
|
||||
));
|
||||
let env = ProposerFactory::new(spawner.clone(), client.clone(), pool.clone(), None, None);
|
||||
// this test checks that blocks are created as soon as transactions are imported into the
|
||||
// pool.
|
||||
let (sender, receiver) = futures::channel::oneshot::channel();
|
||||
let mut sender = Arc::new(Some(sender));
|
||||
let commands_stream =
|
||||
pool.pool().validated_pool().import_notification_stream().map(move |_| {
|
||||
// we're only going to submit one tx so this fn will only be called once.
|
||||
let mut_sender = Arc::get_mut(&mut sender).unwrap();
|
||||
let sender = std::mem::take(mut_sender);
|
||||
EngineCommand::SealNewBlock {
|
||||
create_empty: false,
|
||||
finalize: true,
|
||||
parent_hash: None,
|
||||
sender,
|
||||
}
|
||||
});
|
||||
|
||||
// spawn the background authorship task
|
||||
tokio::spawn(run_manual_seal(ManualSealParams {
|
||||
block_import: client.clone(),
|
||||
env,
|
||||
client: client.clone(),
|
||||
pool: pool.clone(),
|
||||
commands_stream,
|
||||
select_chain,
|
||||
create_inherent_data_providers: |_, _| async { Ok(()) },
|
||||
consensus_data_provider: None,
|
||||
}));
|
||||
|
||||
// submit a transaction to pool.
|
||||
let result = pool.submit_one(genesis_hash, SOURCE, uxt(Alice, 0)).await;
|
||||
// assert that it was successfully imported
|
||||
assert!(result.is_ok());
|
||||
// assert that the background task returns ok
|
||||
let created_block = receiver.await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
created_block,
|
||||
CreatedBlock {
|
||||
hash: created_block.hash,
|
||||
aux: ImportedAux {
|
||||
header_only: false,
|
||||
clear_justification_requests: false,
|
||||
needs_justification: false,
|
||||
bad_justification: false,
|
||||
is_new_best: true,
|
||||
},
|
||||
proof_size: 0
|
||||
}
|
||||
);
|
||||
// assert that there's a new block in the db.
|
||||
assert!(client.header(created_block.hash).unwrap().is_some());
|
||||
assert_eq!(client.header(created_block.hash).unwrap().unwrap().number, 1)
|
||||
}
|
||||
|
||||
// TODO: enable once the flakiness is fixed
|
||||
// See https://github.com/pezkuwichain/pezkuwi-sdk/issues/131
|
||||
//#[tokio::test]
|
||||
#[allow(unused)]
|
||||
async fn instant_seal_delayed_finalize() {
|
||||
let builder = TestClientBuilder::new();
|
||||
let (client, select_chain) = builder.build_with_longest_chain();
|
||||
let client = Arc::new(client);
|
||||
let spawner = pezsp_core::testing::TaskExecutor::new();
|
||||
let genesis_hash = client.info().genesis_hash;
|
||||
let pool_api = Arc::new(FullChainApi::new(client.clone(), None, &spawner.clone()));
|
||||
let pool = Arc::new(BasicPool::with_revalidation_type(
|
||||
Options::default(),
|
||||
true.into(),
|
||||
pool_api,
|
||||
None,
|
||||
RevalidationType::Full,
|
||||
spawner.clone(),
|
||||
0,
|
||||
genesis_hash,
|
||||
genesis_hash,
|
||||
));
|
||||
let env = ProposerFactory::new(spawner.clone(), client.clone(), pool.clone(), None, None);
|
||||
// this test checks that blocks are created as soon as transactions are imported into the
|
||||
// pool.
|
||||
let (sender, receiver) = futures::channel::oneshot::channel();
|
||||
let mut sender = Arc::new(Some(sender));
|
||||
let commands_stream =
|
||||
pool.pool().validated_pool().import_notification_stream().map(move |_| {
|
||||
// we're only going to submit one tx so this fn will only be called once.
|
||||
let mut_sender = Arc::get_mut(&mut sender).unwrap();
|
||||
let sender = std::mem::take(mut_sender);
|
||||
EngineCommand::SealNewBlock {
|
||||
create_empty: false,
|
||||
// set to `false`, expecting to be finalized by delayed finalize
|
||||
finalize: false,
|
||||
parent_hash: None,
|
||||
sender,
|
||||
}
|
||||
});
|
||||
|
||||
// spawn the background authorship task
|
||||
tokio::spawn(run_manual_seal(ManualSealParams {
|
||||
block_import: client.clone(),
|
||||
commands_stream,
|
||||
env,
|
||||
client: client.clone(),
|
||||
pool: pool.clone(),
|
||||
select_chain,
|
||||
create_inherent_data_providers: |_, _| async { Ok(()) },
|
||||
consensus_data_provider: None,
|
||||
}));
|
||||
|
||||
let delay_sec = 5;
|
||||
|
||||
// spawn the background finality task
|
||||
tokio::spawn(run_delayed_finalize(DelayedFinalizeParams {
|
||||
client: client.clone(),
|
||||
delay_sec,
|
||||
spawn_handle: spawner,
|
||||
}));
|
||||
|
||||
let mut finality_stream = client.finality_notification_stream();
|
||||
// submit a transaction to pool.
|
||||
let result = pool.submit_one(genesis_hash, SOURCE, uxt(Alice, 0)).await;
|
||||
// assert that it was successfully imported
|
||||
assert!(result.is_ok());
|
||||
// assert that the background task returns ok
|
||||
let created_block = receiver.await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
created_block,
|
||||
CreatedBlock {
|
||||
hash: created_block.hash,
|
||||
aux: ImportedAux {
|
||||
header_only: false,
|
||||
clear_justification_requests: false,
|
||||
needs_justification: false,
|
||||
bad_justification: false,
|
||||
is_new_best: true,
|
||||
},
|
||||
proof_size: created_block.proof_size
|
||||
}
|
||||
);
|
||||
// assert that there's a new block in the db.
|
||||
assert!(client.header(created_block.hash).unwrap().is_some());
|
||||
assert_eq!(client.header(created_block.hash).unwrap().unwrap().number, 1);
|
||||
|
||||
assert_eq!(client.info().finalized_hash, client.info().genesis_hash);
|
||||
|
||||
let finalized = finality_stream.select_next_some().await;
|
||||
assert_eq!(finalized.hash, created_block.hash);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manual_seal_and_finalization() {
|
||||
let builder = TestClientBuilder::new();
|
||||
let (client, select_chain) = builder.build_with_longest_chain();
|
||||
let client = Arc::new(client);
|
||||
let spawner = pezsp_core::testing::TaskExecutor::new();
|
||||
let genesis_hash = client.info().genesis_hash;
|
||||
let pool_api = Arc::new(FullChainApi::new(client.clone(), None, &spawner.clone()));
|
||||
let pool = Arc::new(BasicPool::with_revalidation_type(
|
||||
Options::default(),
|
||||
true.into(),
|
||||
pool_api,
|
||||
None,
|
||||
RevalidationType::Full,
|
||||
spawner.clone(),
|
||||
0,
|
||||
genesis_hash,
|
||||
genesis_hash,
|
||||
));
|
||||
let env = ProposerFactory::new(spawner.clone(), client.clone(), pool.clone(), None, None);
|
||||
// this test checks that blocks are created as soon as an engine command is sent over the
|
||||
// stream.
|
||||
let (mut sink, commands_stream) = futures::channel::mpsc::channel(1024);
|
||||
|
||||
// spawn the background authorship task
|
||||
tokio::spawn(run_manual_seal(ManualSealParams {
|
||||
block_import: client.clone(),
|
||||
env,
|
||||
client: client.clone(),
|
||||
pool: pool.clone(),
|
||||
commands_stream,
|
||||
select_chain,
|
||||
consensus_data_provider: None,
|
||||
create_inherent_data_providers: |_, _| async { Ok(()) },
|
||||
}));
|
||||
|
||||
// submit a transaction to pool.
|
||||
let result = pool.submit_one(genesis_hash, SOURCE, uxt(Alice, 0)).await;
|
||||
// assert that it was successfully imported
|
||||
assert!(result.is_ok());
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
sink.send(EngineCommand::SealNewBlock {
|
||||
parent_hash: None,
|
||||
sender: Some(tx),
|
||||
create_empty: false,
|
||||
finalize: false,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let created_block = rx.await.unwrap().unwrap();
|
||||
|
||||
// assert that the background task returns ok
|
||||
assert_eq!(
|
||||
created_block,
|
||||
CreatedBlock {
|
||||
hash: created_block.hash,
|
||||
aux: ImportedAux {
|
||||
header_only: false,
|
||||
clear_justification_requests: false,
|
||||
needs_justification: false,
|
||||
bad_justification: false,
|
||||
is_new_best: true,
|
||||
},
|
||||
proof_size: 0
|
||||
}
|
||||
);
|
||||
// assert that there's a new block in the db.
|
||||
let header = client.header(created_block.hash).unwrap().unwrap();
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
sink.send(EngineCommand::FinalizeBlock {
|
||||
sender: Some(tx),
|
||||
hash: header.hash(),
|
||||
justification: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
// check that the background task returns ok:
|
||||
rx.await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manual_seal_fork_blocks() {
|
||||
let builder = TestClientBuilder::new();
|
||||
let (client, select_chain) = builder.build_with_longest_chain();
|
||||
let client = Arc::new(client);
|
||||
let pool_api = Arc::new(FullChainApi::new(
|
||||
client.clone(),
|
||||
None,
|
||||
&pezsp_core::testing::TaskExecutor::new(),
|
||||
));
|
||||
let spawner = pezsp_core::testing::TaskExecutor::new();
|
||||
let genesis_hash = client.info().genesis_hash;
|
||||
let pool = Arc::new(BasicPool::with_revalidation_type(
|
||||
Options::default(),
|
||||
true.into(),
|
||||
pool_api.clone(),
|
||||
None,
|
||||
RevalidationType::Full,
|
||||
spawner.clone(),
|
||||
0,
|
||||
genesis_hash,
|
||||
genesis_hash,
|
||||
));
|
||||
let env = ProposerFactory::new(spawner.clone(), client.clone(), pool.clone(), None, None);
|
||||
// this test checks that blocks are created as soon as an engine command is sent over the
|
||||
// stream.
|
||||
let (mut sink, commands_stream) = futures::channel::mpsc::channel(1024);
|
||||
|
||||
// spawn the background authorship task
|
||||
tokio::spawn(run_manual_seal(ManualSealParams {
|
||||
block_import: client.clone(),
|
||||
env,
|
||||
client: client.clone(),
|
||||
pool: pool.clone(),
|
||||
commands_stream,
|
||||
select_chain,
|
||||
consensus_data_provider: None,
|
||||
create_inherent_data_providers: |_, _| async { Ok(()) },
|
||||
}));
|
||||
|
||||
// submit a transaction to pool.
|
||||
let result = pool.submit_one(genesis_hash, SOURCE, uxt(Alice, 0)).await;
|
||||
// assert that it was successfully imported
|
||||
assert!(result.is_ok());
|
||||
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
sink.send(EngineCommand::SealNewBlock {
|
||||
parent_hash: None,
|
||||
sender: Some(tx),
|
||||
create_empty: false,
|
||||
finalize: false,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let created_block = rx.await.unwrap().unwrap();
|
||||
|
||||
// assert that the background task returns ok
|
||||
assert_eq!(
|
||||
created_block,
|
||||
CreatedBlock {
|
||||
hash: created_block.hash,
|
||||
aux: ImportedAux {
|
||||
header_only: false,
|
||||
clear_justification_requests: false,
|
||||
needs_justification: false,
|
||||
bad_justification: false,
|
||||
is_new_best: true
|
||||
},
|
||||
proof_size: 0
|
||||
}
|
||||
);
|
||||
|
||||
assert!(pool.submit_one(created_block.hash, SOURCE, uxt(Alice, 1)).await.is_ok());
|
||||
|
||||
let header = client.header(created_block.hash).expect("db error").expect("imported above");
|
||||
assert_eq!(header.number, 1);
|
||||
pool.maintain(pezsc_transaction_pool_api::ChainEvent::NewBestBlock {
|
||||
hash: header.hash(),
|
||||
tree_route: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
let (tx1, rx1) = futures::channel::oneshot::channel();
|
||||
assert!(sink
|
||||
.send(EngineCommand::SealNewBlock {
|
||||
parent_hash: Some(created_block.hash),
|
||||
sender: Some(tx1),
|
||||
create_empty: false,
|
||||
finalize: false,
|
||||
})
|
||||
.await
|
||||
.is_ok());
|
||||
assert_matches::assert_matches!(rx1.await.expect("should be no error receiving"), Ok(_));
|
||||
|
||||
assert!(pool.submit_one(created_block.hash, SOURCE, uxt(Bob, 0)).await.is_ok());
|
||||
let (tx2, rx2) = futures::channel::oneshot::channel();
|
||||
assert!(sink
|
||||
.send(EngineCommand::SealNewBlock {
|
||||
parent_hash: Some(created_block.hash),
|
||||
sender: Some(tx2),
|
||||
create_empty: false,
|
||||
finalize: false,
|
||||
})
|
||||
.await
|
||||
.is_ok());
|
||||
let imported = rx2.await.unwrap().unwrap();
|
||||
// assert that fork block is in the db
|
||||
assert!(client.header(imported.hash).unwrap().is_some())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manual_seal_post_hash() {
|
||||
let builder = TestClientBuilder::new();
|
||||
let (client, select_chain) = builder.build_with_longest_chain();
|
||||
let client = Arc::new(client);
|
||||
let spawner = pezsp_core::testing::TaskExecutor::new();
|
||||
let genesis_hash = client.header(client.info().genesis_hash).unwrap().unwrap().hash();
|
||||
let pool = Arc::new(BasicPool::with_revalidation_type(
|
||||
Options::default(),
|
||||
true.into(),
|
||||
api(),
|
||||
None,
|
||||
RevalidationType::Full,
|
||||
spawner.clone(),
|
||||
0,
|
||||
genesis_hash,
|
||||
genesis_hash,
|
||||
));
|
||||
let env = ProposerFactory::new(spawner.clone(), client.clone(), pool.clone(), None, None);
|
||||
|
||||
let (mut sink, commands_stream) = futures::channel::mpsc::channel(1024);
|
||||
|
||||
// spawn the background authorship task
|
||||
tokio::spawn(run_manual_seal(ManualSealParams {
|
||||
block_import: client.clone(),
|
||||
env,
|
||||
client: client.clone(),
|
||||
pool: pool.clone(),
|
||||
commands_stream,
|
||||
select_chain,
|
||||
// use a provider that pushes some post digest data
|
||||
consensus_data_provider: Some(Box::new(TestDigestProvider { _client: client.clone() })),
|
||||
create_inherent_data_providers: |_, _| async { Ok(()) },
|
||||
}));
|
||||
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
sink.send(EngineCommand::SealNewBlock {
|
||||
parent_hash: None,
|
||||
sender: Some(tx),
|
||||
create_empty: true,
|
||||
finalize: false,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let created_block = rx.await.unwrap().unwrap();
|
||||
|
||||
// assert that the background task returned the actual header hash
|
||||
let header = client.header(created_block.hash).unwrap().unwrap();
|
||||
assert_eq!(header.number, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! RPC interface for the `ManualSeal` Engine.
|
||||
|
||||
use crate::error::Error;
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
SinkExt,
|
||||
};
|
||||
use jsonrpsee::{core::async_trait, proc_macros::rpc};
|
||||
use pezsc_consensus::ImportedAux;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use pezsp_runtime::EncodedJustification;
|
||||
|
||||
/// Sender passed to the authorship task to report errors or successes.
|
||||
pub type Sender<T> = Option<oneshot::Sender<std::result::Result<T, Error>>>;
|
||||
|
||||
/// Message sent to the background authorship task, usually by RPC.
|
||||
pub enum EngineCommand<Hash> {
|
||||
/// Tells the engine to propose a new block
|
||||
///
|
||||
/// if create_empty == true, it will create empty blocks if there are no transactions
|
||||
/// in the transaction pool.
|
||||
///
|
||||
/// if finalize == true, the block will be instantly finalized.
|
||||
SealNewBlock {
|
||||
/// if true, empty blocks(without extrinsics) will be created.
|
||||
/// otherwise, will return Error::EmptyTransactionPool.
|
||||
create_empty: bool,
|
||||
/// instantly finalize this block?
|
||||
finalize: bool,
|
||||
/// specify the parent hash of the about-to-created block
|
||||
parent_hash: Option<Hash>,
|
||||
/// sender to report errors/success to the rpc.
|
||||
sender: Sender<CreatedBlock<Hash>>,
|
||||
},
|
||||
/// Tells the engine to finalize the block with the supplied hash
|
||||
FinalizeBlock {
|
||||
/// hash of the block
|
||||
hash: Hash,
|
||||
/// sender to report errors/success to the rpc.
|
||||
sender: Sender<()>,
|
||||
/// finalization justification
|
||||
justification: Option<EncodedJustification>,
|
||||
},
|
||||
}
|
||||
|
||||
/// RPC trait that provides methods for interacting with the manual-seal authorship task over rpc.
|
||||
#[rpc(client, server)]
|
||||
pub trait ManualSealApi<Hash> {
|
||||
/// Instructs the manual-seal authorship task to create a new block
|
||||
#[method(name = "engine_createBlock")]
|
||||
async fn create_block(
|
||||
&self,
|
||||
create_empty: bool,
|
||||
finalize: bool,
|
||||
parent_hash: Option<Hash>,
|
||||
) -> Result<CreatedBlock<Hash>, Error>;
|
||||
|
||||
/// Instructs the manual-seal authorship task to finalize a block
|
||||
#[method(name = "engine_finalizeBlock")]
|
||||
async fn finalize_block(
|
||||
&self,
|
||||
hash: Hash,
|
||||
justification: Option<EncodedJustification>,
|
||||
) -> Result<bool, Error>;
|
||||
}
|
||||
|
||||
/// A struct that implements the [`ManualSealApiServer`].
|
||||
pub struct ManualSeal<Hash> {
|
||||
import_block_channel: mpsc::Sender<EngineCommand<Hash>>,
|
||||
}
|
||||
|
||||
/// return type of `engine_createBlock`
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct CreatedBlock<Hash> {
|
||||
/// hash of the created block.
|
||||
pub hash: Hash,
|
||||
/// some extra details about the import operation
|
||||
pub aux: ImportedAux,
|
||||
/// uncompacted storage proof size (zero mean that there is no proof)
|
||||
pub proof_size: usize,
|
||||
}
|
||||
|
||||
impl<Hash> ManualSeal<Hash> {
|
||||
/// Create new `ManualSeal` with the given reference to the client.
|
||||
pub fn new(import_block_channel: mpsc::Sender<EngineCommand<Hash>>) -> Self {
|
||||
Self { import_block_channel }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<Hash: Send + 'static> ManualSealApiServer<Hash> for ManualSeal<Hash> {
|
||||
async fn create_block(
|
||||
&self,
|
||||
create_empty: bool,
|
||||
finalize: bool,
|
||||
parent_hash: Option<Hash>,
|
||||
) -> Result<CreatedBlock<Hash>, Error> {
|
||||
let mut sink = self.import_block_channel.clone();
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
// NOTE: this sends a Result over the channel.
|
||||
let command = EngineCommand::SealNewBlock {
|
||||
create_empty,
|
||||
finalize,
|
||||
parent_hash,
|
||||
sender: Some(sender),
|
||||
};
|
||||
|
||||
sink.send(command).await?;
|
||||
|
||||
match receiver.await {
|
||||
Ok(Ok(rx)) => Ok(rx),
|
||||
Ok(Err(e)) => Err(e.into()),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn finalize_block(
|
||||
&self,
|
||||
hash: Hash,
|
||||
justification: Option<EncodedJustification>,
|
||||
) -> Result<bool, Error> {
|
||||
let mut sink = self.import_block_channel.clone();
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
let command = EngineCommand::FinalizeBlock { hash, sender: Some(sender), justification };
|
||||
sink.send(command).await?;
|
||||
receiver.await.map(|_| true).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// report any errors or successes encountered by the authorship task back
|
||||
/// to the rpc
|
||||
pub fn send_result<T: std::fmt::Debug>(
|
||||
sender: &mut Sender<T>,
|
||||
result: std::result::Result<T, crate::Error>,
|
||||
) {
|
||||
if let Some(sender) = sender.take() {
|
||||
if let Err(err) = sender.send(result) {
|
||||
match err {
|
||||
Ok(value) => log::warn!("Server is shutting down: {:?}", value),
|
||||
Err(error) => log::warn!("Server is shutting down with error: {}", error),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Sealing/Finalization with no RPC sender such as instant seal or delayed finalize doesn't
|
||||
// report errors over rpc, simply log them.
|
||||
match result {
|
||||
Ok(r) => log::info!("Consensus with no RPC sender success: {:?}", r),
|
||||
Err(e) => log::error!("Consensus with no RPC sender encountered an error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Block sealing utilities
|
||||
|
||||
use crate::{rpc, ConsensusDataProvider, CreatedBlock, Error};
|
||||
use futures::prelude::*;
|
||||
use pezsc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy, ImportResult, StateAction};
|
||||
use pezsc_transaction_pool_api::TransactionPool;
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_blockchain::HeaderBackend;
|
||||
use pezsp_consensus::{self, BlockOrigin, Environment, Proposer, SelectChain};
|
||||
use pezsp_inherents::{CreateInherentDataProviders, InherentDataProvider};
|
||||
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
/// max duration for creating a proposal in secs
|
||||
pub const MAX_PROPOSAL_DURATION: u64 = 10;
|
||||
|
||||
/// params for sealing a new block
|
||||
pub struct SealBlockParams<'a, B: BlockT, BI, SC, C: ProvideRuntimeApi<B>, E, TP, CIDP, P> {
|
||||
/// if true, empty blocks(without extrinsics) will be created.
|
||||
/// otherwise, will return Error::EmptyTransactionPool.
|
||||
pub create_empty: bool,
|
||||
/// instantly finalize this block?
|
||||
pub finalize: bool,
|
||||
/// specify the parent hash of the about-to-created block
|
||||
pub parent_hash: Option<<B as BlockT>::Hash>,
|
||||
/// sender to report errors/success to the rpc.
|
||||
pub sender: rpc::Sender<CreatedBlock<<B as BlockT>::Hash>>,
|
||||
/// transaction pool
|
||||
pub pool: Arc<TP>,
|
||||
/// header backend
|
||||
pub client: Arc<C>,
|
||||
/// Environment trait object for creating a proposer
|
||||
pub env: &'a mut E,
|
||||
/// SelectChain object
|
||||
pub select_chain: &'a SC,
|
||||
/// Digest provider for inclusion in blocks.
|
||||
pub consensus_data_provider: Option<&'a dyn ConsensusDataProvider<B, Proof = P>>,
|
||||
/// block import object
|
||||
pub block_import: &'a mut BI,
|
||||
/// Something that can create the inherent data providers.
|
||||
pub create_inherent_data_providers: &'a CIDP,
|
||||
}
|
||||
|
||||
/// seals a new block with the given params
|
||||
pub async fn seal_block<B, BI, SC, C, E, TP, CIDP, P>(
|
||||
SealBlockParams {
|
||||
create_empty,
|
||||
finalize,
|
||||
pool,
|
||||
parent_hash,
|
||||
client,
|
||||
select_chain,
|
||||
block_import,
|
||||
env,
|
||||
create_inherent_data_providers,
|
||||
consensus_data_provider: digest_provider,
|
||||
mut sender,
|
||||
}: SealBlockParams<'_, B, BI, SC, C, E, TP, CIDP, P>,
|
||||
) where
|
||||
B: BlockT,
|
||||
BI: BlockImport<B, Error = pezsp_consensus::Error> + Send + Sync + 'static,
|
||||
C: HeaderBackend<B> + ProvideRuntimeApi<B>,
|
||||
E: Environment<B>,
|
||||
E::Proposer: Proposer<B, Proof = P>,
|
||||
TP: TransactionPool<Block = B>,
|
||||
SC: SelectChain<B>,
|
||||
CIDP: CreateInherentDataProviders<B, ()>,
|
||||
P: codec::Encode + Send + Sync + 'static,
|
||||
{
|
||||
let future = async {
|
||||
if pool.status().ready == 0 && !create_empty {
|
||||
return Err(Error::EmptyTransactionPool);
|
||||
}
|
||||
|
||||
// get the header to build this new block on.
|
||||
// use the parent_hash supplied via `EngineCommand`
|
||||
// or fetch the best_block.
|
||||
let parent = match parent_hash {
|
||||
Some(hash) =>
|
||||
client.header(hash)?.ok_or_else(|| Error::BlockNotFound(format!("{}", hash)))?,
|
||||
None => select_chain.best_chain().await?,
|
||||
};
|
||||
|
||||
let inherent_data_providers = create_inherent_data_providers
|
||||
.create_inherent_data_providers(parent.hash(), ())
|
||||
.await
|
||||
.map_err(|e| Error::Other(e))?;
|
||||
|
||||
let inherent_data = inherent_data_providers.create_inherent_data().await?;
|
||||
|
||||
let proposer = env.init(&parent).map_err(|err| Error::StringError(err.to_string())).await?;
|
||||
let inherents_len = inherent_data.len();
|
||||
|
||||
let digest = if let Some(digest_provider) = digest_provider {
|
||||
digest_provider.create_digest(&parent, &inherent_data)?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let proposal = proposer
|
||||
.propose(
|
||||
inherent_data.clone(),
|
||||
digest,
|
||||
Duration::from_secs(MAX_PROPOSAL_DURATION),
|
||||
None,
|
||||
)
|
||||
.map_err(|err| Error::StringError(err.to_string()))
|
||||
.await?;
|
||||
|
||||
if proposal.block.extrinsics().len() == inherents_len && !create_empty {
|
||||
return Err(Error::EmptyTransactionPool);
|
||||
}
|
||||
|
||||
let (header, body) = proposal.block.deconstruct();
|
||||
let proof = proposal.proof;
|
||||
let proof_size = proof.encoded_size();
|
||||
let mut params = BlockImportParams::new(BlockOrigin::Own, header.clone());
|
||||
params.body = Some(body);
|
||||
params.finalized = finalize;
|
||||
params.fork_choice = Some(ForkChoiceStrategy::LongestChain);
|
||||
params.state_action = StateAction::ApplyChanges(pezsc_consensus::StorageChanges::Changes(
|
||||
proposal.storage_changes,
|
||||
));
|
||||
|
||||
if let Some(digest_provider) = digest_provider {
|
||||
digest_provider.append_block_import(&parent, &mut params, &inherent_data, proof)?;
|
||||
}
|
||||
|
||||
// Make sure we return the same post-hash that will be calculated when importing the block
|
||||
// This is important in case the digest_provider added any signature, seal, ect.
|
||||
let mut post_header = header.clone();
|
||||
post_header.digest_mut().logs.extend(params.post_digests.iter().cloned());
|
||||
|
||||
match block_import.import_block(params).await? {
|
||||
ImportResult::Imported(aux) => Ok(CreatedBlock {
|
||||
hash: <B as BlockT>::Header::hash(&post_header),
|
||||
aux,
|
||||
proof_size,
|
||||
}),
|
||||
other => Err(other.into()),
|
||||
}
|
||||
};
|
||||
|
||||
rpc::send_result(&mut sender, future.await)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
[package]
|
||||
name = "pezsc-consensus-pow"
|
||||
version = "0.33.0"
|
||||
authors.workspace = true
|
||||
description = "PoW consensus algorithm for bizinikiwi"
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
codec = { features = ["derive"], workspace = true, default-features = true }
|
||||
futures = { workspace = true }
|
||||
futures-timer = { workspace = true }
|
||||
log = { workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
prometheus-endpoint = { workspace = true, default-features = true }
|
||||
pezsc-client-api = { 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-consensus-pow = { 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 }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezsc-client-api/runtime-benchmarks",
|
||||
"pezsc-consensus/runtime-benchmarks",
|
||||
"pezsp-api/runtime-benchmarks",
|
||||
"pezsp-block-builder/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus-pow/runtime-benchmarks",
|
||||
"pezsp-consensus/runtime-benchmarks",
|
||||
"pezsp-inherents/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
Proof of work consensus for Bizinikiwi.
|
||||
|
||||
To use this engine, you can need to have a struct that implements
|
||||
`PowAlgorithm`. After that, pass an instance of the struct, along
|
||||
with other necessary client references to `import_queue` to setup
|
||||
the queue.
|
||||
|
||||
This library also comes with an async mining worker, which can be
|
||||
started via the `start_mining_worker` function. It returns a worker
|
||||
handle together with a future. The future must be pulled. Through
|
||||
the worker handle, you can pull the metadata needed to start the
|
||||
mining process via `MiningWorker::metadata`, and then do the actual
|
||||
mining on a standalone thread. Finally, when a seal is found, call
|
||||
`MiningWorker::submit` to build the block.
|
||||
|
||||
The auxiliary storage for PoW engine only stores the total difficulty.
|
||||
For other storage requirements for particular PoW algorithm (such as
|
||||
the actual difficulty for each particular blocks), you can take a client
|
||||
reference in your `PowAlgorithm` implementation, and use a separate prefix
|
||||
for the auxiliary storage. It is also possible to just use the runtime
|
||||
as the storage, but it is not recommended as it won't work well with light
|
||||
clients.
|
||||
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,672 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Proof of work consensus for Bizinikiwi.
|
||||
//!
|
||||
//! To use this engine, you can need to have a struct that implements
|
||||
//! [`PowAlgorithm`]. After that, pass an instance of the struct, along
|
||||
//! with other necessary client references to [`import_queue`] to setup
|
||||
//! the queue.
|
||||
//!
|
||||
//! This library also comes with an async mining worker, which can be
|
||||
//! started via the [`start_mining_worker`] function. It returns a worker
|
||||
//! handle together with a future. The future must be pulled. Through
|
||||
//! the worker handle, you can pull the metadata needed to start the
|
||||
//! mining process via [`MiningHandle::metadata`], and then do the actual
|
||||
//! mining on a standalone thread. Finally, when a seal is found, call
|
||||
//! [`MiningHandle::submit`] to build the block.
|
||||
//!
|
||||
//! The auxiliary storage for PoW engine only stores the total difficulty.
|
||||
//! For other storage requirements for particular PoW algorithm (such as
|
||||
//! the actual difficulty for each particular blocks), you can take a client
|
||||
//! reference in your [`PowAlgorithm`] implementation, and use a separate prefix
|
||||
//! for the auxiliary storage. It is also possible to just use the runtime
|
||||
//! as the storage, but it is not recommended as it won't work well with light
|
||||
//! clients.
|
||||
|
||||
mod worker;
|
||||
|
||||
pub use crate::worker::{MiningBuild, MiningHandle, MiningMetadata};
|
||||
|
||||
use crate::worker::UntilImportedOrTimeout;
|
||||
use codec::{Decode, Encode};
|
||||
use futures::{Future, StreamExt};
|
||||
use log::*;
|
||||
use prometheus_endpoint::Registry;
|
||||
use pezsc_client_api::{self, backend::AuxStore, BlockOf, BlockchainEvents};
|
||||
use pezsc_consensus::{
|
||||
BasicQueue, BlockCheckParams, BlockImport, BlockImportParams, BoxBlockImport,
|
||||
BoxJustificationImport, ForkChoiceStrategy, ImportResult, Verifier,
|
||||
};
|
||||
use pezsp_api::ProvideRuntimeApi;
|
||||
use pezsp_block_builder::BlockBuilder as BlockBuilderApi;
|
||||
use pezsp_blockchain::HeaderBackend;
|
||||
use pezsp_consensus::{Environment, Error as ConsensusError, Proposer, SelectChain, SyncOracle};
|
||||
use pezsp_consensus_pow::{Seal, TotalDifficulty, POW_ENGINE_ID};
|
||||
use pezsp_inherents::{CreateInherentDataProviders, InherentDataProvider};
|
||||
use pezsp_runtime::{
|
||||
generic::{BlockId, Digest, DigestItem},
|
||||
traits::{Block as BlockT, Header as HeaderT},
|
||||
};
|
||||
use std::{cmp::Ordering, marker::PhantomData, sync::Arc, time::Duration};
|
||||
|
||||
const LOG_TARGET: &str = "pow";
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error<B: BlockT> {
|
||||
#[error("Header uses the wrong engine {0:?}")]
|
||||
WrongEngine([u8; 4]),
|
||||
#[error("Header {0:?} is unsealed")]
|
||||
HeaderUnsealed(B::Hash),
|
||||
#[error("PoW validation error: invalid seal")]
|
||||
InvalidSeal,
|
||||
#[error("PoW validation error: preliminary verification failed")]
|
||||
FailedPreliminaryVerify,
|
||||
#[error("Rejecting block too far in future")]
|
||||
TooFarInFuture,
|
||||
#[error("Fetching best header failed using select chain: {0}")]
|
||||
BestHeaderSelectChain(ConsensusError),
|
||||
#[error("Fetching best header failed: {0}")]
|
||||
BestHeader(pezsp_blockchain::Error),
|
||||
#[error("Best header does not exist")]
|
||||
NoBestHeader,
|
||||
#[error("Block proposing error: {0}")]
|
||||
BlockProposingError(String),
|
||||
#[error("Fetch best hash failed via select chain: {0}")]
|
||||
BestHashSelectChain(ConsensusError),
|
||||
#[error("Error with block built on {0:?}: {1}")]
|
||||
BlockBuiltError(B::Hash, ConsensusError),
|
||||
#[error("Creating inherents failed: {0}")]
|
||||
CreateInherents(pezsp_inherents::Error),
|
||||
#[error("Checking inherents failed: {0}")]
|
||||
CheckInherents(pezsp_inherents::Error),
|
||||
#[error(
|
||||
"Checking inherents unknown error for identifier: {}",
|
||||
String::from_utf8_lossy(.0)
|
||||
)]
|
||||
CheckInherentsUnknownError(pezsp_inherents::InherentIdentifier),
|
||||
#[error("Multiple pre-runtime digests")]
|
||||
MultiplePreRuntimeDigests,
|
||||
#[error(transparent)]
|
||||
Client(pezsp_blockchain::Error),
|
||||
#[error(transparent)]
|
||||
Codec(codec::Error),
|
||||
#[error("{0}")]
|
||||
Environment(String),
|
||||
#[error("{0}")]
|
||||
Runtime(String),
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl<B: BlockT> From<Error<B>> for String {
|
||||
fn from(error: Error<B>) -> String {
|
||||
error.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockT> From<Error<B>> for ConsensusError {
|
||||
fn from(error: Error<B>) -> ConsensusError {
|
||||
ConsensusError::ClientImport(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Auxiliary storage prefix for PoW engine.
|
||||
pub const POW_AUX_PREFIX: [u8; 4] = *b"PoW:";
|
||||
|
||||
/// Get the auxiliary storage key used by engine to store total difficulty.
|
||||
fn aux_key<T: AsRef<[u8]>>(hash: &T) -> Vec<u8> {
|
||||
POW_AUX_PREFIX.iter().chain(hash.as_ref()).copied().collect()
|
||||
}
|
||||
|
||||
/// Intermediate value passed to block importer.
|
||||
#[derive(Encode, Decode, Clone, Debug, Default)]
|
||||
pub struct PowIntermediate<Difficulty> {
|
||||
/// Difficulty of the block, if known.
|
||||
pub difficulty: Option<Difficulty>,
|
||||
}
|
||||
|
||||
/// Intermediate key for PoW engine.
|
||||
pub static INTERMEDIATE_KEY: &[u8] = b"pow1";
|
||||
|
||||
/// Auxiliary storage data for PoW.
|
||||
#[derive(Encode, Decode, Clone, Debug, Default)]
|
||||
pub struct PowAux<Difficulty> {
|
||||
/// Difficulty of the current block.
|
||||
pub difficulty: Difficulty,
|
||||
/// Total difficulty up to current block.
|
||||
pub total_difficulty: Difficulty,
|
||||
}
|
||||
|
||||
impl<Difficulty> PowAux<Difficulty>
|
||||
where
|
||||
Difficulty: Decode + Default,
|
||||
{
|
||||
/// Read the auxiliary from client.
|
||||
pub fn read<C: AuxStore, B: BlockT>(client: &C, hash: &B::Hash) -> Result<Self, Error<B>> {
|
||||
let key = aux_key(&hash);
|
||||
|
||||
match client.get_aux(&key).map_err(Error::Client)? {
|
||||
Some(bytes) => Self::decode(&mut &bytes[..]).map_err(Error::Codec),
|
||||
None => Ok(Self::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Algorithm used for proof of work.
|
||||
pub trait PowAlgorithm<B: BlockT> {
|
||||
/// Difficulty for the algorithm.
|
||||
type Difficulty: TotalDifficulty + Default + Encode + Decode + Ord + Clone + Copy;
|
||||
|
||||
/// Get the next block's difficulty.
|
||||
///
|
||||
/// This function will be called twice during the import process, so the implementation
|
||||
/// should be properly cached.
|
||||
fn difficulty(&self, parent: B::Hash) -> Result<Self::Difficulty, Error<B>>;
|
||||
/// Verify that the seal is valid against given pre hash when parent block is not yet imported.
|
||||
///
|
||||
/// None means that preliminary verify is not available for this algorithm.
|
||||
fn preliminary_verify(
|
||||
&self,
|
||||
_pre_hash: &B::Hash,
|
||||
_seal: &Seal,
|
||||
) -> Result<Option<bool>, Error<B>> {
|
||||
Ok(None)
|
||||
}
|
||||
/// Break a fork choice tie.
|
||||
///
|
||||
/// By default this chooses the earliest block seen. Using uniform tie
|
||||
/// breaking algorithms will help to protect against selfish mining.
|
||||
///
|
||||
/// Returns if the new seal should be considered best block.
|
||||
fn break_tie(&self, _own_seal: &Seal, _new_seal: &Seal) -> bool {
|
||||
false
|
||||
}
|
||||
/// Verify that the difficulty is valid against given seal.
|
||||
fn verify(
|
||||
&self,
|
||||
parent: &BlockId<B>,
|
||||
pre_hash: &B::Hash,
|
||||
pre_digest: Option<&[u8]>,
|
||||
seal: &Seal,
|
||||
difficulty: Self::Difficulty,
|
||||
) -> Result<bool, Error<B>>;
|
||||
}
|
||||
|
||||
/// A block importer for PoW.
|
||||
pub struct PowBlockImport<B: BlockT, I, C, S, Algorithm, CIDP> {
|
||||
algorithm: Algorithm,
|
||||
inner: I,
|
||||
select_chain: S,
|
||||
client: Arc<C>,
|
||||
create_inherent_data_providers: Arc<CIDP>,
|
||||
check_inherents_after: <<B as BlockT>::Header as HeaderT>::Number,
|
||||
}
|
||||
|
||||
impl<B: BlockT, I: Clone, C, S: Clone, Algorithm: Clone, CIDP> Clone
|
||||
for PowBlockImport<B, I, C, S, Algorithm, CIDP>
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
algorithm: self.algorithm.clone(),
|
||||
inner: self.inner.clone(),
|
||||
select_chain: self.select_chain.clone(),
|
||||
client: self.client.clone(),
|
||||
create_inherent_data_providers: self.create_inherent_data_providers.clone(),
|
||||
check_inherents_after: self.check_inherents_after,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, I, C, S, Algorithm, CIDP> PowBlockImport<B, I, C, S, Algorithm, CIDP>
|
||||
where
|
||||
B: BlockT,
|
||||
I: BlockImport<B> + Send + Sync,
|
||||
I::Error: Into<ConsensusError>,
|
||||
C: ProvideRuntimeApi<B> + Send + Sync + HeaderBackend<B> + AuxStore + BlockOf,
|
||||
C::Api: BlockBuilderApi<B>,
|
||||
Algorithm: PowAlgorithm<B>,
|
||||
CIDP: CreateInherentDataProviders<B, ()>,
|
||||
{
|
||||
/// Create a new block import suitable to be used in PoW
|
||||
pub fn new(
|
||||
inner: I,
|
||||
client: Arc<C>,
|
||||
algorithm: Algorithm,
|
||||
check_inherents_after: <<B as BlockT>::Header as HeaderT>::Number,
|
||||
select_chain: S,
|
||||
create_inherent_data_providers: CIDP,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
client,
|
||||
algorithm,
|
||||
check_inherents_after,
|
||||
select_chain,
|
||||
create_inherent_data_providers: Arc::new(create_inherent_data_providers),
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_inherents(
|
||||
&self,
|
||||
block: B,
|
||||
at_hash: B::Hash,
|
||||
inherent_data_providers: CIDP::InherentDataProviders,
|
||||
) -> Result<(), Error<B>> {
|
||||
use pezsp_block_builder::CheckInherentsError;
|
||||
|
||||
if *block.header().number() < self.check_inherents_after {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
pezsp_block_builder::check_inherents(
|
||||
self.client.clone(),
|
||||
at_hash,
|
||||
block,
|
||||
&inherent_data_providers,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
CheckInherentsError::CreateInherentData(e) => Error::CreateInherents(e),
|
||||
CheckInherentsError::Client(e) => Error::Client(e.into()),
|
||||
CheckInherentsError::CheckInherents(e) => Error::CheckInherents(e),
|
||||
CheckInherentsError::CheckInherentsUnknownError(id) =>
|
||||
Error::CheckInherentsUnknownError(id),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B, I, C, S, Algorithm, CIDP> BlockImport<B> for PowBlockImport<B, I, C, S, Algorithm, CIDP>
|
||||
where
|
||||
B: BlockT,
|
||||
I: BlockImport<B> + Send + Sync,
|
||||
I::Error: Into<ConsensusError>,
|
||||
S: SelectChain<B>,
|
||||
C: ProvideRuntimeApi<B> + Send + Sync + HeaderBackend<B> + AuxStore + BlockOf,
|
||||
C::Api: BlockBuilderApi<B>,
|
||||
Algorithm: PowAlgorithm<B> + Send + Sync,
|
||||
Algorithm::Difficulty: 'static + Send,
|
||||
CIDP: CreateInherentDataProviders<B, ()> + Send + Sync,
|
||||
{
|
||||
type Error = ConsensusError;
|
||||
|
||||
async fn check_block(&self, block: BlockCheckParams<B>) -> Result<ImportResult, Self::Error> {
|
||||
self.inner.check_block(block).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn import_block(
|
||||
&self,
|
||||
mut block: BlockImportParams<B>,
|
||||
) -> Result<ImportResult, Self::Error> {
|
||||
let best_header = self
|
||||
.select_chain
|
||||
.best_chain()
|
||||
.await
|
||||
.map_err(|e| format!("Fetch best chain failed via select chain: {}", e))
|
||||
.map_err(ConsensusError::ChainLookup)?;
|
||||
let best_hash = best_header.hash();
|
||||
|
||||
let parent_hash = *block.header.parent_hash();
|
||||
let best_aux = PowAux::read::<_, B>(self.client.as_ref(), &best_hash)?;
|
||||
let mut aux = PowAux::read::<_, B>(self.client.as_ref(), &parent_hash)?;
|
||||
|
||||
if let Some(inner_body) = block.body.take() {
|
||||
let check_block = B::new(block.header.clone(), inner_body);
|
||||
|
||||
if !block.state_action.skip_execution_checks() {
|
||||
self.check_inherents(
|
||||
check_block.clone(),
|
||||
parent_hash,
|
||||
self.create_inherent_data_providers
|
||||
.create_inherent_data_providers(parent_hash, ())
|
||||
.await?,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
block.body = Some(check_block.deconstruct().1);
|
||||
}
|
||||
|
||||
let inner_seal = fetch_seal::<B>(block.post_digests.last(), block.header.hash())?;
|
||||
|
||||
let intermediate = block
|
||||
.remove_intermediate::<PowIntermediate<Algorithm::Difficulty>>(INTERMEDIATE_KEY)?;
|
||||
|
||||
let difficulty = match intermediate.difficulty {
|
||||
Some(difficulty) => difficulty,
|
||||
None => self.algorithm.difficulty(parent_hash)?,
|
||||
};
|
||||
|
||||
let pre_hash = block.header.hash();
|
||||
let pre_digest = find_pre_digest::<B>(&block.header)?;
|
||||
if !self.algorithm.verify(
|
||||
&BlockId::hash(parent_hash),
|
||||
&pre_hash,
|
||||
pre_digest.as_ref().map(|v| &v[..]),
|
||||
&inner_seal,
|
||||
difficulty,
|
||||
)? {
|
||||
return Err(Error::<B>::InvalidSeal.into());
|
||||
}
|
||||
|
||||
aux.difficulty = difficulty;
|
||||
aux.total_difficulty.increment(difficulty);
|
||||
|
||||
let key = aux_key(&block.post_hash());
|
||||
block.auxiliary.push((key, Some(aux.encode())));
|
||||
if block.fork_choice.is_none() {
|
||||
block.fork_choice = Some(ForkChoiceStrategy::Custom(
|
||||
match aux.total_difficulty.cmp(&best_aux.total_difficulty) {
|
||||
Ordering::Less => false,
|
||||
Ordering::Greater => true,
|
||||
Ordering::Equal => {
|
||||
let best_inner_seal =
|
||||
fetch_seal::<B>(best_header.digest().logs.last(), best_hash)?;
|
||||
|
||||
self.algorithm.break_tie(&best_inner_seal, &inner_seal)
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
self.inner.import_block(block).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// A verifier for PoW blocks.
|
||||
pub struct PowVerifier<B: BlockT, Algorithm> {
|
||||
algorithm: Algorithm,
|
||||
_marker: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<B: BlockT, Algorithm> PowVerifier<B, Algorithm> {
|
||||
pub fn new(algorithm: Algorithm) -> Self {
|
||||
Self { algorithm, _marker: PhantomData }
|
||||
}
|
||||
|
||||
fn check_header(&self, mut header: B::Header) -> Result<(B::Header, DigestItem), Error<B>>
|
||||
where
|
||||
Algorithm: PowAlgorithm<B>,
|
||||
{
|
||||
let hash = header.hash();
|
||||
|
||||
let (seal, inner_seal) = match header.digest_mut().pop() {
|
||||
Some(DigestItem::Seal(id, seal)) =>
|
||||
if id == POW_ENGINE_ID {
|
||||
(DigestItem::Seal(id, seal.clone()), seal)
|
||||
} else {
|
||||
return Err(Error::WrongEngine(id));
|
||||
},
|
||||
_ => return Err(Error::HeaderUnsealed(hash)),
|
||||
};
|
||||
|
||||
let pre_hash = header.hash();
|
||||
|
||||
if !self.algorithm.preliminary_verify(&pre_hash, &inner_seal)?.unwrap_or(true) {
|
||||
return Err(Error::FailedPreliminaryVerify);
|
||||
}
|
||||
|
||||
Ok((header, seal))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B: BlockT, Algorithm> Verifier<B> for PowVerifier<B, Algorithm>
|
||||
where
|
||||
Algorithm: PowAlgorithm<B> + Send + Sync,
|
||||
Algorithm::Difficulty: 'static + Send,
|
||||
{
|
||||
async fn verify(
|
||||
&self,
|
||||
mut block: BlockImportParams<B>,
|
||||
) -> Result<BlockImportParams<B>, String> {
|
||||
let hash = block.header.hash();
|
||||
let (checked_header, seal) = self.check_header(block.header)?;
|
||||
|
||||
let intermediate = PowIntermediate::<Algorithm::Difficulty> { difficulty: None };
|
||||
block.header = checked_header;
|
||||
block.post_digests.push(seal);
|
||||
block.insert_intermediate(INTERMEDIATE_KEY, intermediate);
|
||||
block.post_hash = Some(hash);
|
||||
|
||||
Ok(block)
|
||||
}
|
||||
}
|
||||
|
||||
/// The PoW import queue type.
|
||||
pub type PowImportQueue<B> = BasicQueue<B>;
|
||||
|
||||
/// Import queue for PoW engine.
|
||||
pub fn import_queue<B, Algorithm>(
|
||||
block_import: BoxBlockImport<B>,
|
||||
justification_import: Option<BoxJustificationImport<B>>,
|
||||
algorithm: Algorithm,
|
||||
spawner: &impl pezsp_core::traits::SpawnEssentialNamed,
|
||||
registry: Option<&Registry>,
|
||||
) -> Result<PowImportQueue<B>, pezsp_consensus::Error>
|
||||
where
|
||||
B: BlockT,
|
||||
Algorithm: PowAlgorithm<B> + Clone + Send + Sync + 'static,
|
||||
Algorithm::Difficulty: Send,
|
||||
{
|
||||
let verifier = PowVerifier::new(algorithm);
|
||||
|
||||
Ok(BasicQueue::new(verifier, block_import, justification_import, spawner, registry))
|
||||
}
|
||||
|
||||
/// Start the mining worker for PoW. This function provides the necessary helper functions that can
|
||||
/// be used to implement a miner. However, it does not do the CPU-intensive mining itself.
|
||||
///
|
||||
/// Two values are returned -- a worker, which contains functions that allows querying the current
|
||||
/// mining metadata and submitting mined blocks, and a future, which must be polled to fill in
|
||||
/// information in the worker.
|
||||
///
|
||||
/// `pre_runtime` is a parameter that allows a custom additional pre-runtime digest to be inserted
|
||||
/// for blocks being built. This can encode authorship information, or just be a graffiti.
|
||||
pub fn start_mining_worker<Block, C, S, Algorithm, E, SO, L, CIDP>(
|
||||
block_import: BoxBlockImport<Block>,
|
||||
client: Arc<C>,
|
||||
select_chain: S,
|
||||
algorithm: Algorithm,
|
||||
mut env: E,
|
||||
sync_oracle: SO,
|
||||
justification_sync_link: L,
|
||||
pre_runtime: Option<Vec<u8>>,
|
||||
create_inherent_data_providers: CIDP,
|
||||
timeout: Duration,
|
||||
build_time: Duration,
|
||||
) -> (
|
||||
MiningHandle<Block, Algorithm, L, <E::Proposer as Proposer<Block>>::Proof>,
|
||||
impl Future<Output = ()>,
|
||||
)
|
||||
where
|
||||
Block: BlockT,
|
||||
C: BlockchainEvents<Block> + 'static,
|
||||
S: SelectChain<Block> + 'static,
|
||||
Algorithm: PowAlgorithm<Block> + Clone,
|
||||
Algorithm::Difficulty: Send + 'static,
|
||||
E: Environment<Block> + Send + Sync + 'static,
|
||||
E::Error: std::fmt::Debug,
|
||||
E::Proposer: Proposer<Block>,
|
||||
SO: SyncOracle + Clone + Send + Sync + 'static,
|
||||
L: pezsc_consensus::JustificationSyncLink<Block>,
|
||||
CIDP: CreateInherentDataProviders<Block, ()>,
|
||||
{
|
||||
let mut timer = UntilImportedOrTimeout::new(client.import_notification_stream(), timeout);
|
||||
let worker = MiningHandle::new(algorithm.clone(), block_import, justification_sync_link);
|
||||
let worker_ret = worker.clone();
|
||||
|
||||
let task = async move {
|
||||
loop {
|
||||
if timer.next().await.is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
if sync_oracle.is_major_syncing() {
|
||||
debug!(target: LOG_TARGET, "Skipping proposal due to sync.");
|
||||
worker.on_major_syncing();
|
||||
continue;
|
||||
}
|
||||
|
||||
let best_header = match select_chain.best_chain().await {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
target: LOG_TARGET,
|
||||
"Unable to pull new block for authoring. \
|
||||
Select best chain error: {}",
|
||||
err
|
||||
);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
let best_hash = best_header.hash();
|
||||
|
||||
if worker.best_hash() == Some(best_hash) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The worker is locked for the duration of the whole proposing period. Within this
|
||||
// period, the mining target is outdated and useless anyway.
|
||||
|
||||
let difficulty = match algorithm.difficulty(best_hash) {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
target: LOG_TARGET,
|
||||
"Unable to propose new block for authoring. \
|
||||
Fetch difficulty failed: {}",
|
||||
err,
|
||||
);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
let inherent_data_providers = match create_inherent_data_providers
|
||||
.create_inherent_data_providers(best_hash, ())
|
||||
.await
|
||||
{
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
target: LOG_TARGET,
|
||||
"Unable to propose new block for authoring. \
|
||||
Creating inherent data providers failed: {}",
|
||||
err,
|
||||
);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
let inherent_data = match inherent_data_providers.create_inherent_data().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
target: LOG_TARGET,
|
||||
"Unable to propose new block for authoring. \
|
||||
Creating inherent data failed: {}",
|
||||
e,
|
||||
);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
let mut inherent_digest = Digest::default();
|
||||
if let Some(pre_runtime) = &pre_runtime {
|
||||
inherent_digest.push(DigestItem::PreRuntime(POW_ENGINE_ID, pre_runtime.to_vec()));
|
||||
}
|
||||
|
||||
let pre_runtime = pre_runtime.clone();
|
||||
|
||||
let proposer = match env.init(&best_header).await {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
target: LOG_TARGET,
|
||||
"Unable to propose new block for authoring. \
|
||||
Creating proposer failed: {:?}",
|
||||
err,
|
||||
);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
let proposal =
|
||||
match proposer.propose(inherent_data, inherent_digest, build_time, None).await {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
target: LOG_TARGET,
|
||||
"Unable to propose new block for authoring. \
|
||||
Creating proposal failed: {}",
|
||||
err,
|
||||
);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
let build = MiningBuild::<Block, Algorithm, _> {
|
||||
metadata: MiningMetadata {
|
||||
best_hash,
|
||||
pre_hash: proposal.block.header().hash(),
|
||||
pre_runtime: pre_runtime.clone(),
|
||||
difficulty,
|
||||
},
|
||||
proposal,
|
||||
};
|
||||
|
||||
worker.on_build(build);
|
||||
}
|
||||
};
|
||||
|
||||
(worker_ret, task)
|
||||
}
|
||||
|
||||
/// Find PoW pre-runtime.
|
||||
fn find_pre_digest<B: BlockT>(header: &B::Header) -> Result<Option<Vec<u8>>, Error<B>> {
|
||||
let mut pre_digest: Option<_> = None;
|
||||
for log in header.digest().logs() {
|
||||
trace!(target: LOG_TARGET, "Checking log {:?}, looking for pre runtime digest", log);
|
||||
match (log, pre_digest.is_some()) {
|
||||
(DigestItem::PreRuntime(POW_ENGINE_ID, _), true) =>
|
||||
return Err(Error::MultiplePreRuntimeDigests),
|
||||
(DigestItem::PreRuntime(POW_ENGINE_ID, v), false) => {
|
||||
pre_digest = Some(v.clone());
|
||||
},
|
||||
(_, _) => trace!(target: LOG_TARGET, "Ignoring digest not meant for us"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pre_digest)
|
||||
}
|
||||
|
||||
/// Fetch PoW seal.
|
||||
fn fetch_seal<B: BlockT>(digest: Option<&DigestItem>, hash: B::Hash) -> Result<Vec<u8>, Error<B>> {
|
||||
match digest {
|
||||
Some(DigestItem::Seal(id, seal)) =>
|
||||
if id == &POW_ENGINE_ID {
|
||||
Ok(seal.clone())
|
||||
} else {
|
||||
Err(Error::<B>::WrongEngine(*id))
|
||||
},
|
||||
_ => Err(Error::<B>::HeaderUnsealed(hash)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use futures::{
|
||||
prelude::*,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use futures_timer::Delay;
|
||||
use log::*;
|
||||
use parking_lot::Mutex;
|
||||
use pezsc_client_api::ImportNotifications;
|
||||
use pezsc_consensus::{BlockImportParams, BoxBlockImport, StateAction, StorageChanges};
|
||||
use pezsp_consensus::{BlockOrigin, Proposal};
|
||||
use pezsp_runtime::{
|
||||
generic::BlockId,
|
||||
traits::{Block as BlockT, Header as HeaderT},
|
||||
DigestItem,
|
||||
};
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{PowAlgorithm, PowIntermediate, Seal, INTERMEDIATE_KEY, LOG_TARGET, POW_ENGINE_ID};
|
||||
|
||||
/// Mining metadata. This is the information needed to start an actual mining loop.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct MiningMetadata<H, D> {
|
||||
/// Currently known best hash which the pre-hash is built on.
|
||||
pub best_hash: H,
|
||||
/// Mining pre-hash.
|
||||
pub pre_hash: H,
|
||||
/// Pre-runtime digest item.
|
||||
pub pre_runtime: Option<Vec<u8>>,
|
||||
/// Mining target difficulty.
|
||||
pub difficulty: D,
|
||||
}
|
||||
|
||||
/// A build of mining, containing the metadata and the block proposal.
|
||||
pub struct MiningBuild<Block: BlockT, Algorithm: PowAlgorithm<Block>, Proof> {
|
||||
/// Mining metadata.
|
||||
pub metadata: MiningMetadata<Block::Hash, Algorithm::Difficulty>,
|
||||
/// Mining proposal.
|
||||
pub proposal: Proposal<Block, Proof>,
|
||||
}
|
||||
|
||||
/// Version of the mining worker.
|
||||
#[derive(Eq, PartialEq, Clone, Copy)]
|
||||
pub struct Version(usize);
|
||||
|
||||
/// Mining worker that exposes structs to query the current mining build and submit mined blocks.
|
||||
pub struct MiningHandle<
|
||||
Block: BlockT,
|
||||
Algorithm: PowAlgorithm<Block>,
|
||||
L: pezsc_consensus::JustificationSyncLink<Block>,
|
||||
Proof,
|
||||
> {
|
||||
version: Arc<AtomicUsize>,
|
||||
algorithm: Arc<Algorithm>,
|
||||
justification_sync_link: Arc<L>,
|
||||
build: Arc<Mutex<Option<MiningBuild<Block, Algorithm, Proof>>>>,
|
||||
block_import: Arc<Mutex<BoxBlockImport<Block>>>,
|
||||
}
|
||||
|
||||
impl<Block, Algorithm, L, Proof> MiningHandle<Block, Algorithm, L, Proof>
|
||||
where
|
||||
Block: BlockT,
|
||||
Algorithm: PowAlgorithm<Block>,
|
||||
Algorithm::Difficulty: 'static + Send,
|
||||
L: pezsc_consensus::JustificationSyncLink<Block>,
|
||||
{
|
||||
fn increment_version(&self) {
|
||||
self.version.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub(crate) fn new(
|
||||
algorithm: Algorithm,
|
||||
block_import: BoxBlockImport<Block>,
|
||||
justification_sync_link: L,
|
||||
) -> Self {
|
||||
Self {
|
||||
version: Arc::new(AtomicUsize::new(0)),
|
||||
algorithm: Arc::new(algorithm),
|
||||
justification_sync_link: Arc::new(justification_sync_link),
|
||||
build: Arc::new(Mutex::new(None)),
|
||||
block_import: Arc::new(Mutex::new(block_import)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_major_syncing(&self) {
|
||||
let mut build = self.build.lock();
|
||||
*build = None;
|
||||
self.increment_version();
|
||||
}
|
||||
|
||||
pub(crate) fn on_build(&self, value: MiningBuild<Block, Algorithm, Proof>) {
|
||||
let mut build = self.build.lock();
|
||||
*build = Some(value);
|
||||
self.increment_version();
|
||||
}
|
||||
|
||||
/// Get the version of the mining worker.
|
||||
///
|
||||
/// This returns type `Version` which can only compare equality. If `Version` is unchanged, then
|
||||
/// it can be certain that `best_hash` and `metadata` were not changed.
|
||||
pub fn version(&self) -> Version {
|
||||
Version(self.version.load(Ordering::SeqCst))
|
||||
}
|
||||
|
||||
/// Get the current best hash. `None` if the worker has just started or the client is doing
|
||||
/// major syncing.
|
||||
pub fn best_hash(&self) -> Option<Block::Hash> {
|
||||
self.build.lock().as_ref().map(|b| b.metadata.best_hash)
|
||||
}
|
||||
|
||||
/// Get a copy of the current mining metadata, if available.
|
||||
pub fn metadata(&self) -> Option<MiningMetadata<Block::Hash, Algorithm::Difficulty>> {
|
||||
self.build.lock().as_ref().map(|b| b.metadata.clone())
|
||||
}
|
||||
|
||||
/// Submit a mined seal. The seal will be validated again. Returns true if the submission is
|
||||
/// successful.
|
||||
pub async fn submit(&self, seal: Seal) -> bool {
|
||||
if let Some(metadata) = self.metadata() {
|
||||
match self.algorithm.verify(
|
||||
&BlockId::Hash(metadata.best_hash),
|
||||
&metadata.pre_hash,
|
||||
metadata.pre_runtime.as_ref().map(|v| &v[..]),
|
||||
&seal,
|
||||
metadata.difficulty,
|
||||
) {
|
||||
Ok(true) => (),
|
||||
Ok(false) => {
|
||||
warn!(target: LOG_TARGET, "Unable to import mined block: seal is invalid",);
|
||||
return false;
|
||||
},
|
||||
Err(err) => {
|
||||
warn!(target: LOG_TARGET, "Unable to import mined block: {}", err,);
|
||||
return false;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
warn!(target: LOG_TARGET, "Unable to import mined block: metadata does not exist",);
|
||||
return false;
|
||||
}
|
||||
|
||||
let build = if let Some(build) = {
|
||||
let mut build = self.build.lock();
|
||||
let value = build.take();
|
||||
if value.is_some() {
|
||||
self.increment_version();
|
||||
}
|
||||
value
|
||||
} {
|
||||
build
|
||||
} else {
|
||||
warn!(target: LOG_TARGET, "Unable to import mined block: build does not exist",);
|
||||
return false;
|
||||
};
|
||||
|
||||
let seal = DigestItem::Seal(POW_ENGINE_ID, seal);
|
||||
let (header, body) = build.proposal.block.deconstruct();
|
||||
|
||||
let mut import_block = BlockImportParams::new(BlockOrigin::Own, header);
|
||||
import_block.post_digests.push(seal);
|
||||
import_block.body = Some(body);
|
||||
import_block.state_action =
|
||||
StateAction::ApplyChanges(StorageChanges::Changes(build.proposal.storage_changes));
|
||||
|
||||
let intermediate = PowIntermediate::<Algorithm::Difficulty> {
|
||||
difficulty: Some(build.metadata.difficulty),
|
||||
};
|
||||
import_block.insert_intermediate(INTERMEDIATE_KEY, intermediate);
|
||||
|
||||
let header = import_block.post_header();
|
||||
let block_import = self.block_import.lock();
|
||||
|
||||
match block_import.import_block(import_block).await {
|
||||
Ok(res) => {
|
||||
res.handle_justification(
|
||||
&header.hash(),
|
||||
*header.number(),
|
||||
&self.justification_sync_link,
|
||||
);
|
||||
|
||||
info!(
|
||||
target: LOG_TARGET,
|
||||
"✅ Successfully mined block on top of: {}", build.metadata.best_hash
|
||||
);
|
||||
true
|
||||
},
|
||||
Err(err) => {
|
||||
warn!(target: LOG_TARGET, "Unable to import mined block: {}", err,);
|
||||
false
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block, Algorithm, L, Proof> Clone for MiningHandle<Block, Algorithm, L, Proof>
|
||||
where
|
||||
Block: BlockT,
|
||||
Algorithm: PowAlgorithm<Block>,
|
||||
L: pezsc_consensus::JustificationSyncLink<Block>,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
version: self.version.clone(),
|
||||
algorithm: self.algorithm.clone(),
|
||||
justification_sync_link: self.justification_sync_link.clone(),
|
||||
build: self.build.clone(),
|
||||
block_import: self.block_import.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A stream that waits for a block import or timeout.
|
||||
pub struct UntilImportedOrTimeout<Block: BlockT> {
|
||||
import_notifications: ImportNotifications<Block>,
|
||||
timeout: Duration,
|
||||
inner_delay: Option<Delay>,
|
||||
}
|
||||
|
||||
impl<Block: BlockT> UntilImportedOrTimeout<Block> {
|
||||
/// Create a new stream using the given import notification and timeout duration.
|
||||
pub fn new(import_notifications: ImportNotifications<Block>, timeout: Duration) -> Self {
|
||||
Self { import_notifications, timeout, inner_delay: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block: BlockT> Stream for UntilImportedOrTimeout<Block> {
|
||||
type Item = ();
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<()>> {
|
||||
let mut fire = false;
|
||||
|
||||
loop {
|
||||
match Stream::poll_next(Pin::new(&mut self.import_notifications), cx) {
|
||||
Poll::Pending => break,
|
||||
Poll::Ready(Some(_)) => {
|
||||
fire = true;
|
||||
},
|
||||
Poll::Ready(None) => return Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
|
||||
let timeout = self.timeout;
|
||||
let inner_delay = self.inner_delay.get_or_insert_with(|| Delay::new(timeout));
|
||||
|
||||
match Future::poll(Pin::new(inner_delay), cx) {
|
||||
Poll::Pending => (),
|
||||
Poll::Ready(()) => {
|
||||
fire = true;
|
||||
},
|
||||
}
|
||||
|
||||
if fire {
|
||||
self.inner_delay = None;
|
||||
Poll::Ready(Some(()))
|
||||
} else {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
[package]
|
||||
name = "pezsc-consensus-slots"
|
||||
version = "0.33.0"
|
||||
authors.workspace = true
|
||||
description = "Generic slots-based utilities for consensus"
|
||||
edition.workspace = true
|
||||
build = "build.rs"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
codec = { workspace = true, default-features = true }
|
||||
futures = { workspace = true }
|
||||
futures-timer = { workspace = true }
|
||||
log = { workspace = true, default-features = true }
|
||||
pezsc-client-api = { workspace = true, default-features = true }
|
||||
pezsc-consensus = { workspace = true, default-features = true }
|
||||
pezsc-telemetry = { workspace = true, default-features = true }
|
||||
pezsp-arithmetic = { 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-inherents = { workspace = true, default-features = true }
|
||||
pezsp-runtime = { workspace = true, default-features = true }
|
||||
pezsp-state-machine = { workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
bizinikiwi-test-runtime-client = { workspace = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezsc-client-api/runtime-benchmarks",
|
||||
"pezsc-consensus/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus-slots/runtime-benchmarks",
|
||||
"pezsp-consensus/runtime-benchmarks",
|
||||
"pezsp-inherents/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"pezsp-state-machine/runtime-benchmarks",
|
||||
"bizinikiwi-test-runtime-client/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
Slots functionality for Bizinikiwi.
|
||||
|
||||
Some consensus algorithms have a concept of *slots*, which are intervals in
|
||||
time during which certain events can and/or must occur. This crate
|
||||
provides generic functionality for slots.
|
||||
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,25 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
if let Ok(profile) = env::var("PROFILE") {
|
||||
println!("cargo:rustc-cfg=build_profile=\"{}\"", profile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Schema for slots in the aux-db.
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use pezsc_client_api::backend::AuxStore;
|
||||
use pezsp_blockchain::{Error as ClientError, Result as ClientResult};
|
||||
use pezsp_consensus_slots::{EquivocationProof, Slot};
|
||||
use pezsp_runtime::traits::Header;
|
||||
|
||||
const SLOT_HEADER_MAP_KEY: &[u8] = b"slot_header_map";
|
||||
const SLOT_HEADER_START: &[u8] = b"slot_header_start";
|
||||
|
||||
/// We keep at least this number of slots in database.
|
||||
pub const MAX_SLOT_CAPACITY: u64 = 1000;
|
||||
/// We prune slots when they reach this number.
|
||||
pub const PRUNING_BOUND: u64 = 2 * MAX_SLOT_CAPACITY;
|
||||
|
||||
fn load_decode<C, T>(backend: &C, key: &[u8]) -> ClientResult<Option<T>>
|
||||
where
|
||||
C: AuxStore,
|
||||
T: Decode,
|
||||
{
|
||||
match backend.get_aux(key)? {
|
||||
None => Ok(None),
|
||||
Some(t) => T::decode(&mut &t[..])
|
||||
.map_err(|e| {
|
||||
ClientError::Backend(format!("Slots DB is corrupted. Decode error: {}", e))
|
||||
})
|
||||
.map(Some),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the header is an equivocation and returns the proof in that case.
|
||||
///
|
||||
/// Note: it detects equivocations only when slot_now - slot <= MAX_SLOT_CAPACITY.
|
||||
pub fn check_equivocation<C, H, P>(
|
||||
backend: &C,
|
||||
slot_now: Slot,
|
||||
slot: Slot,
|
||||
header: &H,
|
||||
signer: &P,
|
||||
) -> ClientResult<Option<EquivocationProof<H, P>>>
|
||||
where
|
||||
H: Header,
|
||||
C: AuxStore,
|
||||
P: Clone + Encode + Decode + PartialEq,
|
||||
{
|
||||
// We don't check equivocations for old headers out of our capacity.
|
||||
if slot_now.saturating_sub(*slot) > MAX_SLOT_CAPACITY {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Key for this slot.
|
||||
let mut curr_slot_key = SLOT_HEADER_MAP_KEY.to_vec();
|
||||
slot.using_encoded(|s| curr_slot_key.extend(s));
|
||||
|
||||
// Get headers of this slot.
|
||||
let mut headers_with_sig =
|
||||
load_decode::<_, Vec<(H, P)>>(backend, &curr_slot_key[..])?.unwrap_or_else(Vec::new);
|
||||
|
||||
// Get first slot saved.
|
||||
let slot_header_start = SLOT_HEADER_START.to_vec();
|
||||
let first_saved_slot = load_decode::<_, Slot>(backend, &slot_header_start[..])?.unwrap_or(slot);
|
||||
|
||||
if slot_now < first_saved_slot {
|
||||
// The code below assumes that slots will be visited sequentially.
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
for (prev_header, prev_signer) in headers_with_sig.iter() {
|
||||
// A proof of equivocation consists of two headers:
|
||||
// 1) signed by the same voter,
|
||||
if prev_signer == signer {
|
||||
// 2) with different hash
|
||||
return if header.hash() != prev_header.hash() {
|
||||
Ok(Some(EquivocationProof {
|
||||
slot,
|
||||
offender: signer.clone(),
|
||||
first_header: prev_header.clone(),
|
||||
second_header: header.clone(),
|
||||
}))
|
||||
} else {
|
||||
// We don't need to continue in case of duplicated header,
|
||||
// since it's already saved and a possible equivocation
|
||||
// would have been detected before.
|
||||
Ok(None)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let mut keys_to_delete = vec![];
|
||||
let mut new_first_saved_slot = first_saved_slot;
|
||||
|
||||
if *slot_now - *first_saved_slot >= PRUNING_BOUND {
|
||||
let prefix = SLOT_HEADER_MAP_KEY.to_vec();
|
||||
new_first_saved_slot = slot_now.saturating_sub(MAX_SLOT_CAPACITY);
|
||||
|
||||
for s in u64::from(first_saved_slot)..new_first_saved_slot.into() {
|
||||
let mut p = prefix.clone();
|
||||
s.using_encoded(|s| p.extend(s));
|
||||
keys_to_delete.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
headers_with_sig.push((header.clone(), signer.clone()));
|
||||
|
||||
backend.insert_aux(
|
||||
&[
|
||||
(&curr_slot_key[..], headers_with_sig.encode().as_slice()),
|
||||
(&slot_header_start[..], new_first_saved_slot.encode().as_slice()),
|
||||
],
|
||||
&keys_to_delete.iter().map(|k| &k[..]).collect::<Vec<&[u8]>>()[..],
|
||||
)?;
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pezsp_core::{hash::H256, sr25519, Pair};
|
||||
use pezsp_runtime::testing::{Digest as DigestTest, Header as HeaderTest};
|
||||
use bizinikiwi_test_runtime_client;
|
||||
|
||||
use super::{check_equivocation, MAX_SLOT_CAPACITY, PRUNING_BOUND};
|
||||
|
||||
fn create_header(number: u64) -> HeaderTest {
|
||||
// so that different headers for the same number get different hashes
|
||||
let parent_hash = H256::random();
|
||||
|
||||
let header = HeaderTest {
|
||||
parent_hash,
|
||||
number,
|
||||
state_root: Default::default(),
|
||||
extrinsics_root: Default::default(),
|
||||
digest: DigestTest { logs: vec![] },
|
||||
};
|
||||
|
||||
header
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_equivocation_works() {
|
||||
let client = bizinikiwi_test_runtime_client::new();
|
||||
let (pair, _seed) = sr25519::Pair::generate();
|
||||
let public = pair.public();
|
||||
|
||||
let header1 = create_header(1); // @ slot 2
|
||||
let header2 = create_header(2); // @ slot 2
|
||||
let header3 = create_header(2); // @ slot 4
|
||||
let header4 = create_header(3); // @ slot MAX_SLOT_CAPACITY + 4
|
||||
let header5 = create_header(4); // @ slot MAX_SLOT_CAPACITY + 4
|
||||
let header6 = create_header(3); // @ slot 4
|
||||
|
||||
// It's ok to sign same headers.
|
||||
assert!(check_equivocation(&client, 2.into(), 2.into(), &header1, &public)
|
||||
.unwrap()
|
||||
.is_none(),);
|
||||
|
||||
assert!(check_equivocation(&client, 3.into(), 2.into(), &header1, &public)
|
||||
.unwrap()
|
||||
.is_none(),);
|
||||
|
||||
// But not two different headers at the same slot.
|
||||
assert!(check_equivocation(&client, 4.into(), 2.into(), &header2, &public)
|
||||
.unwrap()
|
||||
.is_some(),);
|
||||
|
||||
// Different slot is ok.
|
||||
assert!(check_equivocation(&client, 5.into(), 4.into(), &header3, &public)
|
||||
.unwrap()
|
||||
.is_none(),);
|
||||
|
||||
// Here we trigger pruning and save header 4.
|
||||
assert!(check_equivocation(
|
||||
&client,
|
||||
(PRUNING_BOUND + 2).into(),
|
||||
(MAX_SLOT_CAPACITY + 4).into(),
|
||||
&header4,
|
||||
&public,
|
||||
)
|
||||
.unwrap()
|
||||
.is_none(),);
|
||||
|
||||
// This fails because header 5 is an equivocation of header 4.
|
||||
assert!(check_equivocation(
|
||||
&client,
|
||||
(PRUNING_BOUND + 3).into(),
|
||||
(MAX_SLOT_CAPACITY + 4).into(),
|
||||
&header5,
|
||||
&public,
|
||||
)
|
||||
.unwrap()
|
||||
.is_some(),);
|
||||
|
||||
// This is ok because we pruned the corresponding header. Shows that we are pruning.
|
||||
assert!(check_equivocation(
|
||||
&client,
|
||||
(PRUNING_BOUND + 4).into(),
|
||||
4.into(),
|
||||
&header6,
|
||||
&public,
|
||||
)
|
||||
.unwrap()
|
||||
.is_none(),);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user