diff --git a/Cargo.lock b/Cargo.lock index 0dbafc147f6bb..0476ecbca71ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,6 +487,7 @@ dependencies = [ name = "beefy-gadget" version = "4.0.0-dev" dependencies = [ + "async-trait", "beefy-primitives", "fnv", "futures 0.3.16", @@ -495,6 +496,7 @@ dependencies = [ "parking_lot 0.11.2", "sc-chain-spec", "sc-client-api", + "sc-consensus", "sc-keystore", "sc-network", "sc-network-gossip", @@ -504,6 +506,7 @@ dependencies = [ "sp-application-crypto", "sp-arithmetic", "sp-blockchain", + "sp-consensus", "sp-core", "sp-keystore", "sp-runtime", diff --git a/client/beefy/Cargo.toml b/client/beefy/Cargo.toml index 9761f18d78450..2d385cea4d437 100644 --- a/client/beefy/Cargo.toml +++ b/client/beefy/Cargo.toml @@ -14,6 +14,7 @@ log = "0.4" parking_lot = "0.11" thiserror = "1.0" wasm-timer = "0.2.5" +async-trait = "0.1.50" codec = { version = "2.2.0", package = "parity-scale-codec", features = ["derive"] } prometheus = { version = "0.10.0-dev", package = "substrate-prometheus-endpoint", path = "../../utils/prometheus" } @@ -25,6 +26,7 @@ sp-blockchain = { version = "4.0.0-dev", path = "../../primitives/blockchain" } sp-core = { version = "4.1.0-dev", path = "../../primitives/core" } sp-keystore = { version = "0.10.0", path = "../../primitives/keystore" } sp-runtime = { version = "4.1.0-dev", path = "../../primitives/runtime" } +sp-consensus = { version = "0.10.0-dev", path = "../../primitives/consensus/common" } sc-chain-spec = { version = "4.0.0-dev", path = "../../client/chain-spec" } sc-utils = { version = "4.0.0-dev", path = "../utils" } @@ -32,6 +34,7 @@ sc-client-api = { version = "4.0.0-dev", path = "../api" } sc-keystore = { version = "4.0.0-dev", path = "../keystore" } sc-network = { version = "0.10.0-dev", path = "../network" } sc-network-gossip = { version = "0.10.0-dev", path = "../network-gossip" } +sc-consensus = { version = "0.10.0-dev", path = "../consensus/common" } beefy-primitives = { version = "4.0.0-dev", path = "../../primitives/beefy" } diff --git a/client/beefy/src/import.rs b/client/beefy/src/import.rs new file mode 100644 index 0000000000000..dcafa821c77bd --- /dev/null +++ b/client/beefy/src/import.rs @@ -0,0 +1,197 @@ +use std::{collections::HashMap, marker::PhantomData, sync::Arc}; + +use sc_consensus::{ + BlockCheckParams, BlockImport, BlockImportParams, ImportResult, JustificationImport, +}; + +use sc_client_api::backend::Backend; +use sp_api::{ProvideRuntimeApi, TransactionFor}; +use sp_blockchain::well_known_cache_keys; +use sp_consensus::Error as ConsensusError; +use sp_runtime::{ + generic::{BlockId, OpaqueDigestItemId}, + traits::{Block as BlockT, Header as HeaderT, NumberFor}, + Justification, +}; + +use beefy_primitives::{ + crypto::{AuthorityId, Signature}, + BeefyApi, ConsensusLog, ValidatorSet, BEEFY_ENGINE_ID, +}; + +use crate::{ + justification::decode_and_verify_justification, notification::BeefyJustificationSender, + Client as BeefyClient, +}; + +/// Checks the given header for a consensus digest signalling a beefy authority set change +/// and extracts it. +fn find_beefy_authority_set_change( + header: &B::Header, +) -> Option> { + let id = OpaqueDigestItemId::Consensus(&BEEFY_ENGINE_ID); + + let filter_log = |log: ConsensusLog| match log { + ConsensusLog::AuthoritiesChange(change) => Some(change), + _ => None, + }; + + // find the first consensus digest with the right ID which converts to + // the right kind of consensus log. + header.digest().convert_first(|l| l.try_to(id).and_then(filter_log)) +} + +/// A block-import handler for BEEFY. +/// +/// This scans each imported block for BEEFY justifications and verifies them. +/// Wraps a type `inner` that implements [`BlockImport`] and ultimately defers to it. +pub struct BeefyBlockImport { + client: Arc, + inner: I, + justification_sender: BeefyJustificationSender, Signature>, + _phantom: PhantomData, +} + +impl Clone for BeefyBlockImport { + fn clone(&self) -> Self { + BeefyBlockImport { + client: self.client.clone(), + inner: self.inner.clone(), + justification_sender: self.justification_sender.clone(), + _phantom: PhantomData, + } + } +} + +#[async_trait::async_trait] +impl BlockImport for BeefyBlockImport +where + BE: Backend, + I: BlockImport< + Block, + Error = ConsensusError, + Transaction = sp_api::TransactionFor, + > + Send + + Sync, + Client: BeefyClient, + Client::Api: BeefyApi, + for<'a> &'a Client: + BlockImport>, + TransactionFor: 'static, +{ + type Error = ConsensusError; + type Transaction = TransactionFor; + + async fn import_block( + &mut self, + block: BlockImportParams, + new_cache: HashMap>, + ) -> Result { + let hash = block.post_hash(); + let number = *block.header.number(); + let justifications = block.justifications.clone(); + // If this block contains a beefy authority set change then it must have a beefy + // justification + let change = find_beefy_authority_set_change::(&block.header); + // Run inner block import + let import_result = self.inner.import_block(block, new_cache).await?; + // Try importing beefy justification + let beefy_justification = + justifications.and_then(|just| just.into_justification(BEEFY_ENGINE_ID)); + + if change.is_some() && beefy_justification.is_none() { + return Err(ConsensusError::ClientImport( + "Missing beefy justification in authority set change block".to_string(), + )) + } + if let Some(beefy_justification) = beefy_justification { + self.import_justification(hash, number, (BEEFY_ENGINE_ID, beefy_justification))?; + } + Ok(import_result) + } + + async fn check_block( + &mut self, + block: BlockCheckParams, + ) -> Result { + self.inner.check_block(block.clone()).await + } +} + +#[async_trait::async_trait] +impl JustificationImport + for BeefyBlockImport +where + BE: Backend, + Client: BeefyClient + ProvideRuntimeApi, + Client::Api: BeefyApi, + I: JustificationImport + Send + Sync, +{ + type Error = ConsensusError; + + async fn on_start(&mut self) -> Vec<(Block::Hash, NumberFor)> { + self.inner.on_start().await + } + + async fn import_justification( + &mut self, + hash: Block::Hash, + number: NumberFor, + justification: Justification, + ) -> Result<(), Self::Error> { + // Try Importing Beefy justification + BeefyBlockImport::import_justification(self, hash, number, justification.clone())?; + // Importing for inner BlockImport + self.inner.import_justification(hash, number, justification).await + } +} + +impl BeefyBlockImport +where + BE: Backend, + Client: BeefyClient + ProvideRuntimeApi, + Client::Api: BeefyApi, +{ + /// Import a block justification. + fn import_justification( + &mut self, + hash: Block::Hash, + number: NumberFor, + justification: Justification, + ) -> Result<(), ConsensusError> { + if justification.0 != BEEFY_ENGINE_ID { + return Ok(()) + } + + // This function assumes the Block should have already been imported + let at = BlockId::hash(hash); + let validator_set = self + .client + .runtime_api() + .validator_set(&at) + .map_err(|e| ConsensusError::ClientImport(e.to_string()))?; + + if let Some(validator_set) = validator_set { + let encoded_proof = justification.1; + let _proof = decode_and_verify_justification::( + number, + &encoded_proof[..], + &validator_set, + )?; + } else { + return Err(ConsensusError::ClientImport("Empty validator set".to_string())) + } + Ok(()) + } +} + +impl BeefyBlockImport { + /// Create a new BeefyBlockImport + pub(crate) fn new( + client: Arc, + inner: I, + justification_sender: BeefyJustificationSender, Signature>, + ) -> BeefyBlockImport { + BeefyBlockImport { inner, client, justification_sender, _phantom: PhantomData } + } +} diff --git a/client/beefy/src/justification.rs b/client/beefy/src/justification.rs new file mode 100644 index 0000000000000..c29960a8f5325 --- /dev/null +++ b/client/beefy/src/justification.rs @@ -0,0 +1,58 @@ +use crate::keystore::BeefyKeystore; +use beefy_primitives::{ + crypto::{AuthorityId, Signature}, + ValidatorSet, VersionedFinalityProof, +}; +use codec::{Decode, Encode}; +use sp_consensus::Error as ConsensusError; +use sp_runtime::traits::{Block as BlockT, NumberFor}; + +/// Decodes a Beefy justification and verifies it +pub(crate) fn decode_and_verify_justification( + number: NumberFor, + encoded: &[u8], + validator_set: &ValidatorSet, +) -> Result, Signature>, ConsensusError> { + let finality_proof = + , Signature>>::decode(&mut &*encoded) + .map_err(|_| ConsensusError::InvalidJustification)?; + + if verify_with_validator_set::(number, validator_set, finality_proof.clone())? { + return Ok(finality_proof) + } + + Err(ConsensusError::InvalidJustification) +} + +/// Verify the Beefy provided finality proof +/// against the validtor set at the block it was generated +pub(crate) fn verify_with_validator_set( + number: NumberFor, + validator_set: &ValidatorSet, + proof: VersionedFinalityProof, Signature>, +) -> Result { + let result = match proof { + VersionedFinalityProof::V1(signed_commitment) => { + if validator_set.len() != signed_commitment.signatures.len() || + signed_commitment.commitment.validator_set_id != validator_set.id() || + signed_commitment.commitment.block_number != number + { + return Err(ConsensusError::InvalidJustification) + } + + // Arrangement of signatures in the commitment should be in the same order as validators + // for that set + let message = signed_commitment.commitment.encode(); + validator_set + .validators() + .into_iter() + .zip(signed_commitment.signatures.into_iter()) + .filter(|(.., sig)| sig.is_some()) + .all(|(id, signature)| { + BeefyKeystore::verify(id, signature.as_ref().unwrap(), &message[..]) + }) + }, + }; + + Ok(result) +} diff --git a/client/beefy/src/lib.rs b/client/beefy/src/lib.rs index 9b2bf383df8ef..42a431a2d85c7 100644 --- a/client/beefy/src/lib.rs +++ b/client/beefy/src/lib.rs @@ -19,27 +19,32 @@ use std::sync::Arc; use log::debug; +use notification::BeefyJustificationStream; use prometheus::Registry; use sc_client_api::{Backend, BlockchainEvents, Finalizer}; use sc_network_gossip::{GossipEngine, Network as GossipNetwork}; -use sp_api::ProvideRuntimeApi; +use sp_api::{NumberFor, ProvideRuntimeApi}; use sp_blockchain::HeaderBackend; use sp_keystore::SyncCryptoStorePtr; -use sp_runtime::traits::Block; +use sp_runtime::traits::Block as BlockT; -use beefy_primitives::BeefyApi; +use beefy_primitives::{crypto::Signature, BeefyApi}; use crate::notification::{BeefyBestBlockSender, BeefySignedCommitmentSender}; mod error; mod gossip; +mod import; +mod justification; mod keystore; mod metrics; mod round; mod worker; +use import::BeefyBlockImport; + pub mod notification; pub use beefy_protocol_name::standard_name as protocol_standard_name; @@ -78,6 +83,18 @@ pub fn beefy_peers_set_config( cfg } +/// Produce a BEEFY block import object and a link half for tying it to the client +pub fn block_import( + wrapped_block_import: I, + client: Arc, +) -> (BeefyBlockImport, BeefyJustificationStream, Signature>) +{ + let (justification_sender, justification_stream) = BeefyJustificationStream::channel(); + let import = BeefyBlockImport::new(client.clone(), wrapped_block_import, justification_sender); + + (import, justification_stream) +} + /// 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 @@ -85,7 +102,7 @@ pub fn beefy_peers_set_config( pub trait Client: BlockchainEvents + HeaderBackend + Finalizer + ProvideRuntimeApi + Send + Sync where - B: Block, + B: BlockT, BE: Backend, { // empty @@ -93,7 +110,7 @@ where impl Client for T where - B: Block, + B: BlockT, BE: Backend, T: BlockchainEvents + HeaderBackend @@ -108,7 +125,7 @@ where /// BEEFY gadget initialization parameters. pub struct BeefyParams where - B: Block, + B: BlockT, BE: Backend, C: Client, C::Api: BeefyApi, @@ -124,6 +141,8 @@ where pub network: N, /// BEEFY signed commitment sender pub signed_commitment_sender: BeefySignedCommitmentSender, + /// BEEFY justification stream + pub justification_stream: Option, Signature>>, /// BEEFY best block sender pub beefy_best_block_sender: BeefyBestBlockSender, /// Minimal delta between blocks, BEEFY should vote for @@ -139,7 +158,7 @@ where /// This is a thin shim around running and awaiting a BEEFY worker. pub async fn start_beefy_gadget(beefy_params: BeefyParams) where - B: Block, + B: BlockT, BE: Backend, C: Client, C::Api: BeefyApi, @@ -152,6 +171,7 @@ where network, signed_commitment_sender, beefy_best_block_sender, + justification_stream, min_block_delta, prometheus_registry, protocol_name, @@ -180,6 +200,7 @@ where key_store: key_store.into(), signed_commitment_sender, beefy_best_block_sender, + justification_stream, gossip_engine, gossip_validator, min_block_delta, diff --git a/client/beefy/src/notification.rs b/client/beefy/src/notification.rs index 7c18d809f6efb..07386f5103866 100644 --- a/client/beefy/src/notification.rs +++ b/client/beefy/src/notification.rs @@ -16,6 +16,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use beefy_primitives::VersionedFinalityProof; use sc_utils::notification::{NotificationSender, NotificationStream, TracingKeyStr}; use sp_runtime::traits::{Block as BlockT, NumberFor}; @@ -41,6 +42,15 @@ pub type BeefySignedCommitmentSender = NotificationSender = NotificationStream, BeefySignedCommitmentTracingKey>; +/// The sending half of the notifications channel(s) used to send notifications +/// about imported justifications. +pub type BeefyJustificationSender = NotificationSender>; + +/// The receiving half of a notifications channel used to receive notifications +/// about justifications from imported blocks +pub type BeefyJustificationStream = + NotificationStream, BeefyJustificationTracingKey>; + /// Provides tracing key for BEEFY best block stream. #[derive(Clone)] pub struct BeefyBestBlockTracingKey; @@ -54,3 +64,10 @@ pub struct BeefySignedCommitmentTracingKey; impl TracingKeyStr for BeefySignedCommitmentTracingKey { const TRACING_KEY: &'static str = "mpsc_beefy_signed_commitments_notification_stream"; } + +/// Provides tracing key for BEEFY justification stream. +#[derive(Clone)] +pub struct BeefyJustificationTracingKey; +impl TracingKeyStr for BeefyJustificationTracingKey { + const TRACING_KEY: &'static str = "mpsc_beefy_justification_notification_stream"; +} diff --git a/client/beefy/src/worker.rs b/client/beefy/src/worker.rs index 0c7d8d4ffdc9c..2bb4b1634e3bc 100644 --- a/client/beefy/src/worker.rs +++ b/client/beefy/src/worker.rs @@ -46,7 +46,7 @@ use crate::{ keystore::BeefyKeystore, metric_inc, metric_set, metrics::Metrics, - notification::{BeefyBestBlockSender, BeefySignedCommitmentSender}, + notification::{BeefyBestBlockSender, BeefyJustificationStream, BeefySignedCommitmentSender}, round, Client, }; @@ -59,6 +59,8 @@ where pub key_store: BeefyKeystore, pub signed_commitment_sender: BeefySignedCommitmentSender, pub beefy_best_block_sender: BeefyBestBlockSender, + /// BEEFY justification stream from block import worker + pub justification_stream: Option, Signature>>, pub gossip_engine: GossipEngine, pub gossip_validator: Arc>, pub min_block_delta: u32, @@ -89,6 +91,8 @@ where best_beefy_block: Option>, /// Used to keep RPC worker up to date on latest/best beefy beefy_best_block_sender: BeefyBestBlockSender, + /// BEEFY justification stream from block import worker + _justification_stream: Option, Signature>>, /// Validator set id for the last signed commitment last_signed_id: u64, // keep rustc happy @@ -115,6 +119,7 @@ where key_store, signed_commitment_sender, beefy_best_block_sender, + justification_stream, gossip_engine, gossip_validator, min_block_delta, @@ -134,6 +139,7 @@ where finality_notifications: client.finality_notification_stream(), best_grandpa_block: client.info().finalized_number, best_beefy_block: None, + _justification_stream: justification_stream, last_signed_id: 0, beefy_best_block_sender, _backend: PhantomData,