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,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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user