// This file is part of Substrate.
// 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 .
use crate::{
communication::{
gossip::{proofs_topic, votes_topic, GossipFilterCfg, GossipMessage},
request_response::outgoing_requests_engine::ResponseInfo,
},
error::Error,
find_authorities_change,
justification::BeefyVersionedFinalityProof,
keystore::BeefyKeystore,
metric_inc, metric_set,
metrics::VoterMetrics,
round::{Rounds, VoteImportResult},
BeefyComms, BeefyVoterLinks, LOG_TARGET,
};
use codec::{Codec, Decode, DecodeAll, Encode};
use futures::{stream::Fuse, FutureExt, StreamExt};
use log::{debug, error, info, trace, warn};
use sc_client_api::{Backend, FinalityNotification, FinalityNotifications, HeaderBackend};
use sc_utils::notification::NotificationReceiver;
use sp_api::ProvideRuntimeApi;
use sp_arithmetic::traits::{AtLeast32Bit, Saturating};
use sp_consensus::SyncOracle;
use sp_consensus_beefy::{
check_equivocation_proof,
ecdsa_crypto::{AuthorityId, Signature},
BeefyApi, BeefySignatureHasher, Commitment, EquivocationProof, PayloadProvider, ValidatorSet,
VersionedFinalityProof, VoteMessage, BEEFY_ENGINE_ID,
};
use sp_runtime::{
generic::BlockId,
traits::{Block, Header, NumberFor, Zero},
SaturatedConversion,
};
use std::{
collections::{BTreeMap, VecDeque},
fmt::Debug,
sync::Arc,
};
/// Bound for the number of pending justifications - use 2400 - the max number
/// of justifications possible in a single session.
const MAX_BUFFERED_JUSTIFICATIONS: usize = 2400;
pub(crate) enum RoundAction {
Drop,
Process,
Enqueue,
}
/// Responsible for the voting strategy.
/// It chooses which incoming votes to accept and which votes to generate.
/// Keeps track of voting seen for current and future rounds.
///
/// Note: this is part of `PersistedState` so any changes here should also bump
/// aux-db schema version.
#[derive(Debug, Decode, Encode, PartialEq)]
pub(crate) struct VoterOracle {
/// Queue of known sessions. Keeps track of voting rounds (block numbers) within each session.
///
/// There are three voter states corresponding to three queue states:
/// 1. voter uninitialized: queue empty,
/// 2. up-to-date - all mandatory blocks leading up to current GRANDPA finalized: queue has ONE
/// element, the 'current session' where `mandatory_done == true`,
/// 3. lagging behind GRANDPA: queue has [1, N] elements, where all `mandatory_done == false`.
/// In this state, every time a session gets its mandatory block BEEFY finalized, it's
/// popped off the queue, eventually getting to state `2. up-to-date`.
sessions: VecDeque>,
/// Min delta in block numbers between two blocks, BEEFY should vote on.
min_block_delta: u32,
/// Best block we received a GRANDPA finality for.
best_grandpa_block_header: ::Header,
/// Best block a BEEFY voting round has been concluded for.
best_beefy_block: NumberFor,
}
impl VoterOracle {
/// Verify provided `sessions` satisfies requirements, then build `VoterOracle`.
pub fn checked_new(
sessions: VecDeque>,
min_block_delta: u32,
grandpa_header: ::Header,
best_beefy: NumberFor,
) -> Option {
let mut prev_start = Zero::zero();
let mut prev_validator_id = None;
// verifies the
let mut validate = || -> bool {
let best_grandpa = *grandpa_header.number();
if sessions.is_empty() || best_beefy > best_grandpa {
return false
}
for (idx, session) in sessions.iter().enumerate() {
let start = session.session_start();
if session.validators().is_empty() {
return false
}
if start > best_grandpa || start <= prev_start {
return false
}
#[cfg(not(test))]
if let Some(prev_id) = prev_validator_id {
if session.validator_set_id() <= prev_id {
return false
}
}
if idx != 0 && session.mandatory_done() {
return false
}
prev_start = session.session_start();
prev_validator_id = Some(session.validator_set_id());
}
true
};
if validate() {
Some(VoterOracle {
sessions,
// Always target at least one block better than current best beefy.
min_block_delta: min_block_delta.max(1),
best_grandpa_block_header: grandpa_header,
best_beefy_block: best_beefy,
})
} else {
error!(
target: LOG_TARGET,
"🥩 Invalid sessions queue: {:?}; best-beefy {:?} best-grandpa-header {:?}.",
sessions,
best_beefy,
grandpa_header
);
None
}
}
// Return reference to rounds pertaining to first session in the queue.
// Voting will always happen at the head of the queue.
fn active_rounds(&self) -> Result<&Rounds, Error> {
self.sessions.front().ok_or(Error::UninitSession)
}
// Return mutable reference to rounds pertaining to first session in the queue.
// Voting will always happen at the head of the queue.
fn active_rounds_mut(&mut self) -> Result<&mut Rounds, Error> {
self.sessions.front_mut().ok_or(Error::UninitSession)
}
fn current_validator_set(&self) -> Result<&ValidatorSet, Error> {
self.active_rounds().map(|r| r.validator_set())
}
// Prune the sessions queue to keep the Oracle in one of the expected three states.
//
// To be called on each BEEFY finality and on each new rounds/session addition.
fn try_prune(&mut self) {
if self.sessions.len() > 1 {
// when there's multiple sessions, only keep the `!mandatory_done()` ones.
self.sessions.retain(|s| !s.mandatory_done())
}
}
/// Check if an observed session can be added to the Oracle.
pub fn can_add_session(&self, session_start: NumberFor) -> bool {
let latest_known_session_start =
self.sessions.back().map(|session| session.session_start());
Some(session_start) > latest_known_session_start
}
/// Add new observed session to the Oracle.
pub fn add_session(&mut self, rounds: Rounds) {
self.sessions.push_back(rounds);
// Once we add a new session we can drop/prune previous session if it's been finalized.
self.try_prune();
}
/// Finalize a particular block.
pub fn finalize(&mut self, block: NumberFor) -> Result<(), Error> {
// Conclude voting round for this block.
self.active_rounds_mut()?.conclude(block);
// Prune any now "finalized" sessions from queue.
self.try_prune();
Ok(())
}
/// Return current pending mandatory block, if any, plus its active validator set.
pub fn mandatory_pending(&self) -> Option<(NumberFor, ValidatorSet)> {
self.sessions.front().and_then(|round| {
if round.mandatory_done() {
None
} else {
Some((round.session_start(), round.validator_set().clone()))
}
})
}
/// Return `(A, B)` tuple representing inclusive [A, B] interval of votes to accept.
pub fn accepted_interval(&self) -> Result<(NumberFor, NumberFor), Error> {
let rounds = self.sessions.front().ok_or(Error::UninitSession)?;
if rounds.mandatory_done() {
// There's only one session active and its mandatory is done.
// Accept any vote for a GRANDPA finalized block in a better round.
Ok((
rounds.session_start().max(self.best_beefy_block),
(*self.best_grandpa_block_header.number()),
))
} else {
// Current session has mandatory not done.
// Only accept votes for the mandatory block in the front of queue.
Ok((rounds.session_start(), rounds.session_start()))
}
}
/// Utility function to quickly decide what to do for each round.
pub fn triage_round(&self, round: NumberFor) -> Result {
let (start, end) = self.accepted_interval()?;
if start <= round && round <= end {
Ok(RoundAction::Process)
} else if round > end {
Ok(RoundAction::Enqueue)
} else {
Ok(RoundAction::Drop)
}
}
/// Return `Some(number)` if we should be voting on block `number`,
/// return `None` if there is no block we should vote on.
pub fn voting_target(&self) -> Option> {
let rounds = self.sessions.front().or_else(|| {
debug!(target: LOG_TARGET, "🥩 No voting round started");
None
})?;
let best_grandpa = *self.best_grandpa_block_header.number();
let best_beefy = self.best_beefy_block;
// `target` is guaranteed > `best_beefy` since `min_block_delta` is at least `1`.
let target =
vote_target(best_grandpa, best_beefy, rounds.session_start(), self.min_block_delta);
trace!(
target: LOG_TARGET,
"🥩 best beefy: #{:?}, best finalized: #{:?}, current_vote_target: {:?}",
best_beefy,
best_grandpa,
target
);
target
}
}
/// BEEFY voter state persisted in aux DB.
///
/// Note: Any changes here should also bump aux-db schema version.
#[derive(Debug, Decode, Encode, PartialEq)]
pub(crate) struct PersistedState {
/// Best block we voted on.
best_voted: NumberFor,
/// Chooses which incoming votes to accept and which votes to generate.
/// Keeps track of voting seen for current and future rounds.
voting_oracle: VoterOracle,
/// Pallet-beefy genesis block - block number when BEEFY consensus started for this chain.
pallet_genesis: NumberFor,
}
impl PersistedState {
pub fn checked_new(
grandpa_header: ::Header,
best_beefy: NumberFor,
sessions: VecDeque>,
min_block_delta: u32,
pallet_genesis: NumberFor,
) -> Option {
VoterOracle::checked_new(sessions, min_block_delta, grandpa_header, best_beefy).map(
|voting_oracle| PersistedState {
best_voted: Zero::zero(),
voting_oracle,
pallet_genesis,
},
)
}
pub fn pallet_genesis(&self) -> NumberFor {
self.pallet_genesis
}
pub(crate) fn set_min_block_delta(&mut self, min_block_delta: u32) {
self.voting_oracle.min_block_delta = min_block_delta.max(1);
}
pub fn best_beefy(&self) -> NumberFor {
self.voting_oracle.best_beefy_block
}
pub(crate) fn set_best_beefy(&mut self, best_beefy: NumberFor) {
self.voting_oracle.best_beefy_block = best_beefy;
}
pub(crate) fn set_best_grandpa(&mut self, best_grandpa: ::Header) {
self.voting_oracle.best_grandpa_block_header = best_grandpa;
}
pub fn voting_oracle(&self) -> &VoterOracle {
&self.voting_oracle
}
pub(crate) fn gossip_filter_config(&self) -> Result, Error> {
let (start, end) = self.voting_oracle.accepted_interval()?;
let validator_set = self.voting_oracle.current_validator_set()?;
Ok(GossipFilterCfg { start, end, validator_set })
}
/// Handle session changes by starting new voting round for mandatory blocks.
pub fn init_session_at(
&mut self,
new_session_start: NumberFor,
validator_set: ValidatorSet,
key_store: &BeefyKeystore,
metrics: &Option,
is_authority: bool,
) {
debug!(target: LOG_TARGET, "🥩 New active validator set: {:?}", validator_set);
// BEEFY should finalize a mandatory block during each session.
if let Ok(active_session) = self.voting_oracle.active_rounds() {
if !active_session.mandatory_done() {
debug!(
target: LOG_TARGET,
"🥩 New session {} while active session {} is still lagging.",
validator_set.id(),
active_session.validator_set_id(),
);
metric_inc!(metrics, beefy_lagging_sessions);
}
}
// verify we have some BEEFY key available in keystore when role is authority.
if is_authority && key_store.public_keys().map_or(false, |k| k.is_empty()) {
error!(
target: LOG_TARGET,
"🥩 for session starting at block {:?} no BEEFY authority key found in store, \
you must generate valid session keys \
(https://wiki.polkadot.network/docs/maintain-guides-how-to-validate-polkadot#generating-the-session-keys)",
new_session_start,
);
metric_inc!(metrics, beefy_no_authority_found_in_store);
}
let id = validator_set.id();
self.voting_oracle.add_session(Rounds::new(new_session_start, validator_set));
metric_set!(metrics, beefy_validator_set_id, id);
info!(
target: LOG_TARGET,
"🥩 New Rounds for validator set id: {:?} with session_start {:?}",
id,
new_session_start
);
}
}
/// A BEEFY worker/voter that follows the BEEFY protocol
pub(crate) struct BeefyWorker {
// utilities
pub backend: Arc,
pub runtime: Arc,
pub key_store: BeefyKeystore,
pub payload_provider: P,
pub sync: Arc,
// communication (created once, but returned and reused if worker is restarted/reinitialized)
pub comms: BeefyComms,
// channels
/// Links between the block importer, the background voter and the RPC layer.
pub links: BeefyVoterLinks,
// voter state
/// Buffer holding justifications for future processing.
pub pending_justifications: BTreeMap, BeefyVersionedFinalityProof>,
/// Persisted voter state.
pub persisted_state: PersistedState,
/// BEEFY voter metrics
pub metrics: Option,
/// Node runs under "Authority" role.
pub is_authority: bool,
}
impl BeefyWorker
where
B: Block + Codec,
BE: Backend,
P: PayloadProvider,
S: SyncOracle,
R: ProvideRuntimeApi,
R::Api: BeefyApi,
{
fn best_grandpa_block(&self) -> NumberFor {
*self.persisted_state.voting_oracle.best_grandpa_block_header.number()
}
fn voting_oracle(&self) -> &VoterOracle {
&self.persisted_state.voting_oracle
}
#[cfg(test)]
fn active_rounds(&mut self) -> Result<&Rounds, Error> {
self.persisted_state.voting_oracle.active_rounds()
}
/// Handle session changes by starting new voting round for mandatory blocks.
fn init_session_at(
&mut self,
validator_set: ValidatorSet,
new_session_start: NumberFor,
) {
self.persisted_state.init_session_at(
new_session_start,
validator_set,
&self.key_store,
&self.metrics,
self.is_authority,
);
}
fn handle_finality_notification(
&mut self,
notification: &FinalityNotification,
) -> Result<(), Error> {
let header = ¬ification.header;
debug!(
target: LOG_TARGET,
"🥩 Finality notification: header(number {:?}, hash {:?}) tree_route {:?}",
header.number(),
header.hash(),
notification.tree_route,
);
self.runtime
.runtime_api()
.beefy_genesis(header.hash())
.ok()
.flatten()
.filter(|genesis| *genesis == self.persisted_state.pallet_genesis)
.ok_or(Error::ConsensusReset)?;
let mut new_session_added = false;
if *header.number() > self.best_grandpa_block() {
// update best GRANDPA finalized block we have seen
self.persisted_state.set_best_grandpa(header.clone());
// Check all (newly) finalized blocks for new session(s).
let backend = self.backend.clone();
for header in notification
.tree_route
.iter()
.map(|hash| {
backend
.blockchain()
.expect_header(*hash)
.expect("just finalized block should be available; qed.")
})
.chain(std::iter::once(header.clone()))
{
if let Some(new_validator_set) = find_authorities_change::(&header) {
self.init_session_at(new_validator_set, *header.number());
new_session_added = true;
}
}
if new_session_added {
crate::aux_schema::write_voter_state(&*self.backend, &self.persisted_state)
.map_err(|e| Error::Backend(e.to_string()))?;
}
// Update gossip validator votes filter.
if let Err(e) = self
.persisted_state
.gossip_filter_config()
.map(|filter| self.comms.gossip_validator.update_filter(filter))
{
error!(target: LOG_TARGET, "🥩 Voter error: {:?}", e);
}
}
Ok(())
}
/// Based on [VoterOracle] this vote is either processed here or discarded.
fn triage_incoming_vote(
&mut self,
vote: VoteMessage, AuthorityId, Signature>,
) -> Result<(), Error> {
let block_num = vote.commitment.block_number;
match self.voting_oracle().triage_round(block_num)? {
RoundAction::Process =>
if let Some(finality_proof) = self.handle_vote(vote)? {
let gossip_proof = GossipMessage::::FinalityProof(finality_proof);
let encoded_proof = gossip_proof.encode();
self.comms.gossip_engine.gossip_message(
proofs_topic::(),
encoded_proof,
true,
);
},
RoundAction::Drop => metric_inc!(self.metrics, beefy_stale_votes),
RoundAction::Enqueue => error!(target: LOG_TARGET, "🥩 unexpected vote: {:?}.", vote),
};
Ok(())
}
/// Based on [VoterOracle] this justification is either processed here or enqueued for later.
///
/// Expects `justification` to be valid.
fn triage_incoming_justif(
&mut self,
justification: BeefyVersionedFinalityProof,
) -> Result<(), Error> {
let signed_commitment = match justification {
VersionedFinalityProof::V1(ref sc) => sc,
};
let block_num = signed_commitment.commitment.block_number;
match self.voting_oracle().triage_round(block_num)? {
RoundAction::Process => {
debug!(target: LOG_TARGET, "🥩 Process justification for round: {:?}.", block_num);
metric_inc!(self.metrics, beefy_imported_justifications);
self.finalize(justification)?
},
RoundAction::Enqueue => {
debug!(target: LOG_TARGET, "🥩 Buffer justification for round: {:?}.", block_num);
if self.pending_justifications.len() < MAX_BUFFERED_JUSTIFICATIONS {
self.pending_justifications.entry(block_num).or_insert(justification);
metric_inc!(self.metrics, beefy_buffered_justifications);
} else {
metric_inc!(self.metrics, beefy_buffered_justifications_dropped);
warn!(
target: LOG_TARGET,
"🥩 Buffer justification dropped for round: {:?}.", block_num
);
}
},
RoundAction::Drop => metric_inc!(self.metrics, beefy_stale_justifications),
};
Ok(())
}
fn handle_vote(
&mut self,
vote: VoteMessage, AuthorityId, Signature>,
) -> Result