// 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>, Error> { let rounds = self.persisted_state.voting_oracle.active_rounds_mut()?; let block_number = vote.commitment.block_number; match rounds.add_vote(vote) { VoteImportResult::RoundConcluded(signed_commitment) => { let finality_proof = VersionedFinalityProof::V1(signed_commitment); debug!( target: LOG_TARGET, "🥩 Round #{} concluded, finality_proof: {:?}.", block_number, finality_proof ); // We created the `finality_proof` and know to be valid. // New state is persisted after finalization. self.finalize(finality_proof.clone())?; metric_inc!(self.metrics, beefy_good_votes_processed); return Ok(Some(finality_proof)) }, VoteImportResult::Ok => { // Persist state after handling mandatory block vote. if self .voting_oracle() .mandatory_pending() .map(|(mandatory_num, _)| mandatory_num == block_number) .unwrap_or(false) { crate::aux_schema::write_voter_state(&*self.backend, &self.persisted_state) .map_err(|e| Error::Backend(e.to_string()))?; } metric_inc!(self.metrics, beefy_good_votes_processed); }, VoteImportResult::Equivocation(proof) => { metric_inc!(self.metrics, beefy_equivocation_votes); self.report_equivocation(proof)?; }, VoteImportResult::Invalid => metric_inc!(self.metrics, beefy_invalid_votes), VoteImportResult::Stale => metric_inc!(self.metrics, beefy_stale_votes), }; Ok(None) } /// Provide BEEFY finality for block based on `finality_proof`: /// 1. Prune now-irrelevant past sessions from the oracle, /// 2. Set BEEFY best block, /// 3. Persist voter state, /// 4. Send best block hash and `finality_proof` to RPC worker. /// /// Expects `finality proof` to be valid and for a block > current-best-beefy. fn finalize(&mut self, finality_proof: BeefyVersionedFinalityProof) -> Result<(), Error> { let block_num = match finality_proof { VersionedFinalityProof::V1(ref sc) => sc.commitment.block_number, }; if block_num <= self.persisted_state.voting_oracle.best_beefy_block { // we've already finalized this round before, short-circuit. return Ok(()) } // Finalize inner round and update voting_oracle state. self.persisted_state.voting_oracle.finalize(block_num)?; // Set new best BEEFY block number. self.persisted_state.set_best_beefy(block_num); crate::aux_schema::write_voter_state(&*self.backend, &self.persisted_state) .map_err(|e| Error::Backend(e.to_string()))?; metric_set!(self.metrics, beefy_best_block, block_num); self.comms.on_demand_justifications.cancel_requests_older_than(block_num); if let Err(e) = self .backend .blockchain() .expect_block_hash_from_id(&BlockId::Number(block_num)) .and_then(|hash| { self.links .to_rpc_best_block_sender .notify(|| Ok::<_, ()>(hash)) .expect("forwards closure result; the closure always returns Ok; qed."); self.backend .append_justification(hash, (BEEFY_ENGINE_ID, finality_proof.encode())) }) { debug!( target: LOG_TARGET, "🥩 Error {:?} on appending justification: {:?}", e, finality_proof ); } self.links .to_rpc_justif_sender .notify(|| Ok::<_, ()>(finality_proof)) .expect("forwards closure result; the closure always returns Ok; qed."); // Update gossip validator votes filter. self.persisted_state .gossip_filter_config() .map(|filter| self.comms.gossip_validator.update_filter(filter))?; Ok(()) } /// Handle previously buffered justifications, that now land in the voting interval. fn try_pending_justifications(&mut self) -> Result<(), Error> { // Interval of blocks for which we can process justifications and votes right now. let (start, end) = self.voting_oracle().accepted_interval()?; // Process pending justifications. if !self.pending_justifications.is_empty() { // These are still pending. let still_pending = self.pending_justifications.split_off(&end.saturating_add(1u32.into())); // These can be processed. let justifs_to_process = self.pending_justifications.split_off(&start); // The rest can be dropped. self.pending_justifications = still_pending; for (num, justification) in justifs_to_process.into_iter() { debug!(target: LOG_TARGET, "🥩 Handle buffered justification for: {:?}.", num); metric_inc!(self.metrics, beefy_imported_justifications); if let Err(err) = self.finalize(justification) { error!(target: LOG_TARGET, "🥩 Error finalizing block: {}", err); } } metric_set!( self.metrics, beefy_buffered_justifications, self.pending_justifications.len() ); } Ok(()) } /// Decide if should vote, then vote.. or don't.. fn try_to_vote(&mut self) -> Result<(), Error> { // Vote if there's now a new vote target. if let Some(target) = self.voting_oracle().voting_target() { metric_set!(self.metrics, beefy_should_vote_on, target); if target > self.persisted_state.best_voted { self.do_vote(target)?; } } Ok(()) } /// 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) -> Result<(), Error> { debug!(target: LOG_TARGET, "🥩 Try voting on {}", target_number); // Most of the time we get here, `target` is actually `best_grandpa`, // avoid getting header from backend in that case. let target_header = if target_number == self.best_grandpa_block() { self.persisted_state.voting_oracle.best_grandpa_block_header.clone() } else { let hash = self .backend .blockchain() .expect_block_hash_from_id(&BlockId::Number(target_number)) .map_err(|err| { let err_msg = format!( "Couldn't get hash for block #{:?} (error: {:?}), skipping vote..", target_number, err ); Error::Backend(err_msg) })?; self.backend.blockchain().expect_header(hash).map_err(|err| { let err_msg = format!( "Couldn't get header for block #{:?} ({:?}) (error: {:?}), skipping vote..", target_number, hash, err ); Error::Backend(err_msg) })? }; let target_hash = target_header.hash(); let payload = if let Some(hash) = self.payload_provider.payload(&target_header) { hash } else { warn!(target: LOG_TARGET, "🥩 No MMR root digest found for: {:?}", target_hash); return Ok(()) }; let rounds = self.persisted_state.voting_oracle.active_rounds_mut()?; let (validators, validator_set_id) = (rounds.validators(), rounds.validator_set_id()); let authority_id = if let Some(id) = self.key_store.authority_id(validators) { debug!(target: LOG_TARGET, "🥩 Local authority id: {:?}", id); id } else { debug!( target: LOG_TARGET, "🥩 Missing validator id - can't vote for: {:?}", target_hash ); return Ok(()) }; 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: LOG_TARGET, "🥩 Error signing commitment: {:?}", err); return Ok(()) }, }; trace!( target: LOG_TARGET, "🥩 Produced signature using {:?}, is_valid: {:?}", authority_id, BeefyKeystore::verify(&authority_id, &signature, &encoded_commitment) ); let vote = VoteMessage { commitment, id: authority_id, signature }; if let Some(finality_proof) = self.handle_vote(vote.clone()).map_err(|err| { error!(target: LOG_TARGET, "🥩 Error handling self vote: {}", err); err })? { let encoded_proof = GossipMessage::::FinalityProof(finality_proof).encode(); self.comms .gossip_engine .gossip_message(proofs_topic::(), encoded_proof, true); } else { metric_inc!(self.metrics, beefy_votes_sent); debug!(target: LOG_TARGET, "🥩 Sent vote message: {:?}", vote); let encoded_vote = GossipMessage::::Vote(vote).encode(); self.comms.gossip_engine.gossip_message(votes_topic::(), encoded_vote, false); } // Persist state after vote to avoid double voting in case of voter restarts. self.persisted_state.best_voted = target_number; metric_set!(self.metrics, beefy_best_voted, target_number); crate::aux_schema::write_voter_state(&*self.backend, &self.persisted_state) .map_err(|e| Error::Backend(e.to_string())) } fn process_new_state(&mut self) { // Handle pending justifications and/or votes for now GRANDPA finalized blocks. if let Err(err) = self.try_pending_justifications() { debug!(target: LOG_TARGET, "🥩 {}", err); } // Don't bother voting or requesting justifications during major sync. if !self.sync.is_major_syncing() { // There were external events, 'state' is changed, author a vote if needed/possible. if let Err(err) = self.try_to_vote() { debug!(target: LOG_TARGET, "🥩 {}", err); } // If the current target is a mandatory block, // make sure there's also an on-demand justification request out for it. if let Some((block, active)) = self.voting_oracle().mandatory_pending() { // This only starts new request if there isn't already an active one. self.comms.on_demand_justifications.request(block, active); } } } /// Main loop for BEEFY worker. /// /// Run the main async loop which is driven by finality notifications and gossiped votes. /// Should never end, returns `Error` otherwise. pub(crate) async fn run( mut self, block_import_justif: &mut Fuse>>, finality_notifications: &mut Fuse>, ) -> (Error, BeefyComms) { info!( target: LOG_TARGET, "🥩 run BEEFY worker, best grandpa: #{:?}.", self.best_grandpa_block() ); let mut votes = Box::pin( self.comms .gossip_engine .messages_for(votes_topic::()) .filter_map(|notification| async move { let vote = GossipMessage::::decode_all(&mut ¬ification.message[..]) .ok() .and_then(|message| message.unwrap_vote()); trace!(target: LOG_TARGET, "🥩 Got vote message: {:?}", vote); vote }) .fuse(), ); let mut gossip_proofs = Box::pin( self.comms .gossip_engine .messages_for(proofs_topic::()) .filter_map(|notification| async move { let proof = GossipMessage::::decode_all(&mut ¬ification.message[..]) .ok() .and_then(|message| message.unwrap_finality_proof()); trace!(target: LOG_TARGET, "🥩 Got gossip proof message: {:?}", proof); proof }) .fuse(), ); self.process_new_state(); let error = loop { // Mutable reference used to drive the gossip engine. let mut gossip_engine = &mut self.comms.gossip_engine; // Wait for, and handle external events. // The branches below only change 'state', actual voting happens afterwards, // based on the new resulting 'state'. futures::select_biased! { // Use `select_biased!` to prioritize order below. // Process finality notifications first since these drive the voter. notification = finality_notifications.next() => { if let Some(notif) = notification { if let Err(err) = self.handle_finality_notification(¬if) { break err; } } else { break Error::FinalityStreamTerminated; } }, // Make sure to pump gossip engine. _ = gossip_engine => { break Error::GossipEngineTerminated; }, // Process incoming justifications as these can make some in-flight votes obsolete. response_info = self.comms.on_demand_justifications.next().fuse() => { match response_info { ResponseInfo::ValidProof(justif, peer_report) => { if let Err(err) = self.triage_incoming_justif(justif) { debug!(target: LOG_TARGET, "🥩 {}", err); } self.comms.gossip_engine.report(peer_report.who, peer_report.cost_benefit); }, ResponseInfo::PeerReport(peer_report) => { self.comms.gossip_engine.report(peer_report.who, peer_report.cost_benefit); }, ResponseInfo::Pending => {}, } }, justif = block_import_justif.next() => { if let Some(justif) = justif { // Block import justifications have already been verified to be valid // by `BeefyBlockImport`. if let Err(err) = self.triage_incoming_justif(justif) { debug!(target: LOG_TARGET, "🥩 {}", err); } } else { break Error::BlockImportStreamTerminated; } }, justif = gossip_proofs.next() => { if let Some(justif) = justif { // Gossiped justifications have already been verified by `GossipValidator`. if let Err(err) = self.triage_incoming_justif(justif) { debug!(target: LOG_TARGET, "🥩 {}", err); } } else { break Error::FinalityProofGossipStreamTerminated; } }, // Finally process incoming votes. vote = votes.next() => { if let Some(vote) = vote { // Votes have already been verified to be valid by the gossip validator. if let Err(err) = self.triage_incoming_vote(vote) { debug!(target: LOG_TARGET, "🥩 {}", err); } } else { break Error::VotesGossipStreamTerminated; } }, } // Act on changed 'state'. self.process_new_state(); }; // return error _and_ `comms` that can be reused (error, self.comms) } /// 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(crate) fn report_equivocation( &self, proof: EquivocationProof, AuthorityId, Signature>, ) -> Result<(), Error> { let rounds = self.persisted_state.voting_oracle.active_rounds()?; let (validators, validator_set_id) = (rounds.validators(), rounds.validator_set_id()); let offender_id = proof.offender_id().clone(); if !check_equivocation_proof::<_, _, BeefySignatureHasher>(&proof) { debug!(target: LOG_TARGET, "🥩 Skip report for bad equivocation {:?}", proof); return Ok(()) } else if let Some(local_id) = self.key_store.authority_id(validators) { if offender_id == local_id { warn!(target: LOG_TARGET, "🥩 Skip equivocation report for own equivocation"); return Ok(()) } } let number = *proof.round_number(); let hash = self .backend .blockchain() .expect_block_hash_from_id(&BlockId::Number(number)) .map_err(|err| { let err_msg = format!( "Couldn't get hash for block #{:?} (error: {:?}), skipping report for equivocation", number, err ); Error::Backend(err_msg) })?; let runtime_api = self.runtime.runtime_api(); // generate key ownership proof at that block let key_owner_proof = match runtime_api .generate_key_ownership_proof(hash, validator_set_id, offender_id) .map_err(Error::RuntimeApi)? { Some(proof) => proof, None => { debug!( target: LOG_TARGET, "🥩 Equivocation offender not part of the authority set." ); return Ok(()) }, }; // submit equivocation report at **best** block let best_block_hash = self.backend.blockchain().info().best_hash; runtime_api .submit_report_equivocation_unsigned_extrinsic(best_block_hash, proof, key_owner_proof) .map_err(Error::RuntimeApi)?; Ok(()) } } /// Calculate next block number to vote on. /// /// Return `None` if there is no votable target yet. fn vote_target(best_grandpa: N, best_beefy: N, 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 = if best_beefy < session_start { debug!(target: LOG_TARGET, "🥩 vote target - mandatory block: #{:?}", session_start); session_start } else { let diff = best_grandpa.saturating_sub(best_beefy) + 1u32.into(); let diff = diff.saturated_into::() / 2; let target = best_beefy + min_delta.max(diff.next_power_of_two()).into(); trace!( target: LOG_TARGET, "🥩 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::{ communication::{ gossip::{tests::TestNetwork, GossipValidator}, notification::{BeefyBestBlockStream, BeefyVersionedFinalityProofStream}, request_response::outgoing_requests_engine::OnDemandJustificationsEngine, }, tests::{ create_beefy_keystore, get_beefy_streams, make_beefy_ids, BeefyPeer, BeefyTestNet, TestApi, }, BeefyRPCLinks, KnownPeers, }; use futures::{future::poll_fn, task::Poll}; use parking_lot::Mutex; use sc_client_api::{Backend as BackendT, HeaderBackend}; use sc_network_gossip::GossipEngine; use sc_network_sync::SyncingService; use sc_network_test::TestNetFactory; use sp_blockchain::Backend as BlockchainBackendT; use sp_consensus_beefy::{ known_payloads, known_payloads::MMR_ROOT_ID, mmr::MmrRootProvider, test_utils::{generate_equivocation_proof, Keyring}, ConsensusLog, Payload, SignedCommitment, }; use sp_runtime::traits::{Header as HeaderT, One}; use substrate_test_runtime_client::{ runtime::{Block, Digest, DigestItem, Header}, Backend, }; impl PersistedState { pub fn active_round(&self) -> Result<&Rounds, Error> { self.voting_oracle.active_rounds() } pub fn best_grandpa_number(&self) -> NumberFor { *self.voting_oracle.best_grandpa_block_header.number() } } impl VoterOracle { pub fn sessions(&self) -> &VecDeque> { &self.sessions } } fn create_beefy_worker( peer: &mut BeefyPeer, key: &Keyring, min_block_delta: u32, genesis_validator_set: ValidatorSet, ) -> BeefyWorker< Block, Backend, MmrRootProvider, TestApi, Arc>, TestNetwork, > { let keystore = create_beefy_keystore(key); let (to_rpc_justif_sender, from_voter_justif_stream) = BeefyVersionedFinalityProofStream::::channel(); let (to_rpc_best_block_sender, from_voter_best_beefy_stream) = BeefyBestBlockStream::::channel(); let (_, from_block_import_justif_stream) = BeefyVersionedFinalityProofStream::::channel(); let beefy_rpc_links = BeefyRPCLinks { from_voter_justif_stream, from_voter_best_beefy_stream }; *peer.data.beefy_rpc_links.lock() = Some(beefy_rpc_links); let links = BeefyVoterLinks { from_block_import_justif_stream, to_rpc_justif_sender, to_rpc_best_block_sender, }; let backend = peer.client().as_backend(); let beefy_genesis = 1; let api = Arc::new(TestApi::with_validator_set(&genesis_validator_set)); let network = peer.network_service().clone(); let sync = peer.sync_service().clone(); let notification_service = peer .take_notification_service(&crate::tests::beefy_gossip_proto_name()) .unwrap(); let known_peers = Arc::new(Mutex::new(KnownPeers::new())); let gossip_validator = GossipValidator::new(known_peers.clone(), Arc::new(TestNetwork::new().0)); let gossip_validator = Arc::new(gossip_validator); let gossip_engine = GossipEngine::new( network.clone(), sync.clone(), notification_service, "/beefy/1", gossip_validator.clone(), None, ); let metrics = None; let on_demand_justifications = OnDemandJustificationsEngine::new( network.clone(), "/beefy/justifs/1".into(), known_peers, None, ); // Push 1 block - will start first session. let hashes = peer.push_blocks(1, false); backend.finalize_block(hashes[0], None).unwrap(); let first_header = backend .blockchain() .expect_header(backend.blockchain().info().best_hash) .unwrap(); let persisted_state = PersistedState::checked_new( first_header, Zero::zero(), vec![Rounds::new(One::one(), genesis_validator_set)].into(), min_block_delta, beefy_genesis, ) .unwrap(); let payload_provider = MmrRootProvider::new(api.clone()); let comms = BeefyComms { gossip_engine, gossip_validator, on_demand_justifications }; BeefyWorker { backend, runtime: api, key_store: Some(keystore).into(), metrics, payload_provider, sync: Arc::new(sync), links, comms, pending_justifications: BTreeMap::new(), persisted_state, is_authority: true, } } #[test] fn vote_on_min_block_delta() { let t = vote_target(1u32, 1, 1, 4); assert_eq!(None, t); let t = vote_target(2u32, 1, 1, 4); assert_eq!(None, t); let t = vote_target(4u32, 2, 1, 4); assert_eq!(None, t); let t = vote_target(6u32, 2, 1, 4); assert_eq!(Some(6), t); let t = vote_target(9u32, 4, 1, 4); assert_eq!(Some(8), t); let t = vote_target(10u32, 10, 1, 8); assert_eq!(None, t); let t = vote_target(12u32, 10, 1, 8); assert_eq!(None, t); let t = vote_target(18u32, 10, 1, 8); assert_eq!(Some(18), t); } #[test] fn vote_on_power_of_two() { let t = vote_target(1008u32, 1000, 1, 4); assert_eq!(Some(1004), t); let t = vote_target(1016u32, 1000, 1, 4); assert_eq!(Some(1008), t); let t = vote_target(1032u32, 1000, 1, 4); assert_eq!(Some(1016), t); let t = vote_target(1064u32, 1000, 1, 4); assert_eq!(Some(1032), t); let t = vote_target(1128u32, 1000, 1, 4); assert_eq!(Some(1064), t); let t = vote_target(1256u32, 1000, 1, 4); assert_eq!(Some(1128), t); let t = vote_target(1512u32, 1000, 1, 4); assert_eq!(Some(1256), t); let t = vote_target(1024u32, 1, 1, 4); assert_eq!(Some(513), t); } #[test] fn vote_on_target_block() { let t = vote_target(1008u32, 1002, 1, 4); assert_eq!(Some(1006), t); let t = vote_target(1010u32, 1002, 1, 4); assert_eq!(Some(1006), t); let t = vote_target(1016u32, 1006, 1, 4); assert_eq!(Some(1014), t); let t = vote_target(1022u32, 1006, 1, 4); assert_eq!(Some(1014), t); let t = vote_target(1032u32, 1012, 1, 4); assert_eq!(Some(1028), t); let t = vote_target(1044u32, 1012, 1, 4); assert_eq!(Some(1028), t); let t = vote_target(1064u32, 1014, 1, 4); assert_eq!(Some(1046), t); let t = vote_target(1078u32, 1014, 1, 4); assert_eq!(Some(1046), t); let t = vote_target(1128u32, 1008, 1, 4); assert_eq!(Some(1072), t); let t = vote_target(1136u32, 1008, 1, 4); assert_eq!(Some(1072), t); } #[test] fn vote_on_mandatory_block() { let t = vote_target(1008u32, 1002, 1004, 4); assert_eq!(Some(1004), t); let t = vote_target(1016u32, 1006, 1007, 4); assert_eq!(Some(1007), t); let t = vote_target(1064u32, 1014, 1063, 4); assert_eq!(Some(1063), t); let t = vote_target(1320u32, 1012, 1234, 4); assert_eq!(Some(1234), t); let t = vote_target(1128u32, 1008, 1008, 4); assert_eq!(Some(1072), t); } #[test] fn should_vote_target() { let header = Header::new( 1u32.into(), Default::default(), Default::default(), Default::default(), Digest::default(), ); let mut oracle = VoterOracle:: { best_beefy_block: 0, best_grandpa_block_header: header, min_block_delta: 1, sessions: VecDeque::new(), }; let voting_target_with = |oracle: &mut VoterOracle, best_beefy: NumberFor, best_grandpa: NumberFor| -> Option> { oracle.best_beefy_block = best_beefy; oracle.best_grandpa_block_header.number = best_grandpa; oracle.voting_target() }; // rounds not initialized -> should vote: `None` assert_eq!(voting_target_with(&mut oracle, 0, 1), None); let keys = &[Keyring::Alice]; let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap(); oracle.add_session(Rounds::new(1, validator_set.clone())); // under min delta oracle.min_block_delta = 4; assert_eq!(voting_target_with(&mut oracle, 1, 1), None); assert_eq!(voting_target_with(&mut oracle, 2, 5), None); // vote on min delta assert_eq!(voting_target_with(&mut oracle, 4, 9), Some(8)); oracle.min_block_delta = 8; assert_eq!(voting_target_with(&mut oracle, 10, 18), Some(18)); // vote on power of two oracle.min_block_delta = 1; assert_eq!(voting_target_with(&mut oracle, 1000, 1008), Some(1004)); assert_eq!(voting_target_with(&mut oracle, 1000, 1016), Some(1008)); // nothing new to vote on assert_eq!(voting_target_with(&mut oracle, 1000, 1000), None); // vote on mandatory oracle.sessions.clear(); oracle.add_session(Rounds::new(1000, validator_set.clone())); assert_eq!(voting_target_with(&mut oracle, 0, 1008), Some(1000)); oracle.sessions.clear(); oracle.add_session(Rounds::new(1001, validator_set.clone())); assert_eq!(voting_target_with(&mut oracle, 1000, 1008), Some(1001)); } #[test] fn test_oracle_accepted_interval() { let keys = &[Keyring::Alice]; let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap(); let header = Header::new( 1u32.into(), Default::default(), Default::default(), Default::default(), Digest::default(), ); let mut oracle = VoterOracle:: { best_beefy_block: 0, best_grandpa_block_header: header, min_block_delta: 1, sessions: VecDeque::new(), }; let accepted_interval_with = |oracle: &mut VoterOracle, best_grandpa: NumberFor| -> Result<(NumberFor, NumberFor), Error> { oracle.best_grandpa_block_header.number = best_grandpa; oracle.accepted_interval() }; // rounds not initialized -> should accept votes: `None` assert!(accepted_interval_with(&mut oracle, 1).is_err()); let session_one = 1; oracle.add_session(Rounds::new(session_one, validator_set.clone())); // mandatory not done, only accept mandatory for i in 0..15 { assert_eq!(accepted_interval_with(&mut oracle, i), Ok((session_one, session_one))); } // add more sessions, nothing changes let session_two = 11; let session_three = 21; oracle.add_session(Rounds::new(session_two, validator_set.clone())); oracle.add_session(Rounds::new(session_three, validator_set.clone())); // mandatory not done, should accept mandatory for session_one for i in session_three..session_three + 15 { assert_eq!(accepted_interval_with(&mut oracle, i), Ok((session_one, session_one))); } // simulate finish mandatory for session one, prune oracle oracle.sessions.front_mut().unwrap().test_set_mandatory_done(true); oracle.try_prune(); // session_one pruned, should accept mandatory for session_two for i in session_three..session_three + 15 { assert_eq!(accepted_interval_with(&mut oracle, i), Ok((session_two, session_two))); } // simulate finish mandatory for session two, prune oracle oracle.sessions.front_mut().unwrap().test_set_mandatory_done(true); oracle.try_prune(); // session_two pruned, should accept mandatory for session_three for i in session_three..session_three + 15 { assert_eq!(accepted_interval_with(&mut oracle, i), Ok((session_three, session_three))); } // simulate finish mandatory for session three oracle.sessions.front_mut().unwrap().test_set_mandatory_done(true); // verify all other blocks in this session are now open to voting for i in session_three..session_three + 15 { assert_eq!(accepted_interval_with(&mut oracle, i), Ok((session_three, i))); } // pruning does nothing in this case oracle.try_prune(); for i in session_three..session_three + 15 { assert_eq!(accepted_interval_with(&mut oracle, i), Ok((session_three, i))); } // adding new session automatically prunes "finalized" previous session let session_four = 31; oracle.add_session(Rounds::new(session_four, validator_set.clone())); assert_eq!(oracle.sessions.front().unwrap().session_start(), session_four); assert_eq!( accepted_interval_with(&mut oracle, session_four + 10), Ok((session_four, session_four)) ); } #[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)); } #[tokio::test] async fn should_finalize_correctly() { let keys = [Keyring::Alice]; let validator_set = ValidatorSet::new(make_beefy_ids(&keys), 0).unwrap(); let mut net = BeefyTestNet::new(1); let backend = net.peer(0).client().as_backend(); let mut worker = create_beefy_worker(net.peer(0), &keys[0], 1, validator_set.clone()); // remove default session, will manually add custom one. worker.persisted_state.voting_oracle.sessions.clear(); let keys = keys.iter().cloned().enumerate(); let (mut best_block_streams, mut finality_proofs) = get_beefy_streams(&mut net, keys.clone()); let mut best_block_stream = best_block_streams.drain(..).next().unwrap(); let mut finality_proof = finality_proofs.drain(..).next().unwrap(); let create_finality_proof = |block_num: NumberFor| { let commitment = Commitment { payload: Payload::from_single_entry(known_payloads::MMR_ROOT_ID, vec![]), block_number: block_num, validator_set_id: validator_set.id(), }; VersionedFinalityProof::V1(SignedCommitment { commitment, signatures: vec![None] }) }; // no 'best beefy block' or finality proofs assert_eq!(worker.persisted_state.best_beefy(), 0); poll_fn(move |cx| { assert_eq!(best_block_stream.poll_next_unpin(cx), Poll::Pending); assert_eq!(finality_proof.poll_next_unpin(cx), Poll::Pending); Poll::Ready(()) }) .await; let client = net.peer(0).client().as_client(); // unknown hash for block #1 let (mut best_block_streams, mut finality_proofs) = get_beefy_streams(&mut net, keys.clone()); let mut best_block_stream = best_block_streams.drain(..).next().unwrap(); let mut finality_proof = finality_proofs.drain(..).next().unwrap(); let justif = create_finality_proof(1); // create new session at block #1 worker .persisted_state .voting_oracle .add_session(Rounds::new(1, validator_set.clone())); // try to finalize block #1 worker.finalize(justif.clone()).unwrap(); // verify block finalized assert_eq!(worker.persisted_state.best_beefy(), 1); poll_fn(move |cx| { // expect Some(hash-of-block-1) match best_block_stream.poll_next_unpin(cx) { Poll::Ready(Some(hash)) => { let block_num = client.number(hash).unwrap(); assert_eq!(block_num, Some(1)); }, v => panic!("unexpected value: {:?}", v), } // commitment streamed match finality_proof.poll_next_unpin(cx) { // expect justification Poll::Ready(Some(received)) => assert_eq!(received, justif), v => panic!("unexpected value: {:?}", v), } Poll::Ready(()) }) .await; // 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(); let hashes = net.peer(0).push_blocks(1, false); // finalize 1 and 2 without justifications (hashes does not contain genesis) let hashof2 = hashes[0]; backend.finalize_block(hashof2, None).unwrap(); let justif = create_finality_proof(2); // create new session at block #2 worker.persisted_state.voting_oracle.add_session(Rounds::new(2, validator_set)); worker.finalize(justif).unwrap(); // verify old session pruned assert_eq!(worker.voting_oracle().sessions.len(), 1); // new session starting at #2 is in front assert_eq!(worker.active_rounds().unwrap().session_start(), 2); // verify block finalized assert_eq!(worker.persisted_state.best_beefy(), 2); 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(()) }) .await; // check BEEFY justifications are also appended to backend let justifs = backend.blockchain().justifications(hashof2).unwrap().unwrap(); assert!(justifs.get(BEEFY_ENGINE_ID).is_some()) } #[tokio::test] async fn should_init_session() { let keys = &[Keyring::Alice, Keyring::Bob]; let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap(); let mut net = BeefyTestNet::new(1); let mut worker = create_beefy_worker(net.peer(0), &keys[0], 1, validator_set.clone()); let worker_rounds = worker.active_rounds().unwrap(); assert_eq!(worker_rounds.session_start(), 1); 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(); worker.init_session_at(new_validator_set.clone(), 11); // Since mandatory is not done for old rounds, we still get those. let rounds = worker.persisted_state.voting_oracle.active_rounds_mut().unwrap(); assert_eq!(rounds.validator_set_id(), validator_set.id()); // Let's finalize mandatory. rounds.test_set_mandatory_done(true); worker.persisted_state.voting_oracle.try_prune(); // Now we should get the next round. let rounds = worker.active_rounds().unwrap(); // Expect new values. assert_eq!(rounds.session_start(), 11); assert_eq!(rounds.validators(), new_validator_set.validators()); assert_eq!(rounds.validator_set_id(), new_validator_set.id()); } #[tokio::test] async fn should_not_report_bad_old_or_self_equivocations() { let block_num = 1; let set_id = 1; let keys = [Keyring::Alice]; let validator_set = ValidatorSet::new(make_beefy_ids(&keys), set_id).unwrap(); // Alice votes on good MMR roots, equivocations are allowed/expected let mut api_alice = TestApi::with_validator_set(&validator_set); api_alice.allow_equivocations(); let api_alice = Arc::new(api_alice); let mut net = BeefyTestNet::new(1); let mut worker = create_beefy_worker(net.peer(0), &keys[0], 1, validator_set.clone()); worker.runtime = api_alice.clone(); // let there be a block with num = 1: let _ = net.peer(0).push_blocks(1, false); let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]); // generate an equivocation proof, with Bob as perpetrator let good_proof = generate_equivocation_proof( (block_num, payload1.clone(), set_id, &Keyring::Bob), (block_num, payload2.clone(), set_id, &Keyring::Bob), ); { // expect voter (Alice) to successfully report it assert_eq!(worker.report_equivocation(good_proof.clone()), Ok(())); // verify Alice reports Bob equivocation to runtime let reported = api_alice.reported_equivocations.as_ref().unwrap().lock(); assert_eq!(reported.len(), 1); assert_eq!(*reported.get(0).unwrap(), good_proof); } api_alice.reported_equivocations.as_ref().unwrap().lock().clear(); // now let's try with a bad proof let mut bad_proof = good_proof.clone(); bad_proof.first.id = Keyring::Charlie.public(); // bad proofs are simply ignored assert_eq!(worker.report_equivocation(bad_proof), Ok(())); // verify nothing reported to runtime assert!(api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty()); // now let's try with old set it let mut old_proof = good_proof.clone(); old_proof.first.commitment.validator_set_id = 0; old_proof.second.commitment.validator_set_id = 0; // old proofs are simply ignored assert_eq!(worker.report_equivocation(old_proof), Ok(())); // verify nothing reported to runtime assert!(api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty()); // now let's try reporting a self-equivocation let self_proof = generate_equivocation_proof( (block_num, payload1.clone(), set_id, &Keyring::Alice), (block_num, payload2.clone(), set_id, &Keyring::Alice), ); // equivocations done by 'self' are simply ignored (not reported) assert_eq!(worker.report_equivocation(self_proof), Ok(())); // verify nothing reported to runtime assert!(api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty()); } }