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:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
@@ -0,0 +1,74 @@
[package]
name = "pezsc-consensus-beefy"
version = "13.0.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
repository.workspace = true
description = "BEEFY Client gadget for bizinikiwi"
homepage.workspace = true
[lints]
workspace = true
[dependencies]
array-bytes = { workspace = true, default-features = true }
async-channel = { workspace = true }
async-trait = { workspace = true }
codec = { features = ["derive"], workspace = true, default-features = true }
futures = { workspace = true }
log = { workspace = true, default-features = true }
parking_lot = { workspace = true, default-features = true }
prometheus-endpoint = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-consensus = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsc-network-gossip = { workspace = true, default-features = true }
pezsc-network-sync = { workspace = true, default-features = true }
pezsc-network-types = { workspace = true, default-features = true }
pezsc-utils = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-application-crypto = { workspace = true, default-features = true }
pezsp-arithmetic = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-consensus-beefy = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-keystore = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
thiserror = { workspace = true }
tokio = { workspace = true, default-features = true }
wasm-timer = { workspace = true }
[dev-dependencies]
pezsc-block-builder = { workspace = true, default-features = true }
pezsc-network-test = { workspace = true }
serde = { workspace = true, default-features = true }
pezsp-mmr-primitives = { workspace = true, default-features = true }
pezsp-tracing = { workspace = true, default-features = true }
bizinikiwi-test-runtime-client = { workspace = true }
[features]
# This feature adds BLS crypto primitives. It should not be used in production since
# the BLS implementation and interface may still be subject to significant change.
bls-experimental = [
"pezsp-application-crypto/bls-experimental",
"pezsp-consensus-beefy/bls-experimental",
"pezsp-core/bls-experimental",
]
runtime-benchmarks = [
"pezsc-block-builder/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-consensus/runtime-benchmarks",
"pezsc-network-gossip/runtime-benchmarks",
"pezsc-network-sync/runtime-benchmarks",
"pezsc-network-test/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus-beefy/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-mmr-primitives/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"bizinikiwi-test-runtime-client/runtime-benchmarks",
]
+373
View File
@@ -0,0 +1,373 @@
# BEEFY
**BEEFY** (**B**ridge **E**fficiency **E**nabling **F**inality **Y**ielder) is a secondary
protocol running along GRANDPA Finality to support efficient bridging with non-Bizinikiwi
blockchains, currently mainly ETH mainnet.
It can be thought of as an (optional) Bridge-specific Gadget to the GRANDPA Finality protocol.
The Protocol piggybacks on many assumptions provided by GRANDPA, and is required to be built
on top of it to work correctly.
BEEFY is a consensus protocol designed with efficient trustless bridging in mind. It means
that building a light client of BEEFY protocol should be optimized for restricted environments
like Ethereum Smart Contracts or On-Chain State Transition Function (e.g. Bizinikiwi Runtime).
Note that BEEFY is not a standalone protocol, it is meant to be running alongside GRANDPA, a
finality gadget created for Bizinikiwi/PezkuwiChain ecosystem. More details about GRANDPA can be found
in the [whitepaper](https://github.com/w3f/consensus/blob/master/pdf/grandpa.pdf).
# Context
## Bridges
We want to be able to "bridge" different blockchains. We do so by safely sharing and verifying
information about each chains state, i.e. blockchain `A` should be able to verify that blockchain
`B` is at block #X.
## Finality
Finality in blockchains is a concept that means that after a given block #X has been finalized,
it will never be reverted (e.g. due to a re-org). As such, we can be assured that any transaction
that exists in this block will never be reverted.
## GRANDPA
GRANDPA is our finality gadget. It allows a set of nodes to come to BFT agreement on what is the
canonical chain. It requires that 2/3 of the validator set agrees on a prefix of the canonical
chain, which then becomes finalized.
![img](https://miro.medium.com/max/955/1*NTg26i4xbO3JncF_Usu9MA.png)
### Difficulties of GRANDPA finality proofs
```rust
struct Justification<Block: BlockT> {
round: u64,
commit: Commit<Block>,
votes_ancestries: Vec<Block::Header>,
}
struct Commit<Hash, Number, Signature, Id> {
target_hash: Hash,
target_number: Number,
precommits: Vec<SignedPrecommit<Hash, Number, Signature, Id>>,
}
struct SignedPrecommit<Hash, Number, Signature, Id> {
precommit: Precommit<Hash, Number>,
signature: Signature,
id: Id,
}
struct Precommit<Hash, Number> {
target_hash: Hash,
target_number: Number,
}
```
The main difficulty of verifying GRANDPA finality proofs comes from the fact that voters are
voting on different things. In GRANDPA each voter will vote for the block they think is the
latest one, and the protocol will come to agreement on what is the common ancestor which has >
2/3 support.
This creates two sets of inefficiencies:
- We may need to have each validator's vote data because they're all potentially different (i.e.
just the signature isn't enough).
- We may need to attach a couple of headers to the finality proof in order to be able to verify
all of the votes' ancestries.
Additionally, since our interim goal is to bridge to Ethereum there is also a difficulty related
to "incompatible" crypto schemes. We use \`ed25519\` signatures in GRANDPA which we can't
efficiently verify in the EVM.
Hence,
### Goals of BEEFY
1. Allow customisation of crypto to adapt for different targets. Support thresholds signatures as
well eventually.
1. Minimize the size of the "signed payload" and the finality proof.
1. Unify data types and use backward-compatible versioning so that the protocol can be extended
(additional payload, different crypto) without breaking existing light clients.
And since BEEFY is required to be running on top of GRANDPA. This allows us to take couple of
shortcuts:
1. BEEFY validator set is **the same** as GRANDPA's (i.e. the same bonded actors), they might be
identified by different session keys though.
1. BEEFY runs on **finalized** canonical chain, i.e. no forks (note Misbehavior
section though).
1. From a single validator perspective, BEEFY has at most one active voting round. Since GRANDPA
validators are reaching finality, we assume they are on-line and well-connected and have
similar view of the state of the blockchain.
# The BEEFY Protocol
## Mental Model
BEEFY should be considered as an extra voting round done by GRANDPA validators for the current
best finalized block. Similarly to how GRANDPA is lagging behind best produced (non-finalized)
block, BEEFY is going to lag behind best GRANDPA (finalized) block.
```
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ │ │ │ │ │ │ │ │ │
│ B1 │ │ B2 │ │ B3 │ │ B4 │ │ B5 │
│ │ │ │ │ │ │ │ │ │
└──────┘ └───▲──┘ └──────┘ └───▲──┘ └───▲──┘
│ │ │
Best BEEFY block───────────────┘ │ │
│ │
Best GRANDPA block───────────────────────────────┘ │
Best produced block───────────────────────────────────────┘
```
A pseudo-algorithm of behaviour for a fully-synced BEEFY validator is:
```
loop {
let (best_beefy, best_grandpa) = wait_for_best_blocks();
let block_to_vote_on = choose_next_beefy_block(
best_beefy,
best_grandpa
);
let payload_to_vote_on = retrieve_payload(block_to_vote_on);
let commitment = (block_to_vote_on, payload_to_vote_on);
let signature = sign_with_current_session_key(commitment);
broadcast_vote(commitment, signature);
}
```
## Details
Before we jump into describing how BEEFY works in details, let's agree on the terms we are going
to use and actors in the system. All nodes in the network need to participate in the BEEFY
networking protocol, but we can identify two distinct actors though: **regular nodes** and
**BEEFY validators**.
Validators are expected to actively participate in the protocol, by producing and broadcasting
**votes**. Votes are simply their signatures over a **Commitment**. A Commitment consists of a
**payload** (an opaque blob of bytes extracted from a block or state at that block, expected to
be some form of crypto accumulator (like Merkle Tree Hash or Merkle Mountain Range Root Hash))
and **block number** from which this payload originates. Additionally, Commitment contains BEEFY
**validator set id** at that particular block. Note the block is finalized, so there is no
ambiguity despite using block number instead of a hash. A collection of **votes**, or rather
a Commitment and a collection of signatures is going to be called **Signed Commitment**. A valid
(see later for the rules) Signed Commitment is also called a **BEEFY Justification** or
**BEEFY Finality Proof**. For more details on the actual data structures please see
[BEEFY primitives definitions](https://github.com/pezkuwichain/pezkuwi-sdk/tree/master/bizinikiwi/primitives/consensus/beefy/src).
A **round** is an attempt by BEEFY validators to produce a BEEFY Justification. **Round number**
is simply defined as a block number the validators are voting for, or to be more precise, the
Commitment for that block number. Round ends when the next round is started, which may happen
when one of the events occur:
1. Either the node collects `2/3rd + 1` valid votes for that round.
2. Or the node receives a BEEFY Justification for a block greater than the current best BEEFY block.
In both cases the node proceeds to determining the new round number using "Round Selection"
procedure.
Regular nodes are expected to:
1. Receive & validate votes for the current round and broadcast them to their peers.
1. Receive & validate BEEFY Justifications and broadcast them to their peers.
1. Return BEEFY Justifications for **Mandatory Blocks** on demand.
1. Optionally return BEEFY Justifications for non-mandatory blocks on demand.
Validators are expected to additionally:
1. Produce & broadcast vote for the current round.
Both kinds of actors are expected to fully participate in the protocol ONLY IF they believe they
are up-to-date with the rest of the network, i.e. they are fully synced. Before this happens,
the node should continue processing imported BEEFY Justifications and votes without actively
voting themselves.
### Round Selection
Every node (both regular nodes and validators) need to determine locally what they believe
current round number is. The choice is based on their knowledge of:
1. Best GRANDPA finalized block number (`best_grandpa`).
1. Best BEEFY finalized block number (`best_beefy`).
1. Starting block of current session (`session_start`).
**Session** means a period of time (or rather number of blocks) where validator set (keys) do not change.
See `pallet_session` for implementation details in `FRAME` context. Since we piggy-back on
GRANDPA, session boundaries for BEEFY are exactly the same as the ones for GRANDPA.
We define two kinds of blocks from the perspective of BEEFY protocol:
1. **Mandatory Blocks**
2. **Non-mandatory Blocks**
Mandatory blocks are the ones that MUST have BEEFY justification. That means that the validators
will always start and conclude a round at mandatory blocks. For non-mandatory blocks, there may
or may not be a justification and validators may never choose these blocks to start a round.
Every **first block in** each **session** is considered a **mandatory block**. All other blocks
in the session are non-mandatory, however validators are encouraged to finalize as many blocks as
possible to enable lower latency for light clients and hence end users. Since GRANDPA is
considering session boundary blocks as mandatory as well, `session_start` block will always have
both GRANDPA and BEEFY Justification.
Therefore, to determine current round number nodes use a formula:
```
round_number =
(1 - M) * session_start
+ M * (best_beefy + NEXT_POWER_OF_TWO((best_grandpa - best_beefy + 1) / 2))
```
where:
- `M` is `1` if mandatory block in current session is already finalized and `0` otherwise.
- `NEXT_POWER_OF_TWO(x)` returns the smallest number greater or equal to `x` that is a power of two.
In other words, the next round number should be the oldest mandatory block without a justification,
or the highest GRANDPA-finalized block, whose block number difference with `best_beefy` block is
a power of two. The mental model for round selection is to first finalize the mandatory block and
then to attempt to pick a block taking into account how fast BEEFY catches up with GRANDPA.
In case GRANDPA makes progress, but BEEFY seems to be lagging behind, validators are changing
rounds less often to increase the chance of concluding them.
As mentioned earlier, every time the node picks a new `round_number` (and validator casts a vote)
it ends the previous one, no matter if finality was reached (i.e. the round concluded) or not.
Votes for an inactive round should not be propagated.
Note that since BEEFY only votes for GRANDPA-finalized blocks, `session_start` here actually means:
"the latest session for which the start of is GRANDPA-finalized", i.e. block production might
have already progressed, but BEEFY needs to first finalize the mandatory block of the older
session.
In good networking conditions BEEFY may end up finalizing each and every block (if GRANDPA does
the same). Practically, with short block times, it's going to be rare and might be excessive, so
it's suggested for implementations to introduce a `min_delta` parameter which will limit the
frequency with which new rounds are started. The affected component of the formula would be:
`best_beefy + MAX(min_delta, NEXT_POWER_OF_TWO(...))`, so we start a new round only if the
power-of-two component is greater than the min delta. Note that if `round_number > best_grandpa`
the validators are not expected to start any round.
### Catch up
Every session is guaranteed to have at least one BEEFY-finalized block. However it also means
that the round at mandatory block must be concluded even though, a new session has already started
(i.e. the on-chain component has selected a new validator set and GRANDPA might have already
finalized the transition). In such case BEEFY must "catch up" the previous sessions and make sure to
conclude rounds for mandatory blocks. Note that older sessions must obviously be finalized by the
validator set at that point in time, not the latest/current one.
### Initial Sync
It's all rainbows and unicorns when the node is fully synced with the network. However during cold
startup it will have hard time determining the current round number. Because of that nodes that
are not fully synced should not participate in BEEFY protocol at all.
During the sync we should make sure to also fetch BEEFY justifications for all mandatory blocks.
This can happen asynchronously, but validators, before starting to vote, need to be certain
about the last session that contains a concluded round on mandatory block in order to initiate the
catch up procedure.
### Gossip
Nodes participating in BEEFY protocol are expected to gossip messages around.
The protocol defines following messages:
1. Votes for the current round,
2. BEEFY Justifications for recently concluded rounds,
3. BEEFY Justification for the latest mandatory block,
Each message is additionally associated with a **topic**, which can be either:
1. the round number (i.e. topic associated with a particular round),
2. or the global topic (independent from the rounds).
Round-specific topic should only be used to gossip the votes, other messages are gossiped
periodically on the global topic. Let's now dive into description of the messages.
- **Votes**
- Votes are sent on the round-specific topic.
- Vote is considered valid when:
- The commitment matches local commitment.
- The validator is part of the current validator set.
- The signature is correct.
- **BEEFY Justification**
- Justifications are sent on the global topic.
- Justification is considered worthwhile to gossip when:
- It is for a recent (implementation specific) round or the latest mandatory round.
- All signatures are valid and there is at least `2/3rd + 1` of them.
- Signatories are part of the current validator set.
- Mandatory justifications should be announced periodically.
## Misbehavior
Similarly to other PoS protocols, BEEFY considers casting two different votes in the same round a
misbehavior. I.e. for a particular `round_number`, the validator produces signatures for 2 different
`Commitment`s and broadcasts them. This is called **equivocation**.
On top of this, voting on an incorrect **payload** is considered a misbehavior as well, and since
we piggy-back on GRANDPA there is no ambiguity in terms of the fork validators should be voting for.
Misbehavior should be penalized. If more validators misbehave in the exact same `round` the
penalty should be more severe, up to the entire bonded stake in case we reach `1/3rd + 1`
validators misbehaving.
## Ethereum
Initial version of BEEFY was made to enable efficient bridging with Ethereum, where the light
client is a Solidity Smart Contract compiled to EVM bytecode. Hence the choice of the initial
cryptography for BEEFY: `secp256k1` and usage of `keccak256` hashing function.
### Future: Supporting multiple crypto
While BEEFY currently works with `secp256k1` signatures, we intend in the future to support
multiple signature schemes.
This means that multiple kinds of `SignedCommitment`s might exist and only together they form a
full `BEEFY Justification`.
## BEEFY Key
The current cryptographic scheme used by BEEFY is `ecdsa`. This is **different** from other
schemes like `sr25519` and `ed25519` which are commonly used in Bizinikiwi configurations for
other pallets (BABE, GRANDPA, AuRa, etc). The most noticeable difference is that an `ecdsa`
public key is `33` bytes long, instead of `32` bytes for a `sr25519` based public key. So, a
BEEFY key [sticks out](https://github.com/paritytech/polkadot/blob/25951e45b1907853f120c752aaa01631a0b3e783/node/service/src/chain_spec.rs#L738)
among the other public keys a bit.
For other crypto (using the default Bizinikiwi configuration) the `AccountId` (32-bytes) matches
the `PublicKey`, but note that it's not the case for BEEFY. As a consequence of this, you can
**not** convert the `AccountId` raw bytes into a BEEFY `PublicKey`.
The easiest way to generate or view hex-encoded or SS58-encoded BEEFY Public Key is by using the
[Subkey](https://bizinikiwi.dev/docs/en/knowledgebase/integrate/subkey) tool. Generate a BEEFY key
using the following command
```sh
subkey generate --scheme ecdsa
```
The output will look something like
```sh
Secret phrase `sunset anxiety liberty mention dwarf actress advice stove peasant olive kite rebuild` is account:
Secret seed: 0x9f844e21444683c8fcf558c4c11231a14ed9dea6f09a8cc505604368ef204a61
Public key (hex): 0x02d69740c3bbfbdbb365886c8270c4aafd17cbffb2e04ecef581e6dced5aded2cd
Public key (SS58): KW7n1vMENCBLQpbT5FWtmYWHNvEyGjSrNL4JE32mDds3xnXTf
Account ID: 0x295509ae9a9b04ade5f1756b5f58f4161cf57037b4543eac37b3b555644f6aed
SS58 Address: 5Czu5hudL79ETnQt6GAkVJHGhDQ6Qv3VWq54zN1CPKzKzYGu
```
In case your BEEFY keys are using the wrong cryptographic scheme, you will see an invalid public
key format message at node startup. Basically something like
```sh
...
2021-05-28 12:37:51 [Relaychain] Invalid BEEFY PublicKey format!
...
```
# BEEFY Light Client
TODO
@@ -0,0 +1,47 @@
[package]
name = "pezsc-consensus-beefy-rpc"
version = "13.0.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
repository.workspace = true
description = "RPC for the BEEFY Client gadget for bizinikiwi"
homepage.workspace = true
[lints]
workspace = true
[dependencies]
codec = { features = ["derive"], workspace = true, default-features = true }
futures = { workspace = true }
jsonrpsee = { features = [
"client-core",
"macros",
"server-core",
], workspace = true }
log = { workspace = true, default-features = true }
parking_lot = { workspace = true, default-features = true }
pezsc-consensus-beefy = { workspace = true, default-features = true }
pezsc-rpc = { workspace = true, default-features = true }
serde = { features = ["derive"], workspace = true, default-features = true }
pezsp-application-crypto = { workspace = true, default-features = true }
pezsp-consensus-beefy = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
thiserror = { workspace = true }
[dev-dependencies]
pezsc-rpc = { features = [
"test-helpers",
], workspace = true, default-features = true }
bizinikiwi-test-runtime-client = { workspace = true }
tokio = { features = ["macros"], workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezsc-consensus-beefy/runtime-benchmarks",
"pezsc-rpc/runtime-benchmarks",
"pezsp-consensus-beefy/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"bizinikiwi-test-runtime-client/runtime-benchmarks",
]
@@ -0,0 +1,305 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! RPC API for BEEFY.
#![warn(missing_docs)]
use parking_lot::RwLock;
use pezsp_consensus_beefy::AuthorityIdBound;
use std::sync::Arc;
use pezsc_rpc::{
utils::{BoundedVecDeque, PendingSubscription},
SubscriptionTaskExecutor,
};
use pezsp_application_crypto::RuntimeAppPublic;
use pezsp_runtime::traits::Block as BlockT;
use futures::{task::SpawnError, FutureExt, StreamExt};
use jsonrpsee::{
core::async_trait,
proc_macros::rpc,
types::{ErrorObject, ErrorObjectOwned},
PendingSubscriptionSink,
};
use log::warn;
use pezsc_consensus_beefy::communication::notification::{
BeefyBestBlockStream, BeefyVersionedFinalityProofStream,
};
mod notification;
#[derive(Debug, thiserror::Error)]
/// Top-level error type for the RPC handler
pub enum Error {
/// The BEEFY RPC endpoint is not ready.
#[error("BEEFY RPC endpoint not ready")]
EndpointNotReady,
/// The BEEFY RPC background task failed to spawn.
#[error("BEEFY RPC background task failed to spawn")]
RpcTaskFailure(#[from] SpawnError),
}
/// The error codes returned by jsonrpc.
pub enum ErrorCode {
/// Returned when BEEFY RPC endpoint is not ready.
NotReady = 1,
/// Returned on BEEFY RPC background task failure.
TaskFailure = 2,
}
impl From<Error> for ErrorCode {
fn from(error: Error) -> Self {
match error {
Error::EndpointNotReady => ErrorCode::NotReady,
Error::RpcTaskFailure(_) => ErrorCode::TaskFailure,
}
}
}
impl From<Error> for ErrorObjectOwned {
fn from(error: Error) -> Self {
let message = error.to_string();
let code = ErrorCode::from(error);
ErrorObject::owned(code as i32, message, None::<()>)
}
}
// Provides RPC methods for interacting with BEEFY.
#[rpc(client, server)]
pub trait BeefyApi<Notification, Hash> {
/// Returns the block most recently finalized by BEEFY, alongside its justification.
#[subscription(
name = "beefy_subscribeJustifications" => "beefy_justifications",
unsubscribe = "beefy_unsubscribeJustifications",
item = Notification,
)]
fn subscribe_justifications(&self);
/// Returns hash of the latest BEEFY finalized block as seen by this client.
///
/// The latest BEEFY block might not be available if the BEEFY gadget is not running
/// in the network or if the client is still initializing or syncing with the network.
/// In such case an error would be returned.
#[method(name = "beefy_getFinalizedHead")]
async fn latest_finalized(&self) -> Result<Hash, Error>;
}
/// Implements the BeefyApi RPC trait for interacting with BEEFY.
pub struct Beefy<Block: BlockT, AuthorityId: AuthorityIdBound> {
finality_proof_stream: BeefyVersionedFinalityProofStream<Block, AuthorityId>,
beefy_best_block: Arc<RwLock<Option<Block::Hash>>>,
executor: SubscriptionTaskExecutor,
}
impl<Block, AuthorityId> Beefy<Block, AuthorityId>
where
Block: BlockT,
AuthorityId: AuthorityIdBound,
{
/// Creates a new Beefy Rpc handler instance.
pub fn new(
finality_proof_stream: BeefyVersionedFinalityProofStream<Block, AuthorityId>,
best_block_stream: BeefyBestBlockStream<Block>,
executor: SubscriptionTaskExecutor,
) -> Result<Self, Error> {
let beefy_best_block = Arc::new(RwLock::new(None));
let stream = best_block_stream.subscribe(100_000);
let closure_clone = beefy_best_block.clone();
let future = stream.for_each(move |best_beefy| {
let async_clone = closure_clone.clone();
async move { *async_clone.write() = Some(best_beefy) }
});
executor.spawn("bizinikiwi-rpc-subscription", Some("rpc"), future.map(drop).boxed());
Ok(Self { finality_proof_stream, beefy_best_block, executor })
}
}
#[async_trait]
impl<Block, AuthorityId> BeefyApiServer<notification::EncodedVersionedFinalityProof, Block::Hash>
for Beefy<Block, AuthorityId>
where
Block: BlockT,
AuthorityId: AuthorityIdBound,
<AuthorityId as RuntimeAppPublic>::Signature: Send + Sync,
{
fn subscribe_justifications(&self, pending: PendingSubscriptionSink) {
let stream = self
.finality_proof_stream
.subscribe(100_000)
.map(|vfp| notification::EncodedVersionedFinalityProof::new::<Block, AuthorityId>(vfp));
pezsc_rpc::utils::spawn_subscription_task(
&self.executor,
PendingSubscription::from(pending).pipe_from_stream(stream, BoundedVecDeque::default()),
);
}
async fn latest_finalized(&self) -> Result<Block::Hash, Error> {
self.beefy_best_block.read().as_ref().cloned().ok_or(Error::EndpointNotReady)
}
}
#[cfg(test)]
mod tests {
use super::*;
use codec::{Decode, Encode};
use jsonrpsee::{core::EmptyServerParams as EmptyParams, RpcModule};
use pezsc_consensus_beefy::{
communication::notification::BeefyVersionedFinalityProofSender,
justification::BeefyVersionedFinalityProof,
};
use pezsp_consensus_beefy::{ecdsa_crypto, known_payloads, Payload, SignedCommitment};
use pezsp_runtime::traits::{BlakeTwo256, Hash};
use bizinikiwi_test_runtime_client::runtime::Block;
fn setup_io_handler() -> (
RpcModule<Beefy<Block, ecdsa_crypto::AuthorityId>>,
BeefyVersionedFinalityProofSender<Block, ecdsa_crypto::AuthorityId>,
) {
let (_, stream) = BeefyBestBlockStream::<Block>::channel();
setup_io_handler_with_best_block_stream(stream)
}
fn setup_io_handler_with_best_block_stream(
best_block_stream: BeefyBestBlockStream<Block>,
) -> (
RpcModule<Beefy<Block, ecdsa_crypto::AuthorityId>>,
BeefyVersionedFinalityProofSender<Block, ecdsa_crypto::AuthorityId>,
) {
let (finality_proof_sender, finality_proof_stream) =
BeefyVersionedFinalityProofStream::<Block, ecdsa_crypto::AuthorityId>::channel();
let handler =
Beefy::new(finality_proof_stream, best_block_stream, pezsc_rpc::testing::test_executor())
.expect("Setting up the BEEFY RPC handler works");
(handler.into_rpc(), finality_proof_sender)
}
#[tokio::test]
async fn uninitialized_rpc_handler() {
let (rpc, _) = setup_io_handler();
let request = r#"{"jsonrpc":"2.0","method":"beefy_getFinalizedHead","params":[],"id":1}"#;
let expected_response = r#"{"jsonrpc":"2.0","id":1,"error":{"code":1,"message":"BEEFY RPC endpoint not ready"}}"#;
let (response, _) = rpc.raw_json_request(&request, 1).await.unwrap();
assert_eq!(expected_response, response);
}
#[tokio::test]
async fn latest_finalized_rpc() {
let (sender, stream) = BeefyBestBlockStream::<Block>::channel();
let (io, _) = setup_io_handler_with_best_block_stream(stream);
let hash = BlakeTwo256::hash(b"42");
let r: Result<(), ()> = sender.notify(|| Ok(hash));
r.unwrap();
// Verify RPC `beefy_getFinalizedHead` returns expected hash.
let request = r#"{"jsonrpc":"2.0","method":"beefy_getFinalizedHead","params":[],"id":1}"#;
let expected = "{\
\"jsonrpc\":\"2.0\",\
\"id\":1,\
\"result\":\"0x2f0039e93a27221fcf657fb877a1d4f60307106113e885096cb44a461cd0afbf\"\
}";
let not_ready: &str = "{\
\"jsonrpc\":\"2.0\",\
\"id\":1,\
\"error\":{\"code\":1,\"message\":\"BEEFY RPC endpoint not ready\"}\
}";
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
while std::time::Instant::now() < deadline {
let (response, _) = io.raw_json_request(request, 1).await.expect("RPC requests work");
if response != not_ready {
assert_eq!(response, expected);
// Success
return;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
panic!(
"Deadline reached while waiting for best BEEFY block to update. Perhaps the background task is broken?"
);
}
#[tokio::test]
async fn subscribe_and_unsubscribe_with_wrong_id() {
let (rpc, _) = setup_io_handler();
// Subscribe call.
let _sub = rpc
.subscribe_unbounded("beefy_subscribeJustifications", EmptyParams::new())
.await
.unwrap();
// Unsubscribe with wrong ID
let (response, _) = rpc
.raw_json_request(
r#"{"jsonrpc":"2.0","method":"beefy_unsubscribeJustifications","params":["FOO"],"id":1}"#,
1,
)
.await
.unwrap();
let expected = r#"{"jsonrpc":"2.0","id":1,"result":false}"#;
assert_eq!(response, expected);
}
fn create_finality_proof() -> BeefyVersionedFinalityProof<Block, ecdsa_crypto::AuthorityId> {
let payload =
Payload::from_single_entry(known_payloads::MMR_ROOT_ID, "Hello World!".encode());
BeefyVersionedFinalityProof::<Block, ecdsa_crypto::AuthorityId>::V1(SignedCommitment {
commitment: pezsp_consensus_beefy::Commitment {
payload,
block_number: 5,
validator_set_id: 0,
},
signatures: vec![],
})
}
#[tokio::test]
async fn subscribe_and_listen_to_one_justification() {
let (rpc, finality_proof_sender) = setup_io_handler();
// Subscribe
let mut sub = rpc
.subscribe_unbounded("beefy_subscribeJustifications", EmptyParams::new())
.await
.unwrap();
// Notify with finality_proof
let finality_proof = create_finality_proof();
let r: Result<(), ()> = finality_proof_sender.notify(|| Ok(finality_proof.clone()));
r.unwrap();
// Inspect what we received
let (bytes, recv_sub_id) = sub.next::<pezsp_core::Bytes>().await.unwrap().unwrap();
let recv_finality_proof: BeefyVersionedFinalityProof<Block, ecdsa_crypto::AuthorityId> =
Decode::decode(&mut &bytes[..]).unwrap();
assert_eq!(&recv_sub_id, sub.subscription_id());
assert_eq!(recv_finality_proof, finality_proof);
}
}
@@ -0,0 +1,44 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use codec::Encode;
use serde::{Deserialize, Serialize};
use pezsp_consensus_beefy::AuthorityIdBound;
use pezsp_runtime::traits::Block as BlockT;
/// An encoded finality proof proving that the given header has been finalized.
/// The given bytes should be the SCALE-encoded representation of a
/// `pezsp_consensus_beefy::VersionedFinalityProof`.
#[derive(Clone, Serialize, Deserialize)]
pub struct EncodedVersionedFinalityProof(pezsp_core::Bytes);
impl EncodedVersionedFinalityProof {
pub fn new<Block, AuthorityId>(
finality_proof: pezsc_consensus_beefy::justification::BeefyVersionedFinalityProof<
Block,
AuthorityId,
>,
) -> Self
where
Block: BlockT,
AuthorityId: AuthorityIdBound,
{
EncodedVersionedFinalityProof(finality_proof.encode().into())
}
}
@@ -0,0 +1,117 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Schema for BEEFY state persisted in the aux-db.
use crate::{error::Error, worker::PersistedState, LOG_TARGET};
use codec::{Decode, Encode};
use log::{debug, trace, warn};
use pezsc_client_api::{backend::AuxStore, Backend};
use pezsp_blockchain::{Error as ClientError, Result as ClientResult};
use pezsp_consensus_beefy::AuthorityIdBound;
use pezsp_runtime::traits::Block as BlockT;
const VERSION_KEY: &[u8] = b"beefy_auxschema_version";
const WORKER_STATE_KEY: &[u8] = b"beefy_voter_state";
const CURRENT_VERSION: u32 = 4;
pub(crate) fn write_current_version<BE: AuxStore>(backend: &BE) -> Result<(), Error> {
debug!(target: LOG_TARGET, "🥩 write aux schema version {:?}", CURRENT_VERSION);
AuxStore::insert_aux(backend, &[(VERSION_KEY, CURRENT_VERSION.encode().as_slice())], &[])
.map_err(|e| Error::Backend(e.to_string()))
}
/// Write voter state.
pub(crate) fn write_voter_state<B: BlockT, BE: AuxStore, AuthorityId: AuthorityIdBound>(
backend: &BE,
state: &PersistedState<B, AuthorityId>,
) -> ClientResult<()> {
trace!(target: LOG_TARGET, "🥩 persisting {:?}", state);
AuxStore::insert_aux(backend, &[(WORKER_STATE_KEY, state.encode().as_slice())], &[])
}
fn load_decode<BE: AuxStore, T: Decode>(backend: &BE, key: &[u8]) -> ClientResult<Option<T>> {
match backend.get_aux(key)? {
None => Ok(None),
Some(t) => T::decode(&mut &t[..])
.map_err(|e| ClientError::Backend(format!("BEEFY DB is corrupted: {}", e)))
.map(Some),
}
}
/// Load or initialize persistent data from backend.
pub(crate) fn load_persistent<B, BE, AuthorityId: AuthorityIdBound>(
backend: &BE,
) -> ClientResult<Option<PersistedState<B, AuthorityId>>>
where
B: BlockT,
BE: Backend<B>,
{
let version: Option<u32> = load_decode(backend, VERSION_KEY)?;
match version {
None => (),
Some(v) if 1 <= v && v <= 3 =>
// versions 1, 2 & 3 are obsolete and should be ignored
{
warn!(target: LOG_TARGET, "🥩 backend contains a BEEFY state of an obsolete version {v}. ignoring...")
},
Some(4) =>
return load_decode::<_, PersistedState<B, AuthorityId>>(backend, WORKER_STATE_KEY),
other =>
return Err(ClientError::Backend(format!("Unsupported BEEFY DB version: {:?}", other))),
}
// No persistent state found in DB.
Ok(None)
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::tests::BeefyTestNet;
use pezsc_network_test::TestNetFactory;
use pezsp_consensus_beefy::ecdsa_crypto;
// also used in tests.rs
pub fn verify_persisted_version<B: BlockT, BE: Backend<B>>(backend: &BE) -> bool {
let version: u32 = load_decode(backend, VERSION_KEY).unwrap().unwrap();
version == CURRENT_VERSION
}
#[tokio::test]
async fn should_load_persistent_sanity_checks() {
let mut net = BeefyTestNet::new(1);
let backend = net.peer(0).client().as_backend();
// version not available in db -> None
assert_eq!(load_persistent::<_, _, ecdsa_crypto::AuthorityId>(&*backend).unwrap(), None);
// populate version in db
write_current_version(&*backend).unwrap();
// verify correct version is retrieved
assert_eq!(load_decode(&*backend, VERSION_KEY).unwrap(), Some(CURRENT_VERSION));
// version is available in db but state isn't -> None
assert_eq!(load_persistent::<_, _, ecdsa_crypto::AuthorityId>(&*backend).unwrap(), None);
// full `PersistedState` load is tested in `tests.rs`.
}
}
@@ -0,0 +1,917 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::{collections::BTreeSet, sync::Arc, time::Duration};
use pezsc_network::{NetworkPeers, ReputationChange};
use pezsc_network_gossip::{MessageIntent, ValidationResult, Validator, ValidatorContext};
use pezsc_network_types::PeerId;
use pezsp_runtime::traits::{Block, Hash, Header, NumberFor};
use codec::{Decode, DecodeAll, Encode};
use log::{debug, trace};
use parking_lot::{Mutex, RwLock};
use wasm_timer::Instant;
use crate::{
communication::{benefit, cost, peers::KnownPeers},
justification::{
proof_block_num_and_set_id, verify_with_validator_set, BeefyVersionedFinalityProof,
},
keystore::BeefyKeystore,
LOG_TARGET,
};
use pezsp_application_crypto::RuntimeAppPublic;
use pezsp_consensus_beefy::{AuthorityIdBound, ValidatorSet, ValidatorSetId, VoteMessage};
// Timeout for rebroadcasting messages.
#[cfg(not(test))]
const REBROADCAST_AFTER: Duration = Duration::from_secs(60);
#[cfg(test)]
const REBROADCAST_AFTER: Duration = Duration::from_secs(5);
#[derive(Debug, PartialEq)]
pub(super) enum Action<H> {
// repropagate under given topic, to the given peers, applying cost/benefit to originator.
Keep(H, ReputationChange),
// discard, applying cost/benefit to originator.
Discard(ReputationChange),
// ignore, no cost/benefit applied to originator.
DiscardNoReport,
}
/// An outcome of examining a message.
#[derive(Debug, PartialEq, Clone, Copy)]
enum Consider {
/// Accept the message.
Accept,
/// Message is too early. Reject.
RejectPast,
/// Message is from the future. Reject.
RejectFuture,
/// Message cannot be evaluated. Reject.
CannotEvaluate,
}
/// BEEFY gossip message type that gets encoded and sent on the network.
#[derive(Debug, Encode, Decode)]
pub(crate) enum GossipMessage<B: Block, AuthorityId: AuthorityIdBound> {
/// BEEFY message with commitment and single signature.
Vote(VoteMessage<NumberFor<B>, AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature>),
/// BEEFY justification with commitment and signatures.
FinalityProof(BeefyVersionedFinalityProof<B, AuthorityId>),
}
impl<B: Block, AuthorityId: AuthorityIdBound> GossipMessage<B, AuthorityId> {
/// Return inner vote if this message is a Vote.
pub fn unwrap_vote(
self,
) -> Option<VoteMessage<NumberFor<B>, AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature>>
{
match self {
GossipMessage::Vote(vote) => Some(vote),
GossipMessage::FinalityProof(_) => None,
}
}
/// Return inner finality proof if this message is a FinalityProof.
pub fn unwrap_finality_proof(self) -> Option<BeefyVersionedFinalityProof<B, AuthorityId>> {
match self {
GossipMessage::Vote(_) => None,
GossipMessage::FinalityProof(proof) => Some(proof),
}
}
}
/// Gossip engine votes messages topic
pub(crate) fn votes_topic<B: Block>() -> B::Hash
where
B: Block,
{
<<B::Header as Header>::Hashing as Hash>::hash(b"beefy-votes")
}
/// Gossip engine justifications messages topic
pub(crate) fn proofs_topic<B: Block>() -> B::Hash
where
B: Block,
{
<<B::Header as Header>::Hashing as Hash>::hash(b"beefy-justifications")
}
#[derive(Clone, Debug)]
pub(crate) struct GossipFilterCfg<'a, B: Block, AuthorityId: AuthorityIdBound> {
pub start: NumberFor<B>,
pub end: NumberFor<B>,
pub validator_set: &'a ValidatorSet<AuthorityId>,
}
#[derive(Clone, Debug)]
struct FilterInner<B: Block, AuthorityId: AuthorityIdBound> {
pub start: NumberFor<B>,
pub end: NumberFor<B>,
pub validator_set: ValidatorSet<AuthorityId>,
}
struct Filter<B: Block, AuthorityId: AuthorityIdBound> {
// specifies live rounds
inner: Option<FilterInner<B, AuthorityId>>,
// cache of seen valid justifications in active rounds
rounds_with_valid_proofs: BTreeSet<NumberFor<B>>,
}
impl<B: Block, AuthorityId: AuthorityIdBound> Filter<B, AuthorityId> {
pub fn new() -> Self {
Self { inner: None, rounds_with_valid_proofs: BTreeSet::new() }
}
/// Update filter to new `start` and `set_id`.
fn update(&mut self, cfg: GossipFilterCfg<B, AuthorityId>) {
self.rounds_with_valid_proofs
.retain(|&round| round >= cfg.start && round <= cfg.end);
// only clone+overwrite big validator_set if set_id changed
match self.inner.as_mut() {
Some(f) if f.validator_set.id() == cfg.validator_set.id() => {
f.start = cfg.start;
f.end = cfg.end;
},
_ =>
self.inner = Some(FilterInner {
start: cfg.start,
end: cfg.end,
validator_set: cfg.validator_set.clone(),
}),
}
}
/// Accept if `max(session_start, best_beefy) <= round <= best_grandpa`,
/// and vote `set_id` matches session set id.
///
/// Latest concluded round is still considered alive to allow proper gossiping for it.
fn consider_vote(&self, round: NumberFor<B>, set_id: ValidatorSetId) -> Consider {
self.inner
.as_ref()
.map(|f|
// only from current set and only [filter.start, filter.end]
if set_id < f.validator_set.id() || round < f.start {
Consider::RejectPast
} else if set_id > f.validator_set.id() || round > f.end {
Consider::RejectFuture
} else {
Consider::Accept
})
.unwrap_or(Consider::CannotEvaluate)
}
/// Return true if `round` is >= than `max(session_start, best_beefy)`,
/// and proof `set_id` matches session set id.
///
/// Latest concluded round is still considered alive to allow proper gossiping for it.
fn consider_finality_proof(&self, round: NumberFor<B>, set_id: ValidatorSetId) -> Consider {
self.inner
.as_ref()
.map(|f|
// only from current set and only >= filter.start
if round < f.start || set_id < f.validator_set.id() {
Consider::RejectPast
} else if set_id > f.validator_set.id() {
Consider::RejectFuture
} else {
Consider::Accept
}
)
.unwrap_or(Consider::CannotEvaluate)
}
/// Add new _known_ `round` to the set of seen valid justifications.
fn mark_round_as_proven(&mut self, round: NumberFor<B>) {
self.rounds_with_valid_proofs.insert(round);
}
/// Check if `round` is already part of seen valid justifications.
fn is_already_proven(&self, round: NumberFor<B>) -> bool {
self.rounds_with_valid_proofs.contains(&round)
}
fn validator_set(&self) -> Option<&ValidatorSet<AuthorityId>> {
self.inner.as_ref().map(|f| &f.validator_set)
}
}
/// BEEFY gossip validator
///
/// Validate BEEFY gossip messages and limit the number of live BEEFY voting rounds.
///
/// Allows messages for 'rounds >= last concluded' to flow, everything else gets
/// rejected/expired.
///
///All messaging is handled in a single BEEFY global topic.
pub(crate) struct GossipValidator<B, N, AuthorityId: AuthorityIdBound>
where
B: Block,
{
votes_topic: B::Hash,
justifs_topic: B::Hash,
gossip_filter: RwLock<Filter<B, AuthorityId>>,
next_rebroadcast: Mutex<Instant>,
known_peers: Arc<Mutex<KnownPeers<B>>>,
network: Arc<N>,
}
impl<B, N, AuthorityId> GossipValidator<B, N, AuthorityId>
where
B: Block,
AuthorityId: AuthorityIdBound,
{
pub(crate) fn new(known_peers: Arc<Mutex<KnownPeers<B>>>, network: Arc<N>) -> Self {
Self {
votes_topic: votes_topic::<B>(),
justifs_topic: proofs_topic::<B>(),
gossip_filter: RwLock::new(Filter::new()),
next_rebroadcast: Mutex::new(Instant::now() + REBROADCAST_AFTER),
known_peers,
network,
}
}
/// Update gossip validator filter.
///
/// Only votes for `set_id` and rounds `start <= round <= end` will be accepted.
pub(crate) fn update_filter(&self, filter: GossipFilterCfg<B, AuthorityId>) {
debug!(
target: LOG_TARGET,
"🥩 New gossip filter: start {:?}, end {:?}, validator set id {:?}",
filter.start, filter.end, filter.validator_set.id()
);
self.gossip_filter.write().update(filter);
}
}
impl<B, N, AuthorityId> GossipValidator<B, N, AuthorityId>
where
B: Block,
N: NetworkPeers,
AuthorityId: AuthorityIdBound,
{
fn report(&self, who: PeerId, cost_benefit: ReputationChange) {
self.network.report_peer(who, cost_benefit);
}
fn validate_vote(
&self,
vote: VoteMessage<NumberFor<B>, AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature>,
sender: &PeerId,
) -> Action<B::Hash> {
let round = vote.commitment.block_number;
let set_id = vote.commitment.validator_set_id;
self.known_peers.lock().note_vote_for(*sender, round);
// Verify general usefulness of the message.
// We are going to discard old votes right away (without verification).
{
let filter = self.gossip_filter.read();
match filter.consider_vote(round, set_id) {
Consider::RejectPast => return Action::Discard(cost::OUTDATED_MESSAGE),
Consider::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE),
// When we can't evaluate, it's our fault (e.g. filter not initialized yet), we
// discard the vote without punishing or rewarding the sending peer.
Consider::CannotEvaluate => return Action::DiscardNoReport,
Consider::Accept => {},
}
// ensure authority is part of the set.
if !filter
.validator_set()
.map(|set| set.validators().contains(&vote.id))
.unwrap_or(false)
{
debug!(target: LOG_TARGET, "Message from voter not in validator set: {}", vote.id);
return Action::Discard(cost::UNKNOWN_VOTER);
}
}
if BeefyKeystore::verify(&vote.id, &vote.signature, &vote.commitment.encode()) {
Action::Keep(self.votes_topic, benefit::VOTE_MESSAGE)
} else {
debug!(
target: LOG_TARGET,
"🥩 Bad signature on message: {:?}, from: {:?}", vote, sender
);
Action::Discard(cost::BAD_SIGNATURE)
}
}
fn validate_finality_proof(
&self,
proof: BeefyVersionedFinalityProof<B, AuthorityId>,
sender: &PeerId,
) -> Action<B::Hash> {
let (round, set_id) = proof_block_num_and_set_id::<B, AuthorityId>(&proof);
self.known_peers.lock().note_vote_for(*sender, round);
let action = {
let guard = self.gossip_filter.read();
// Verify general usefulness of the justification.
match guard.consider_finality_proof(round, set_id) {
Consider::RejectPast => return Action::Discard(cost::OUTDATED_MESSAGE),
Consider::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE),
// When we can't evaluate, it's our fault (e.g. filter not initialized yet), we
// discard the proof without punishing or rewarding the sending peer.
Consider::CannotEvaluate => return Action::DiscardNoReport,
Consider::Accept => {},
}
if guard.is_already_proven(round) {
return Action::Discard(benefit::NOT_INTERESTED);
}
// Verify justification signatures.
guard
.validator_set()
.map(|validator_set| {
if let Err((_, signatures_checked)) =
verify_with_validator_set::<B, AuthorityId>(round, validator_set, &proof)
{
debug!(
target: LOG_TARGET,
"🥩 Bad signatures on message: {:?}, from: {:?}", proof, sender
);
let mut cost = cost::INVALID_PROOF;
cost.value +=
cost::PER_SIGNATURE_CHECKED.saturating_mul(signatures_checked as i32);
Action::Discard(cost)
} else {
Action::Keep(self.justifs_topic, benefit::VALIDATED_PROOF)
}
})
// When we can't evaluate, it's our fault (e.g. filter not initialized yet), we
// discard the proof without punishing or rewarding the sending peer.
.unwrap_or(Action::DiscardNoReport)
};
if matches!(action, Action::Keep(_, _)) {
self.gossip_filter.write().mark_round_as_proven(round);
}
action
}
}
impl<B, N, AuthorityId> Validator<B> for GossipValidator<B, N, AuthorityId>
where
B: Block,
AuthorityId: AuthorityIdBound,
N: NetworkPeers + Send + Sync,
{
fn peer_disconnected(&self, _context: &mut dyn ValidatorContext<B>, who: &PeerId) {
self.known_peers.lock().remove(who);
}
fn validate(
&self,
context: &mut dyn ValidatorContext<B>,
sender: &PeerId,
mut data: &[u8],
) -> ValidationResult<B::Hash> {
let raw = data;
let action = match GossipMessage::<B, AuthorityId>::decode_all(&mut data) {
Ok(GossipMessage::Vote(msg)) => self.validate_vote(msg, sender),
Ok(GossipMessage::FinalityProof(proof)) => self.validate_finality_proof(proof, sender),
Err(e) => {
debug!(target: LOG_TARGET, "Error decoding message: {}", e);
let bytes = raw.len().min(i32::MAX as usize) as i32;
let cost = ReputationChange::new(
bytes.saturating_mul(cost::PER_UNDECODABLE_BYTE),
"BEEFY: Bad packet",
);
Action::Discard(cost)
},
};
match action {
Action::Keep(topic, cb) => {
self.report(*sender, cb);
context.broadcast_message(topic, data.to_vec(), false);
ValidationResult::ProcessAndKeep(topic)
},
Action::Discard(cb) => {
self.report(*sender, cb);
ValidationResult::Discard
},
Action::DiscardNoReport => ValidationResult::Discard,
}
}
fn message_expired<'a>(&'a self) -> Box<dyn FnMut(B::Hash, &[u8]) -> bool + 'a> {
let filter = self.gossip_filter.read();
Box::new(move |_topic, mut data| {
match GossipMessage::<B, AuthorityId>::decode_all(&mut data) {
Ok(GossipMessage::Vote(msg)) => {
let round = msg.commitment.block_number;
let set_id = msg.commitment.validator_set_id;
let expired = filter.consider_vote(round, set_id) != Consider::Accept;
trace!(target: LOG_TARGET, "🥩 Vote for round #{} expired: {}", round, expired);
expired
},
Ok(GossipMessage::FinalityProof(proof)) => {
let (round, set_id) = proof_block_num_and_set_id::<B, AuthorityId>(&proof);
let expired = filter.consider_finality_proof(round, set_id) != Consider::Accept;
trace!(
target: LOG_TARGET,
"🥩 Finality proof for round #{} expired: {}",
round,
expired
);
expired
},
Err(_) => true,
}
})
}
fn message_allowed<'a>(
&'a self,
) -> Box<dyn FnMut(&PeerId, MessageIntent, &B::Hash, &[u8]) -> bool + 'a> {
let do_rebroadcast = {
let now = Instant::now();
let mut next_rebroadcast = self.next_rebroadcast.lock();
if now >= *next_rebroadcast {
trace!(target: LOG_TARGET, "🥩 Gossip rebroadcast");
*next_rebroadcast = now + REBROADCAST_AFTER;
true
} else {
false
}
};
let filter = self.gossip_filter.read();
Box::new(move |_who, intent, _topic, mut data| {
if let MessageIntent::PeriodicRebroadcast = intent {
return do_rebroadcast;
}
match GossipMessage::<B, AuthorityId>::decode_all(&mut data) {
Ok(GossipMessage::Vote(msg)) => {
let round = msg.commitment.block_number;
let set_id = msg.commitment.validator_set_id;
let allowed = filter.consider_vote(round, set_id) == Consider::Accept;
trace!(target: LOG_TARGET, "🥩 Vote for round #{} allowed: {}", round, allowed);
allowed
},
Ok(GossipMessage::FinalityProof(proof)) => {
let (round, set_id) = proof_block_num_and_set_id::<B, AuthorityId>(&proof);
let allowed = filter.consider_finality_proof(round, set_id) == Consider::Accept;
trace!(
target: LOG_TARGET,
"🥩 Finality proof for round #{} allowed: {}",
round,
allowed
);
allowed
},
Err(_) => false,
}
})
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::{communication::peers::PeerReport, keystore::BeefyKeystore};
use pezsc_network_test::Block;
use pezsp_application_crypto::key_types::BEEFY as BEEFY_KEY_TYPE;
use pezsp_consensus_beefy::{
ecdsa_crypto, known_payloads, test_utils::Keyring, Commitment, MmrRootHash, Payload,
SignedCommitment, VoteMessage,
};
use pezsp_keystore::{testing::MemoryKeystore, Keystore};
pub(crate) struct TestNetwork {
report_sender: futures::channel::mpsc::UnboundedSender<PeerReport>,
}
impl TestNetwork {
pub fn new() -> (Self, futures::channel::mpsc::UnboundedReceiver<PeerReport>) {
let (tx, rx) = futures::channel::mpsc::unbounded();
(Self { report_sender: tx }, rx)
}
}
#[async_trait::async_trait]
impl NetworkPeers for TestNetwork {
fn set_authorized_peers(&self, _: std::collections::HashSet<PeerId>) {
unimplemented!()
}
fn set_authorized_only(&self, _: bool) {
unimplemented!()
}
fn add_known_address(&self, _: PeerId, _: pezsc_network::Multiaddr) {
unimplemented!()
}
fn report_peer(&self, peer_id: PeerId, cost_benefit: ReputationChange) {
let _ = self.report_sender.unbounded_send(PeerReport { who: peer_id, cost_benefit });
}
fn peer_reputation(&self, _: &PeerId) -> i32 {
unimplemented!()
}
fn disconnect_peer(&self, _: PeerId, _: pezsc_network::ProtocolName) {
unimplemented!()
}
fn accept_unreserved_peers(&self) {
unimplemented!()
}
fn deny_unreserved_peers(&self) {
unimplemented!()
}
fn add_reserved_peer(
&self,
_: pezsc_network::config::MultiaddrWithPeerId,
) -> Result<(), String> {
unimplemented!()
}
fn remove_reserved_peer(&self, _: PeerId) {
unimplemented!()
}
fn set_reserved_peers(
&self,
_: pezsc_network::ProtocolName,
_: std::collections::HashSet<pezsc_network::Multiaddr>,
) -> Result<(), String> {
unimplemented!()
}
fn add_peers_to_reserved_set(
&self,
_: pezsc_network::ProtocolName,
_: std::collections::HashSet<pezsc_network::Multiaddr>,
) -> Result<(), String> {
unimplemented!()
}
fn remove_peers_from_reserved_set(
&self,
_: pezsc_network::ProtocolName,
_: Vec<PeerId>,
) -> Result<(), String> {
unimplemented!()
}
fn sync_num_connected(&self) -> usize {
unimplemented!()
}
fn peer_role(&self, _: PeerId, _: Vec<u8>) -> Option<pezsc_network::ObservedRole> {
unimplemented!()
}
async fn reserved_peers(&self) -> Result<Vec<PeerId>, ()> {
unimplemented!();
}
}
struct TestContext;
impl<B: pezsp_runtime::traits::Block> ValidatorContext<B> for TestContext {
fn broadcast_topic(&mut self, _topic: B::Hash, _force: bool) {
unimplemented!()
}
fn broadcast_message(&mut self, _topic: B::Hash, _message: Vec<u8>, _force: bool) {}
fn send_message(&mut self, _who: &pezsc_network_types::PeerId, _message: Vec<u8>) {
unimplemented!()
}
fn send_topic(&mut self, _who: &pezsc_network_types::PeerId, _topic: B::Hash, _force: bool) {
unimplemented!()
}
}
pub fn sign_commitment<BN: Encode>(
who: &Keyring<ecdsa_crypto::AuthorityId>,
commitment: &Commitment<BN>,
) -> ecdsa_crypto::Signature {
let store = MemoryKeystore::new();
store.ecdsa_generate_new(BEEFY_KEY_TYPE, Some(&who.to_seed())).unwrap();
let beefy_keystore: BeefyKeystore<ecdsa_crypto::AuthorityId> = Some(store.into()).into();
beefy_keystore.sign(&who.public(), &commitment.encode()).unwrap()
}
fn dummy_vote(
block_number: u64,
) -> VoteMessage<u64, ecdsa_crypto::AuthorityId, ecdsa_crypto::Signature> {
let payload = Payload::from_single_entry(
known_payloads::MMR_ROOT_ID,
MmrRootHash::default().encode(),
);
let commitment = Commitment { payload, block_number, validator_set_id: 0 };
let signature = sign_commitment(&Keyring::Alice, &commitment);
VoteMessage { commitment, id: Keyring::Alice.public(), signature }
}
pub fn dummy_proof(
block_number: u64,
validator_set: &ValidatorSet<ecdsa_crypto::AuthorityId>,
) -> BeefyVersionedFinalityProof<Block, ecdsa_crypto::AuthorityId> {
let payload = Payload::from_single_entry(
known_payloads::MMR_ROOT_ID,
MmrRootHash::default().encode(),
);
let commitment = Commitment { payload, block_number, validator_set_id: validator_set.id() };
let signatures = validator_set
.validators()
.iter()
.map(|validator: &ecdsa_crypto::AuthorityId| {
Some(sign_commitment(
&Keyring::<ecdsa_crypto::AuthorityId>::from_public(validator).unwrap(),
&commitment,
))
})
.collect();
BeefyVersionedFinalityProof::<Block, ecdsa_crypto::AuthorityId>::V1(SignedCommitment {
commitment,
signatures,
})
}
#[test]
fn should_validate_messages() {
let keys = vec![Keyring::<ecdsa_crypto::AuthorityId>::Alice.public()];
let validator_set =
ValidatorSet::<ecdsa_crypto::AuthorityId>::new(keys.clone(), 0).unwrap();
let (network, mut report_stream) = TestNetwork::new();
let gv = GossipValidator::<Block, _, ecdsa_crypto::AuthorityId>::new(
Arc::new(Mutex::new(KnownPeers::new())),
Arc::new(network),
);
let sender = PeerId::random();
let mut context = TestContext;
// reject message, decoding error
let bad_encoding = b"0000000000".as_slice();
let expected_cost = ReputationChange::new(
(bad_encoding.len() as i32).saturating_mul(cost::PER_UNDECODABLE_BYTE),
"BEEFY: Bad packet",
);
let mut expected_report = PeerReport { who: sender, cost_benefit: expected_cost };
let res = gv.validate(&mut context, &sender, bad_encoding);
assert!(matches!(res, ValidationResult::Discard));
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
// verify votes validation
let vote = dummy_vote(3);
let encoded =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::Vote(vote.clone()).encode();
// filter not initialized
let res = gv.validate(&mut context, &sender, &encoded);
assert!(matches!(res, ValidationResult::Discard));
// nothing reported
assert!(report_stream.try_next().is_err());
gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set });
// nothing in cache first time
let res = gv.validate(&mut context, &sender, &encoded);
assert!(matches!(res, ValidationResult::ProcessAndKeep(_)));
expected_report.cost_benefit = benefit::VOTE_MESSAGE;
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
// reject vote, voter not in validator set
let mut bad_vote = vote.clone();
bad_vote.id = Keyring::Bob.public();
let bad_vote = GossipMessage::<Block, ecdsa_crypto::AuthorityId>::Vote(bad_vote).encode();
let res = gv.validate(&mut context, &sender, &bad_vote);
assert!(matches!(res, ValidationResult::Discard));
expected_report.cost_benefit = cost::UNKNOWN_VOTER;
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
// reject if the round is not GRANDPA finalized
gv.update_filter(GossipFilterCfg { start: 1, end: 2, validator_set: &validator_set });
let number = vote.commitment.block_number;
let set_id = vote.commitment.validator_set_id;
assert_eq!(gv.gossip_filter.read().consider_vote(number, set_id), Consider::RejectFuture);
let res = gv.validate(&mut context, &sender, &encoded);
assert!(matches!(res, ValidationResult::Discard));
expected_report.cost_benefit = cost::FUTURE_MESSAGE;
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
// reject if the round is not live anymore
gv.update_filter(GossipFilterCfg { start: 7, end: 10, validator_set: &validator_set });
let number = vote.commitment.block_number;
let set_id = vote.commitment.validator_set_id;
assert_eq!(gv.gossip_filter.read().consider_vote(number, set_id), Consider::RejectPast);
let res = gv.validate(&mut context, &sender, &encoded);
assert!(matches!(res, ValidationResult::Discard));
expected_report.cost_benefit = cost::OUTDATED_MESSAGE;
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
// now verify proofs validation
// reject old proof
let proof = dummy_proof(5, &validator_set);
let encoded_proof =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
let res = gv.validate(&mut context, &sender, &encoded_proof);
assert!(matches!(res, ValidationResult::Discard));
expected_report.cost_benefit = cost::OUTDATED_MESSAGE;
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
// accept next proof with good set_id
let proof = dummy_proof(7, &validator_set);
let encoded_proof =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
let res = gv.validate(&mut context, &sender, &encoded_proof);
assert!(matches!(res, ValidationResult::ProcessAndKeep(_)));
expected_report.cost_benefit = benefit::VALIDATED_PROOF;
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
// accept future proof with good set_id
let proof = dummy_proof(20, &validator_set);
let encoded_proof =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
let res = gv.validate(&mut context, &sender, &encoded_proof);
assert!(matches!(res, ValidationResult::ProcessAndKeep(_)));
expected_report.cost_benefit = benefit::VALIDATED_PROOF;
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
// reject proof, future set_id
let bad_validator_set = ValidatorSet::<ecdsa_crypto::AuthorityId>::new(keys, 1).unwrap();
let proof = dummy_proof(20, &bad_validator_set);
let encoded_proof =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
let res = gv.validate(&mut context, &sender, &encoded_proof);
assert!(matches!(res, ValidationResult::Discard));
expected_report.cost_benefit = cost::FUTURE_MESSAGE;
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
// reject proof, bad signatures (Bob instead of Alice)
let bad_validator_set =
ValidatorSet::<ecdsa_crypto::AuthorityId>::new(vec![Keyring::Bob.public()], 0).unwrap();
let proof = dummy_proof(21, &bad_validator_set);
let encoded_proof =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
let res = gv.validate(&mut context, &sender, &encoded_proof);
assert!(matches!(res, ValidationResult::Discard));
expected_report.cost_benefit = cost::INVALID_PROOF;
expected_report.cost_benefit.value += cost::PER_SIGNATURE_CHECKED;
assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report);
}
#[test]
fn messages_allowed_and_expired() {
let keys = vec![Keyring::Alice.public()];
let validator_set =
ValidatorSet::<ecdsa_crypto::AuthorityId>::new(keys.clone(), 0).unwrap();
let gv = GossipValidator::<Block, _, ecdsa_crypto::AuthorityId>::new(
Arc::new(Mutex::new(KnownPeers::new())),
Arc::new(TestNetwork::new().0),
);
gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set });
let sender = pezsc_network_types::PeerId::random();
let topic = Default::default();
let intent = MessageIntent::Broadcast;
// conclude 2
gv.update_filter(GossipFilterCfg { start: 2, end: 10, validator_set: &validator_set });
let mut allowed = gv.message_allowed();
let mut expired = gv.message_expired();
// check bad vote format
assert!(!allowed(&sender, intent, &topic, &mut [0u8; 16]));
assert!(expired(topic, &mut [0u8; 16]));
// inactive round 1 -> expired
let vote = dummy_vote(1);
let mut encoded_vote =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::Vote(vote).encode();
assert!(!allowed(&sender, intent, &topic, &mut encoded_vote));
assert!(expired(topic, &mut encoded_vote));
let proof = dummy_proof(1, &validator_set);
let mut encoded_proof =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
assert!(!allowed(&sender, intent, &topic, &mut encoded_proof));
assert!(expired(topic, &mut encoded_proof));
// active round 2 -> !expired - concluded but still gossiped
let vote = dummy_vote(2);
let mut encoded_vote =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::Vote(vote).encode();
assert!(allowed(&sender, intent, &topic, &mut encoded_vote));
assert!(!expired(topic, &mut encoded_vote));
let proof = dummy_proof(2, &validator_set);
let mut encoded_proof =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
assert!(allowed(&sender, intent, &topic, &mut encoded_proof));
assert!(!expired(topic, &mut encoded_proof));
// using wrong set_id -> !allowed, expired
let bad_validator_set =
ValidatorSet::<ecdsa_crypto::AuthorityId>::new(keys.clone(), 1).unwrap();
let proof = dummy_proof(2, &bad_validator_set);
let mut encoded_proof =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
assert!(!allowed(&sender, intent, &topic, &mut encoded_proof));
assert!(expired(topic, &mut encoded_proof));
// in progress round 3 -> !expired
let vote = dummy_vote(3);
let mut encoded_vote =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::Vote(vote).encode();
assert!(allowed(&sender, intent, &topic, &mut encoded_vote));
assert!(!expired(topic, &mut encoded_vote));
let proof = dummy_proof(3, &validator_set);
let mut encoded_proof =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
assert!(allowed(&sender, intent, &topic, &mut encoded_proof));
assert!(!expired(topic, &mut encoded_proof));
// unseen round 4 -> !expired
let vote = dummy_vote(4);
let mut encoded_vote =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::Vote(vote).encode();
assert!(allowed(&sender, intent, &topic, &mut encoded_vote));
assert!(!expired(topic, &mut encoded_vote));
let proof = dummy_proof(4, &validator_set);
let mut encoded_proof =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
assert!(allowed(&sender, intent, &topic, &mut encoded_proof));
assert!(!expired(topic, &mut encoded_proof));
// future round 11 -> expired
let vote = dummy_vote(11);
let mut encoded_vote =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::Vote(vote).encode();
assert!(!allowed(&sender, intent, &topic, &mut encoded_vote));
assert!(expired(topic, &mut encoded_vote));
// future proofs allowed while same set_id -> allowed
let proof = dummy_proof(11, &validator_set);
let mut encoded_proof =
GossipMessage::<Block, ecdsa_crypto::AuthorityId>::FinalityProof(proof).encode();
assert!(allowed(&sender, intent, &topic, &mut encoded_proof));
assert!(!expired(topic, &mut encoded_proof));
}
#[test]
fn messages_rebroadcast() {
let keys = vec![Keyring::Alice.public()];
let validator_set =
ValidatorSet::<ecdsa_crypto::AuthorityId>::new(keys.clone(), 0).unwrap();
let gv = GossipValidator::<Block, _, ecdsa_crypto::AuthorityId>::new(
Arc::new(Mutex::new(KnownPeers::new())),
Arc::new(TestNetwork::new().0),
);
gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set });
let sender = pezsc_network_types::PeerId::random();
let topic = Default::default();
let vote = dummy_vote(1);
let mut encoded_vote = vote.encode();
// re-broadcasting only allowed at `REBROADCAST_AFTER` intervals
let intent = MessageIntent::PeriodicRebroadcast;
let mut allowed = gv.message_allowed();
// rebroadcast not allowed so soon after GossipValidator creation
assert!(!allowed(&sender, intent, &topic, &mut encoded_vote));
// hack the inner deadline to be `now`
*gv.next_rebroadcast.lock() = Instant::now();
// still not allowed on old `allowed` closure result
assert!(!allowed(&sender, intent, &topic, &mut encoded_vote));
// renew closure result
let mut allowed = gv.message_allowed();
// rebroadcast should be allowed now
assert!(allowed(&sender, intent, &topic, &mut encoded_vote));
}
}
@@ -0,0 +1,160 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Communication streams for the BEEFY networking protocols.
pub mod notification;
pub mod request_response;
pub(crate) mod gossip;
pub(crate) mod peers;
pub(crate) mod beefy_protocol_name {
use array_bytes::bytes2hex;
use pezsc_network::ProtocolName;
/// BEEFY votes gossip protocol name suffix.
const GOSSIP_NAME: &str = "/beefy/2";
/// BEEFY justifications protocol name suffix.
const JUSTIFICATIONS_NAME: &str = "/beefy/justifications/1";
/// Name of the votes gossip protocol used by BEEFY.
///
/// Must be registered towards the networking in order for BEEFY voter to properly function.
pub fn gossip_protocol_name<Hash: AsRef<[u8]>>(
genesis_hash: Hash,
fork_id: Option<&str>,
) -> ProtocolName {
let genesis_hash = genesis_hash.as_ref();
if let Some(fork_id) = fork_id {
format!("/{}/{}{}", bytes2hex("", genesis_hash), fork_id, GOSSIP_NAME).into()
} else {
format!("/{}{}", bytes2hex("", genesis_hash), GOSSIP_NAME).into()
}
}
/// Name of the BEEFY justifications request-response protocol.
pub fn justifications_protocol_name<Hash: AsRef<[u8]>>(
genesis_hash: Hash,
fork_id: Option<&str>,
) -> ProtocolName {
let genesis_hash = genesis_hash.as_ref();
if let Some(fork_id) = fork_id {
format!("/{}/{}{}", bytes2hex("", genesis_hash), fork_id, JUSTIFICATIONS_NAME).into()
} else {
format!("/{}{}", bytes2hex("", genesis_hash), JUSTIFICATIONS_NAME).into()
}
}
}
/// Returns the configuration value to put in
/// [`pezsc_network::config::FullNetworkConfiguration`].
/// For standard protocol name see [`beefy_protocol_name::gossip_protocol_name`].
pub fn beefy_peers_set_config<
B: pezsp_runtime::traits::Block,
N: pezsc_network::NetworkBackend<B, <B as pezsp_runtime::traits::Block>::Hash>,
>(
gossip_protocol_name: pezsc_network::ProtocolName,
metrics: pezsc_network::service::NotificationMetrics,
peer_store_handle: std::sync::Arc<dyn pezsc_network::peer_store::PeerStoreProvider>,
) -> (N::NotificationProtocolConfig, Box<dyn pezsc_network::NotificationService>) {
let (cfg, notification_service) = N::notification_config(
gossip_protocol_name,
Vec::new(),
1024 * 1024,
None,
pezsc_network::config::SetConfig {
in_peers: 25,
out_peers: 25,
reserved_nodes: Vec::new(),
non_reserved_mode: pezsc_network::config::NonReservedPeerMode::Accept,
},
metrics,
peer_store_handle,
);
(cfg, notification_service)
}
// cost scalars for reporting peers.
mod cost {
use pezsc_network::ReputationChange as Rep;
// Message that's for an outdated round.
pub(super) const OUTDATED_MESSAGE: Rep = Rep::new(-50, "BEEFY: Past message");
// Message that's from the future relative to our current set-id.
pub(super) const FUTURE_MESSAGE: Rep = Rep::new(-100, "BEEFY: Future message");
// Vote message containing bad signature.
pub(super) const BAD_SIGNATURE: Rep = Rep::new(-100, "BEEFY: Bad signature");
// Message received with vote from voter not in validator set.
pub(super) const UNKNOWN_VOTER: Rep = Rep::new(-150, "BEEFY: Unknown voter");
// Message containing invalid proof.
pub(super) const INVALID_PROOF: Rep = Rep::new(-5000, "BEEFY: Invalid commit");
// Reputation cost per signature checked for invalid proof.
pub(super) const PER_SIGNATURE_CHECKED: i32 = -25;
// Reputation cost per byte for un-decodable message.
pub(super) const PER_UNDECODABLE_BYTE: i32 = -5;
// On-demand request was refused by peer.
pub(super) const REFUSAL_RESPONSE: Rep = Rep::new(-100, "BEEFY: Proof request refused");
// On-demand request for a proof that can't be found in the backend.
pub(super) const UNKNOWN_PROOF_REQUEST: Rep = Rep::new(-150, "BEEFY: Unknown proof request");
}
// benefit scalars for reporting peers.
mod benefit {
use pezsc_network::ReputationChange as Rep;
pub(super) const VOTE_MESSAGE: Rep = Rep::new(100, "BEEFY: Round vote message");
pub(super) const NOT_INTERESTED: Rep = Rep::new(10, "BEEFY: Not interested in round");
pub(super) const VALIDATED_PROOF: Rep = Rep::new(100, "BEEFY: Justification");
}
#[cfg(test)]
mod tests {
use super::*;
use pezsp_core::H256;
#[test]
fn beefy_protocols_names() {
use beefy_protocol_name::{gossip_protocol_name, justifications_protocol_name};
// Create protocol name using random genesis hash.
let genesis_hash = H256::random();
let genesis_hex = array_bytes::bytes2hex("", genesis_hash);
let expected_gossip_name = format!("/{}/beefy/2", genesis_hex);
let gossip_proto_name = gossip_protocol_name(&genesis_hash, None);
assert_eq!(gossip_proto_name.to_string(), expected_gossip_name);
let expected_justif_name = format!("/{}/beefy/justifications/1", genesis_hex);
let justif_proto_name = justifications_protocol_name(&genesis_hash, None);
assert_eq!(justif_proto_name.to_string(), expected_justif_name);
// Create protocol name using hardcoded genesis hash. Verify exact representation.
let genesis_hash = [
50, 4, 60, 123, 58, 106, 216, 246, 194, 188, 139, 193, 33, 212, 202, 171, 9, 55, 123,
94, 8, 43, 12, 251, 187, 57, 173, 19, 188, 74, 205, 147,
];
let genesis_hex = "32043c7b3a6ad8f6c2bc8bc121d4caab09377b5e082b0cfbbb39ad13bc4acd93";
let expected_gossip_name = format!("/{}/beefy/2", genesis_hex);
let gossip_proto_name = gossip_protocol_name(&genesis_hash, None);
assert_eq!(gossip_proto_name.to_string(), expected_gossip_name);
let expected_justif_name = format!("/{}/beefy/justifications/1", genesis_hex);
let justif_proto_name = justifications_protocol_name(&genesis_hash, None);
assert_eq!(justif_proto_name.to_string(), expected_justif_name);
}
}
@@ -0,0 +1,57 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use pezsc_utils::notification::{NotificationSender, NotificationStream, TracingKeyStr};
use pezsp_runtime::traits::Block as BlockT;
use crate::justification::BeefyVersionedFinalityProof;
/// The sending half of the notifications channel(s) used to send
/// notifications about best BEEFY block from the gadget side.
pub type BeefyBestBlockSender<Block> = NotificationSender<<Block as BlockT>::Hash>;
/// The receiving half of a notifications channel used to receive
/// notifications about best BEEFY blocks determined on the gadget side.
pub type BeefyBestBlockStream<Block> =
NotificationStream<<Block as BlockT>::Hash, BeefyBestBlockTracingKey>;
/// The sending half of the notifications channel(s) used to send notifications
/// about versioned finality proof generated at the end of a BEEFY round.
pub type BeefyVersionedFinalityProofSender<Block, AuthorityId> =
NotificationSender<BeefyVersionedFinalityProof<Block, AuthorityId>>;
/// The receiving half of a notifications channel used to receive notifications
/// about versioned finality proof generated at the end of a BEEFY round.
pub type BeefyVersionedFinalityProofStream<Block, AuthorityId> = NotificationStream<
BeefyVersionedFinalityProof<Block, AuthorityId>,
BeefyVersionedFinalityProofTracingKey,
>;
/// Provides tracing key for BEEFY best block stream.
#[derive(Clone)]
pub struct BeefyBestBlockTracingKey;
impl TracingKeyStr for BeefyBestBlockTracingKey {
const TRACING_KEY: &'static str = "mpsc_beefy_best_block_notification_stream";
}
/// Provides tracing key for BEEFY versioned finality proof stream.
#[derive(Clone)]
pub struct BeefyVersionedFinalityProofTracingKey;
impl TracingKeyStr for BeefyVersionedFinalityProofTracingKey {
const TRACING_KEY: &'static str = "mpsc_beefy_versioned_finality_proof_notification_stream";
}
@@ -0,0 +1,133 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Logic for keeping track of BEEFY peers.
use pezsc_network::ReputationChange;
use pezsc_network_types::PeerId;
use pezsp_runtime::traits::{Block, NumberFor, Zero};
use std::collections::{HashMap, VecDeque};
/// Report specifying a reputation change for a given peer.
#[derive(Debug, PartialEq)]
pub struct PeerReport {
pub who: PeerId,
pub cost_benefit: ReputationChange,
}
struct PeerData<B: Block> {
last_voted_on: NumberFor<B>,
}
impl<B: Block> Default for PeerData<B> {
fn default() -> Self {
PeerData { last_voted_on: Zero::zero() }
}
}
/// Keep a simple map of connected peers
/// and the most recent voting round they participated in.
pub struct KnownPeers<B: Block> {
live: HashMap<PeerId, PeerData<B>>,
}
impl<B: Block> KnownPeers<B> {
pub fn new() -> Self {
Self { live: HashMap::new() }
}
/// Note vote round number for `peer`.
pub fn note_vote_for(&mut self, peer: PeerId, round: NumberFor<B>) {
let data = self.live.entry(peer).or_default();
data.last_voted_on = round.max(data.last_voted_on);
}
/// Remove connected `peer`.
pub fn remove(&mut self, peer: &PeerId) {
self.live.remove(peer);
}
/// Return _filtered and cloned_ list of peers that have voted on higher than `block`.
pub fn further_than(&self, block: NumberFor<B>) -> VecDeque<PeerId> {
self.live
.iter()
.filter_map(|(k, v)| (v.last_voted_on > block).then_some(k))
.cloned()
.collect()
}
/// Answer whether `peer` is part of `KnownPeers` set.
pub fn contains(&self, peer: &PeerId) -> bool {
self.live.contains_key(peer)
}
/// Number of peers in the set.
pub fn len(&self) -> usize {
self.live.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_track_known_peers_progress() {
let (alice, bob, charlie) = (PeerId::random(), PeerId::random(), PeerId::random());
let mut peers = KnownPeers::<pezsc_network_test::Block>::new();
assert!(peers.live.is_empty());
// 'Tracked' Bob seen voting for 5.
peers.note_vote_for(bob, 5);
// Previously unseen Charlie now seen voting for 10.
peers.note_vote_for(charlie, 10);
assert_eq!(peers.live.len(), 2);
assert!(!peers.contains(&alice));
assert!(peers.contains(&bob));
assert!(peers.contains(&charlie));
// Get peers at block > 4
let further_than_4 = peers.further_than(4);
// Should be Bob and Charlie
assert_eq!(further_than_4.len(), 2);
assert!(further_than_4.contains(&bob));
assert!(further_than_4.contains(&charlie));
// 'Tracked' Alice seen voting for 10.
peers.note_vote_for(alice, 10);
// Get peers at block > 9
let further_than_9 = peers.further_than(9);
// Should be Charlie and Alice
assert_eq!(further_than_9.len(), 2);
assert!(further_than_9.contains(&charlie));
assert!(further_than_9.contains(&alice));
// Remove Alice
peers.remove(&alice);
assert_eq!(peers.live.len(), 2);
assert!(!peers.contains(&alice));
// Get peers at block >= 9
let further_than_9 = peers.further_than(9);
// Now should be just Charlie
assert_eq!(further_than_9.len(), 1);
assert!(further_than_9.contains(&charlie));
}
}
@@ -0,0 +1,224 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Bizinikiwi.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Bizinikiwi is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Bizinikiwi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Bizinikiwi. If not, see <https://www.gnu.org/licenses/>.
//! Helper for handling (i.e. answering) BEEFY justifications requests from a remote peer.
use codec::DecodeAll;
use futures::{channel::oneshot, StreamExt};
use log::{debug, trace};
use pezsc_client_api::BlockBackend;
use pezsc_network::{
config as netconfig, service::traits::RequestResponseConfig, types::ProtocolName,
NetworkBackend, ReputationChange,
};
use pezsc_network_types::PeerId;
use pezsp_consensus_beefy::BEEFY_ENGINE_ID;
use pezsp_runtime::traits::Block;
use std::{marker::PhantomData, sync::Arc};
use crate::{
communication::{
cost,
request_response::{
on_demand_justifications_protocol_config, Error, JustificationRequest,
BEEFY_SYNC_LOG_TARGET,
},
},
metric_inc,
metrics::{register_metrics, OnDemandIncomingRequestsMetrics},
};
/// A request coming in, including a sender for sending responses.
#[derive(Debug)]
pub(crate) struct IncomingRequest<B: Block> {
/// `PeerId` of sending peer.
pub peer: PeerId,
/// The sent request.
pub payload: JustificationRequest<B>,
/// Sender for sending response back.
pub pending_response: oneshot::Sender<netconfig::OutgoingResponse>,
}
impl<B: Block> IncomingRequest<B> {
/// Create new `IncomingRequest`.
pub fn new(
peer: PeerId,
payload: JustificationRequest<B>,
pending_response: oneshot::Sender<netconfig::OutgoingResponse>,
) -> Self {
Self { peer, payload, pending_response }
}
/// Try building from raw network request.
///
/// This function will fail if the request cannot be decoded and will apply passed in
/// reputation changes in that case.
///
/// Params:
/// - The raw request to decode
/// - Reputation changes to apply for the peer in case decoding fails.
pub fn try_from_raw<F>(
raw: netconfig::IncomingRequest,
reputation_changes_on_err: F,
) -> Result<Self, Error>
where
F: FnOnce(usize) -> Vec<ReputationChange>,
{
let netconfig::IncomingRequest { payload, peer, pending_response } = raw;
let payload = match JustificationRequest::decode_all(&mut payload.as_ref()) {
Ok(payload) => payload,
Err(err) => {
let response = netconfig::OutgoingResponse {
result: Err(()),
reputation_changes: reputation_changes_on_err(payload.len()),
sent_feedback: None,
};
if let Err(_) = pending_response.send(response) {
return Err(Error::DecodingErrorNoReputationChange(peer, err));
}
return Err(Error::DecodingError(peer, err));
},
};
Ok(Self::new(peer, payload, pending_response))
}
}
/// Receiver for incoming BEEFY justifications requests.
///
/// Takes care of decoding and handling of invalid encoded requests.
pub(crate) struct IncomingRequestReceiver {
raw: async_channel::Receiver<netconfig::IncomingRequest>,
}
impl IncomingRequestReceiver {
pub fn new(inner: async_channel::Receiver<netconfig::IncomingRequest>) -> Self {
Self { raw: inner }
}
/// Try to receive the next incoming request.
///
/// Any received request will be decoded, on decoding errors the provided reputation changes
/// will be applied and an error will be reported.
pub async fn recv<B, F>(&mut self, reputation_changes: F) -> Result<IncomingRequest<B>, Error>
where
B: Block,
F: FnOnce(usize) -> Vec<ReputationChange>,
{
let req = match self.raw.next().await {
None => return Err(Error::RequestChannelExhausted),
Some(raw) => IncomingRequest::<B>::try_from_raw(raw, reputation_changes)?,
};
Ok(req)
}
}
/// Handler for incoming BEEFY justifications requests from a remote peer.
pub struct BeefyJustifsRequestHandler<B, Client> {
pub(crate) request_receiver: IncomingRequestReceiver,
pub(crate) justif_protocol_name: ProtocolName,
pub(crate) client: Arc<Client>,
pub(crate) metrics: Option<OnDemandIncomingRequestsMetrics>,
pub(crate) _block: PhantomData<B>,
}
impl<B, Client> BeefyJustifsRequestHandler<B, Client>
where
B: Block,
Client: BlockBackend<B> + Send + Sync,
{
/// Create a new [`BeefyJustifsRequestHandler`].
pub fn new<Hash: AsRef<[u8]>, Network: NetworkBackend<B, <B as Block>::Hash>>(
genesis_hash: Hash,
fork_id: Option<&str>,
client: Arc<Client>,
prometheus_registry: Option<prometheus_endpoint::Registry>,
) -> (Self, Network::RequestResponseProtocolConfig) {
let (request_receiver, config): (_, Network::RequestResponseProtocolConfig) =
on_demand_justifications_protocol_config::<_, _, Network>(genesis_hash, fork_id);
let justif_protocol_name = config.protocol_name().clone();
let metrics = register_metrics(prometheus_registry);
(
Self { request_receiver, justif_protocol_name, client, metrics, _block: PhantomData },
config,
)
}
/// Network request-response protocol name used by this handler.
pub fn protocol_name(&self) -> ProtocolName {
self.justif_protocol_name.clone()
}
// Sends back justification response if justification found in client backend.
fn handle_request(&self, request: IncomingRequest<B>) -> Result<(), Error> {
let mut reputation_changes = vec![];
let maybe_encoded_proof = self
.client
.block_hash(request.payload.begin)
.ok()
.flatten()
.and_then(|hash| self.client.justifications(hash).ok().flatten())
.and_then(|justifs| justifs.get(BEEFY_ENGINE_ID).cloned())
.ok_or_else(|| reputation_changes.push(cost::UNKNOWN_PROOF_REQUEST));
request
.pending_response
.send(netconfig::OutgoingResponse {
result: maybe_encoded_proof,
reputation_changes,
sent_feedback: None,
})
.map_err(|_| Error::SendResponse)
}
/// Run [`BeefyJustifsRequestHandler`].
///
/// Should never end, returns `Error` otherwise.
pub async fn run(&mut self) -> Error {
trace!(target: BEEFY_SYNC_LOG_TARGET, "🥩 Running BeefyJustifsRequestHandler");
while let Ok(request) = self
.request_receiver
.recv(|bytes| {
let bytes = bytes.min(i32::MAX as usize) as i32;
vec![ReputationChange::new(
bytes.saturating_mul(cost::PER_UNDECODABLE_BYTE),
"BEEFY: Bad request payload",
)]
})
.await
{
let peer = request.peer;
match self.handle_request(request) {
Ok(()) => {
metric_inc!(self.metrics, beefy_successful_justification_responses);
debug!(
target: BEEFY_SYNC_LOG_TARGET,
"🥩 Handled BEEFY justification request from {:?}.", peer
)
},
Err(e) => {
// peer reputation changes already applied in `self.handle_request()`
metric_inc!(self.metrics, beefy_failed_justification_responses);
debug!(
target: BEEFY_SYNC_LOG_TARGET,
"🥩 Failed to handle BEEFY justification request from {:?}: {}", peer, e,
)
},
}
}
Error::RequestsReceiverStreamClosed
}
}
@@ -0,0 +1,113 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Request/response protocol for syncing BEEFY justifications.
mod incoming_requests_handler;
pub(crate) mod outgoing_requests_engine;
pub use incoming_requests_handler::BeefyJustifsRequestHandler;
use std::time::Duration;
use codec::{Decode, Encode, Error as CodecError};
use pezsc_network::NetworkBackend;
use pezsc_network_types::PeerId;
use pezsp_runtime::traits::{Block, NumberFor};
use crate::communication::{beefy_protocol_name::justifications_protocol_name, peers::PeerReport};
use incoming_requests_handler::IncomingRequestReceiver;
// 10 seems reasonable, considering justifs are explicitly requested only
// for mandatory blocks, by nodes that are syncing/catching-up.
const JUSTIF_CHANNEL_SIZE: usize = 10;
const MAX_RESPONSE_SIZE: u64 = 1024 * 1024;
const JUSTIF_REQUEST_TIMEOUT: Duration = Duration::from_secs(3);
const BEEFY_SYNC_LOG_TARGET: &str = "beefy::sync";
/// Get the configuration for the BEEFY justifications Request/response protocol.
///
/// Returns a receiver for messages received on this protocol and the requested
/// `ProtocolConfig`.
///
/// Consider using [`BeefyJustifsRequestHandler`] instead of this low-level function.
pub(crate) fn on_demand_justifications_protocol_config<
Hash: AsRef<[u8]>,
B: Block,
Network: NetworkBackend<B, <B as Block>::Hash>,
>(
genesis_hash: Hash,
fork_id: Option<&str>,
) -> (IncomingRequestReceiver, Network::RequestResponseProtocolConfig) {
let name = justifications_protocol_name(genesis_hash, fork_id);
let fallback_names = vec![];
let (tx, rx) = async_channel::bounded(JUSTIF_CHANNEL_SIZE);
let rx = IncomingRequestReceiver::new(rx);
let cfg = Network::request_response_config(
name,
fallback_names,
32,
MAX_RESPONSE_SIZE,
// We are connected to all validators:
JUSTIF_REQUEST_TIMEOUT,
Some(tx),
);
(rx, cfg)
}
/// BEEFY justification request.
#[derive(Debug, Clone, Encode, Decode)]
pub struct JustificationRequest<B: Block> {
/// Start collecting proofs from this block.
pub begin: NumberFor<B>,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Client(#[from] pezsp_blockchain::Error),
#[error(transparent)]
RuntimeApi(#[from] pezsp_api::ApiError),
/// Decoding failed, we were able to change the peer's reputation accordingly.
#[error("Decoding request failed for peer {0}.")]
DecodingError(PeerId, #[source] CodecError),
/// Decoding failed, but sending reputation change failed.
#[error("Decoding request failed for peer {0}, and changing reputation failed.")]
DecodingErrorNoReputationChange(PeerId, #[source] CodecError),
/// Incoming request stream exhausted. Should only happen on shutdown.
#[error("Incoming request channel got closed.")]
RequestChannelExhausted,
#[error("Failed to send response.")]
SendResponse,
#[error("Received invalid response.")]
InvalidResponse(PeerReport),
#[error("Internal error while getting response.")]
ResponseError,
#[error("On-demand requests receiver stream terminated.")]
RequestsReceiverStreamClosed,
}
@@ -0,0 +1,284 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Generating request logic for request/response protocol for syncing BEEFY justifications.
use codec::Encode;
use futures::channel::{oneshot, oneshot::Canceled};
use log::{debug, warn};
use parking_lot::Mutex;
use pezsc_network::{
request_responses::{IfDisconnected, RequestFailure},
NetworkRequest, ProtocolName,
};
use pezsc_network_types::PeerId;
use pezsp_consensus_beefy::{AuthorityIdBound, ValidatorSet};
use pezsp_runtime::traits::{Block, NumberFor};
use std::{collections::VecDeque, result::Result, sync::Arc};
use crate::{
communication::{
benefit, cost,
peers::PeerReport,
request_response::{Error, JustificationRequest, BEEFY_SYNC_LOG_TARGET},
},
justification::{decode_and_verify_finality_proof, BeefyVersionedFinalityProof},
metric_inc, metric_set,
metrics::{register_metrics, OnDemandOutgoingRequestsMetrics},
KnownPeers,
};
/// Response type received from network.
type Response = Result<(Vec<u8>, ProtocolName), RequestFailure>;
/// Used to receive a response from the network.
type ResponseReceiver = oneshot::Receiver<Response>;
#[derive(Clone, Debug)]
struct RequestInfo<B: Block, AuthorityId: AuthorityIdBound> {
block: NumberFor<B>,
active_set: ValidatorSet<AuthorityId>,
}
enum State<B: Block, AuthorityId: AuthorityIdBound> {
Idle,
AwaitingResponse(PeerId, RequestInfo<B, AuthorityId>, ResponseReceiver),
}
/// Possible engine responses.
pub(crate) enum ResponseInfo<B: Block, AuthorityId: AuthorityIdBound> {
/// No peer response available yet.
Pending,
/// Valid justification provided alongside peer reputation changes.
ValidProof(BeefyVersionedFinalityProof<B, AuthorityId>, PeerReport),
/// No justification yet, only peer reputation changes.
PeerReport(PeerReport),
}
pub struct OnDemandJustificationsEngine<B: Block, AuthorityId: AuthorityIdBound> {
network: Arc<dyn NetworkRequest + Send + Sync>,
protocol_name: ProtocolName,
live_peers: Arc<Mutex<KnownPeers<B>>>,
peers_cache: VecDeque<PeerId>,
state: State<B, AuthorityId>,
metrics: Option<OnDemandOutgoingRequestsMetrics>,
}
impl<B: Block, AuthorityId: AuthorityIdBound> OnDemandJustificationsEngine<B, AuthorityId> {
pub fn new(
network: Arc<dyn NetworkRequest + Send + Sync>,
protocol_name: ProtocolName,
live_peers: Arc<Mutex<KnownPeers<B>>>,
prometheus_registry: Option<prometheus_endpoint::Registry>,
) -> Self {
let metrics = register_metrics(prometheus_registry);
Self {
network,
protocol_name,
live_peers,
peers_cache: VecDeque::new(),
state: State::Idle,
metrics,
}
}
fn reset_peers_cache_for_block(&mut self, block: NumberFor<B>) {
self.peers_cache = self.live_peers.lock().further_than(block);
}
fn try_next_peer(&mut self) -> Option<PeerId> {
let live = self.live_peers.lock();
while let Some(peer) = self.peers_cache.pop_front() {
if live.contains(&peer) {
return Some(peer);
}
}
None
}
fn request_from_peer(&mut self, peer: PeerId, req_info: RequestInfo<B, AuthorityId>) {
debug!(
target: BEEFY_SYNC_LOG_TARGET,
"🥩 requesting justif #{:?} from peer {:?}", req_info.block, peer,
);
let payload = JustificationRequest::<B> { begin: req_info.block }.encode();
let (tx, rx) = oneshot::channel();
self.network.start_request(
peer,
self.protocol_name.clone(),
payload,
None,
tx,
IfDisconnected::ImmediateError,
);
self.state = State::AwaitingResponse(peer, req_info, rx);
}
/// Start new justification request for `block`, if no other request is in progress.
///
/// `active_set` will be used to verify validity of potential responses.
pub fn request(&mut self, block: NumberFor<B>, active_set: ValidatorSet<AuthorityId>) {
// ignore new requests while there's already one pending
if matches!(self.state, State::AwaitingResponse(_, _, _)) {
return;
}
self.reset_peers_cache_for_block(block);
// Start the requests engine - each unsuccessful received response will automatically
// trigger a new request to the next peer in the `peers_cache` until there are none left.
if let Some(peer) = self.try_next_peer() {
self.request_from_peer(peer, RequestInfo { block, active_set });
} else {
metric_inc!(self.metrics, beefy_on_demand_justification_no_peer_to_request_from);
debug!(
target: BEEFY_SYNC_LOG_TARGET,
"🥩 no good peers to request justif #{:?} from", block
);
}
}
/// Cancel any pending request for block numbers smaller or equal to `block`.
pub fn cancel_requests_older_than(&mut self, block: NumberFor<B>) {
match &self.state {
State::AwaitingResponse(_, req_info, _) if req_info.block <= block => {
debug!(
target: BEEFY_SYNC_LOG_TARGET,
"🥩 cancel pending request for justification #{:?}", req_info.block
);
self.state = State::Idle;
},
_ => (),
}
}
fn process_response(
&mut self,
peer: &PeerId,
req_info: &RequestInfo<B, AuthorityId>,
response: Result<Response, Canceled>,
) -> Result<BeefyVersionedFinalityProof<B, AuthorityId>, Error> {
response
.map_err(|e| {
debug!(
target: BEEFY_SYNC_LOG_TARGET,
"🥩 on-demand sc-network channel sender closed, err: {:?}", e
);
Error::ResponseError
})?
.map_err(|e| {
debug!(
target: BEEFY_SYNC_LOG_TARGET,
"🥩 for on demand justification #{:?}, peer {:?} error: {:?}",
req_info.block,
peer,
e
);
match e {
RequestFailure::Refused => {
metric_inc!(self.metrics, beefy_on_demand_justification_peer_refused);
let peer_report =
PeerReport { who: *peer, cost_benefit: cost::REFUSAL_RESPONSE };
Error::InvalidResponse(peer_report)
},
_ => {
metric_inc!(self.metrics, beefy_on_demand_justification_peer_error);
Error::ResponseError
},
}
})
.and_then(|(encoded, _)| {
decode_and_verify_finality_proof::<B, AuthorityId>(
&encoded[..],
req_info.block,
&req_info.active_set,
)
.map_err(|(err, signatures_checked)| {
metric_inc!(self.metrics, beefy_on_demand_justification_invalid_proof);
debug!(
target: BEEFY_SYNC_LOG_TARGET,
"🥩 for on demand justification #{:?}, peer {:?} responded with invalid proof: {:?}",
req_info.block, peer, err
);
let mut cost = cost::INVALID_PROOF;
cost.value +=
cost::PER_SIGNATURE_CHECKED.saturating_mul(signatures_checked as i32);
Error::InvalidResponse(PeerReport { who: *peer, cost_benefit: cost })
})
})
}
pub(crate) async fn next(&mut self) -> ResponseInfo<B, AuthorityId> {
let (peer, req_info, resp) = match &mut self.state {
State::Idle => {
futures::future::pending::<()>().await;
return ResponseInfo::Pending;
},
State::AwaitingResponse(peer, req_info, receiver) => {
let resp = receiver.await;
(*peer, req_info.clone(), resp)
},
};
// We received the awaited response. Our 'receiver' will never generate any other response,
// meaning we're done with current state. Move the engine to `State::Idle`.
self.state = State::Idle;
metric_set!(self.metrics, beefy_on_demand_live_peers, self.live_peers.lock().len() as u64);
let block = req_info.block;
match self.process_response(&peer, &req_info, resp) {
Err(err) => {
// No valid justification received, try next peer in our set.
if let Some(peer) = self.try_next_peer() {
self.request_from_peer(peer, req_info);
} else {
metric_inc!(
self.metrics,
beefy_on_demand_justification_no_peer_to_request_from
);
let num_cache = self.peers_cache.len();
let num_live = self.live_peers.lock().len();
warn!(
target: BEEFY_SYNC_LOG_TARGET,
"🥩 ran out of peers to request justif #{block:?} from num_cache={num_cache} num_live={num_live} err={err:?}",
);
}
// Report peer based on error type.
if let Error::InvalidResponse(peer_report) = err {
ResponseInfo::PeerReport(peer_report)
} else {
ResponseInfo::Pending
}
},
Ok(proof) => {
metric_inc!(self.metrics, beefy_on_demand_justification_good_proof);
debug!(
target: BEEFY_SYNC_LOG_TARGET,
"🥩 received valid on-demand justif #{block:?} from {peer:?}",
);
let peer_report = PeerReport { who: peer, cost_benefit: benefit::VALIDATED_PROOF };
ResponseInfo::ValidProof(proof, peer_report)
},
}
}
}
@@ -0,0 +1,71 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! BEEFY gadget specific errors
//!
//! Used for BEEFY gadget internal error handling only
use pezsp_blockchain::Error as ClientError;
use std::fmt::Debug;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Backend: {0}")]
Backend(String),
#[error("Keystore error: {0}")]
Keystore(String),
#[error("Runtime api error: {0}")]
RuntimeApi(pezsp_api::ApiError),
#[error("Signature error: {0}")]
Signature(String),
#[error("Session uninitialized")]
UninitSession,
#[error("pezpallet-beefy was reset")]
ConsensusReset,
#[error("Block import stream terminated")]
BlockImportStreamTerminated,
#[error("Gossip Engine terminated")]
GossipEngineTerminated,
#[error("Finality proofs gossiping stream terminated")]
FinalityProofGossipStreamTerminated,
#[error("Finality stream terminated")]
FinalityStreamTerminated,
#[error("Votes gossiping stream terminated")]
VotesGossipStreamTerminated,
}
impl From<ClientError> for Error {
fn from(e: ClientError) -> Self {
Self::Backend(e.to_string())
}
}
#[cfg(test)]
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Error::Backend(s1), Error::Backend(s2)) => s1 == s2,
(Error::Keystore(s1), Error::Keystore(s2)) => s1 == s2,
(Error::RuntimeApi(_), Error::RuntimeApi(_)) => true,
(Error::Signature(s1), Error::Signature(s2)) => s1 == s2,
(Error::UninitSession, Error::UninitSession) => true,
(Error::ConsensusReset, Error::ConsensusReset) => true,
_ => false,
}
}
}
@@ -0,0 +1,167 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::{error::Error, keystore::BeefyKeystore, round::Rounds, LOG_TARGET};
use log::{debug, error, warn};
use pezsc_client_api::Backend;
use pezsp_api::ProvideRuntimeApi;
use pezsp_application_crypto::RuntimeAppPublic;
use pezsp_blockchain::HeaderBackend;
use pezsp_consensus_beefy::{
check_double_voting_proof, AuthorityIdBound, BeefyApi, BeefySignatureHasher, DoubleVotingProof,
OpaqueKeyOwnershipProof, ValidatorSetId,
};
use pezsp_runtime::{
generic::BlockId,
traits::{Block, NumberFor},
};
use std::{marker::PhantomData, sync::Arc};
/// Helper struct containing the key ownership proof for a validator.
pub struct ProvedValidator {
pub key_owner_proof: OpaqueKeyOwnershipProof,
}
/// Helper used to check and report equivocations.
pub struct Fisherman<B, BE, RuntimeApi, AuthorityId: AuthorityIdBound> {
backend: Arc<BE>,
runtime: Arc<RuntimeApi>,
key_store: Arc<BeefyKeystore<AuthorityId>>,
_phantom: PhantomData<B>,
}
impl<B: Block, BE: Backend<B>, RuntimeApi: ProvideRuntimeApi<B>, AuthorityId>
Fisherman<B, BE, RuntimeApi, AuthorityId>
where
RuntimeApi::Api: BeefyApi<B, AuthorityId>,
AuthorityId: AuthorityIdBound,
{
pub fn new(
backend: Arc<BE>,
runtime: Arc<RuntimeApi>,
keystore: Arc<BeefyKeystore<AuthorityId>>,
) -> Self {
Self { backend, runtime, key_store: keystore, _phantom: Default::default() }
}
fn prove_offenders<'a>(
&self,
at: BlockId<B>,
offender_ids: impl Iterator<Item = &'a AuthorityId>,
validator_set_id: ValidatorSetId,
) -> Result<Vec<ProvedValidator>, Error> {
let hash = match at {
BlockId::Hash(hash) => hash,
BlockId::Number(number) => self
.backend
.blockchain()
.expect_block_hash_from_id(&BlockId::Number(number))
.map_err(|err| {
Error::Backend(format!(
"Couldn't get hash for block #{:?} (error: {:?}). \
Skipping report for equivocation",
at, err
))
})?,
};
let runtime_api = self.runtime.runtime_api();
let mut proved_offenders = vec![];
for offender_id in offender_ids {
match runtime_api.generate_key_ownership_proof(
hash,
validator_set_id,
offender_id.clone(),
) {
Ok(Some(key_owner_proof)) => {
proved_offenders.push(ProvedValidator { key_owner_proof });
},
Ok(None) => {
debug!(
target: LOG_TARGET,
"🥩 Equivocation offender {} not part of the authority set {}.",
offender_id, validator_set_id
);
},
Err(e) => {
error!(
target: LOG_TARGET,
"🥩 Error generating key ownership proof for equivocation offender {} \
in authority set {}: {}",
offender_id, validator_set_id, e
);
},
};
}
Ok(proved_offenders)
}
/// Report the given equivocation to the BEEFY runtime module. This method
/// generates a session membership proof of the offender and then submits an
/// extrinsic to report the equivocation. In particular, the session membership
/// proof must be generated at the block at which the given set was active which
/// isn't necessarily the best block if there are pending authority set changes.
pub fn report_double_voting(
&self,
proof: DoubleVotingProof<
NumberFor<B>,
AuthorityId,
<AuthorityId as RuntimeAppPublic>::Signature,
>,
active_rounds: &Rounds<B, AuthorityId>,
) -> Result<(), Error> {
let (validators, validator_set_id) =
(active_rounds.validators(), active_rounds.validator_set_id());
let offender_id = proof.offender_id();
if !check_double_voting_proof::<_, _, BeefySignatureHasher>(&proof) {
debug!(target: LOG_TARGET, "🥩 Skipping report for bad equivocation {:?}", proof);
return Ok(());
}
if let Some(local_id) = self.key_store.authority_id(validators) {
if offender_id == &local_id {
warn!(target: LOG_TARGET, "🥩 Skipping report for own equivocation");
return Ok(());
}
}
let key_owner_proofs = self.prove_offenders(
BlockId::Number(*proof.round_number()),
vec![offender_id].into_iter(),
validator_set_id,
)?;
// submit equivocation report at **best** block
let best_block_hash = self.backend.blockchain().info().best_hash;
for ProvedValidator { key_owner_proof, .. } in key_owner_proofs {
self.runtime
.runtime_api()
.submit_report_double_voting_unsigned_extrinsic(
best_block_hash,
proof.clone(),
key_owner_proof,
)
.map_err(Error::RuntimeApi)?;
}
Ok(())
}
}
@@ -0,0 +1,200 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::sync::Arc;
use log::debug;
use pezsp_api::ProvideRuntimeApi;
use pezsp_consensus::Error as ConsensusError;
use pezsp_consensus_beefy::{AuthorityIdBound, BeefyApi, BEEFY_ENGINE_ID};
use pezsp_runtime::{
traits::{Block as BlockT, Header as HeaderT, NumberFor},
EncodedJustification,
};
use pezsc_client_api::{backend::Backend, TrieCacheContext};
use pezsc_consensus::{BlockCheckParams, BlockImport, BlockImportParams, ImportResult};
use crate::{
communication::notification::BeefyVersionedFinalityProofSender,
justification::{decode_and_verify_finality_proof, BeefyVersionedFinalityProof},
metric_inc,
metrics::BlockImportMetrics,
LOG_TARGET,
};
/// A block-import handler for BEEFY.
///
/// This scans each imported block for BEEFY justifications and verifies them.
/// Wraps a `inner: BlockImport` and ultimately defers to it.
///
/// When using BEEFY, the block import worker should be using this block import object.
pub struct BeefyBlockImport<Block: BlockT, Backend, RuntimeApi, I, AuthorityId: AuthorityIdBound> {
backend: Arc<Backend>,
runtime: Arc<RuntimeApi>,
inner: I,
justification_sender: BeefyVersionedFinalityProofSender<Block, AuthorityId>,
metrics: Option<BlockImportMetrics>,
}
impl<Block: BlockT, BE, Runtime, I: Clone, AuthorityId: AuthorityIdBound> Clone
for BeefyBlockImport<Block, BE, Runtime, I, AuthorityId>
{
fn clone(&self) -> Self {
BeefyBlockImport {
backend: self.backend.clone(),
runtime: self.runtime.clone(),
inner: self.inner.clone(),
justification_sender: self.justification_sender.clone(),
metrics: self.metrics.clone(),
}
}
}
impl<Block: BlockT, BE, Runtime, I, AuthorityId: AuthorityIdBound>
BeefyBlockImport<Block, BE, Runtime, I, AuthorityId>
{
/// Create a new BeefyBlockImport.
pub fn new(
backend: Arc<BE>,
runtime: Arc<Runtime>,
inner: I,
justification_sender: BeefyVersionedFinalityProofSender<Block, AuthorityId>,
metrics: Option<BlockImportMetrics>,
) -> BeefyBlockImport<Block, BE, Runtime, I, AuthorityId> {
BeefyBlockImport { backend, runtime, inner, justification_sender, metrics }
}
}
impl<Block, BE, Runtime, I, AuthorityId> BeefyBlockImport<Block, BE, Runtime, I, AuthorityId>
where
Block: BlockT,
BE: Backend<Block>,
Runtime: ProvideRuntimeApi<Block>,
Runtime::Api: BeefyApi<Block, AuthorityId> + Send,
AuthorityId: AuthorityIdBound,
{
fn decode_and_verify(
&self,
encoded: &EncodedJustification,
number: NumberFor<Block>,
hash: <Block as BlockT>::Hash,
) -> Result<BeefyVersionedFinalityProof<Block, AuthorityId>, ConsensusError> {
use ConsensusError::ClientImport as ImportError;
let beefy_genesis = self
.runtime
.runtime_api()
.beefy_genesis(hash)
.map_err(|e| ImportError(e.to_string()))?
.ok_or_else(|| ImportError("Unknown BEEFY genesis".to_string()))?;
if number < beefy_genesis {
return Err(ImportError("BEEFY genesis is set for future block".to_string()));
}
let validator_set = self
.runtime
.runtime_api()
.validator_set(hash)
.map_err(|e| ImportError(e.to_string()))?
.ok_or_else(|| ImportError("Unknown validator set".to_string()))?;
decode_and_verify_finality_proof::<Block, AuthorityId>(&encoded[..], number, &validator_set)
.map_err(|(err, _)| err)
}
}
#[async_trait::async_trait]
impl<Block, BE, Runtime, I, AuthorityId> BlockImport<Block>
for BeefyBlockImport<Block, BE, Runtime, I, AuthorityId>
where
Block: BlockT,
BE: Backend<Block>,
I: BlockImport<Block, Error = ConsensusError> + Send + Sync,
Runtime: ProvideRuntimeApi<Block> + Send + Sync,
Runtime::Api: BeefyApi<Block, AuthorityId>,
AuthorityId: AuthorityIdBound,
{
type Error = ConsensusError;
async fn import_block(
&self,
mut block: BlockImportParams<Block>,
) -> Result<ImportResult, Self::Error> {
let hash = block.post_hash();
let number = *block.header.number();
let beefy_encoded = block.justifications.as_mut().and_then(|just| {
let encoded = just.get(BEEFY_ENGINE_ID).cloned();
// Remove BEEFY justification from the list before giving to `inner`; we send it to the
// voter (beefy-gadget) and it will append it to the backend after block is finalized.
just.remove(BEEFY_ENGINE_ID);
encoded
});
// Run inner block import.
let inner_import_result = self.inner.import_block(block).await?;
match self.backend.state_at(hash, TrieCacheContext::Untrusted) {
Ok(_) => {},
Err(_) => {
// The block is imported as part of some chain sync.
// The voter doesn't need to process it now.
// It will be detected and processed as part of the voter state init.
return Ok(inner_import_result);
},
}
match (beefy_encoded, &inner_import_result) {
(Some(encoded), ImportResult::Imported(_)) => {
match self.decode_and_verify(&encoded, number, hash) {
Ok(proof) => {
// The proof is valid and the block is imported and final, we can import.
debug!(
target: LOG_TARGET,
"🥩 import justif {} for block number {:?}.", proof, number
);
// Send the justification to the BEEFY voter for processing.
self.justification_sender
.notify(|| Ok::<_, ()>(proof))
.expect("the closure always returns Ok; qed.");
metric_inc!(self.metrics, beefy_good_justification_imports);
},
Err(err) => {
debug!(
target: LOG_TARGET,
"🥩 error importing BEEFY justification for block {:?}: {:?}",
number,
err,
);
metric_inc!(self.metrics, beefy_bad_justification_imports);
},
}
},
_ => (),
}
Ok(inner_import_result)
}
async fn check_block(
&self,
block: BlockCheckParams<Block>,
) -> Result<ImportResult, Self::Error> {
self.inner.check_block(block).await
}
}
@@ -0,0 +1,223 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use codec::DecodeAll;
use pezsp_application_crypto::RuntimeAppPublic;
use pezsp_consensus::Error as ConsensusError;
use pezsp_consensus_beefy::{
AuthorityIdBound, BeefySignatureHasher, KnownSignature, ValidatorSet, ValidatorSetId,
VersionedFinalityProof,
};
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
/// A finality proof with matching BEEFY authorities' signatures.
pub type BeefyVersionedFinalityProof<Block, AuthorityId> =
VersionedFinalityProof<NumberFor<Block>, <AuthorityId as RuntimeAppPublic>::Signature>;
pub(crate) fn proof_block_num_and_set_id<Block: BlockT, AuthorityId: AuthorityIdBound>(
proof: &BeefyVersionedFinalityProof<Block, AuthorityId>,
) -> (NumberFor<Block>, ValidatorSetId) {
match proof {
VersionedFinalityProof::V1(sc) =>
(sc.commitment.block_number, sc.commitment.validator_set_id),
}
}
/// Decode and verify a Beefy FinalityProof.
pub(crate) fn decode_and_verify_finality_proof<Block: BlockT, AuthorityId: AuthorityIdBound>(
encoded: &[u8],
target_number: NumberFor<Block>,
validator_set: &ValidatorSet<AuthorityId>,
) -> Result<BeefyVersionedFinalityProof<Block, AuthorityId>, (ConsensusError, u32)> {
let proof = <BeefyVersionedFinalityProof<Block, AuthorityId>>::decode_all(&mut &*encoded)
.map_err(|_| (ConsensusError::InvalidJustification, 0))?;
verify_with_validator_set::<Block, AuthorityId>(target_number, validator_set, &proof)?;
Ok(proof)
}
/// Verify the Beefy finality proof against the validator set at the block it was generated.
pub(crate) fn verify_with_validator_set<'a, Block: BlockT, AuthorityId: AuthorityIdBound>(
target_number: NumberFor<Block>,
validator_set: &'a ValidatorSet<AuthorityId>,
proof: &'a BeefyVersionedFinalityProof<Block, AuthorityId>,
) -> Result<
Vec<KnownSignature<&'a AuthorityId, &'a <AuthorityId as RuntimeAppPublic>::Signature>>,
(ConsensusError, u32),
> {
match proof {
VersionedFinalityProof::V1(signed_commitment) => {
let signatories = signed_commitment
.verify_signatures::<_, BeefySignatureHasher>(target_number, validator_set)
.map_err(|checked_signatures| {
(ConsensusError::InvalidJustification, checked_signatures)
})?;
if signatories.len() >= crate::round::threshold(validator_set.len()) {
Ok(signatories)
} else {
Err((
ConsensusError::InvalidJustification,
signed_commitment.signature_count() as u32,
))
}
},
}
}
#[cfg(test)]
pub(crate) mod tests {
use codec::Encode;
use pezsp_consensus_beefy::{
ecdsa_crypto, known_payloads, test_utils::Keyring, Commitment, Payload, SignedCommitment,
VersionedFinalityProof,
};
use bizinikiwi_test_runtime_client::runtime::Block;
use super::*;
use crate::tests::make_beefy_ids;
pub(crate) fn new_finality_proof(
block_num: NumberFor<Block>,
validator_set: &ValidatorSet<ecdsa_crypto::AuthorityId>,
keys: &[Keyring<ecdsa_crypto::AuthorityId>],
) -> BeefyVersionedFinalityProof<Block, ecdsa_crypto::AuthorityId> {
let commitment = Commitment {
payload: Payload::from_single_entry(known_payloads::MMR_ROOT_ID, vec![]),
block_number: block_num,
validator_set_id: validator_set.id(),
};
let message = commitment.encode();
let signatures = keys.iter().map(|key| Some(key.sign(&message))).collect();
VersionedFinalityProof::V1(SignedCommitment { commitment, signatures })
}
#[test]
fn should_verify_with_validator_set() {
let keys = &[Keyring::Alice, Keyring::Bob, Keyring::Charlie];
let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap();
// build valid justification
let block_num = 42;
let proof = new_finality_proof(block_num, &validator_set, keys);
let good_proof = proof.clone().into();
// should verify successfully
verify_with_validator_set::<Block, ecdsa_crypto::AuthorityId>(
block_num,
&validator_set,
&good_proof,
)
.unwrap();
// wrong block number -> should fail verification
let good_proof = proof.clone().into();
match verify_with_validator_set::<Block, ecdsa_crypto::AuthorityId>(
block_num + 1,
&validator_set,
&good_proof,
) {
Err((ConsensusError::InvalidJustification, 0)) => (),
e => assert!(false, "Got unexpected {:?}", e),
};
// wrong validator set id -> should fail verification
let good_proof = proof.clone().into();
let other = ValidatorSet::new(make_beefy_ids(keys), 1).unwrap();
match verify_with_validator_set::<Block, ecdsa_crypto::AuthorityId>(
block_num,
&other,
&good_proof,
) {
Err((ConsensusError::InvalidJustification, 0)) => (),
e => assert!(false, "Got unexpected {:?}", e),
};
// wrong signatures length -> should fail verification
let mut bad_proof = proof.clone();
// change length of signatures
let bad_signed_commitment = match bad_proof {
VersionedFinalityProof::V1(ref mut sc) => sc,
};
bad_signed_commitment.signatures.pop().flatten().unwrap();
match verify_with_validator_set::<Block, ecdsa_crypto::AuthorityId>(
block_num + 1,
&validator_set,
&bad_proof.into(),
) {
Err((ConsensusError::InvalidJustification, 0)) => (),
e => assert!(false, "Got unexpected {:?}", e),
};
// not enough signatures -> should fail verification
let mut bad_proof = proof.clone();
let bad_signed_commitment = match bad_proof {
VersionedFinalityProof::V1(ref mut sc) => sc,
};
// remove a signature (but same length)
*bad_signed_commitment.signatures.first_mut().unwrap() = None;
match verify_with_validator_set::<Block, ecdsa_crypto::AuthorityId>(
block_num,
&validator_set,
&bad_proof.into(),
) {
Err((ConsensusError::InvalidJustification, 2)) => (),
e => assert!(false, "Got unexpected {:?}", e),
};
// not enough _correct_ signatures -> should fail verification
let mut bad_proof = proof.clone();
let bad_signed_commitment = match bad_proof {
VersionedFinalityProof::V1(ref mut sc) => sc,
};
// change a signature to a different key
*bad_signed_commitment.signatures.first_mut().unwrap() = Some(
Keyring::<ecdsa_crypto::AuthorityId>::Dave
.sign(&bad_signed_commitment.commitment.encode()),
);
match verify_with_validator_set::<Block, ecdsa_crypto::AuthorityId>(
block_num,
&validator_set,
&bad_proof.into(),
) {
Err((ConsensusError::InvalidJustification, 3)) => (),
e => assert!(false, "Got unexpected {:?}", e),
};
}
#[test]
fn should_decode_and_verify_finality_proof() {
let keys = &[Keyring::Alice, Keyring::Bob];
let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap();
let block_num = 1;
// build valid justification
let proof = new_finality_proof(block_num, &validator_set, keys);
let versioned_proof: BeefyVersionedFinalityProof<Block, ecdsa_crypto::AuthorityId> =
proof.into();
let encoded = versioned_proof.encode();
// should successfully decode and verify
let verified = decode_and_verify_finality_proof::<Block, ecdsa_crypto::AuthorityId>(
&encoded,
block_num,
&validator_set,
)
.unwrap();
assert_eq!(verified, versioned_proof);
}
}
@@ -0,0 +1,559 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use codec::Decode;
use log::warn;
use pezsp_application_crypto::{key_types::BEEFY as BEEFY_KEY_TYPE, AppCrypto, RuntimeAppPublic};
#[cfg(feature = "bls-experimental")]
use pezsp_core::ecdsa_bls381;
use pezsp_core::{ecdsa, keccak_256};
use pezsp_keystore::KeystorePtr;
use std::marker::PhantomData;
use pezsp_consensus_beefy::{AuthorityIdBound, BeefyAuthorityId, BeefySignatureHasher};
use crate::{error, LOG_TARGET};
/// A BEEFY specific keystore implemented as a `Newtype`. This is basically a
/// wrapper around [`pezsp_keystore::Keystore`] and allows to customize
/// common cryptographic functionality.
pub(crate) struct BeefyKeystore<AuthorityId: AuthorityIdBound>(
Option<KeystorePtr>,
PhantomData<fn() -> AuthorityId>,
);
impl<AuthorityId: AuthorityIdBound> BeefyKeystore<AuthorityId> {
/// Check if the keystore contains a private key for one of the public keys
/// contained in `keys`. A public key with a matching private key is known
/// as a local authority id.
///
/// Return the public key for which we also do have a private key. If no
/// matching private key is found, `None` will be returned.
pub fn authority_id(&self, keys: &[AuthorityId]) -> Option<AuthorityId> {
let store = self.0.clone()?;
// we do check for multiple private keys as a key store sanity check.
let public: Vec<AuthorityId> = keys
.iter()
.filter(|k| {
store
.has_keys(&[(<AuthorityId as RuntimeAppPublic>::to_raw_vec(k), BEEFY_KEY_TYPE)])
})
.cloned()
.collect();
if public.len() > 1 {
warn!(
target: LOG_TARGET,
"🥩 Multiple private keys found for: {:?} ({})",
public,
public.len()
);
}
public.get(0).cloned()
}
/// Sign `message` with the `public` key.
///
/// Note that `message` usually will be pre-hashed before being signed.
///
/// Return the message signature or an error in case of failure.
pub fn sign(
&self,
public: &AuthorityId,
message: &[u8],
) -> Result<<AuthorityId as RuntimeAppPublic>::Signature, error::Error> {
let store = self.0.clone().ok_or_else(|| error::Error::Keystore("no Keystore".into()))?;
// ECDSA should use ecdsa_sign_prehashed since it needs to be hashed by keccak_256 instead
// of blake2. As such we need to deal with producing the signatures case-by-case
let signature_byte_array: Vec<u8> = match <AuthorityId as AppCrypto>::CRYPTO_ID {
ecdsa::CRYPTO_ID => {
let msg_hash = keccak_256(message);
let public: ecdsa::Public = ecdsa::Public::try_from(public.as_slice()).unwrap();
let sig = store
.ecdsa_sign_prehashed(BEEFY_KEY_TYPE, &public, &msg_hash)
.map_err(|e| error::Error::Keystore(e.to_string()))?
.ok_or_else(|| {
error::Error::Signature("ecdsa_sign_prehashed() failed".to_string())
})?;
let sig_ref: &[u8] = sig.as_ref();
sig_ref.to_vec()
},
#[cfg(feature = "bls-experimental")]
ecdsa_bls381::CRYPTO_ID => {
let public: ecdsa_bls381::Public =
ecdsa_bls381::Public::try_from(public.as_slice()).unwrap();
let sig = store
.ecdsa_bls381_sign_with_keccak256(BEEFY_KEY_TYPE, &public, &message)
.map_err(|e| error::Error::Keystore(e.to_string()))?
.ok_or_else(|| error::Error::Signature("bls381_sign() failed".to_string()))?;
let sig_ref: &[u8] = sig.as_ref();
sig_ref.to_vec()
},
_ => Err(error::Error::Keystore("key type is not supported by BEEFY Keystore".into()))?,
};
//check that `sig` has the expected result type
let signature = <AuthorityId as RuntimeAppPublic>::Signature::decode(
&mut signature_byte_array.as_slice(),
)
.map_err(|_| {
error::Error::Signature(format!(
"invalid signature {:?} for key {:?}",
signature_byte_array, public
))
})?;
Ok(signature)
}
/// Returns a vector of [`pezsp_consensus_beefy::crypto::Public`] keys which are currently
/// supported (i.e. found in the keystore).
pub fn public_keys(&self) -> Result<Vec<AuthorityId>, error::Error> {
let store = self.0.clone().ok_or_else(|| error::Error::Keystore("no Keystore".into()))?;
let pk = match <AuthorityId as AppCrypto>::CRYPTO_ID {
ecdsa::CRYPTO_ID => store
.ecdsa_public_keys(BEEFY_KEY_TYPE)
.drain(..)
.map(|pk| AuthorityId::try_from(pk.as_ref()))
.collect::<Result<Vec<_>, _>>()
.or_else(|_| {
Err(error::Error::Keystore(
"unable to convert public key into authority id".into(),
))
}),
#[cfg(feature = "bls-experimental")]
ecdsa_bls381::CRYPTO_ID => store
.ecdsa_bls381_public_keys(BEEFY_KEY_TYPE)
.drain(..)
.map(|pk| AuthorityId::try_from(pk.as_ref()))
.collect::<Result<Vec<_>, _>>()
.or_else(|_| {
Err(error::Error::Keystore(
"unable to convert public key into authority id".into(),
))
}),
_ => Err(error::Error::Keystore("key type is not supported by BEEFY Keystore".into())),
};
pk
}
/// Use the `public` key to verify that `sig` is a valid signature for `message`.
///
/// Return `true` if the signature is authentic, `false` otherwise.
pub fn verify(
public: &AuthorityId,
sig: &<AuthorityId as RuntimeAppPublic>::Signature,
message: &[u8],
) -> bool {
BeefyAuthorityId::<BeefySignatureHasher>::verify(public, sig, message)
}
}
impl<AuthorityId: AuthorityIdBound> From<Option<KeystorePtr>> for BeefyKeystore<AuthorityId> {
fn from(store: Option<KeystorePtr>) -> BeefyKeystore<AuthorityId> {
BeefyKeystore(store, PhantomData)
}
}
#[cfg(test)]
pub mod tests {
#[cfg(feature = "bls-experimental")]
use pezsp_consensus_beefy::ecdsa_bls_crypto;
use pezsp_consensus_beefy::{
ecdsa_crypto,
test_utils::{BeefySignerAuthority, Keyring},
};
use pezsp_core::Pair as PairT;
use pezsp_keystore::{testing::MemoryKeystore, Keystore};
use super::*;
use crate::error::Error;
fn keystore() -> KeystorePtr {
MemoryKeystore::new().into()
}
fn pair_verify_should_work<
AuthorityId: AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
>()
where
<AuthorityId as pezsp_runtime::RuntimeAppPublic>::Signature:
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<pezsp_runtime::traits::Keccak256>,
{
let msg = b"I am Alice!";
let sig = Keyring::<AuthorityId>::Alice.sign(b"I am Alice!");
assert!(<AuthorityId as BeefyAuthorityId<BeefySignatureHasher>>::verify(
&Keyring::Alice.public(),
&sig,
&msg.as_slice(),
));
// different public key -> fail
assert!(!<AuthorityId as BeefyAuthorityId<BeefySignatureHasher>>::verify(
&Keyring::Bob.public(),
&sig,
&msg.as_slice(),
));
let msg = b"I am not Alice!";
// different msg -> fail
assert!(!<AuthorityId as BeefyAuthorityId<BeefySignatureHasher>>::verify(
&Keyring::Alice.public(),
&sig,
&msg.as_slice(),
));
}
/// Generate key pair in the given store using the provided seed
fn generate_in_store<AuthorityId>(
store: KeystorePtr,
key_type: pezsp_application_crypto::KeyTypeId,
owner: Option<Keyring<AuthorityId>>,
) -> AuthorityId
where
AuthorityId:
AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<BeefySignatureHasher>,
<AuthorityId as RuntimeAppPublic>::Signature:
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
{
let optional_seed: Option<String> = owner.map(|owner| owner.to_seed());
match <AuthorityId as AppCrypto>::CRYPTO_ID {
ecdsa::CRYPTO_ID => {
let pk = store.ecdsa_generate_new(key_type, optional_seed.as_deref()).ok().unwrap();
AuthorityId::decode(&mut pk.as_ref()).unwrap()
},
#[cfg(feature = "bls-experimental")]
ecdsa_bls381::CRYPTO_ID => {
let pk = store
.ecdsa_bls381_generate_new(key_type, optional_seed.as_deref())
.ok()
.unwrap();
AuthorityId::decode(&mut pk.as_ref()).unwrap()
},
_ => panic!("Requested CRYPTO_ID is not supported by the BEEFY Keyring"),
}
}
#[test]
fn pair_verify_should_work_ecdsa() {
pair_verify_should_work::<ecdsa_crypto::AuthorityId>();
}
#[cfg(feature = "bls-experimental")]
#[test]
fn pair_verify_should_work_ecdsa_n_bls() {
pair_verify_should_work::<ecdsa_bls_crypto::AuthorityId>();
}
fn pair_works<
AuthorityId: AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
>()
where
<AuthorityId as pezsp_runtime::RuntimeAppPublic>::Signature:
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<pezsp_runtime::traits::Keccak256>,
{
let want = <AuthorityId as AppCrypto>::Pair::from_string("//Alice", None)
.expect("Pair failed")
.to_raw_vec();
let got = Keyring::<AuthorityId>::Alice.pair().to_raw_vec();
assert_eq!(want, got);
let want = <AuthorityId as AppCrypto>::Pair::from_string("//Bob", None)
.expect("Pair failed")
.to_raw_vec();
let got = Keyring::<AuthorityId>::Bob.pair().to_raw_vec();
assert_eq!(want, got);
let want = <AuthorityId as AppCrypto>::Pair::from_string("//Charlie", None)
.expect("Pair failed")
.to_raw_vec();
let got = Keyring::<AuthorityId>::Charlie.pair().to_raw_vec();
assert_eq!(want, got);
let want = <AuthorityId as AppCrypto>::Pair::from_string("//Dave", None)
.expect("Pair failed")
.to_raw_vec();
let got = Keyring::<AuthorityId>::Dave.pair().to_raw_vec();
assert_eq!(want, got);
let want = <AuthorityId as AppCrypto>::Pair::from_string("//Eve", None)
.expect("Pair failed")
.to_raw_vec();
let got = Keyring::<AuthorityId>::Eve.pair().to_raw_vec();
assert_eq!(want, got);
let want = <AuthorityId as AppCrypto>::Pair::from_string("//Ferdie", None)
.expect("Pair failed")
.to_raw_vec();
let got = Keyring::<AuthorityId>::Ferdie.pair().to_raw_vec();
assert_eq!(want, got);
let want = <AuthorityId as AppCrypto>::Pair::from_string("//One", None)
.expect("Pair failed")
.to_raw_vec();
let got = Keyring::<AuthorityId>::One.pair().to_raw_vec();
assert_eq!(want, got);
let want = <AuthorityId as AppCrypto>::Pair::from_string("//Two", None)
.expect("Pair failed")
.to_raw_vec();
let got = Keyring::<AuthorityId>::Two.pair().to_raw_vec();
assert_eq!(want, got);
}
#[test]
fn ecdsa_pair_works() {
pair_works::<ecdsa_crypto::AuthorityId>();
}
#[cfg(feature = "bls-experimental")]
#[test]
fn ecdsa_n_bls_pair_works() {
pair_works::<ecdsa_bls_crypto::AuthorityId>();
}
fn authority_id_works<
AuthorityId: AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
>()
where
<AuthorityId as pezsp_runtime::RuntimeAppPublic>::Signature:
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<pezsp_runtime::traits::Keccak256>,
{
let store = keystore();
generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, Some(Keyring::Alice));
let alice = Keyring::<AuthorityId>::Alice.public();
let bob = Keyring::Bob.public();
let charlie = Keyring::Charlie.public();
let beefy_store: BeefyKeystore<AuthorityId> = Some(store).into();
let mut keys = vec![bob, charlie];
let id = beefy_store.authority_id(keys.as_slice());
assert!(id.is_none());
keys.push(alice.clone());
let id = beefy_store.authority_id(keys.as_slice()).unwrap();
assert_eq!(id, alice);
}
#[test]
fn authority_id_works_for_ecdsa() {
authority_id_works::<ecdsa_crypto::AuthorityId>();
}
#[cfg(feature = "bls-experimental")]
#[test]
fn authority_id_works_for_ecdsa_n_bls() {
authority_id_works::<ecdsa_bls_crypto::AuthorityId>();
}
fn sign_works<
AuthorityId: AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
>()
where
<AuthorityId as pezsp_runtime::RuntimeAppPublic>::Signature:
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<pezsp_runtime::traits::Keccak256>,
{
let store = keystore();
generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, Some(Keyring::Alice));
let alice = Keyring::Alice.public();
let store: BeefyKeystore<AuthorityId> = Some(store).into();
let msg = b"are you involved or committed?";
let sig1 = store.sign(&alice, msg).unwrap();
let sig2 = Keyring::<AuthorityId>::Alice.sign(msg);
assert_eq!(sig1, sig2);
}
#[test]
fn sign_works_for_ecdsa() {
sign_works::<ecdsa_crypto::AuthorityId>();
}
#[cfg(feature = "bls-experimental")]
#[test]
fn sign_works_for_ecdsa_n_bls() {
sign_works::<ecdsa_bls_crypto::AuthorityId>();
}
fn sign_error<
AuthorityId: AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
>(
expected_error_message: &str,
) where
<AuthorityId as pezsp_runtime::RuntimeAppPublic>::Signature:
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<pezsp_runtime::traits::Keccak256>,
{
let store = keystore();
generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, Some(Keyring::Bob));
let store: BeefyKeystore<AuthorityId> = Some(store).into();
let alice = Keyring::Alice.public();
let msg = b"are you involved or committed?";
let sig = store.sign(&alice, msg).err().unwrap();
let err = Error::Signature(expected_error_message.to_string());
assert_eq!(sig, err);
}
#[test]
fn sign_error_for_ecdsa() {
sign_error::<ecdsa_crypto::AuthorityId>("ecdsa_sign_prehashed() failed");
}
#[cfg(feature = "bls-experimental")]
#[test]
fn sign_error_for_ecdsa_n_bls() {
sign_error::<ecdsa_bls_crypto::AuthorityId>("bls381_sign() failed");
}
#[test]
fn sign_no_keystore() {
let store: BeefyKeystore<ecdsa_crypto::Public> = None.into();
let alice = Keyring::Alice.public();
let msg = b"are you involved or committed";
let sig = store.sign(&alice, msg).err().unwrap();
let err = Error::Keystore("no Keystore".to_string());
assert_eq!(sig, err);
}
fn verify_works<
AuthorityId: AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
>()
where
<AuthorityId as pezsp_runtime::RuntimeAppPublic>::Signature:
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<pezsp_runtime::traits::Keccak256>,
{
let store = keystore();
generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, Some(Keyring::Alice));
let store: BeefyKeystore<AuthorityId> = Some(store).into();
let alice = Keyring::Alice.public();
// `msg` and `sig` match
let msg = b"are you involved or committed?";
let sig = store.sign(&alice, msg).unwrap();
assert!(BeefyKeystore::verify(&alice, &sig, msg));
// `msg and `sig` don't match
let msg = b"you are just involved";
assert!(!BeefyKeystore::verify(&alice, &sig, msg));
}
#[test]
fn verify_works_for_ecdsa() {
verify_works::<ecdsa_crypto::AuthorityId>();
}
#[cfg(feature = "bls-experimental")]
#[test]
fn verify_works_for_ecdsa_n_bls() {
verify_works::<ecdsa_bls_crypto::AuthorityId>();
}
// Note that we use keys with and without a seed for this test.
fn public_keys_works<
AuthorityId: AuthorityIdBound + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Public>,
>()
where
<AuthorityId as pezsp_runtime::RuntimeAppPublic>::Signature:
Send + Sync + From<<<AuthorityId as AppCrypto>::Pair as AppCrypto>::Signature>,
<AuthorityId as AppCrypto>::Pair: BeefySignerAuthority<pezsp_runtime::traits::Keccak256>,
{
const TEST_TYPE: pezsp_application_crypto::KeyTypeId =
pezsp_application_crypto::KeyTypeId(*b"test");
let store = keystore();
// test keys
let _ = generate_in_store::<AuthorityId>(store.clone(), TEST_TYPE, Some(Keyring::Alice));
let _ = generate_in_store::<AuthorityId>(store.clone(), TEST_TYPE, Some(Keyring::Bob));
// BEEFY keys
let _ =
generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, Some(Keyring::Dave));
let _ = generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, Some(Keyring::Eve));
let _ = generate_in_store::<AuthorityId>(store.clone(), TEST_TYPE, None);
let _ = generate_in_store::<AuthorityId>(store.clone(), TEST_TYPE, None);
let key1 = generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, None);
let key2 = generate_in_store::<AuthorityId>(store.clone(), BEEFY_KEY_TYPE, None);
let store: BeefyKeystore<AuthorityId> = Some(store).into();
let keys = store.public_keys().ok().unwrap();
assert!(keys.len() == 4);
assert!(keys.contains(&Keyring::Dave.public()));
assert!(keys.contains(&Keyring::Eve.public()));
assert!(keys.contains(&key1));
assert!(keys.contains(&key2));
}
#[test]
fn public_keys_works_for_ecdsa_keystore() {
public_keys_works::<ecdsa_crypto::AuthorityId>();
}
#[cfg(feature = "bls-experimental")]
#[test]
fn public_keys_works_for_ecdsa_n_bls() {
public_keys_works::<ecdsa_bls_crypto::AuthorityId>();
}
}
@@ -0,0 +1,814 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::{
communication::{
notification::{
BeefyBestBlockSender, BeefyBestBlockStream, BeefyVersionedFinalityProofSender,
BeefyVersionedFinalityProofStream,
},
peers::KnownPeers,
request_response::{
outgoing_requests_engine::OnDemandJustificationsEngine, BeefyJustifsRequestHandler,
},
},
error::Error,
import::BeefyBlockImport,
metrics::register_metrics,
};
use futures::{stream::Fuse, FutureExt, StreamExt};
use log::{debug, error, info, trace, warn};
use parking_lot::Mutex;
use prometheus_endpoint::Registry;
use pezsc_client_api::{Backend, BlockBackend, BlockchainEvents, FinalityNotification, Finalizer};
use pezsc_consensus::BlockImport;
use pezsc_network::{NetworkRequest, NotificationService, ProtocolName};
use pezsc_network_gossip::{GossipEngine, Network as GossipNetwork, Syncing as GossipSyncing};
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver};
use pezsp_api::ProvideRuntimeApi;
use pezsp_blockchain::{Backend as BlockchainBackend, HeaderBackend};
use pezsp_consensus::{Error as ConsensusError, SyncOracle};
use pezsp_consensus_beefy::{
AuthorityIdBound, BeefyApi, ConsensusLog, PayloadProvider, ValidatorSet, BEEFY_ENGINE_ID,
};
use pezsp_keystore::KeystorePtr;
use pezsp_runtime::traits::{Block, Header as HeaderT, NumberFor, Zero};
use std::{
collections::{BTreeMap, VecDeque},
future::Future,
marker::PhantomData,
pin::Pin,
sync::Arc,
time::Duration,
};
mod aux_schema;
mod error;
mod keystore;
mod metrics;
mod round;
mod worker;
pub mod communication;
pub mod import;
pub mod justification;
use crate::{
communication::gossip::GossipValidator,
fisherman::Fisherman,
justification::BeefyVersionedFinalityProof,
keystore::BeefyKeystore,
metrics::VoterMetrics,
round::Rounds,
worker::{BeefyWorker, PersistedState},
};
pub use communication::beefy_protocol_name::{
gossip_protocol_name, justifications_protocol_name as justifs_protocol_name,
};
use pezsp_runtime::generic::OpaqueDigestItemId;
mod fisherman;
#[cfg(test)]
mod tests;
const LOG_TARGET: &str = "beefy";
const HEADER_SYNC_DELAY: Duration = Duration::from_secs(60);
type FinalityNotifications<Block> =
pezsc_utils::mpsc::TracingUnboundedReceiver<UnpinnedFinalityNotification<Block>>;
/// A convenience BEEFY client trait that defines all the type bounds a BEEFY client
/// has to satisfy. Ideally that should actually be a trait alias. Unfortunately as
/// of today, Rust does not allow a type alias to be used as a trait bound. Tracking
/// issue is <https://github.com/rust-lang/rust/issues/41517>.
pub trait Client<B, BE>:
BlockchainEvents<B> + HeaderBackend<B> + Finalizer<B, BE> + Send + Sync
where
B: Block,
BE: Backend<B>,
{
// empty
}
impl<B, BE, T> Client<B, BE> for T
where
B: Block,
BE: Backend<B>,
T: BlockchainEvents<B>
+ HeaderBackend<B>
+ Finalizer<B, BE>
+ ProvideRuntimeApi<B>
+ Send
+ Sync,
{
// empty
}
/// Links between the block importer, the background voter and the RPC layer,
/// to be used by the voter.
#[derive(Clone)]
pub struct BeefyVoterLinks<B: Block, AuthorityId: AuthorityIdBound> {
// BlockImport -> Voter links
/// Stream of BEEFY signed commitments from block import to voter.
pub from_block_import_justif_stream: BeefyVersionedFinalityProofStream<B, AuthorityId>,
// Voter -> RPC links
/// Sends BEEFY signed commitments from voter to RPC.
pub to_rpc_justif_sender: BeefyVersionedFinalityProofSender<B, AuthorityId>,
/// Sends BEEFY best block hashes from voter to RPC.
pub to_rpc_best_block_sender: BeefyBestBlockSender<B>,
}
/// Links used by the BEEFY RPC layer, from the BEEFY background voter.
#[derive(Clone)]
pub struct BeefyRPCLinks<B: Block, AuthorityId: AuthorityIdBound> {
/// Stream of signed commitments coming from the voter.
pub from_voter_justif_stream: BeefyVersionedFinalityProofStream<B, AuthorityId>,
/// Stream of BEEFY best block hashes coming from the voter.
pub from_voter_best_beefy_stream: BeefyBestBlockStream<B>,
}
/// Make block importer and link half necessary to tie the background voter to it.
pub fn beefy_block_import_and_links<B, BE, RuntimeApi, I, AuthorityId: AuthorityIdBound>(
wrapped_block_import: I,
backend: Arc<BE>,
runtime: Arc<RuntimeApi>,
prometheus_registry: Option<Registry>,
) -> (
BeefyBlockImport<B, BE, RuntimeApi, I, AuthorityId>,
BeefyVoterLinks<B, AuthorityId>,
BeefyRPCLinks<B, AuthorityId>,
)
where
B: Block,
BE: Backend<B>,
I: BlockImport<B, Error = ConsensusError> + Send + Sync,
RuntimeApi: ProvideRuntimeApi<B> + Send + Sync,
RuntimeApi::Api: BeefyApi<B, AuthorityId>,
AuthorityId: AuthorityIdBound,
{
// Voter -> RPC links
let (to_rpc_justif_sender, from_voter_justif_stream) =
BeefyVersionedFinalityProofStream::<B, AuthorityId>::channel();
let (to_rpc_best_block_sender, from_voter_best_beefy_stream) =
BeefyBestBlockStream::<B>::channel();
// BlockImport -> Voter links
let (to_voter_justif_sender, from_block_import_justif_stream) =
BeefyVersionedFinalityProofStream::<B, AuthorityId>::channel();
let metrics = register_metrics(prometheus_registry);
// BlockImport
let import = BeefyBlockImport::new(
backend,
runtime,
wrapped_block_import,
to_voter_justif_sender,
metrics,
);
let voter_links = BeefyVoterLinks {
from_block_import_justif_stream,
to_rpc_justif_sender,
to_rpc_best_block_sender,
};
let rpc_links = BeefyRPCLinks { from_voter_best_beefy_stream, from_voter_justif_stream };
(import, voter_links, rpc_links)
}
/// BEEFY gadget network parameters.
pub struct BeefyNetworkParams<B: Block, N, S> {
/// Network implementing gossip, requests and sync-oracle.
pub network: Arc<N>,
/// Syncing service implementing a sync oracle and an event stream for peers.
pub sync: Arc<S>,
/// Handle for receiving notification events.
pub notification_service: Box<dyn NotificationService>,
/// Chain specific BEEFY gossip protocol name. See
/// [`communication::beefy_protocol_name::gossip_protocol_name`].
pub gossip_protocol_name: ProtocolName,
/// Chain specific BEEFY on-demand justifications protocol name. See
/// [`communication::beefy_protocol_name::justifications_protocol_name`].
pub justifications_protocol_name: ProtocolName,
pub _phantom: PhantomData<B>,
}
/// BEEFY gadget initialization parameters.
pub struct BeefyParams<B: Block, BE, C, N, P, R, S, AuthorityId: AuthorityIdBound> {
/// BEEFY client
pub client: Arc<C>,
/// Client Backend
pub backend: Arc<BE>,
/// BEEFY Payload provider
pub payload_provider: P,
/// Runtime Api Provider
pub runtime: Arc<R>,
/// Local key store
pub key_store: Option<KeystorePtr>,
/// BEEFY voter network params
pub network_params: BeefyNetworkParams<B, N, S>,
/// Minimal delta between blocks, BEEFY should vote for
pub min_block_delta: u32,
/// Prometheus metric registry
pub prometheus_registry: Option<Registry>,
/// Links between the block importer, the background voter and the RPC layer.
pub links: BeefyVoterLinks<B, AuthorityId>,
/// Handler for incoming BEEFY justifications requests from a remote peer.
pub on_demand_justifications_handler: BeefyJustifsRequestHandler<B, C>,
/// Whether running under "Authority" role.
pub is_authority: bool,
}
/// Helper object holding BEEFY worker communication/gossip components.
///
/// These are created once, but will be reused if worker is restarted/reinitialized.
pub(crate) struct BeefyComms<B: Block, N, AuthorityId: AuthorityIdBound> {
pub gossip_engine: GossipEngine<B>,
pub gossip_validator: Arc<GossipValidator<B, N, AuthorityId>>,
pub on_demand_justifications: OnDemandJustificationsEngine<B, AuthorityId>,
}
/// Helper builder object for building [worker::BeefyWorker].
///
/// It has to do it in two steps: initialization and build, because the first step can sleep waiting
/// for certain chain and backend conditions, and while sleeping we still need to pump the
/// GossipEngine. Once initialization is done, the GossipEngine (and other pieces) are added to get
/// the complete [worker::BeefyWorker] object.
pub(crate) struct BeefyWorkerBuilder<B: Block, BE, RuntimeApi, AuthorityId: AuthorityIdBound> {
// utilities
backend: Arc<BE>,
runtime: Arc<RuntimeApi>,
key_store: BeefyKeystore<AuthorityId>,
// voter metrics
metrics: Option<VoterMetrics>,
persisted_state: PersistedState<B, AuthorityId>,
}
impl<B, BE, R, AuthorityId> BeefyWorkerBuilder<B, BE, R, AuthorityId>
where
B: Block + codec::Codec,
BE: Backend<B>,
R: ProvideRuntimeApi<B>,
R::Api: BeefyApi<B, AuthorityId>,
AuthorityId: AuthorityIdBound,
{
/// This will wait for the chain to enable BEEFY (if not yet enabled) and also wait for the
/// backend to sync all headers required by the voter to build a contiguous chain of mandatory
/// justifications. Then it builds the initial voter state using a combination of previously
/// persisted state in AUX DB and latest chain information/progress.
///
/// Returns a sane `BeefyWorkerBuilder` that can build the `BeefyWorker`.
pub async fn async_initialize<N>(
backend: Arc<BE>,
runtime: Arc<R>,
key_store: BeefyKeystore<AuthorityId>,
metrics: Option<VoterMetrics>,
min_block_delta: u32,
gossip_validator: Arc<GossipValidator<B, N, AuthorityId>>,
finality_notifications: &mut Fuse<FinalityNotifications<B>>,
is_authority: bool,
) -> Result<Self, Error> {
// Wait for BEEFY pallet to be active before starting voter.
let (beefy_genesis, best_grandpa) =
wait_for_runtime_pallet(&*runtime, finality_notifications).await?;
let persisted_state = Self::load_or_init_state(
beefy_genesis,
best_grandpa,
min_block_delta,
backend.clone(),
runtime.clone(),
&key_store,
&metrics,
is_authority,
)
.await?;
// Update the gossip validator with the right starting round and set id.
persisted_state
.gossip_filter_config()
.map(|f| gossip_validator.update_filter(f))?;
Ok(BeefyWorkerBuilder { backend, runtime, key_store, metrics, persisted_state })
}
/// Takes rest of missing pieces as params and builds the `BeefyWorker`.
pub fn build<P, S, N>(
self,
payload_provider: P,
sync: Arc<S>,
comms: BeefyComms<B, N, AuthorityId>,
links: BeefyVoterLinks<B, AuthorityId>,
pending_justifications: BTreeMap<NumberFor<B>, BeefyVersionedFinalityProof<B, AuthorityId>>,
is_authority: bool,
) -> BeefyWorker<B, BE, P, R, S, N, AuthorityId> {
let key_store = Arc::new(self.key_store);
BeefyWorker {
backend: self.backend.clone(),
runtime: self.runtime.clone(),
key_store: key_store.clone(),
payload_provider,
sync,
fisherman: Arc::new(Fisherman::new(self.backend, self.runtime, key_store)),
metrics: self.metrics,
persisted_state: self.persisted_state,
comms,
links,
pending_justifications,
is_authority,
}
}
// If no persisted state present, walk back the chain from first GRANDPA notification to either:
// - latest BEEFY finalized block, or if none found on the way,
// - BEEFY pallet genesis;
// Enqueue any BEEFY mandatory blocks (session boundaries) found on the way, for voter to
// finalize.
async fn init_state(
beefy_genesis: NumberFor<B>,
best_grandpa: <B as Block>::Header,
min_block_delta: u32,
backend: Arc<BE>,
runtime: Arc<R>,
) -> Result<PersistedState<B, AuthorityId>, Error> {
let blockchain = backend.blockchain();
let beefy_genesis = runtime
.runtime_api()
.beefy_genesis(best_grandpa.hash())
.ok()
.flatten()
.filter(|genesis| *genesis == beefy_genesis)
.ok_or_else(|| Error::Backend("BEEFY pallet expected to be active.".into()))?;
// Walk back the imported blocks and initialize voter either, at the last block with
// a BEEFY justification, or at pallet genesis block; voter will resume from there.
let mut sessions = VecDeque::new();
let mut header = best_grandpa.clone();
let state = loop {
if let Some(true) = blockchain
.justifications(header.hash())
.ok()
.flatten()
.map(|justifs| justifs.get(BEEFY_ENGINE_ID).is_some())
{
debug!(
target: LOG_TARGET,
"🥩 Initialize BEEFY voter at last BEEFY finalized block: {:?}.",
*header.number()
);
let best_beefy = *header.number();
// If no session boundaries detected so far, just initialize new rounds here.
if sessions.is_empty() {
let active_set =
expect_validator_set(runtime.as_ref(), backend.as_ref(), &header).await?;
let mut rounds = Rounds::new(best_beefy, active_set);
// Mark the round as already finalized.
rounds.conclude(best_beefy);
sessions.push_front(rounds);
}
let state = PersistedState::checked_new(
best_grandpa,
best_beefy,
sessions,
min_block_delta,
beefy_genesis,
)
.ok_or_else(|| Error::Backend("Invalid BEEFY chain".into()))?;
break state;
}
if *header.number() == beefy_genesis {
// We've reached BEEFY genesis, initialize voter here.
let genesis_set =
expect_validator_set(runtime.as_ref(), backend.as_ref(), &header).await?;
info!(
target: LOG_TARGET,
"🥩 Loading BEEFY voter state from genesis on what appears to be first startup. \
Starting voting rounds at block {:?}, genesis validator set {:?}.",
beefy_genesis,
genesis_set,
);
sessions.push_front(Rounds::new(beefy_genesis, genesis_set));
break PersistedState::checked_new(
best_grandpa,
Zero::zero(),
sessions,
min_block_delta,
beefy_genesis,
)
.ok_or_else(|| Error::Backend("Invalid BEEFY chain".into()))?;
}
if let Some(active) = find_authorities_change::<B, AuthorityId>(&header) {
debug!(
target: LOG_TARGET,
"🥩 Marking block {:?} as BEEFY Mandatory.",
*header.number()
);
sessions.push_front(Rounds::new(*header.number(), active));
}
// Move up the chain.
header = wait_for_parent_header(blockchain, header, HEADER_SYNC_DELAY).await?;
};
aux_schema::write_current_version(backend.as_ref())?;
aux_schema::write_voter_state(backend.as_ref(), &state)?;
Ok(state)
}
async fn load_or_init_state(
beefy_genesis: NumberFor<B>,
best_grandpa: <B as Block>::Header,
min_block_delta: u32,
backend: Arc<BE>,
runtime: Arc<R>,
key_store: &BeefyKeystore<AuthorityId>,
metrics: &Option<VoterMetrics>,
is_authority: bool,
) -> Result<PersistedState<B, AuthorityId>, Error> {
// Initialize voter state from AUX DB if compatible.
if let Some(mut state) = crate::aux_schema::load_persistent(backend.as_ref())?
// Verify state pallet genesis matches runtime.
.filter(|state| state.pezpallet_genesis() == beefy_genesis)
{
// Overwrite persisted state with current best GRANDPA block.
state.set_best_grandpa(best_grandpa.clone());
// Overwrite persisted data with newly provided `min_block_delta`.
state.set_min_block_delta(min_block_delta);
debug!(target: LOG_TARGET, "🥩 Loading BEEFY voter state from db.");
trace!(target: LOG_TARGET, "🥩 Loaded state: {:?}.", state);
// Make sure that all the headers that we need have been synced.
let mut new_sessions = vec![];
let mut header = best_grandpa.clone();
while *header.number() > state.best_beefy() {
if state.voting_oracle().can_add_session(*header.number()) {
if let Some(active) = find_authorities_change::<B, AuthorityId>(&header) {
new_sessions.push((active, *header.number()));
}
}
header =
wait_for_parent_header(backend.blockchain(), header, HEADER_SYNC_DELAY).await?;
}
// Make sure we didn't miss any sessions during node restart.
for (validator_set, new_session_start) in new_sessions.drain(..).rev() {
debug!(
target: LOG_TARGET,
"🥩 Handling missed BEEFY session after node restart: {:?}.",
new_session_start
);
state.init_session_at(
new_session_start,
validator_set,
key_store,
metrics,
is_authority,
);
}
return Ok(state);
}
// No valid voter-state persisted, re-initialize from pallet genesis.
Self::init_state(beefy_genesis, best_grandpa, min_block_delta, backend, runtime).await
}
}
/// Finality notification for consumption by BEEFY worker.
/// This is a stripped down version of `pezsc_client_api::FinalityNotification` which does not keep
/// blocks pinned.
struct UnpinnedFinalityNotification<B: Block> {
/// Finalized block header hash.
pub hash: B::Hash,
/// Finalized block header.
pub header: B::Header,
/// Path from the old finalized to new finalized parent (implicitly finalized blocks).
///
/// This maps to the range `(old_finalized, new_finalized)`.
pub tree_route: Arc<[B::Hash]>,
}
impl<B: Block> From<FinalityNotification<B>> for UnpinnedFinalityNotification<B> {
fn from(value: FinalityNotification<B>) -> Self {
UnpinnedFinalityNotification {
hash: value.hash,
header: value.header,
tree_route: value.tree_route,
}
}
}
/// Start the BEEFY gadget.
///
/// This is a thin shim around running and awaiting a BEEFY worker.
pub async fn start_beefy_gadget<B, BE, C, N, P, R, S, AuthorityId>(
beefy_params: BeefyParams<B, BE, C, N, P, R, S, AuthorityId>,
) where
B: Block,
BE: Backend<B>,
C: Client<B, BE> + BlockBackend<B>,
P: PayloadProvider<B> + Clone,
R: ProvideRuntimeApi<B>,
R::Api: BeefyApi<B, AuthorityId>,
N: GossipNetwork<B> + NetworkRequest + Send + Sync + 'static,
S: GossipSyncing<B> + SyncOracle + 'static,
AuthorityId: AuthorityIdBound,
{
let BeefyParams {
client,
backend,
payload_provider,
runtime,
key_store,
network_params,
min_block_delta,
prometheus_registry,
links,
mut on_demand_justifications_handler,
is_authority,
} = beefy_params;
let BeefyNetworkParams {
network,
sync,
notification_service,
gossip_protocol_name,
justifications_protocol_name,
..
} = network_params;
let metrics = register_metrics(prometheus_registry.clone());
let mut block_import_justif = links.from_block_import_justif_stream.subscribe(100_000).fuse();
// Subscribe to finality notifications and justifications before waiting for runtime pallet and
// reuse the streams, so we don't miss notifications while waiting for pallet to be available.
let finality_notifications = client.finality_notification_stream();
let (mut transformer, mut finality_notifications) =
finality_notification_transformer_future(finality_notifications);
let known_peers = Arc::new(Mutex::new(KnownPeers::new()));
// Default votes filter is to discard everything.
// Validator is updated later with correct starting round and set id.
let gossip_validator =
communication::gossip::GossipValidator::new(known_peers.clone(), network.clone());
let gossip_validator = Arc::new(gossip_validator);
let gossip_engine = GossipEngine::new(
network.clone(),
sync.clone(),
notification_service,
gossip_protocol_name.clone(),
gossip_validator.clone(),
None,
);
// The `GossipValidator` adds and removes known peers based on valid votes and network
// events.
let on_demand_justifications = OnDemandJustificationsEngine::new(
network.clone(),
justifications_protocol_name.clone(),
known_peers,
prometheus_registry.clone(),
);
let mut beefy_comms = BeefyComms { gossip_engine, gossip_validator, on_demand_justifications };
// We re-create and re-run the worker in this loop in order to quickly reinit and resume after
// select recoverable errors.
loop {
// Make sure to pump gossip engine while waiting for initialization conditions.
let worker_builder = futures::select! {
builder_init_result = BeefyWorkerBuilder::async_initialize(
backend.clone(),
runtime.clone(),
key_store.clone().into(),
metrics.clone(),
min_block_delta,
beefy_comms.gossip_validator.clone(),
&mut finality_notifications,
is_authority,
).fuse() => {
match builder_init_result {
Ok(builder) => builder,
Err(e) => {
error!(target: LOG_TARGET, "🥩 Error: {:?}. Terminating.", e);
return
},
}
},
// Pump gossip engine.
_ = &mut beefy_comms.gossip_engine => {
error!(target: LOG_TARGET, "🥩 Gossip engine has unexpectedly terminated.");
return
},
_ = &mut transformer => {
error!(target: LOG_TARGET, "🥩 Finality notification transformer task has unexpectedly terminated.");
return
},
};
let worker = worker_builder.build(
payload_provider.clone(),
sync.clone(),
beefy_comms,
links.clone(),
BTreeMap::new(),
is_authority,
);
futures::select! {
result = worker.run(&mut block_import_justif, &mut finality_notifications).fuse() => {
match result {
(error::Error::ConsensusReset, reuse_comms) => {
error!(target: LOG_TARGET, "🥩 Error: {:?}. Restarting voter.", error::Error::ConsensusReset);
beefy_comms = reuse_comms;
continue;
},
(err, _) => {
error!(target: LOG_TARGET, "🥩 Error: {:?}. Terminating.", err)
}
}
},
odj_handler_error = on_demand_justifications_handler.run().fuse() => {
error!(target: LOG_TARGET, "🥩 Error: {:?}. Terminating.", odj_handler_error)
},
_ = &mut transformer => {
error!(target: LOG_TARGET, "🥩 Finality notification transformer task has unexpectedly terminated.");
}
}
return;
}
}
/// Produce a future that transformes finality notifications into a struct that does not keep blocks
/// pinned.
fn finality_notification_transformer_future<B>(
mut finality_notifications: pezsc_client_api::FinalityNotifications<B>,
) -> (
Pin<Box<futures::future::Fuse<impl Future<Output = ()> + Sized>>>,
Fuse<TracingUnboundedReceiver<UnpinnedFinalityNotification<B>>>,
)
where
B: Block,
{
let (tx, rx) = tracing_unbounded("beefy-notification-transformer-channel", 10000);
let transformer_fut = async move {
while let Some(notification) = finality_notifications.next().await {
debug!(target: LOG_TARGET, "🥩 Transforming grandpa notification. #{}({:?})", notification.header.number(), notification.hash);
if let Err(err) = tx.unbounded_send(UnpinnedFinalityNotification::from(notification)) {
error!(target: LOG_TARGET, "🥩 Unable to send transformed notification. Shutting down. err = {}", err);
return;
};
}
};
(Box::pin(transformer_fut.fuse()), rx.fuse())
}
/// Waits until the parent header of `current` is available and returns it.
///
/// When the node uses GRANDPA warp sync it initially downloads only the mandatory GRANDPA headers.
/// The rest of the headers (gap sync) are lazily downloaded later. But the BEEFY voter also needs
/// the headers in range `[beefy_genesis..=best_grandpa]` to be available. This helper method
/// enables us to wait until these headers have been synced.
async fn wait_for_parent_header<B, BC>(
blockchain: &BC,
current: <B as Block>::Header,
delay: Duration,
) -> Result<<B as Block>::Header, Error>
where
B: Block,
BC: BlockchainBackend<B>,
{
if *current.number() == Zero::zero() {
let msg = format!("header {} is Genesis, there is no parent for it", current.hash());
warn!(target: LOG_TARGET, "{}", msg);
return Err(Error::Backend(msg));
}
loop {
match blockchain
.header(*current.parent_hash())
.map_err(|e| Error::Backend(e.to_string()))?
{
Some(parent) => return Ok(parent),
None => {
info!(
target: LOG_TARGET,
"🥩 Parent of header number {} not found. \
BEEFY gadget waiting for header sync to finish ...",
current.number()
);
tokio::time::sleep(delay).await;
},
}
}
}
/// Wait for BEEFY runtime pallet to be available, return active validator set.
/// Should be called only once during worker initialization.
async fn wait_for_runtime_pallet<B, R, AuthorityId: AuthorityIdBound>(
runtime: &R,
finality: &mut Fuse<FinalityNotifications<B>>,
) -> Result<(NumberFor<B>, <B as Block>::Header), Error>
where
B: Block,
R: ProvideRuntimeApi<B>,
R::Api: BeefyApi<B, AuthorityId>,
{
info!(target: LOG_TARGET, "🥩 BEEFY gadget waiting for BEEFY pallet to become available...");
loop {
let notif = finality.next().await.ok_or_else(|| {
let err_msg = "🥩 Finality stream has unexpectedly terminated.".into();
error!(target: LOG_TARGET, "{}", err_msg);
Error::Backend(err_msg)
})?;
let at = notif.header.hash();
if let Some(start) = runtime.runtime_api().beefy_genesis(at).ok().flatten() {
if *notif.header.number() >= start {
// Beefy pallet available, return header for best grandpa at the time.
info!(
target: LOG_TARGET,
"🥩 BEEFY pallet available: block {:?} beefy genesis {:?}",
notif.header.number(), start
);
return Ok((start, notif.header));
}
}
}
}
/// Provides validator set active `at_header`. It tries to get it from state, otherwise falls
/// back to walk up the chain looking the validator set enactment in header digests.
///
/// Note: function will `async::sleep()` when walking back the chain if some needed header hasn't
/// been synced yet (as it happens when warp syncing when headers are synced in the background).
async fn expect_validator_set<B, BE, R, AuthorityId: AuthorityIdBound>(
runtime: &R,
backend: &BE,
at_header: &B::Header,
) -> Result<ValidatorSet<AuthorityId>, Error>
where
B: Block,
BE: Backend<B>,
R: ProvideRuntimeApi<B>,
R::Api: BeefyApi<B, AuthorityId>,
{
let blockchain = backend.blockchain();
// Walk up the chain looking for the validator set active at 'at_header'. Process both state and
// header digests.
debug!(
target: LOG_TARGET,
"🥩 Trying to find validator set active at header(number {:?}, hash {:?})",
at_header.number(),
at_header.hash()
);
let mut header = at_header.clone();
loop {
debug!(target: LOG_TARGET, "🥩 Looking for auth set change at block number: {:?}", *header.number());
if let Ok(Some(active)) = runtime.runtime_api().validator_set(header.hash()) {
return Ok(active);
} else {
match find_authorities_change::<B, AuthorityId>(&header) {
Some(active) => return Ok(active),
// Move up the chain. Ultimately we'll get it from chain genesis state, or error out
// there.
None =>
header = wait_for_parent_header(blockchain, header, HEADER_SYNC_DELAY)
.await
.map_err(|e| Error::Backend(e.to_string()))?,
}
}
}
}
/// Scan the `header` digest log for a BEEFY validator set change. Return either the new
/// validator set or `None` in case no validator set change has been signaled.
pub(crate) fn find_authorities_change<B, AuthorityId>(
header: &B::Header,
) -> Option<ValidatorSet<AuthorityId>>
where
B: Block,
AuthorityId: AuthorityIdBound,
{
let id = OpaqueDigestItemId::Consensus(&BEEFY_ENGINE_ID);
let filter = |log: ConsensusLog<AuthorityId>| match log {
ConsensusLog::AuthoritiesChange(validator_set) => Some(validator_set),
_ => None,
};
header.digest().convert_first(|l| l.try_to(id).and_then(filter))
}
@@ -0,0 +1,354 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! BEEFY Prometheus metrics definition
use crate::LOG_TARGET;
use log::{debug, error};
use prometheus_endpoint::{register, Counter, Gauge, PrometheusError, Registry, U64};
/// Helper trait for registering BEEFY metrics to Prometheus registry.
pub(crate) trait PrometheusRegister<T: Sized = Self>: Sized {
const DESCRIPTION: &'static str;
fn register(registry: &Registry) -> Result<Self, PrometheusError>;
}
/// BEEFY voting-related metrics exposed through Prometheus
#[derive(Clone, Debug)]
pub struct VoterMetrics {
/// Current active validator set id
pub beefy_validator_set_id: Gauge<U64>,
/// Total number of votes sent by this node
pub beefy_votes_sent: Counter<U64>,
/// Best block finalized by BEEFY
pub beefy_best_block: Gauge<U64>,
/// Best block BEEFY voted on
pub beefy_best_voted: Gauge<U64>,
/// Next block BEEFY should vote on
pub beefy_should_vote_on: Gauge<U64>,
/// Number of sessions with lagging signed commitment on mandatory block
pub beefy_lagging_sessions: Counter<U64>,
/// Number of times no Authority public key found in store
pub beefy_no_authority_found_in_store: Counter<U64>,
/// Number of good votes successfully handled
pub beefy_good_votes_processed: Counter<U64>,
/// Number of equivocation votes received
pub beefy_equivocation_votes: Counter<U64>,
/// Number of invalid votes received
pub beefy_invalid_votes: Counter<U64>,
/// Number of valid but stale votes received
pub beefy_stale_votes: Counter<U64>,
/// Number of currently buffered justifications
pub beefy_buffered_justifications: Gauge<U64>,
/// Number of valid but stale justifications received
pub beefy_stale_justifications: Counter<U64>,
/// Number of valid justifications successfully imported
pub beefy_imported_justifications: Counter<U64>,
/// Number of justifications dropped due to full buffers
pub beefy_buffered_justifications_dropped: Counter<U64>,
}
impl PrometheusRegister for VoterMetrics {
const DESCRIPTION: &'static str = "voter";
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
beefy_validator_set_id: register(
Gauge::new(
"bizinikiwi_beefy_validator_set_id",
"Current BEEFY active validator set id.",
)?,
registry,
)?,
beefy_votes_sent: register(
Counter::new("bizinikiwi_beefy_votes_sent", "Number of votes sent by this node")?,
registry,
)?,
beefy_best_block: register(
Gauge::new("bizinikiwi_beefy_best_block", "Best block finalized by BEEFY")?,
registry,
)?,
beefy_best_voted: register(
Gauge::new("bizinikiwi_beefy_best_voted", "Best block voted on by BEEFY")?,
registry,
)?,
beefy_should_vote_on: register(
Gauge::new("bizinikiwi_beefy_should_vote_on", "Next block, BEEFY should vote on")?,
registry,
)?,
beefy_lagging_sessions: register(
Counter::new(
"bizinikiwi_beefy_lagging_sessions",
"Number of sessions with lagging signed commitment on mandatory block",
)?,
registry,
)?,
beefy_no_authority_found_in_store: register(
Counter::new(
"bizinikiwi_beefy_no_authority_found_in_store",
"Number of times no Authority public key found in store",
)?,
registry,
)?,
beefy_good_votes_processed: register(
Counter::new(
"bizinikiwi_beefy_successful_handled_votes",
"Number of good votes successfully handled",
)?,
registry,
)?,
beefy_equivocation_votes: register(
Counter::new(
"bizinikiwi_beefy_equivocation_votes",
"Number of equivocation votes received",
)?,
registry,
)?,
beefy_invalid_votes: register(
Counter::new("bizinikiwi_beefy_invalid_votes", "Number of invalid votes received")?,
registry,
)?,
beefy_stale_votes: register(
Counter::new(
"bizinikiwi_beefy_stale_votes",
"Number of valid but stale votes received",
)?,
registry,
)?,
beefy_buffered_justifications: register(
Gauge::new(
"bizinikiwi_beefy_buffered_justifications",
"Number of currently buffered justifications",
)?,
registry,
)?,
beefy_stale_justifications: register(
Counter::new(
"bizinikiwi_beefy_stale_justifications",
"Number of valid but stale justifications received",
)?,
registry,
)?,
beefy_imported_justifications: register(
Counter::new(
"bizinikiwi_beefy_imported_justifications",
"Number of valid justifications successfully imported",
)?,
registry,
)?,
beefy_buffered_justifications_dropped: register(
Counter::new(
"bizinikiwi_beefy_buffered_justifications_dropped",
"Number of justifications dropped due to full buffers",
)?,
registry,
)?,
})
}
}
/// BEEFY block-import-related metrics exposed through Prometheus
#[derive(Clone, Debug)]
pub struct BlockImportMetrics {
/// Number of Good Justification imports
pub beefy_good_justification_imports: Counter<U64>,
/// Number of Bad Justification imports
pub beefy_bad_justification_imports: Counter<U64>,
}
impl PrometheusRegister for BlockImportMetrics {
const DESCRIPTION: &'static str = "block-import";
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
beefy_good_justification_imports: register(
Counter::new(
"bizinikiwi_beefy_good_justification_imports",
"Number of good justifications on block-import",
)?,
registry,
)?,
beefy_bad_justification_imports: register(
Counter::new(
"bizinikiwi_beefy_bad_justification_imports",
"Number of bad justifications on block-import",
)?,
registry,
)?,
})
}
}
/// BEEFY on-demand-justifications-related metrics exposed through Prometheus
#[derive(Clone, Debug)]
pub struct OnDemandIncomingRequestsMetrics {
/// Number of Successful Justification responses
pub beefy_successful_justification_responses: Counter<U64>,
/// Number of Failed Justification responses
pub beefy_failed_justification_responses: Counter<U64>,
}
impl PrometheusRegister for OnDemandIncomingRequestsMetrics {
const DESCRIPTION: &'static str = "on-demand incoming justification requests";
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
beefy_successful_justification_responses: register(
Counter::new(
"bizinikiwi_beefy_successful_justification_responses",
"Number of Successful Justification responses",
)?,
registry,
)?,
beefy_failed_justification_responses: register(
Counter::new(
"bizinikiwi_beefy_failed_justification_responses",
"Number of Failed Justification responses",
)?,
registry,
)?,
})
}
}
/// BEEFY on-demand-justifications-related metrics exposed through Prometheus
#[derive(Clone, Debug)]
pub struct OnDemandOutgoingRequestsMetrics {
/// Number of times there was no good peer to request justification from
pub beefy_on_demand_justification_no_peer_to_request_from: Counter<U64>,
/// Number of on-demand justification peer refused valid requests
pub beefy_on_demand_justification_peer_refused: Counter<U64>,
/// Number of on-demand justification peer error
pub beefy_on_demand_justification_peer_error: Counter<U64>,
/// Number of on-demand justification invalid proof
pub beefy_on_demand_justification_invalid_proof: Counter<U64>,
/// Number of on-demand justification good proof
pub beefy_on_demand_justification_good_proof: Counter<U64>,
/// Number of live beefy peers available for requests.
pub beefy_on_demand_live_peers: Gauge<U64>,
}
impl PrometheusRegister for OnDemandOutgoingRequestsMetrics {
const DESCRIPTION: &'static str = "on-demand outgoing justification requests";
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
beefy_on_demand_justification_no_peer_to_request_from: register(
Counter::new(
"bizinikiwi_beefy_on_demand_justification_no_peer_to_request_from",
"Number of times there was no good peer to request justification from",
)?,
registry,
)?,
beefy_on_demand_justification_peer_refused: register(
Counter::new(
"beefy_on_demand_justification_peer_refused",
"Number of on-demand justification peer refused valid requests",
)?,
registry,
)?,
beefy_on_demand_justification_peer_error: register(
Counter::new(
"bizinikiwi_beefy_on_demand_justification_peer_error",
"Number of on-demand justification peer error",
)?,
registry,
)?,
beefy_on_demand_justification_invalid_proof: register(
Counter::new(
"bizinikiwi_beefy_on_demand_justification_invalid_proof",
"Number of on-demand justification invalid proof",
)?,
registry,
)?,
beefy_on_demand_justification_good_proof: register(
Counter::new(
"bizinikiwi_beefy_on_demand_justification_good_proof",
"Number of on-demand justification good proof",
)?,
registry,
)?,
beefy_on_demand_live_peers: register(
Gauge::new(
"bizinikiwi_beefy_on_demand_live_peers",
"Number of live beefy peers available for requests.",
)?,
registry,
)?,
})
}
}
pub(crate) fn register_metrics<T: PrometheusRegister>(
prometheus_registry: Option<prometheus_endpoint::Registry>,
) -> Option<T> {
prometheus_registry.as_ref().map(T::register).and_then(|result| match result {
Ok(metrics) => {
debug!(target: LOG_TARGET, "🥩 Registered {} metrics", T::DESCRIPTION);
Some(metrics)
},
Err(err) => {
error!(
target: LOG_TARGET,
"🥩 Failed to register {} metrics: {:?}",
T::DESCRIPTION,
err
);
None
},
})
}
// Note: we use the `format` macro to convert an expr into a `u64`. This will fail,
// if expr does not derive `Display`.
#[macro_export]
macro_rules! metric_set {
($metrics:expr, $m:ident, $v:expr) => {{
let val: u64 = format!("{}", $v).parse().unwrap();
if let Some(metrics) = $metrics.as_ref() {
metrics.$m.set(val);
}
}};
}
#[macro_export]
macro_rules! metric_inc {
($metrics:expr, $m:ident) => {{
if let Some(metrics) = $metrics.as_ref() {
metrics.$m.inc();
}
}};
}
#[macro_export]
macro_rules! metric_get {
($metrics:expr, $m:ident) => {{
$metrics.as_ref().map(|metrics| metrics.$m.clone())
}};
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
#[test]
fn should_register_metrics() {
let registry = Some(Registry::new());
assert!(register_metrics::<VoterMetrics>(registry.clone()).is_some());
assert!(register_metrics::<BlockImportMetrics>(registry.clone()).is_some());
assert!(register_metrics::<OnDemandIncomingRequestsMetrics>(registry.clone()).is_some());
assert!(register_metrics::<OnDemandOutgoingRequestsMetrics>(registry.clone()).is_some());
}
}
@@ -0,0 +1,528 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::LOG_TARGET;
use codec::{Decode, Encode};
use log::{debug, info};
use pezsp_application_crypto::RuntimeAppPublic;
use pezsp_consensus_beefy::{
AuthorityIdBound, Commitment, DoubleVotingProof, SignedCommitment, ValidatorSet,
ValidatorSetId, VoteMessage,
};
use pezsp_runtime::traits::{Block, NumberFor};
use std::collections::BTreeMap;
/// Tracks for each round which validators have voted/signed and
/// whether the local `self` validator has voted/signed.
///
/// Does not do any validation on votes or signatures, layers above need to handle that (gossip).
#[derive(Debug, Decode, Encode, PartialEq)]
pub(crate) struct RoundTracker<AuthorityId: AuthorityIdBound> {
votes: BTreeMap<AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature>,
}
impl<AuthorityId: AuthorityIdBound> Default for RoundTracker<AuthorityId> {
fn default() -> Self {
Self { votes: Default::default() }
}
}
impl<AuthorityId: AuthorityIdBound> RoundTracker<AuthorityId> {
fn add_vote(
&mut self,
vote: (AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature),
) -> bool {
if self.votes.contains_key(&vote.0) {
return false;
}
self.votes.insert(vote.0, vote.1);
true
}
fn is_done(&self, threshold: usize) -> bool {
self.votes.len() >= threshold
}
}
/// Minimum size of `authorities` subset that produced valid signatures for a block to finalize.
pub fn threshold(authorities: usize) -> usize {
let faulty = authorities.saturating_sub(1) / 3;
authorities - faulty
}
#[derive(Debug, PartialEq)]
pub enum VoteImportResult<B: Block, AuthorityId: AuthorityIdBound> {
Ok,
RoundConcluded(SignedCommitment<NumberFor<B>, <AuthorityId as RuntimeAppPublic>::Signature>),
DoubleVoting(
DoubleVotingProof<NumberFor<B>, AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature>,
),
Invalid,
Stale,
}
/// Keeps track of all voting rounds (block numbers) within a session.
/// Only round numbers > `best_done` are of interest, all others are considered stale.
///
/// Does not do any validation on votes or signatures, layers above need to handle that (gossip).
#[derive(Debug, Decode, Encode, PartialEq)]
pub(crate) struct Rounds<B: Block, AuthorityId: AuthorityIdBound> {
rounds: BTreeMap<Commitment<NumberFor<B>>, RoundTracker<AuthorityId>>,
previous_votes: BTreeMap<
(AuthorityId, NumberFor<B>),
VoteMessage<NumberFor<B>, AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature>,
>,
session_start: NumberFor<B>,
validator_set: ValidatorSet<AuthorityId>,
mandatory_done: bool,
best_done: Option<NumberFor<B>>,
}
impl<B, AuthorityId> Rounds<B, AuthorityId>
where
B: Block,
AuthorityId: AuthorityIdBound,
{
pub(crate) fn new(
session_start: NumberFor<B>,
validator_set: ValidatorSet<AuthorityId>,
) -> Self {
Rounds {
rounds: BTreeMap::new(),
previous_votes: BTreeMap::new(),
session_start,
validator_set,
mandatory_done: false,
best_done: None,
}
}
pub(crate) fn validator_set(&self) -> &ValidatorSet<AuthorityId> {
&self.validator_set
}
pub(crate) fn validator_set_id(&self) -> ValidatorSetId {
self.validator_set.id()
}
pub(crate) fn validators(&self) -> &[AuthorityId] {
self.validator_set.validators()
}
pub(crate) fn session_start(&self) -> NumberFor<B> {
self.session_start
}
pub(crate) fn mandatory_done(&self) -> bool {
self.mandatory_done
}
pub(crate) fn add_vote(
&mut self,
vote: VoteMessage<NumberFor<B>, AuthorityId, <AuthorityId as RuntimeAppPublic>::Signature>,
) -> VoteImportResult<B, AuthorityId> {
let num = vote.commitment.block_number;
let vote_key = (vote.id.clone(), num);
if num < self.session_start || Some(num) <= self.best_done {
debug!(target: LOG_TARGET, "🥩 received vote for old stale round {:?}, ignoring", num);
return VoteImportResult::Stale;
} else if vote.commitment.validator_set_id != self.validator_set_id() {
debug!(
target: LOG_TARGET,
"🥩 expected set_id {:?}, ignoring vote {:?}.",
self.validator_set_id(),
vote,
);
return VoteImportResult::Invalid;
} else if !self.validators().iter().any(|id| &vote.id == id) {
debug!(
target: LOG_TARGET,
"🥩 received vote {:?} from validator that is not in the validator set, ignoring",
vote
);
return VoteImportResult::Invalid;
}
if let Some(previous_vote) = self.previous_votes.get(&vote_key) {
// is the same public key voting for a different payload?
if previous_vote.commitment.payload != vote.commitment.payload {
debug!(
target: LOG_TARGET,
"🥩 detected equivocated vote: 1st: {:?}, 2nd: {:?}", previous_vote, vote
);
return VoteImportResult::DoubleVoting(DoubleVotingProof {
first: previous_vote.clone(),
second: vote,
});
}
} else {
// this is the first vote sent by `id` for `num`, all good
self.previous_votes.insert(vote_key, vote.clone());
}
// add valid vote
let round = self.rounds.entry(vote.commitment.clone()).or_default();
if round.add_vote((vote.id, vote.signature)) &&
round.is_done(threshold(self.validator_set.len()))
{
if let Some(round) = self.rounds.remove_entry(&vote.commitment) {
return VoteImportResult::RoundConcluded(self.signed_commitment(round));
}
}
VoteImportResult::Ok
}
fn signed_commitment(
&mut self,
round: (Commitment<NumberFor<B>>, RoundTracker<AuthorityId>),
) -> SignedCommitment<NumberFor<B>, <AuthorityId as RuntimeAppPublic>::Signature> {
let votes = round.1.votes;
let signatures = self
.validators()
.iter()
.map(|authority_id| votes.get(authority_id).cloned())
.collect();
SignedCommitment { commitment: round.0, signatures }
}
pub(crate) fn conclude(&mut self, round_num: NumberFor<B>) {
// Remove this and older (now stale) rounds.
self.rounds.retain(|commitment, _| commitment.block_number > round_num);
self.previous_votes.retain(|&(_, number), _| number > round_num);
self.mandatory_done = self.mandatory_done || round_num == self.session_start;
self.best_done = self.best_done.max(Some(round_num));
if round_num == self.session_start {
info!(target: LOG_TARGET, "🥩 Concluded mandatory round #{}", round_num);
} else {
debug!(target: LOG_TARGET, "🥩 Concluded optional round #{}", round_num);
}
}
}
#[cfg(test)]
mod tests {
use pezsc_network_test::Block;
use pezsp_consensus_beefy::{
ecdsa_crypto, known_payloads::MMR_ROOT_ID, test_utils::Keyring, Commitment,
DoubleVotingProof, Payload, SignedCommitment, ValidatorSet, VoteMessage,
};
use super::{threshold, Block as BlockT, RoundTracker, Rounds};
use crate::round::VoteImportResult;
impl<B> Rounds<B, ecdsa_crypto::AuthorityId>
where
B: BlockT,
{
pub(crate) fn test_set_mandatory_done(&mut self, done: bool) {
self.mandatory_done = done;
}
}
#[test]
fn round_tracker() {
let mut rt = RoundTracker::<ecdsa_crypto::AuthorityId>::default();
let bob_vote = (
Keyring::<ecdsa_crypto::AuthorityId>::Bob.public(),
Keyring::<ecdsa_crypto::AuthorityId>::Bob.sign(b"I am committed"),
);
let threshold = 2;
// adding new vote allowed
assert!(rt.add_vote(bob_vote.clone()));
// adding existing vote not allowed
assert!(!rt.add_vote(bob_vote));
// vote is not done
assert!(!rt.is_done(threshold));
let alice_vote = (
Keyring::<ecdsa_crypto::AuthorityId>::Alice.public(),
Keyring::<ecdsa_crypto::AuthorityId>::Alice.sign(b"I am committed"),
);
// adding new vote (self vote this time) allowed
assert!(rt.add_vote(alice_vote));
// vote is now done
assert!(rt.is_done(threshold));
}
#[test]
fn vote_threshold() {
assert_eq!(threshold(1), 1);
assert_eq!(threshold(2), 2);
assert_eq!(threshold(3), 3);
assert_eq!(threshold(4), 3);
assert_eq!(threshold(100), 67);
assert_eq!(threshold(300), 201);
}
#[test]
fn new_rounds() {
pezsp_tracing::try_init_simple();
let validators = ValidatorSet::<ecdsa_crypto::AuthorityId>::new(
vec![Keyring::Alice.public(), Keyring::Bob.public(), Keyring::Charlie.public()],
42,
)
.unwrap();
let session_start = 1u64.into();
let rounds = Rounds::<Block, ecdsa_crypto::AuthorityId>::new(session_start, validators);
assert_eq!(42, rounds.validator_set_id());
assert_eq!(1, rounds.session_start());
assert_eq!(
&vec![
Keyring::<ecdsa_crypto::AuthorityId>::Alice.public(),
Keyring::<ecdsa_crypto::AuthorityId>::Bob.public(),
Keyring::<ecdsa_crypto::AuthorityId>::Charlie.public()
],
rounds.validators()
);
}
#[test]
fn add_and_conclude_votes() {
pezsp_tracing::try_init_simple();
let validators = ValidatorSet::<ecdsa_crypto::AuthorityId>::new(
vec![
Keyring::Alice.public(),
Keyring::Bob.public(),
Keyring::Charlie.public(),
Keyring::Eve.public(),
],
Default::default(),
)
.unwrap();
let validator_set_id = validators.id();
let session_start = 1u64.into();
let mut rounds = Rounds::<Block, ecdsa_crypto::AuthorityId>::new(session_start, validators);
let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![]);
let block_number = 1;
let commitment = Commitment { block_number, payload, validator_set_id };
let mut vote = VoteMessage {
id: Keyring::Alice.public(),
commitment: commitment.clone(),
signature: Keyring::<ecdsa_crypto::AuthorityId>::Alice.sign(b"I am committed"),
};
// add 1st good vote
assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Ok);
// double voting (same vote), ok, no effect
assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Ok);
vote.id = Keyring::Dave.public();
vote.signature = Keyring::<ecdsa_crypto::AuthorityId>::Dave.sign(b"I am committed");
// invalid vote (Dave is not a validator)
assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Invalid);
vote.id = Keyring::Bob.public();
vote.signature = Keyring::<ecdsa_crypto::AuthorityId>::Bob.sign(b"I am committed");
// add 2nd good vote
assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Ok);
vote.id = Keyring::Charlie.public();
vote.signature = Keyring::<ecdsa_crypto::AuthorityId>::Charlie.sign(b"I am committed");
// add 3rd good vote -> round concluded -> signatures present
assert_eq!(
rounds.add_vote(vote.clone()),
VoteImportResult::RoundConcluded(SignedCommitment {
commitment,
signatures: vec![
Some(Keyring::<ecdsa_crypto::AuthorityId>::Alice.sign(b"I am committed")),
Some(Keyring::<ecdsa_crypto::AuthorityId>::Bob.sign(b"I am committed")),
Some(Keyring::<ecdsa_crypto::AuthorityId>::Charlie.sign(b"I am committed")),
None,
]
})
);
rounds.conclude(block_number);
vote.id = Keyring::Eve.public();
vote.signature = Keyring::<ecdsa_crypto::AuthorityId>::Eve.sign(b"I am committed");
// Eve is a validator, but round was concluded, adding vote disallowed
assert_eq!(rounds.add_vote(vote), VoteImportResult::Stale);
}
#[test]
fn old_rounds_not_accepted() {
pezsp_tracing::try_init_simple();
let validators = ValidatorSet::<ecdsa_crypto::AuthorityId>::new(
vec![Keyring::Alice.public(), Keyring::Bob.public(), Keyring::Charlie.public()],
42,
)
.unwrap();
let validator_set_id = validators.id();
// active rounds starts at block 10
let session_start = 10u64.into();
let mut rounds = Rounds::<Block, ecdsa_crypto::AuthorityId>::new(session_start, validators);
// vote on round 9
let block_number = 9;
let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![]);
let commitment = Commitment { block_number, payload, validator_set_id };
let mut vote = VoteMessage {
id: Keyring::Alice.public(),
commitment,
signature: Keyring::<ecdsa_crypto::AuthorityId>::Alice.sign(b"I am committed"),
};
// add vote for previous session, should fail
assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Stale);
// no votes present
assert!(rounds.rounds.is_empty());
// simulate 11 was concluded
rounds.best_done = Some(11);
// add votes for current session, but already concluded rounds, should fail
vote.commitment.block_number = 10;
assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Stale);
vote.commitment.block_number = 11;
assert_eq!(rounds.add_vote(vote.clone()), VoteImportResult::Stale);
// no votes present
assert!(rounds.rounds.is_empty());
// add vote for active round 12
vote.commitment.block_number = 12;
assert_eq!(rounds.add_vote(vote), VoteImportResult::Ok);
// good vote present
assert_eq!(rounds.rounds.len(), 1);
}
#[test]
fn multiple_rounds() {
pezsp_tracing::try_init_simple();
let validators = ValidatorSet::<ecdsa_crypto::AuthorityId>::new(
vec![Keyring::Alice.public(), Keyring::Bob.public(), Keyring::Charlie.public()],
Default::default(),
)
.unwrap();
let validator_set_id = validators.id();
let session_start = 1u64.into();
let mut rounds = Rounds::<Block, ecdsa_crypto::AuthorityId>::new(session_start, validators);
let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![]);
let commitment = Commitment { block_number: 1, payload, validator_set_id };
let mut alice_vote = VoteMessage {
id: Keyring::Alice.public(),
commitment: commitment.clone(),
signature: Keyring::<ecdsa_crypto::AuthorityId>::Alice.sign(b"I am committed"),
};
let mut bob_vote = VoteMessage {
id: Keyring::Bob.public(),
commitment: commitment.clone(),
signature: Keyring::<ecdsa_crypto::AuthorityId>::Bob.sign(b"I am committed"),
};
let mut charlie_vote = VoteMessage {
id: Keyring::Charlie.public(),
commitment,
signature: Keyring::<ecdsa_crypto::AuthorityId>::Charlie.sign(b"I am committed"),
};
let expected_signatures = vec![
Some(Keyring::<ecdsa_crypto::AuthorityId>::Alice.sign(b"I am committed")),
Some(Keyring::<ecdsa_crypto::AuthorityId>::Bob.sign(b"I am committed")),
Some(Keyring::<ecdsa_crypto::AuthorityId>::Charlie.sign(b"I am committed")),
];
// round 1 - only 2 out of 3 vote
assert_eq!(rounds.add_vote(alice_vote.clone()), VoteImportResult::Ok);
assert_eq!(rounds.add_vote(charlie_vote.clone()), VoteImportResult::Ok);
// should be 1 active round
assert_eq!(1, rounds.rounds.len());
// round 2 - only Charlie votes
charlie_vote.commitment.block_number = 2;
assert_eq!(rounds.add_vote(charlie_vote.clone()), VoteImportResult::Ok);
// should be 2 active rounds
assert_eq!(2, rounds.rounds.len());
// round 3 - all validators vote -> round is concluded
alice_vote.commitment.block_number = 3;
bob_vote.commitment.block_number = 3;
charlie_vote.commitment.block_number = 3;
assert_eq!(rounds.add_vote(alice_vote.clone()), VoteImportResult::Ok);
assert_eq!(rounds.add_vote(bob_vote.clone()), VoteImportResult::Ok);
assert_eq!(
rounds.add_vote(charlie_vote.clone()),
VoteImportResult::RoundConcluded(SignedCommitment {
commitment: charlie_vote.commitment,
signatures: expected_signatures
})
);
// should be only 2 active since this one auto-concluded
assert_eq!(2, rounds.rounds.len());
// conclude round 2
rounds.conclude(2);
// should be no more active rounds since 2 was officially concluded and round "1" is stale
assert!(rounds.rounds.is_empty());
// conclude round 3
rounds.conclude(3);
assert!(rounds.previous_votes.is_empty());
}
#[test]
fn should_provide_equivocation_proof() {
pezsp_tracing::try_init_simple();
let validators = ValidatorSet::<ecdsa_crypto::AuthorityId>::new(
vec![Keyring::Alice.public(), Keyring::Bob.public()],
Default::default(),
)
.unwrap();
let validator_set_id = validators.id();
let session_start = 1u64.into();
let mut rounds = Rounds::<Block, ecdsa_crypto::AuthorityId>::new(session_start, validators);
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![1, 1, 1, 1]);
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![2, 2, 2, 2]);
let commitment1 = Commitment { block_number: 1, payload: payload1, validator_set_id };
let commitment2 = Commitment { block_number: 1, payload: payload2, validator_set_id };
let alice_vote1 = VoteMessage {
id: Keyring::Alice.public(),
commitment: commitment1,
signature: Keyring::<ecdsa_crypto::AuthorityId>::Alice.sign(b"I am committed"),
};
let mut alice_vote2 = alice_vote1.clone();
alice_vote2.commitment = commitment2;
let expected_result = VoteImportResult::DoubleVoting(DoubleVotingProof {
first: alice_vote1.clone(),
second: alice_vote2.clone(),
});
// vote on one payload - ok
assert_eq!(rounds.add_vote(alice_vote1), VoteImportResult::Ok);
// vote on _another_ commitment/payload -> expected equivocation proof
assert_eq!(rounds.add_vote(alice_vote2), expected_result);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff