// This file is part of Substrate. // Copyright (C) 2021-2022 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 std::{ collections::{BTreeMap, BTreeSet}, fmt::Debug, marker::PhantomData, sync::Arc, time::Duration, }; use codec::{Codec, Decode, Encode}; use futures::{future, FutureExt, StreamExt}; use log::{debug, error, info, log_enabled, trace, warn}; use parking_lot::Mutex; use sc_client_api::{Backend, FinalityNotification, FinalityNotifications}; use sc_network_gossip::GossipEngine; use sp_api::{BlockId, ProvideRuntimeApi}; use sp_arithmetic::traits::{AtLeast32Bit, Saturating}; use sp_consensus::SyncOracle; use sp_mmr_primitives::MmrApi; use sp_runtime::{ generic::OpaqueDigestItemId, traits::{Block, Header, NumberFor}, SaturatedConversion, }; use beefy_primitives::{ crypto::{AuthorityId, Signature}, known_payload_ids, BeefyApi, Commitment, ConsensusLog, MmrRootHash, Payload, SignedCommitment, ValidatorSet, VersionedFinalityProof, VoteMessage, BEEFY_ENGINE_ID, GENESIS_AUTHORITY_SET_ID, }; use crate::{ error, gossip::{topic, GossipValidator}, keystore::BeefyKeystore, metric_inc, metric_set, metrics::Metrics, notification::{BeefyBestBlockSender, BeefySignedCommitmentSender}, round::Rounds, Client, }; pub(crate) struct WorkerParams { pub client: Arc, pub backend: Arc, pub runtime: Arc, pub key_store: BeefyKeystore, pub signed_commitment_sender: BeefySignedCommitmentSender, pub beefy_best_block_sender: BeefyBestBlockSender, pub gossip_engine: GossipEngine, pub gossip_validator: Arc>, pub min_block_delta: u32, pub metrics: Option, pub sync_oracle: SO, } /// A BEEFY worker plays the BEEFY protocol pub(crate) struct BeefyWorker { client: Arc, backend: Arc, runtime: Arc, key_store: BeefyKeystore, signed_commitment_sender: BeefySignedCommitmentSender, gossip_engine: Arc>>, gossip_validator: Arc>, /// Min delta in block numbers between two blocks, BEEFY should vote on min_block_delta: u32, metrics: Option, rounds: Option>, /// Buffer holding votes for blocks that the client hasn't seen finality for. pending_votes: BTreeMap, Vec, AuthorityId, Signature>>>, finality_notifications: FinalityNotifications, /// Best block we received a GRANDPA notification for best_grandpa_block_header: ::Header, /// Best block a BEEFY voting round has been concluded for best_beefy_block: Option>, /// Used to keep RPC worker up to date on latest/best beefy beefy_best_block_sender: BeefyBestBlockSender, /// Validator set id for the last signed commitment last_signed_id: u64, /// Handle to the sync oracle sync_oracle: SO, // keep rustc happy _backend: PhantomData, } impl BeefyWorker where B: Block + Codec, BE: Backend, C: Client, R: ProvideRuntimeApi, R::Api: BeefyApi + MmrApi, SO: SyncOracle + Send + Sync + Clone + 'static, { /// Return a new BEEFY worker instance. /// /// Note that a BEEFY worker is only fully functional if a corresponding /// BEEFY pallet has been deployed on-chain. /// /// The BEEFY pallet is needed in order to keep track of the BEEFY authority set. pub(crate) fn new(worker_params: WorkerParams) -> Self { let WorkerParams { client, backend, runtime, key_store, signed_commitment_sender, beefy_best_block_sender, gossip_engine, gossip_validator, min_block_delta, metrics, sync_oracle, } = worker_params; let last_finalized_header = client .expect_header(BlockId::number(client.info().finalized_number)) .expect("latest block always has header available; qed."); BeefyWorker { client: client.clone(), backend, runtime, key_store, signed_commitment_sender, gossip_engine: Arc::new(Mutex::new(gossip_engine)), gossip_validator, // always target at least one block better than current best beefy min_block_delta: min_block_delta.max(1), metrics, rounds: None, pending_votes: BTreeMap::new(), finality_notifications: client.finality_notification_stream(), best_grandpa_block_header: last_finalized_header, best_beefy_block: None, last_signed_id: 0, beefy_best_block_sender, sync_oracle, _backend: PhantomData, } } /// Return `Some(number)` if we should be voting on block `number` now, /// return `None` if there is no block we should vote on now. fn current_vote_target(&self) -> Option> { let rounds = if let Some(r) = &self.rounds { r } else { debug!(target: "beefy", "🥩 No voting round started"); return None }; let best_finalized = *self.best_grandpa_block_header.number(); // `target` is guaranteed > `best_beefy` since `min_block_delta` is at least `1`. let target = vote_target( best_finalized, self.best_beefy_block, *rounds.session_start(), self.min_block_delta, ); trace!( target: "beefy", "🥩 best beefy: #{:?}, best finalized: #{:?}, current_vote_target: {:?}", self.best_beefy_block, best_finalized, target ); if let Some(target) = &target { metric_set!(self, beefy_should_vote_on, target); } target } /// Verify `active` validator set for `block` against the key store /// /// We want to make sure that we have _at least one_ key in our keystore that /// is part of the validator set, that's because if there are no local keys /// then we can't perform our job as a validator. /// /// Note that for a non-authority node there will be no keystore, and we will /// return an error and don't check. The error can usually be ignored. fn verify_validator_set( &self, block: &NumberFor, active: &ValidatorSet, ) -> Result<(), error::Error> { let active: BTreeSet<&AuthorityId> = active.validators().iter().collect(); let public_keys = self.key_store.public_keys()?; let store: BTreeSet<&AuthorityId> = public_keys.iter().collect(); if store.intersection(&active).count() == 0 { let msg = "no authority public key found in store".to_string(); debug!(target: "beefy", "🥩 for block {:?} {}", block, msg); Err(error::Error::Keystore(msg)) } else { Ok(()) } } /// Set best BEEFY block to `block_num`. /// /// Also sends/updates the best BEEFY block hash to the RPC worker. fn set_best_beefy_block(&mut self, block_num: NumberFor) { if Some(block_num) > self.best_beefy_block { // Try to get block hash ourselves. let block_hash = match self.client.hash(block_num) { Ok(h) => h, Err(e) => { error!(target: "beefy", "🥩 Failed to get hash for block number {}: {}", block_num, e); None }, }; // Update RPC worker with new best BEEFY block hash. block_hash.map(|hash| { self.beefy_best_block_sender .notify(|| Ok::<_, ()>(hash)) .expect("forwards closure result; the closure always returns Ok; qed.") }); // Set new best BEEFY block number. self.best_beefy_block = Some(block_num); metric_set!(self, beefy_best_block, block_num); } else { debug!(target: "beefy", "🥩 Can't set best beefy to older: {}", block_num); } } /// Handle session changes by starting new voting round for mandatory blocks. fn init_session_at( &mut self, active: ValidatorSet, new_session_start: NumberFor, ) { debug!(target: "beefy", "🥩 New active validator set: {:?}", active); metric_set!(self, beefy_validator_set_id, active.id()); // BEEFY should produce a signed commitment for each session if active.id() != self.last_signed_id + 1 && active.id() != GENESIS_AUTHORITY_SET_ID && self.last_signed_id != 0 { debug!( target: "beefy", "🥩 Detected skipped session: active-id {:?}, last-signed-id {:?}", active.id(), self.last_signed_id, ); metric_inc!(self, beefy_skipped_sessions); } if log_enabled!(target: "beefy", log::Level::Debug) { // verify the new validator set - only do it if we're also logging the warning let _ = self.verify_validator_set(&new_session_start, &active); } let id = active.id(); self.rounds = Some(Rounds::new(new_session_start, active)); info!(target: "beefy", "🥩 New Rounds for validator set id: {:?} with session_start {:?}", id, new_session_start); } fn handle_finality_notification(&mut self, notification: &FinalityNotification) { debug!(target: "beefy", "🥩 Finality notification: {:?}", notification); let number = *notification.header.number(); // On start-up ignore old finality notifications that we're not interested in. if number <= *self.best_grandpa_block_header.number() { debug!(target: "beefy", "🥩 Got unexpected finality for old block #{:?}", number); return } // update best GRANDPA finalized block we have seen self.best_grandpa_block_header = notification.header.clone(); self.handle_finality(¬ification.header); } fn handle_finality(&mut self, header: &B::Header) { // Check for and handle potential new session. if let Some(new_validator_set) = find_authorities_change::(header) { self.init_session_at(new_validator_set, *header.number()); } // Handle any pending votes for now finalized blocks. self.check_pending_votes(); // Vote if there's now a new vote target. if let Some(target_number) = self.current_vote_target() { self.do_vote(target_number); } } // Handles all buffered votes for now finalized blocks. fn check_pending_votes(&mut self) { let not_finalized = self.best_grandpa_block_header.number().saturating_add(1u32.into()); let still_pending = self.pending_votes.split_off(¬_finalized); let votes_to_handle = std::mem::replace(&mut self.pending_votes, still_pending); for (num, votes) in votes_to_handle.into_iter() { if Some(num) > self.best_beefy_block { debug!(target: "beefy", "🥩 Handling buffered votes for now GRANDPA finalized block: {:?}.", num); for v in votes.into_iter() { self.handle_vote( (v.commitment.payload, v.commitment.block_number), (v.id, v.signature), false, ); } } else { debug!(target: "beefy", "🥩 Dropping outdated buffered votes for now BEEFY finalized block: {:?}.", num); } } } fn handle_vote( &mut self, round: (Payload, NumberFor), vote: (AuthorityId, Signature), self_vote: bool, ) { self.gossip_validator.note_round(round.1); let rounds = if let Some(rounds) = self.rounds.as_mut() { rounds } else { debug!(target: "beefy", "🥩 Missing validator set - can't handle vote {:?}", vote); return }; if rounds.add_vote(&round, vote, self_vote) { if let Some(signatures) = rounds.try_conclude(&round) { self.gossip_validator.conclude_round(round.1); // id is stored for skipped session metric calculation self.last_signed_id = rounds.validator_set_id(); let block_num = round.1; let commitment = Commitment { payload: round.0, block_number: block_num, validator_set_id: self.last_signed_id, }; let signed_commitment = SignedCommitment { commitment, signatures }; metric_set!(self, beefy_round_concluded, block_num); info!(target: "beefy", "🥩 Round #{} concluded, committed: {:?}.", round.1, signed_commitment); if let Err(e) = self.backend.append_justification( BlockId::Number(block_num), ( BEEFY_ENGINE_ID, VersionedFinalityProof::V1(signed_commitment.clone()).encode(), ), ) { debug!(target: "beefy", "🥩 Error {:?} on appending justification: {:?}", e, signed_commitment); } self.signed_commitment_sender .notify(|| Ok::<_, ()>(signed_commitment)) .expect("forwards closure result; the closure always returns Ok; qed."); self.set_best_beefy_block(block_num); // Vote if there's now a new vote target. if let Some(target_number) = self.current_vote_target() { self.do_vote(target_number); } } } } /// Create and gossip Signed Commitment for block number `target_number`. /// /// Also handle this self vote by calling `self.handle_vote()` for it. fn do_vote(&mut self, target_number: NumberFor) { debug!(target: "beefy", "🥩 Try voting on {}", target_number); // Most of the time we get here, `target` is actually `best_grandpa`, // avoid asking `client` for header in that case. let target_header = if target_number == *self.best_grandpa_block_header.number() { self.best_grandpa_block_header.clone() } else { match self.client.expect_header(BlockId::Number(target_number)) { Ok(h) => h, Err(err) => { debug!( target: "beefy", "🥩 Could not get header for block #{:?} (error: {:?}), skipping vote..", target_number, err ); return }, } }; let target_hash = target_header.hash(); let mmr_root = if let Some(hash) = self.get_mmr_root_digest(&target_header) { hash } else { warn!(target: "beefy", "🥩 No MMR root digest found for: {:?}", target_hash); return }; let payload = Payload::new(known_payload_ids::MMR_ROOT_ID, mmr_root.encode()); let (validators, validator_set_id) = if let Some(rounds) = &self.rounds { if !rounds.should_self_vote(&(payload.clone(), target_number)) { debug!(target: "beefy", "🥩 Don't double vote for block number: {:?}", target_number); return } (rounds.validators(), rounds.validator_set_id()) } else { debug!(target: "beefy", "🥩 Missing validator set - can't vote for: {:?}", target_hash); return }; let authority_id = if let Some(id) = self.key_store.authority_id(validators) { debug!(target: "beefy", "🥩 Local authority id: {:?}", id); id } else { debug!(target: "beefy", "🥩 Missing validator id - can't vote for: {:?}", target_hash); return }; let commitment = Commitment { payload, block_number: target_number, validator_set_id }; let encoded_commitment = commitment.encode(); let signature = match self.key_store.sign(&authority_id, &*encoded_commitment) { Ok(sig) => sig, Err(err) => { warn!(target: "beefy", "🥩 Error signing commitment: {:?}", err); return }, }; trace!( target: "beefy", "🥩 Produced signature using {:?}, is_valid: {:?}", authority_id, BeefyKeystore::verify(&authority_id, &signature, &*encoded_commitment) ); let message = VoteMessage { commitment, id: authority_id, signature }; let encoded_message = message.encode(); metric_inc!(self, beefy_votes_sent); debug!(target: "beefy", "🥩 Sent vote message: {:?}", message); self.handle_vote( (message.commitment.payload, message.commitment.block_number), (message.id, message.signature), true, ); self.gossip_engine.lock().gossip_message(topic::(), encoded_message, false); } /// Wait for BEEFY runtime pallet to be available. async fn wait_for_runtime_pallet(&mut self) { self.client .finality_notification_stream() .take_while(|notif| { let at = BlockId::hash(notif.header.hash()); if let Some(active) = self.runtime.runtime_api().validator_set(&at).ok().flatten() { if active.id() == GENESIS_AUTHORITY_SET_ID { // When starting from genesis, there is no session boundary digest. // Just initialize `rounds` to Block #1 as BEEFY mandatory block. self.init_session_at(active, 1u32.into()); } // In all other cases, we just go without `rounds` initialized, meaning the // worker won't vote until it witnesses a session change. // Once we'll implement 'initial sync' (catch-up), the worker will be able to // start voting right away. self.handle_finality_notification(notif); future::ready(false) } else { trace!(target: "beefy", "🥩 Finality notification: {:?}", notif); debug!(target: "beefy", "🥩 Waiting for BEEFY pallet to become available..."); future::ready(true) } }) .for_each(|_| future::ready(())) .await; // get a new stream that provides _new_ notifications (from here on out) self.finality_notifications = self.client.finality_notification_stream(); } /// Main loop for BEEFY worker. /// /// Wait for BEEFY runtime pallet to be available, then start the main async loop /// which is driven by finality notifications and gossiped votes. pub(crate) async fn run(mut self) { info!(target: "beefy", "🥩 run BEEFY worker, best grandpa: #{:?}.", self.best_grandpa_block_header.number()); self.wait_for_runtime_pallet().await; let mut votes = Box::pin(self.gossip_engine.lock().messages_for(topic::()).filter_map( |notification| async move { trace!(target: "beefy", "🥩 Got vote message: {:?}", notification); VoteMessage::, AuthorityId, Signature>::decode( &mut ¬ification.message[..], ) .ok() }, )); loop { while self.sync_oracle.is_major_syncing() { debug!(target: "beefy", "Waiting for major sync to complete..."); futures_timer::Delay::new(Duration::from_secs(5)).await; } let engine = self.gossip_engine.clone(); let gossip_engine = future::poll_fn(|cx| engine.lock().poll_unpin(cx)); futures::select! { notification = self.finality_notifications.next().fuse() => { if let Some(notification) = notification { self.handle_finality_notification(¬ification); } else { return; } }, vote = votes.next().fuse() => { if let Some(vote) = vote { let block_num = vote.commitment.block_number; if block_num > *self.best_grandpa_block_header.number() { // Only handle votes for blocks we _know_ have been finalized. // Buffer vote to be handled later. debug!( target: "beefy", "🥩 Buffering vote for not (yet) finalized block: {:?}.", block_num ); self.pending_votes.entry(block_num).or_default().push(vote); } else { self.handle_vote( (vote.commitment.payload, vote.commitment.block_number), (vote.id, vote.signature), false ); } } else { return; } }, _ = gossip_engine.fuse() => { error!(target: "beefy", "🥩 Gossip engine has terminated."); return; } } } } /// Simple wrapper that gets MMR root from header digests or from client state. fn get_mmr_root_digest(&self, header: &B::Header) -> Option { find_mmr_root_digest::(header).or_else(|| { self.runtime .runtime_api() .mmr_root(&BlockId::hash(header.hash())) .ok() .and_then(|r| r.ok()) }) } } /// Extract the MMR root hash from a digest in the given header, if it exists. fn find_mmr_root_digest(header: &B::Header) -> Option where B: Block, { let id = OpaqueDigestItemId::Consensus(&BEEFY_ENGINE_ID); let filter = |log: ConsensusLog| match log { ConsensusLog::MmrRoot(root) => Some(root), _ => None, }; header.digest().convert_first(|l| l.try_to(id).and_then(filter)) } /// 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. fn find_authorities_change(header: &B::Header) -> Option> where B: Block, { let id = OpaqueDigestItemId::Consensus(&BEEFY_ENGINE_ID); let filter = |log: ConsensusLog| match log { ConsensusLog::AuthoritiesChange(validator_set) => Some(validator_set), _ => None, }; header.digest().convert_first(|l| l.try_to(id).and_then(filter)) } /// Calculate next block number to vote on. /// /// Return `None` if there is no voteable target yet. fn vote_target( best_grandpa: N, best_beefy: Option, session_start: N, min_delta: u32, ) -> Option where N: AtLeast32Bit + Copy + Debug, { // if the mandatory block (session_start) does not have a beefy justification yet, // we vote on it let target = match best_beefy { None => { debug!( target: "beefy", "🥩 vote target - mandatory block: #{:?}", session_start, ); session_start }, Some(bbb) if bbb < session_start => { debug!( target: "beefy", "🥩 vote target - mandatory block: #{:?}", session_start, ); session_start }, Some(bbb) => { let diff = best_grandpa.saturating_sub(bbb) + 1u32.into(); let diff = diff.saturated_into::() / 2; let target = bbb + min_delta.max(diff.next_power_of_two()).into(); debug!( target: "beefy", "🥩 vote target - diff: {:?}, next_power_of_two: {:?}, target block: #{:?}", diff, diff.next_power_of_two(), target, ); target }, }; // Don't vote for targets until they've been finalized // (`target` can be > `best_grandpa` when `min_delta` is big enough). if target > best_grandpa { None } else { Some(target) } } #[cfg(test)] pub(crate) mod tests { use super::*; use crate::{ keystore::tests::Keyring, notification::{BeefyBestBlockStream, BeefySignedCommitmentStream}, tests::{ create_beefy_keystore, get_beefy_streams, make_beefy_ids, two_validators::TestApi, BeefyPeer, BeefyTestNet, BEEFY_PROTOCOL_NAME, }, }; use futures::{executor::block_on, future::poll_fn, task::Poll}; use crate::tests::BeefyLinkHalf; use sc_client_api::HeaderBackend; use sc_network::NetworkService; use sc_network_test::{PeersFullClient, TestNetFactory}; use sp_api::HeaderT; use substrate_test_runtime_client::{ runtime::{Block, Digest, DigestItem, Header, H256}, Backend, }; fn create_beefy_worker( peer: &BeefyPeer, key: &Keyring, min_block_delta: u32, ) -> BeefyWorker>> { let keystore = create_beefy_keystore(*key); let (signed_commitment_sender, signed_commitment_stream) = BeefySignedCommitmentStream::::channel(); let (beefy_best_block_sender, beefy_best_block_stream) = BeefyBestBlockStream::::channel(); let beefy_link_half = BeefyLinkHalf { signed_commitment_stream, beefy_best_block_stream }; *peer.data.beefy_link_half.lock() = Some(beefy_link_half); let api = Arc::new(TestApi {}); let network = peer.network_service().clone(); let sync_oracle = network.clone(); let gossip_validator = Arc::new(crate::gossip::GossipValidator::new()); let gossip_engine = GossipEngine::new(network, BEEFY_PROTOCOL_NAME, gossip_validator.clone(), None); let worker_params = crate::worker::WorkerParams { client: peer.client().as_client(), backend: peer.client().as_backend(), runtime: api, key_store: Some(keystore).into(), signed_commitment_sender, beefy_best_block_sender, gossip_engine, gossip_validator, min_block_delta, metrics: None, sync_oracle, }; BeefyWorker::<_, _, _, _, _>::new(worker_params) } #[test] fn vote_on_min_block_delta() { let t = vote_target(1u32, Some(1), 1, 4); assert_eq!(None, t); let t = vote_target(2u32, Some(1), 1, 4); assert_eq!(None, t); let t = vote_target(4u32, Some(2), 1, 4); assert_eq!(None, t); let t = vote_target(6u32, Some(2), 1, 4); assert_eq!(Some(6), t); let t = vote_target(9u32, Some(4), 1, 4); assert_eq!(Some(8), t); let t = vote_target(10u32, Some(10), 1, 8); assert_eq!(None, t); let t = vote_target(12u32, Some(10), 1, 8); assert_eq!(None, t); let t = vote_target(18u32, Some(10), 1, 8); assert_eq!(Some(18), t); } #[test] fn vote_on_power_of_two() { let t = vote_target(1008u32, Some(1000), 1, 4); assert_eq!(Some(1004), t); let t = vote_target(1016u32, Some(1000), 1, 4); assert_eq!(Some(1008), t); let t = vote_target(1032u32, Some(1000), 1, 4); assert_eq!(Some(1016), t); let t = vote_target(1064u32, Some(1000), 1, 4); assert_eq!(Some(1032), t); let t = vote_target(1128u32, Some(1000), 1, 4); assert_eq!(Some(1064), t); let t = vote_target(1256u32, Some(1000), 1, 4); assert_eq!(Some(1128), t); let t = vote_target(1512u32, Some(1000), 1, 4); assert_eq!(Some(1256), t); let t = vote_target(1024u32, Some(1), 1, 4); assert_eq!(Some(513), t); } #[test] fn vote_on_target_block() { let t = vote_target(1008u32, Some(1002), 1, 4); assert_eq!(Some(1006), t); let t = vote_target(1010u32, Some(1002), 1, 4); assert_eq!(Some(1006), t); let t = vote_target(1016u32, Some(1006), 1, 4); assert_eq!(Some(1014), t); let t = vote_target(1022u32, Some(1006), 1, 4); assert_eq!(Some(1014), t); let t = vote_target(1032u32, Some(1012), 1, 4); assert_eq!(Some(1028), t); let t = vote_target(1044u32, Some(1012), 1, 4); assert_eq!(Some(1028), t); let t = vote_target(1064u32, Some(1014), 1, 4); assert_eq!(Some(1046), t); let t = vote_target(1078u32, Some(1014), 1, 4); assert_eq!(Some(1046), t); let t = vote_target(1128u32, Some(1008), 1, 4); assert_eq!(Some(1072), t); let t = vote_target(1136u32, Some(1008), 1, 4); assert_eq!(Some(1072), t); } #[test] fn vote_on_mandatory_block() { let t = vote_target(1008u32, Some(1002), 1004, 4); assert_eq!(Some(1004), t); let t = vote_target(1016u32, Some(1006), 1007, 4); assert_eq!(Some(1007), t); let t = vote_target(1064u32, Some(1014), 1063, 4); assert_eq!(Some(1063), t); let t = vote_target(1320u32, Some(1012), 1234, 4); assert_eq!(Some(1234), t); let t = vote_target(1128u32, Some(1008), 1008, 4); assert_eq!(Some(1072), t); } #[test] fn extract_authorities_change_digest() { let mut header = Header::new( 1u32.into(), Default::default(), Default::default(), Default::default(), Digest::default(), ); // verify empty digest shows nothing assert!(find_authorities_change::(&header).is_none()); let peers = &[Keyring::One, Keyring::Two]; let id = 42; let validator_set = ValidatorSet::new(make_beefy_ids(peers), id).unwrap(); header.digest_mut().push(DigestItem::Consensus( BEEFY_ENGINE_ID, ConsensusLog::::AuthoritiesChange(validator_set.clone()).encode(), )); // verify validator set is correctly extracted from digest let extracted = find_authorities_change::(&header); assert_eq!(extracted, Some(validator_set)); } #[test] fn extract_mmr_root_digest() { let mut header = Header::new( 1u32.into(), Default::default(), Default::default(), Default::default(), Digest::default(), ); // verify empty digest shows nothing assert!(find_mmr_root_digest::(&header).is_none()); let mmr_root_hash = H256::random(); header.digest_mut().push(DigestItem::Consensus( BEEFY_ENGINE_ID, ConsensusLog::::MmrRoot(mmr_root_hash.clone()).encode(), )); // verify validator set is correctly extracted from digest let extracted = find_mmr_root_digest::(&header); assert_eq!(extracted, Some(mmr_root_hash)); } #[test] fn should_vote_target() { let keys = &[Keyring::Alice]; let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap(); let mut net = BeefyTestNet::new(1, 0); let mut worker = create_beefy_worker(&net.peer(0), &keys[0], 1); // rounds not initialized -> should vote: `None` assert_eq!(worker.current_vote_target(), None); let set_up = |worker: &mut BeefyWorker< Block, Backend, PeersFullClient, TestApi, Arc>, >, best_grandpa: u64, best_beefy: Option, session_start: u64, min_delta: u32| { let grandpa_header = Header::new( best_grandpa, Default::default(), Default::default(), Default::default(), Default::default(), ); worker.best_grandpa_block_header = grandpa_header; worker.best_beefy_block = best_beefy; worker.min_block_delta = min_delta; worker.rounds = Some(Rounds::new(session_start, validator_set.clone())); }; // under min delta set_up(&mut worker, 1, Some(1), 1, 4); assert_eq!(worker.current_vote_target(), None); set_up(&mut worker, 5, Some(2), 1, 4); assert_eq!(worker.current_vote_target(), None); // vote on min delta set_up(&mut worker, 9, Some(4), 1, 4); assert_eq!(worker.current_vote_target(), Some(8)); set_up(&mut worker, 18, Some(10), 1, 8); assert_eq!(worker.current_vote_target(), Some(18)); // vote on power of two set_up(&mut worker, 1008, Some(1000), 1, 1); assert_eq!(worker.current_vote_target(), Some(1004)); set_up(&mut worker, 1016, Some(1000), 1, 2); assert_eq!(worker.current_vote_target(), Some(1008)); // nothing new to vote on set_up(&mut worker, 1000, Some(1000), 1, 1); assert_eq!(worker.current_vote_target(), None); // vote on mandatory set_up(&mut worker, 1008, None, 1000, 8); assert_eq!(worker.current_vote_target(), Some(1000)); set_up(&mut worker, 1008, Some(1000), 1001, 8); assert_eq!(worker.current_vote_target(), Some(1001)); } #[test] fn keystore_vs_validator_set() { let keys = &[Keyring::Alice]; let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap(); let mut net = BeefyTestNet::new(1, 0); let mut worker = create_beefy_worker(&net.peer(0), &keys[0], 1); // keystore doesn't contain other keys than validators' assert_eq!(worker.verify_validator_set(&1, &validator_set), Ok(())); // unknown `Bob` key let keys = &[Keyring::Bob]; let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap(); let err_msg = "no authority public key found in store".to_string(); let expected = Err(error::Error::Keystore(err_msg)); assert_eq!(worker.verify_validator_set(&1, &validator_set), expected); // worker has no keystore worker.key_store = None.into(); let expected_err = Err(error::Error::Keystore("no Keystore".into())); assert_eq!(worker.verify_validator_set(&1, &validator_set), expected_err); } #[test] fn setting_best_beefy_block() { let keys = &[Keyring::Alice]; let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap(); let mut net = BeefyTestNet::new(1, 0); let mut worker = create_beefy_worker(&net.peer(0), &keys[0], 1); let (mut best_block_streams, _) = get_beefy_streams(&mut net, keys); let mut best_block_stream = best_block_streams.drain(..).next().unwrap(); // no 'best beefy block' assert_eq!(worker.best_beefy_block, None); block_on(poll_fn(move |cx| { assert_eq!(best_block_stream.poll_next_unpin(cx), Poll::Pending); Poll::Ready(()) })); // unknown hash for block #1 let (mut best_block_streams, _) = get_beefy_streams(&mut net, keys); let mut best_block_stream = best_block_streams.drain(..).next().unwrap(); worker.set_best_beefy_block(1); assert_eq!(worker.best_beefy_block, Some(1)); block_on(poll_fn(move |cx| { assert_eq!(best_block_stream.poll_next_unpin(cx), Poll::Pending); Poll::Ready(()) })); // generate 2 blocks, try again expect success let (mut best_block_streams, _) = get_beefy_streams(&mut net, keys); let mut best_block_stream = best_block_streams.drain(..).next().unwrap(); net.generate_blocks(2, 10, &validator_set, false); worker.set_best_beefy_block(2); assert_eq!(worker.best_beefy_block, Some(2)); block_on(poll_fn(move |cx| { match best_block_stream.poll_next_unpin(cx) { // expect Some(hash-of-block-2) Poll::Ready(Some(hash)) => { let block_num = net.peer(0).client().as_client().number(hash).unwrap(); assert_eq!(block_num, Some(2)); }, v => panic!("unexpected value: {:?}", v), } Poll::Ready(()) })); } #[test] fn setting_initial_session() { let keys = &[Keyring::Alice]; let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap(); let mut net = BeefyTestNet::new(1, 0); let mut worker = create_beefy_worker(&net.peer(0), &keys[0], 1); assert!(worker.rounds.is_none()); // verify setting the correct validator sets and boundary for genesis session worker.init_session_at(validator_set.clone(), 1); let worker_rounds = worker.rounds.as_ref().unwrap(); assert_eq!(worker_rounds.session_start(), &1); // in genesis case both current and prev validator sets are the same assert_eq!(worker_rounds.validators(), validator_set.validators()); assert_eq!(worker_rounds.validator_set_id(), validator_set.id()); // new validator set let keys = &[Keyring::Bob]; let new_validator_set = ValidatorSet::new(make_beefy_ids(keys), 1).unwrap(); // verify setting the correct validator sets and boundary for non-genesis session worker.init_session_at(new_validator_set.clone(), 11); let worker_rounds = worker.rounds.as_ref().unwrap(); assert_eq!(worker_rounds.session_start(), &11); assert_eq!(worker_rounds.validators(), new_validator_set.validators()); assert_eq!(worker_rounds.validator_set_id(), new_validator_set.id()); } }