diff --git a/client/beefy/src/error.rs b/client/beefy/src/error.rs index b5bddd665108e..4befb59dd39aa 100644 --- a/client/beefy/src/error.rs +++ b/client/beefy/src/error.rs @@ -14,15 +14,17 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +//! BEEFY gadget specific errors +//! +//! Used for BEEFY gadget interal error handling only + use std::fmt::Debug; use sp_core::crypto::Public; -/// BEEFY gadget specific errors -/// Note that this type is currently used for BEEFY gadget internal -/// error handling only. +/// Crypto related errors #[derive(Debug, thiserror::Error)] -pub(crate) enum Error { +pub(crate) enum Crypto { /// Check signature error #[error("Message signature {0} by {1:?} is invalid.")] InvalidSignature(String, Id), @@ -30,3 +32,11 @@ pub(crate) enum Error { #[error("Failed to sign comitment using key: {0:?}. Reason: {1}")] CannotSign(Id, String), } + +/// Lifecycle related errors +#[derive(Debug, thiserror::Error)] +pub(crate) enum Lifecycle { + /// Can't fetch validator set from BEEFY pallet + #[error("Failed to fetch validator set: {0}")] + MissingValidatorSet(String), +} diff --git a/client/beefy/src/lib.rs b/client/beefy/src/lib.rs index fc8e778d61e6e..465ad47058e36 100644 --- a/client/beefy/src/lib.rs +++ b/client/beefy/src/lib.rs @@ -14,20 +14,23 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use std::{convert::TryFrom, fmt::Debug, sync::Arc}; + use beefy_primitives::BeefyApi; use codec::Codec; -use sc_client_api::{Backend as BackendT, BlockchainEvents, Finalizer}; + +use sc_client_api::{Backend, BlockchainEvents, Finalizer}; use sc_network_gossip::{ GossipEngine, Network as GossipNetwork, ValidationResult as GossipValidationResult, Validator as GossipValidator, ValidatorContext as GossipValidatorContext, }; -use sp_api::{BlockId, ProvideRuntimeApi}; + +use sp_api::ProvideRuntimeApi; use sp_application_crypto::AppPublic; use sp_blockchain::HeaderBackend; use sp_consensus::SyncOracle as SyncOracleT; use sp_keystore::SyncCryptoStorePtr; -use sp_runtime::traits::{Block as BlockT, Zero}; -use std::{convert::TryFrom, fmt::Debug, sync::Arc}; +use sp_runtime::traits::Block; mod error; mod round; @@ -52,74 +55,89 @@ pub fn beefy_peers_set_config() -> sc_network::config::NonDefaultSetConfig { } } +/// 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 . +pub(crate) trait Client: + BlockchainEvents + HeaderBackend + Finalizer + ProvideRuntimeApi + Send + Sync +where + B: Block, + BE: Backend, + P: sp_core::Pair, + P::Public: AppPublic + Codec, + P::Signature: Clone + Codec + Debug + PartialEq + TryFrom>, +{ + // empty +} + +impl Client for T +where + B: Block, + BE: Backend, + P: sp_core::Pair, + P::Public: AppPublic + Codec, + P::Signature: Clone + Codec + Debug + PartialEq + TryFrom>, + T: BlockchainEvents + HeaderBackend + Finalizer + ProvideRuntimeApi + Send + Sync, +{ + // empty +} + /// Allows all gossip messages to get through. struct AllowAll { topic: Hash, } -impl GossipValidator for AllowAll +impl GossipValidator for AllowAll where - Block: BlockT, + B: Block, { fn validate( &self, - _context: &mut dyn GossipValidatorContext, + _context: &mut dyn GossipValidatorContext, _sender: &sc_network::PeerId, _data: &[u8], - ) -> GossipValidationResult { + ) -> GossipValidationResult { GossipValidationResult::ProcessAndKeep(self.topic) } } -pub async fn start_beefy_gadget( - client: Arc, +/// Start the BEEFY gadget. +/// +/// This is a thin shim around running and awaiting a BEEFY worker. The [`Client`] +/// convenience trait is not used here on purpose. We don't want to leak it into the +/// public interface of the BEEFY gadget. +pub async fn start_beefy_gadget( + client: Arc, key_store: SyncCryptoStorePtr, - network: Network, - signed_commitment_sender: notification::BeefySignedCommitmentSender, - _sync_oracle: SyncOracle, + network: N, + signed_commitment_sender: notification::BeefySignedCommitmentSender, + _sync_oracle: SO, ) where - Block: BlockT, - Pair: sp_core::Pair, - Pair::Public: AppPublic + Codec, - Pair::Signature: Clone + Codec + Debug + PartialEq + TryFrom>, - Backend: BackendT, - Client: BlockchainEvents - + HeaderBackend - + Finalizer - + ProvideRuntimeApi - + Send - + Sync, - Client::Api: BeefyApi, - Network: GossipNetwork + Clone + Send + 'static, - SyncOracle: SyncOracleT + Send + 'static, + B: Block, + P: sp_core::Pair, + P::Public: AppPublic + Codec, + P::Signature: Clone + Codec + Debug + PartialEq + TryFrom>, + BE: Backend, + C: BlockchainEvents + HeaderBackend + Finalizer + ProvideRuntimeApi + Send + Sync, + C::Api: BeefyApi, + N: GossipNetwork + Clone + Send + 'static, + SO: SyncOracleT + Send + 'static, { let gossip_engine = GossipEngine::new( network, BEEFY_PROTOCOL_NAME, Arc::new(AllowAll { - topic: worker::topic::(), + topic: worker::topic::(), }), None, ); - let at = BlockId::hash(client.info().best_hash); - - let validator_set = client - .runtime_api() - .validator_set(&at) - .expect("Failed to get BEEFY validator set"); - - let best_finalized_block = client.info().finalized_number; - let best_block_voted_on = Zero::zero(); - - let worker = worker::BeefyWorker::<_, Pair::Public, Pair::Signature, _>::new( - validator_set, + let worker = worker::BeefyWorker::<_, P::Signature, _, BE, P>::new( + client.clone(), key_store, - client.finality_notification_stream(), - gossip_engine, signed_commitment_sender, - best_finalized_block, - best_block_voted_on, + gossip_engine, ); worker.run().await diff --git a/client/beefy/src/worker.rs b/client/beefy/src/worker.rs index 3e9acf5bb34ea..2867f895826cc 100644 --- a/client/beefy/src/worker.rs +++ b/client/beefy/src/worker.rs @@ -14,28 +14,45 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + marker::PhantomData, + sync::Arc, +}; + use beefy_primitives::{ - Commitment, ConsensusLog, MmrRootHash, SignedCommitment, ValidatorSet, ValidatorSetId, BEEFY_ENGINE_ID, KEY_TYPE, + BeefyApi, Commitment, ConsensusLog, MmrRootHash, SignedCommitment, ValidatorSet, ValidatorSetId, BEEFY_ENGINE_ID, + KEY_TYPE, }; use codec::{Codec, Decode, Encode}; -use futures::{future, FutureExt, Stream, StreamExt}; +use futures::{future, FutureExt, StreamExt}; use hex::ToHex; use log::{debug, error, info, trace, warn}; use parking_lot::Mutex; -use sc_client_api::FinalityNotification; + +use sc_client_api::{Backend, FinalityNotification, FinalityNotifications}; use sc_network_gossip::GossipEngine; -use sp_application_crypto::Public; + +use sp_api::BlockId; +use sp_application_crypto::{AppPublic, Public}; use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr}; use sp_runtime::{ generic::OpaqueDigestItemId, - traits::{Block as BlockT, Hash as HashT, Header as HeaderT, NumberFor}, + traits::{Block, Hash, Header, NumberFor, Zero}, }; -use std::{convert::TryInto, fmt::Debug, sync::Arc}; -use crate::{error, notification, round}; +use crate::{ + error::{self}, + notification, round, Client, +}; -pub(crate) fn topic() -> Block::Hash { - <::Hashing as HashT>::hash(b"beefy") +/// Gossip engine messages topic +pub(crate) fn topic() -> B::Hash +where + B: Block, +{ + <::Hashing as Hash>::hash(b"beefy") } #[derive(Debug, Decode, Encode)] @@ -44,76 +61,136 @@ struct VoteMessage { id: Id, signature: Signature, } +#[derive(PartialEq)] +/// Worker lifecycle state +enum State { + /// A new worker that still needs to be initialized. + New, + /// A worker that validates and votes for commitments + Validate, + /// A worker that acts as a goosip relay only + Gossip, +} -pub(crate) struct BeefyWorker { - local_id: Option, +pub(crate) struct BeefyWorker +where + B: Block, + BE: Backend, + P: sp_core::Pair, + P::Public: AppPublic + Codec, + P::Signature: Clone + Codec + Debug + PartialEq + TryFrom>, + C: Client, +{ + state: State, + local_id: Option, key_store: SyncCryptoStorePtr, min_interval: u32, - rounds: round::Rounds, Id, Signature>, - finality_notifications: FinalityNotifications, - gossip_engine: Arc>>, - signed_commitment_sender: notification::BeefySignedCommitmentSender, - best_finalized_block: NumberFor, - best_block_voted_on: NumberFor, + rounds: round::Rounds, P::Public, S>, + finality_notifications: FinalityNotifications, + gossip_engine: Arc>>, + signed_commitment_sender: notification::BeefySignedCommitmentSender, + best_finalized_block: NumberFor, + best_block_voted_on: NumberFor, validator_set_id: ValidatorSetId, + client: Arc, + _backend: PhantomData, + _pair: PhantomData

, } -impl BeefyWorker +impl BeefyWorker where - Block: BlockT, - Id: Public + Debug, + B: Block, + BE: Backend, + P: sp_core::Pair, + P::Public: AppPublic + Codec, + P::Signature: Clone + Codec + Debug + PartialEq + TryFrom>, + C: Client, + C::Api: BeefyApi, { + /// Retrun a new BEEFY worker instance. + /// + /// Note that full BEEFY worker initialization can only be completed, if an + /// on-chain BEEFY pallet is available. Reason is that the current active + /// validator set has to be fetched from the on-chain BEFFY pallet. + /// + /// For this reason, BEEFY worker initialization completes only after a finality + /// notification has been received. Such a notifcation is basically an indication + /// that an on-chain BEEFY pallet is available. pub(crate) fn new( - validator_set: ValidatorSet, + client: Arc, key_store: SyncCryptoStorePtr, - finality_notifications: FinalityNotifications, - gossip_engine: GossipEngine, - signed_commitment_sender: notification::BeefySignedCommitmentSender, - best_finalized_block: NumberFor, - best_block_voted_on: NumberFor, + signed_commitment_sender: notification::BeefySignedCommitmentSender, + gossip_engine: GossipEngine, ) -> Self { + BeefyWorker { + state: State::New, + local_id: None, + key_store, + min_interval: 2, + rounds: round::Rounds::new(vec![]), + finality_notifications: client.finality_notification_stream(), + gossip_engine: Arc::new(Mutex::new(gossip_engine)), + signed_commitment_sender, + best_finalized_block: client.info().finalized_number, + best_block_voted_on: Zero::zero(), + validator_set_id: 0, + client, + _backend: PhantomData, + _pair: PhantomData, + } + } + + fn init_validator_set(&mut self) -> Result<(), error::Lifecycle> { + let at = BlockId::hash(self.client.info().best_hash); + + let validator_set = self + .client + .runtime_api() + .validator_set(&at) + .map_err(|err| error::Lifecycle::MissingValidatorSet(err.to_string()))?; + let local_id = match validator_set .validators .iter() - .find(|id| SyncCryptoStore::has_keys(&*key_store, &[(id.to_raw_vec(), KEY_TYPE)])) + .find(|id| SyncCryptoStore::has_keys(&*self.key_store, &[(id.to_raw_vec(), KEY_TYPE)])) { Some(id) => { info!(target: "beefy", "🥩 Starting BEEFY worker with local id: {:?}", id); + self.state = State::Validate; Some(id.clone()) } None => { info!(target: "beefy", "🥩 No local id found, BEEFY worker will be gossip only."); + self.state = State::Gossip; None } }; - BeefyWorker { - local_id, - key_store, - min_interval: 2, - rounds: round::Rounds::new(validator_set.validators), - finality_notifications, - gossip_engine: Arc::new(Mutex::new(gossip_engine)), - signed_commitment_sender, - best_finalized_block, - best_block_voted_on, - validator_set_id: validator_set.id, - } + self.local_id = local_id; + self.rounds = round::Rounds::new(validator_set.validators.clone()); + + debug!(target: "beefy", "🥩 Validator set with id {} initialized", validator_set.id); + + Ok(()) } } -impl BeefyWorker +impl BeefyWorker where - Block: BlockT, - Id: Codec + Debug + PartialEq + Public, - Signature: Clone + Codec + Debug + PartialEq + std::convert::TryFrom>, - FinalityNotifications: Stream> + Unpin, + B: Block, + S: Clone + Codec + Debug + PartialEq + std::convert::TryFrom>, + BE: Backend, + P: sp_core::Pair, + P::Public: AppPublic + Codec, + P::Signature: Clone + Codec + Debug + PartialEq + TryFrom>, + C: Client, + C::Api: BeefyApi, { - fn should_vote_on(&self, number: NumberFor) -> bool { + fn should_vote_on(&self, number: NumberFor) -> bool { use sp_runtime::{traits::Saturating, SaturatedConversion}; // we only vote as a validator - if self.local_id.is_none() { + if self.state != State::Validate { return false; } @@ -134,38 +211,38 @@ where number == next_block_to_vote_on } - fn sign_commitment(&self, id: &Id, commitment: &[u8]) -> Result> { + fn sign_commitment(&self, id: &P::Public, commitment: &[u8]) -> Result> { let sig = SyncCryptoStore::sign_with(&*self.key_store, KEY_TYPE, &id.to_public_crypto_pair(), &commitment) - .map_err(|e| error::Error::CannotSign((*id).clone(), e.to_string()))? - .ok_or_else(|| error::Error::CannotSign((*id).clone(), "No key in KeyStore found".into()))?; + .map_err(|e| error::Crypto::CannotSign((*id).clone(), e.to_string()))? + .ok_or_else(|| error::Crypto::CannotSign((*id).clone(), "No key in KeyStore found".into()))?; let sig = sig .clone() .try_into() - .map_err(|_| error::Error::InvalidSignature(sig.encode_hex(), (*id).clone()))?; + .map_err(|_| error::Crypto::InvalidSignature(sig.encode_hex(), (*id).clone()))?; Ok(sig) } - fn handle_finality_notification(&mut self, notification: FinalityNotification) { + fn handle_finality_notification(&mut self, notification: FinalityNotification) { debug!(target: "beefy", "🥩 Finality notification: {:?}", notification); if self.should_vote_on(*notification.header.number()) { let local_id = if let Some(id) = &self.local_id { id } else { - warn!(target: "beefy", "🥩 Missing validator id - can't vote for: {:?}", notification.header.hash()); + error!(target: "beefy", "🥩 Missing validator id - can't vote for: {:?}", notification.header.hash()); return; }; - let mmr_root = if let Some(hash) = find_mmr_root_digest::(¬ification.header) { + let mmr_root = if let Some(hash) = find_mmr_root_digest::(¬ification.header) { hash } else { warn!(target: "beefy", "🥩 No MMR root digest found for: {:?}", notification.header.hash()); return; }; - if let Some(new) = find_authorities_change::(¬ification.header) { + if let Some(new) = find_authorities_change::(¬ification.header) { debug!(target: "beefy", "🥩 New validator set: {:?}", new); self.validator_set_id = new.id; }; @@ -194,7 +271,7 @@ where self.gossip_engine .lock() - .gossip_message(topic::(), message.encode(), false); + .gossip_message(topic::(), message.encode(), false); debug!(target: "beefy", "🥩 Sent vote message: {:?}", message); @@ -207,7 +284,7 @@ where self.best_finalized_block = *notification.header.number(); } - fn handle_vote(&mut self, round: (MmrRootHash, NumberFor), vote: (Id, Signature)) { + fn handle_vote(&mut self, round: (MmrRootHash, NumberFor), vote: (P::Public, S)) { // TODO: validate signature let vote_added = self.rounds.add_vote(round, vote); @@ -229,11 +306,11 @@ where } pub(crate) async fn run(mut self) { - let mut votes = Box::pin(self.gossip_engine.lock().messages_for(topic::()).filter_map( + let mut votes = Box::pin(self.gossip_engine.lock().messages_for(topic::()).filter_map( |notification| async move { debug!(target: "beefy", "🥩 Got vote message: {:?}", notification); - VoteMessage::, Id, Signature>::decode(&mut ¬ification.message[..]).ok() + VoteMessage::, P::Public, S>::decode(&mut ¬ification.message[..]).ok() }, )); @@ -244,12 +321,22 @@ where futures::select! { notification = self.finality_notifications.next().fuse() => { if let Some(notification) = notification { + if self.state == State::New { + match self.init_validator_set() { + Ok(()) => (), + Err(err) => { + // we don't treat this as an error here because there really is + // nothing a node operator could do in order to remedy the error. + info!(target: "beefy", "🥩 Init validator set failed: {:?}", err); + } + } + } self.handle_finality_notification(notification); } else { return; } }, - vote = votes.next() => { + vote = votes.next().fuse() => { if let Some(vote) = vote { self.handle_vote( (vote.commitment.payload, vote.commitment.block_number), @@ -269,8 +356,9 @@ where } /// Extract the MMR root hash from a digest in the given header, if it exists. -fn find_mmr_root_digest(header: &Block::Header) -> Option +fn find_mmr_root_digest(header: &B::Header) -> Option where + B: Block, Id: Codec, { header.digest().logs().iter().find_map(|log| { @@ -285,7 +373,7 @@ where /// validator set or `None` in case no validator set change has been signaled. fn find_authorities_change(header: &B::Header) -> Option> where - B: BlockT, + B: Block, Id: Codec, { let id = OpaqueDigestItemId::Consensus(&BEEFY_ENGINE_ID);