diff --git a/client/consensus/beefy/src/communication/fisherman.rs b/client/consensus/beefy/src/communication/fisherman.rs new file mode 100644 index 0000000000000..2fa91ce783966 --- /dev/null +++ b/client/consensus/beefy/src/communication/fisherman.rs @@ -0,0 +1,244 @@ +// 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::{ + error::Error, + justification::BeefyVersionedFinalityProof, + keystore::{BeefyKeystore, BeefySignatureHasher}, + LOG_TARGET, +}; +use log::debug; +use sc_client_api::Backend; +use sp_api::ProvideRuntimeApi; +use sp_blockchain::HeaderBackend; +use sp_consensus_beefy::{ + check_fork_equivocation_proof, + ecdsa_crypto::{AuthorityId, Signature}, + BeefyApi, ForkEquivocationProof, Payload, PayloadProvider, SignedCommitment, ValidatorSet, + VoteMessage, +}; +use sp_runtime::{ + generic::BlockId, + traits::{Block, Header, NumberFor}, +}; +use std::{marker::PhantomData, sync::Arc}; + +pub(crate) trait BeefyFisherman: Send + Sync { + /// Check `vote` for contained block against expected payload. + fn check_vote( + &self, + vote: VoteMessage, AuthorityId, Signature>, + ) -> Result<(), Error>; + + /// Check `signed_commitment` for contained block against expected payload. + fn check_signed_commitment( + &self, + signed_commitment: SignedCommitment, Signature>, + ) -> Result<(), Error>; + + /// Check `proof` for contained block against expected payload. + fn check_proof(&self, proof: BeefyVersionedFinalityProof) -> Result<(), Error>; +} + +/// Helper wrapper used to check gossiped votes for (historical) equivocations, +/// and report any such protocol infringements. +pub(crate) struct Fisherman { + pub backend: Arc, + pub runtime: Arc, + pub key_store: Arc, + pub payload_provider: P, + pub _phantom: PhantomData, +} + +impl Fisherman +where + B: Block, + BE: Backend, + P: PayloadProvider, + R: ProvideRuntimeApi + Send + Sync, + R::Api: BeefyApi, +{ + fn expected_header_and_payload( + &self, + number: NumberFor, + ) -> Result<(B::Header, Payload), Error> { + // This should be un-ambiguous since `number` is finalized. + let hash = self + .backend + .blockchain() + .expect_block_hash_from_id(&BlockId::Number(number)) + .map_err(|e| Error::Backend(e.to_string()))?; + let header = self + .backend + .blockchain() + .expect_header(hash) + .map_err(|e| Error::Backend(e.to_string()))?; + self.payload_provider + .payload(&header) + .map(|payload| (header, payload)) + .ok_or_else(|| Error::Backend("BEEFY Payload not found".into())) + } + + fn active_validator_set_at( + &self, + header: &B::Header, + ) -> Result, Error> { + self.runtime + .runtime_api() + .validator_set(header.hash()) + .map_err(Error::RuntimeApi)? + .ok_or_else(|| Error::Backend("could not get BEEFY validator set".into())) + } + + pub(crate) fn report_fork_equivocation( + &self, + proof: ForkEquivocationProof, AuthorityId, Signature, B::Header>, + ) -> Result<(), Error> { + let validator_set = self.active_validator_set_at(&proof.correct_header)?; + let set_id = validator_set.id(); + + let expected_header_hash = self + .backend + .blockchain() + .expect_block_hash_from_id(&BlockId::Number(proof.commitment.block_number)) + .map_err(|e| Error::Backend(e.to_string()))?; + + if proof.commitment.validator_set_id != set_id || + !check_fork_equivocation_proof::< + NumberFor, + AuthorityId, + BeefySignatureHasher, + B::Header, + >(&proof, &expected_header_hash) + { + debug!(target: LOG_TARGET, "🥩 Skip report for bad invalid fork proof {:?}", proof); + return Ok(()) + } + + let offender_ids = + proof.signatories.iter().cloned().map(|(id, _sig)| id).collect::>(); + if let Some(local_id) = self.key_store.authority_id(validator_set.validators()) { + if offender_ids.contains(&local_id) { + debug!(target: LOG_TARGET, "🥩 Skip equivocation report for own equivocation"); + // TODO: maybe error here instead? + return Ok(()) + } + } + + let hash = proof.correct_header.hash(); + let runtime_api = self.runtime.runtime_api(); + + // generate key ownership proof at that block + let key_owner_proofs = offender_ids + .iter() + .filter_map(|id| { + match runtime_api.generate_key_ownership_proof(hash, set_id, id.clone()) { + Ok(Some(proof)) => Some(Ok(proof)), + Ok(None) => { + debug!( + target: LOG_TARGET, + "🥩 Invalid fork vote offender not part of the authority set." + ); + None + }, + Err(e) => Some(Err(Error::RuntimeApi(e))), + } + }) + .collect::>()?; + + // submit invalid fork vote report at **best** block + let best_block_hash = self.backend.blockchain().info().best_hash; + runtime_api + .submit_report_fork_equivocation_unsigned_extrinsic( + best_block_hash, + proof, + key_owner_proofs, + ) + .map_err(Error::RuntimeApi)?; + + Ok(()) + } +} + +impl BeefyFisherman for Fisherman +where + B: Block, + BE: Backend, + P: PayloadProvider, + R: ProvideRuntimeApi + Send + Sync, + R::Api: BeefyApi, +{ + /// Check `vote` for contained block against expected payload. + fn check_vote( + &self, + vote: VoteMessage, AuthorityId, Signature>, + ) -> Result<(), Error> { + let number = vote.commitment.block_number; + let (correct_header, expected_payload) = self.expected_header_and_payload(number)?; + if vote.commitment.payload != expected_payload { + let proof = ForkEquivocationProof { + commitment: vote.commitment, + signatories: vec![(vote.id, vote.signature)], + correct_header: correct_header.clone(), + }; + self.report_fork_equivocation(proof)?; + } + Ok(()) + } + + /// Check `signed_commitment` for contained block against expected payload. + fn check_signed_commitment( + &self, + signed_commitment: SignedCommitment, Signature>, + ) -> Result<(), Error> { + let SignedCommitment { commitment, signatures } = signed_commitment; + let number = commitment.block_number; + let (correct_header, expected_payload) = self.expected_header_and_payload(number)?; + if commitment.payload != expected_payload { + let validator_set = self.active_validator_set_at(&correct_header)?; + if signatures.len() != validator_set.validators().len() { + // invalid proof + return Ok(()) + } + // report every signer of the bad justification + let signatories = validator_set + .validators() + .iter() + .cloned() + .zip(signatures.into_iter()) + .filter_map(|(id, signature)| signature.map(|sig| (id, sig))) + .collect(); + + let proof = ForkEquivocationProof { + commitment, + signatories, + correct_header: correct_header.clone(), + }; + self.report_fork_equivocation(proof)?; + } + Ok(()) + } + + /// Check `proof` for contained block against expected payload. + fn check_proof(&self, proof: BeefyVersionedFinalityProof) -> Result<(), Error> { + match proof { + BeefyVersionedFinalityProof::::V1(signed_commitment) => + self.check_signed_commitment(signed_commitment), + } + } +} diff --git a/client/consensus/beefy/src/communication/gossip.rs b/client/consensus/beefy/src/communication/gossip.rs index 8c025ca067619..64da631362132 100644 --- a/client/consensus/beefy/src/communication/gossip.rs +++ b/client/consensus/beefy/src/communication/gossip.rs @@ -32,6 +32,7 @@ use wasm_timer::Instant; use crate::{ communication::{ benefit, cost, + fisherman::BeefyFisherman, peers::{KnownPeers, PeerReport}, }, justification::{ @@ -225,26 +226,29 @@ impl Filter { /// Allows messages for 'rounds >= last concluded' to flow, everything else gets /// rejected/expired. /// +/// Messages for active and expired rounds are validated for expected payloads and attempts +/// to create forks before head of GRANDPA are reported. +/// ///All messaging is handled in a single BEEFY global topic. -pub(crate) struct GossipValidator -where - B: Block, -{ +pub(crate) struct GossipValidator { votes_topic: B::Hash, justifs_topic: B::Hash, gossip_filter: RwLock>, next_rebroadcast: Mutex, known_peers: Arc>>, report_sender: TracingUnboundedSender, + pub(crate) fisherman: F, } -impl GossipValidator +impl GossipValidator where B: Block, + F: BeefyFisherman, { pub(crate) fn new( known_peers: Arc>>, - ) -> (GossipValidator, TracingUnboundedReceiver) { + fisherman: F, + ) -> (GossipValidator, TracingUnboundedReceiver) { let (tx, rx) = tracing_unbounded("mpsc_beefy_gossip_validator", 10_000); let val = GossipValidator { votes_topic: votes_topic::(), @@ -253,6 +257,7 @@ where next_rebroadcast: Mutex::new(Instant::now() + REBROADCAST_AFTER), known_peers, report_sender: tx, + fisherman, }; (val, rx) } @@ -287,9 +292,18 @@ where let filter = self.gossip_filter.read(); match filter.consider_vote(round, set_id) { - Consider::RejectPast => return Action::Discard(cost::OUTDATED_MESSAGE), Consider::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE), Consider::RejectOutOfScope => return Action::Discard(cost::OUT_OF_SCOPE_MESSAGE), + Consider::RejectPast => { + // We know `vote` is for some past (finalized) block. Have fisherman check + // for equivocations. Best-effort, ignore errors such as state pruned. + let _ = self.fisherman.check_vote(vote); + // TODO: maybe raise cost reputation when seeing votes that are intentional + // spam: votes that trigger fisherman reports, but don't go through either + // because signer is/was not authority or similar reasons. + // The idea is to more quickly disconnect neighbors which are attempting DoS. + return Action::Discard(cost::OUTDATED_MESSAGE) + }, Consider::Accept => {}, } @@ -331,9 +345,18 @@ where let guard = self.gossip_filter.read(); // Verify general usefulness of the justification. match guard.consider_finality_proof(round, set_id) { - Consider::RejectPast => return Action::Discard(cost::OUTDATED_MESSAGE), Consider::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE), Consider::RejectOutOfScope => return Action::Discard(cost::OUT_OF_SCOPE_MESSAGE), + Consider::RejectPast => { + // We know `proof` is for some past (finalized) block. Have fisherman check + // for equivocations. Best-effort, ignore errors such as state pruned. + let _ = self.fisherman.check_proof(proof); + // TODO: maybe raise cost reputation when seeing votes that are intentional + // spam: votes that trigger fisherman reports, but don't go through either because + // signer is/was not authority or similar reasons. + // The idea is to more quickly disconnect neighbors which are attempting DoS. + return Action::Discard(cost::OUTDATED_MESSAGE) + }, Consider::Accept => {}, } // Verify justification signatures. @@ -359,9 +382,10 @@ where } } -impl Validator for GossipValidator +impl Validator for GossipValidator where B: Block, + F: BeefyFisherman, { fn peer_disconnected(&self, _context: &mut dyn ValidatorContext, who: &PeerId) { self.known_peers.lock().remove(who); @@ -474,7 +498,7 @@ where #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::keystore::BeefyKeystore; + use crate::{keystore::BeefyKeystore, tests::DummyFisherman}; use sc_network_test::Block; use sp_application_crypto::key_types::BEEFY as BEEFY_KEY_TYPE; use sp_consensus_beefy::{ @@ -482,6 +506,7 @@ pub(crate) mod tests { SignedCommitment, VoteMessage, }; use sp_keystore::{testing::MemoryKeystore, Keystore}; + use std::marker::PhantomData; #[test] fn known_votes_insert_remove() { @@ -577,8 +602,9 @@ pub(crate) mod tests { fn should_validate_messages() { let keys = vec![Keyring::Alice.public()]; let validator_set = ValidatorSet::::new(keys.clone(), 0).unwrap(); + let fisherman = DummyFisherman { _phantom: PhantomData:: }; let (gv, mut report_stream) = - GossipValidator::::new(Arc::new(Mutex::new(KnownPeers::new()))); + GossipValidator::new(Arc::new(Mutex::new(KnownPeers::new())), fisherman); let sender = PeerId::random(); let mut context = TestContext; @@ -705,7 +731,8 @@ pub(crate) mod tests { fn messages_allowed_and_expired() { let keys = vec![Keyring::Alice.public()]; let validator_set = ValidatorSet::::new(keys.clone(), 0).unwrap(); - let (gv, _) = GossipValidator::::new(Arc::new(Mutex::new(KnownPeers::new()))); + let fisherman = DummyFisherman { _phantom: PhantomData:: }; + let (gv, _) = GossipValidator::new(Arc::new(Mutex::new(KnownPeers::new())), fisherman); gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set }); let sender = sc_network::PeerId::random(); let topic = Default::default(); @@ -782,7 +809,8 @@ pub(crate) mod tests { fn messages_rebroadcast() { let keys = vec![Keyring::Alice.public()]; let validator_set = ValidatorSet::::new(keys.clone(), 0).unwrap(); - let (gv, _) = GossipValidator::::new(Arc::new(Mutex::new(KnownPeers::new()))); + let fisherman = DummyFisherman { _phantom: PhantomData:: }; + let (gv, _) = GossipValidator::new(Arc::new(Mutex::new(KnownPeers::new())), fisherman); gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set }); let sender = sc_network::PeerId::random(); let topic = Default::default(); diff --git a/client/consensus/beefy/src/communication/mod.rs b/client/consensus/beefy/src/communication/mod.rs index 7f9535bfc23f1..c31447d08e565 100644 --- a/client/consensus/beefy/src/communication/mod.rs +++ b/client/consensus/beefy/src/communication/mod.rs @@ -21,6 +21,7 @@ pub mod notification; pub mod request_response; +pub(crate) mod fisherman; pub(crate) mod gossip; pub(crate) mod peers; diff --git a/client/consensus/beefy/src/lib.rs b/client/consensus/beefy/src/lib.rs index 0b3baa007c1ce..d739a8143b525 100644 --- a/client/consensus/beefy/src/lib.rs +++ b/client/consensus/beefy/src/lib.rs @@ -18,6 +18,7 @@ use crate::{ communication::{ + fisherman::Fisherman, notification::{ BeefyBestBlockSender, BeefyBestBlockStream, BeefyVersionedFinalityProofSender, BeefyVersionedFinalityProofStream, @@ -28,6 +29,7 @@ use crate::{ }, }, import::BeefyBlockImport, + keystore::BeefyKeystore, metrics::register_metrics, round::Rounds, worker::PersistedState, @@ -219,10 +221,10 @@ pub async fn start_beefy_gadget( beefy_params: BeefyParams, ) where B: Block, - BE: Backend, + BE: Backend + 'static, C: Client + BlockBackend, - P: PayloadProvider + Clone, - R: ProvideRuntimeApi, + P: PayloadProvider, + R: ProvideRuntimeApi + Send + Sync + 'static, R::Api: BeefyApi + MmrApi>, N: GossipNetwork + NetworkRequest + Send + Sync + 'static, S: GossipSyncing + SyncOracle + 'static, @@ -240,6 +242,8 @@ pub async fn start_beefy_gadget( mut on_demand_justifications_handler, } = beefy_params; + let key_store: Arc = Arc::new(key_store.into()); + let BeefyNetworkParams { network, sync, @@ -259,10 +263,17 @@ pub async fn start_beefy_gadget( // select recoverable errors. loop { let known_peers = Arc::new(Mutex::new(KnownPeers::new())); + let fisherman = Fisherman { + backend: backend.clone(), + runtime: runtime.clone(), + key_store: key_store.clone(), + payload_provider: payload_provider.clone(), + _phantom: PhantomData, + }; // Default votes filter is to discard everything. // Validator is updated later with correct starting round and set id. let (gossip_validator, gossip_report_stream) = - communication::gossip::GossipValidator::new(known_peers.clone()); + communication::gossip::GossipValidator::new(known_peers.clone(), fisherman); let gossip_validator = Arc::new(gossip_validator); let mut gossip_engine = GossipEngine::new( network.clone(), diff --git a/client/consensus/beefy/src/round.rs b/client/consensus/beefy/src/round.rs index 6f400ce47843c..3abe662fc27fd 100644 --- a/client/consensus/beefy/src/round.rs +++ b/client/consensus/beefy/src/round.rs @@ -22,7 +22,7 @@ use codec::{Decode, Encode}; use log::debug; use sp_consensus_beefy::{ ecdsa_crypto::{AuthorityId, Signature}, - Commitment, EquivocationProof, SignedCommitment, ValidatorSet, ValidatorSetId, VoteMessage, + Commitment, SignedCommitment, ValidatorSet, ValidatorSetId, VoteEquivocationProof, VoteMessage, }; use sp_runtime::traits::{Block, NumberFor}; use std::collections::BTreeMap; @@ -61,7 +61,7 @@ pub fn threshold(authorities: usize) -> usize { pub enum VoteImportResult { Ok, RoundConcluded(SignedCommitment, Signature>), - Equivocation(EquivocationProof, AuthorityId, Signature>), + Equivocation(VoteEquivocationProof, AuthorityId, Signature>), Invalid, Stale, } @@ -153,7 +153,7 @@ where target: LOG_TARGET, "🥩 detected equivocated vote: 1st: {:?}, 2nd: {:?}", previous_vote, vote ); - return VoteImportResult::Equivocation(EquivocationProof { + return VoteImportResult::Equivocation(VoteEquivocationProof { first: previous_vote.clone(), second: vote, }) @@ -203,8 +203,8 @@ mod tests { use sc_network_test::Block; use sp_consensus_beefy::{ - known_payloads::MMR_ROOT_ID, Commitment, EquivocationProof, Keyring, Payload, - SignedCommitment, ValidatorSet, VoteMessage, + known_payloads::MMR_ROOT_ID, Commitment, Keyring, Payload, SignedCommitment, ValidatorSet, + VoteEquivocationProof, VoteMessage, }; use super::{threshold, AuthorityId, Block as BlockT, RoundTracker, Rounds}; @@ -485,7 +485,7 @@ mod tests { let mut alice_vote2 = alice_vote1.clone(); alice_vote2.commitment = commitment2; - let expected_result = VoteImportResult::Equivocation(EquivocationProof { + let expected_result = VoteImportResult::Equivocation(VoteEquivocationProof { first: alice_vote1.clone(), second: alice_vote2.clone(), }); diff --git a/client/consensus/beefy/src/tests.rs b/client/consensus/beefy/src/tests.rs index 3bb65e9d57f43..5522e782fde86 100644 --- a/client/consensus/beefy/src/tests.rs +++ b/client/consensus/beefy/src/tests.rs @@ -22,12 +22,14 @@ use crate::{ aux_schema::{load_persistent, tests::verify_persisted_version}, beefy_block_import_and_links, communication::{ + fisherman::BeefyFisherman, gossip::{ proofs_topic, tests::sign_commitment, votes_topic, GossipFilterCfg, GossipMessage, GossipValidator, }, request_response::{on_demand_justifications_protocol_config, BeefyJustifsRequestHandler}, }, + error::Error, gossip_protocol_name, justification::*, load_or_init_voter_state, wait_for_runtime_pallet, BeefyRPCLinks, BeefyVoterLinks, KnownPeers, @@ -42,21 +44,21 @@ use sc_consensus::{ }; use sc_network::{config::RequestResponseConfig, ProtocolName}; use sc_network_test::{ - Block, BlockImportAdapter, FullPeerConfig, PassThroughVerifier, Peer, PeersClient, + Block, BlockImportAdapter, FullPeerConfig, Header, PassThroughVerifier, Peer, PeersClient, PeersFullClient, TestNetFactory, }; use sc_utils::notification::NotificationReceiver; use serde::{Deserialize, Serialize}; -use sp_api::{ApiRef, ProvideRuntimeApi}; +use sp_api::{ApiRef, BlockT, ProvideRuntimeApi}; use sp_application_crypto::key_types::BEEFY as BEEFY_KEY_TYPE; use sp_consensus::BlockOrigin; use sp_consensus_beefy::{ ecdsa_crypto::{AuthorityId, Signature}, known_payloads, mmr::{find_mmr_root_digest, MmrRootProvider}, - BeefyApi, Commitment, ConsensusLog, EquivocationProof, Keyring as BeefyKeyring, MmrRootHash, - OpaqueKeyOwnershipProof, Payload, SignedCommitment, ValidatorSet, ValidatorSetId, - VersionedFinalityProof, VoteMessage, BEEFY_ENGINE_ID, + BeefyApi, Commitment, ConsensusLog, ForkEquivocationProof, Keyring as BeefyKeyring, + MmrRootHash, OpaqueKeyOwnershipProof, Payload, SignedCommitment, ValidatorSet, ValidatorSetId, + VersionedFinalityProof, VoteEquivocationProof, VoteMessage, BEEFY_ENGINE_ID, }; use sp_core::H256; use sp_keystore::{testing::MemoryKeystore, Keystore, KeystorePtr}; @@ -244,13 +246,38 @@ impl TestNetFactory for BeefyTestNet { } } +pub(crate) struct DummyFisherman { + pub _phantom: PhantomData, +} + +impl BeefyFisherman for DummyFisherman { + fn check_proof(&self, _: BeefyVersionedFinalityProof) -> Result<(), Error> { + Ok(()) + } + fn check_signed_commitment( + &self, + _: SignedCommitment, Signature>, + ) -> Result<(), Error> { + Ok(()) + } + fn check_vote( + &self, + _: VoteMessage, AuthorityId, Signature>, + ) -> Result<(), Error> { + Ok(()) + } +} + #[derive(Clone)] pub(crate) struct TestApi { pub beefy_genesis: u64, pub validator_set: BeefyValidatorSet, pub mmr_root_hash: MmrRootHash, - pub reported_equivocations: - Option, AuthorityId, Signature>>>>>, + pub reported_vote_equivocations: + Option, AuthorityId, Signature>>>>>, + pub reported_fork_equivocations: Option< + Arc, AuthorityId, Signature, Header>>>>, + >, } impl TestApi { @@ -263,7 +290,8 @@ impl TestApi { beefy_genesis, validator_set: validator_set.clone(), mmr_root_hash, - reported_equivocations: None, + reported_vote_equivocations: None, + reported_fork_equivocations: None, } } @@ -272,12 +300,14 @@ impl TestApi { beefy_genesis: 1, validator_set: validator_set.clone(), mmr_root_hash: GOOD_MMR_ROOT, - reported_equivocations: None, + reported_vote_equivocations: None, + reported_fork_equivocations: None, } } pub fn allow_equivocations(&mut self) { - self.reported_equivocations = Some(Arc::new(Mutex::new(vec![]))); + self.reported_vote_equivocations = Some(Arc::new(Mutex::new(vec![]))); + self.reported_fork_equivocations = Some(Arc::new(Mutex::new(vec![]))); } } @@ -303,11 +333,23 @@ sp_api::mock_impl_runtime_apis! { Some(self.inner.validator_set.clone()) } - fn submit_report_equivocation_unsigned_extrinsic( - proof: EquivocationProof, AuthorityId, Signature>, + fn submit_report_vote_equivocation_unsigned_extrinsic( + proof: VoteEquivocationProof, AuthorityId, Signature>, _dummy: OpaqueKeyOwnershipProof, ) -> Option<()> { - if let Some(equivocations_buf) = self.inner.reported_equivocations.as_ref() { + if let Some(equivocations_buf) = self.inner.reported_vote_equivocations.as_ref() { + equivocations_buf.lock().push(proof); + None + } else { + panic!("Equivocations not expected, but following proof was reported: {:?}", proof); + } + } + + fn submit_report_fork_equivocation_unsigned_extrinsic( + proof: ForkEquivocationProof, AuthorityId, Signature, Header>, + _dummy: Vec, + ) -> Option<()> { + if let Some(equivocations_buf) = self.inner.reported_fork_equivocations.as_ref() { equivocations_buf.lock().push(proof); None } else { @@ -364,8 +406,9 @@ async fn voter_init_setup( api: &TestApi, ) -> sp_blockchain::Result> { let backend = net.peer(0).client().as_backend(); + let fisherman = DummyFisherman { _phantom: PhantomData }; let known_peers = Arc::new(Mutex::new(KnownPeers::new())); - let (gossip_validator, _) = GossipValidator::new(known_peers); + let (gossip_validator, _) = GossipValidator::new(known_peers, fisherman); let gossip_validator = Arc::new(gossip_validator); let mut gossip_engine = sc_network_gossip::GossipEngine::new( net.peer(0).network_service().clone(), @@ -386,7 +429,7 @@ fn initialize_beefy( min_block_delta: u32, ) -> impl Future where - API: ProvideRuntimeApi + Sync + Send, + API: ProvideRuntimeApi + Sync + Send + 'static, API::Api: BeefyApi + MmrApi>, { let tasks = FuturesUnordered::new(); @@ -1223,7 +1266,7 @@ async fn beefy_finalizing_after_pallet_genesis() { } #[tokio::test] -async fn beefy_reports_equivocations() { +async fn beefy_reports_vote_equivocations() { sp_tracing::try_init_simple(); let peers = [BeefyKeyring::Alice, BeefyKeyring::Bob, BeefyKeyring::Charlie]; @@ -1273,21 +1316,22 @@ async fn beefy_reports_equivocations() { // run for up to 5 seconds waiting for Alice's report of Bob/Bob_Prime equivocation. for wait_ms in [250, 500, 1250, 3000] { run_for(Duration::from_millis(wait_ms), &net).await; - if !api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty() { + if !api_alice.reported_vote_equivocations.as_ref().unwrap().lock().is_empty() { break } } // Verify expected equivocation - let alice_reported_equivocations = api_alice.reported_equivocations.as_ref().unwrap().lock(); - assert_eq!(alice_reported_equivocations.len(), 1); - let equivocation_proof = alice_reported_equivocations.get(0).unwrap(); + let alice_reported_vote_equivocations = + api_alice.reported_vote_equivocations.as_ref().unwrap().lock(); + assert_eq!(alice_reported_vote_equivocations.len(), 1); + let equivocation_proof = alice_reported_vote_equivocations.get(0).unwrap(); assert_eq!(equivocation_proof.first.id, BeefyKeyring::Bob.public()); assert_eq!(equivocation_proof.first.commitment.block_number, 1); // Verify neither Bob or Bob_Prime report themselves as equivocating. - assert!(api_bob.reported_equivocations.as_ref().unwrap().lock().is_empty()); - assert!(api_bob_prime.reported_equivocations.as_ref().unwrap().lock().is_empty()); + assert!(api_bob.reported_vote_equivocations.as_ref().unwrap().lock().is_empty()); + assert!(api_bob_prime.reported_vote_equivocations.as_ref().unwrap().lock().is_empty()); // sanity verify no new blocks have been finalized by BEEFY streams_empty_after_timeout(best_blocks, &net, None).await; @@ -1310,9 +1354,10 @@ async fn gossipped_finality_proofs() { let beefy_peers = peers.iter().enumerate().map(|(id, key)| (id, key, api.clone())).collect(); let charlie = &net.peers[2]; + let fisherman = DummyFisherman { _phantom: PhantomData:: }; let known_peers = Arc::new(Mutex::new(KnownPeers::::new())); // Charlie will run just the gossip engine and not the full voter. - let (gossip_validator, _) = GossipValidator::new(known_peers); + let (gossip_validator, _) = GossipValidator::new(known_peers, fisherman); let charlie_gossip_validator = Arc::new(gossip_validator); charlie_gossip_validator.update_filter(GossipFilterCfg:: { start: 1, @@ -1383,7 +1428,7 @@ async fn gossipped_finality_proofs() { // Simulate Charlie vote on #2 let header = net.lock().peer(2).client().as_client().expect_header(finalize).unwrap(); - let mmr_root = find_mmr_root_digest::(&header).unwrap(); + let mmr_root = find_mmr_root_digest::
(&header).unwrap(); let payload = Payload::from_single_entry(known_payloads::MMR_ROOT_ID, mmr_root.encode()); let commitment = Commitment { payload, block_number, validator_set_id: validator_set.id() }; let signature = sign_commitment(&BeefyKeyring::Charlie, &commitment); diff --git a/client/consensus/beefy/src/worker.rs b/client/consensus/beefy/src/worker.rs index 0d3845a270368..cec0afbcfdbf4 100644 --- a/client/consensus/beefy/src/worker.rs +++ b/client/consensus/beefy/src/worker.rs @@ -18,6 +18,7 @@ use crate::{ communication::{ + fisherman::BeefyFisherman, gossip::{proofs_topic, votes_topic, GossipFilterCfg, GossipMessage, GossipValidator}, peers::PeerReport, request_response::outgoing_requests_engine::{OnDemandJustificationsEngine, ResponseInfo}, @@ -40,10 +41,10 @@ use sp_api::{BlockId, ProvideRuntimeApi}; use sp_arithmetic::traits::{AtLeast32Bit, Saturating}; use sp_consensus::SyncOracle; use sp_consensus_beefy::{ - check_equivocation_proof, + check_vote_equivocation_proof, ecdsa_crypto::{AuthorityId, Signature}, - BeefyApi, Commitment, ConsensusLog, EquivocationProof, PayloadProvider, ValidatorSet, - VersionedFinalityProof, VoteMessage, BEEFY_ENGINE_ID, + BeefyApi, Commitment, ConsensusLog, PayloadProvider, ValidatorSet, VersionedFinalityProof, + VoteEquivocationProof, VoteMessage, BEEFY_ENGINE_ID, }; use sp_runtime::{ generic::OpaqueDigestItemId, @@ -314,17 +315,17 @@ impl PersistedState { } /// A BEEFY worker plays the BEEFY protocol -pub(crate) struct BeefyWorker { +pub(crate) struct BeefyWorker { // utilities pub backend: Arc, pub payload_provider: P, pub runtime: Arc, pub sync: Arc, - pub key_store: BeefyKeystore, + pub key_store: Arc, // communication pub gossip_engine: GossipEngine, - pub gossip_validator: Arc>, + pub gossip_validator: Arc>, pub gossip_report_stream: TracingUnboundedReceiver, pub on_demand_justifications: OnDemandJustificationsEngine, @@ -341,7 +342,7 @@ pub(crate) struct BeefyWorker { pub persisted_state: PersistedState, } -impl BeefyWorker +impl BeefyWorker where B: Block + Codec, BE: Backend, @@ -349,6 +350,7 @@ where S: SyncOracle, R: ProvideRuntimeApi, R::Api: BeefyApi, + F: BeefyFisherman, { fn best_grandpa_block(&self) -> NumberFor { *self.persisted_state.voting_oracle.best_grandpa_block_header.number() @@ -573,7 +575,7 @@ where }, VoteImportResult::Equivocation(proof) => { metric_inc!(self, beefy_equivocation_votes); - self.report_equivocation(proof)?; + self.report_vote_equivocation(proof)?; }, VoteImportResult::Invalid => metric_inc!(self, beefy_invalid_votes), VoteImportResult::Stale => metric_inc!(self, beefy_stale_votes), @@ -918,15 +920,15 @@ where /// 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( + pub(crate) fn report_vote_equivocation( &self, - proof: EquivocationProof, AuthorityId, Signature>, + proof: VoteEquivocationProof, 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) { + if !check_vote_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) { @@ -967,7 +969,11 @@ where // 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) + .submit_report_vote_equivocation_unsigned_extrinsic( + best_block_hash, + proof, + key_owner_proof, + ) .map_err(Error::RuntimeApi)?; Ok(()) @@ -1029,7 +1035,10 @@ where pub(crate) mod tests { use super::*; use crate::{ - communication::notification::{BeefyBestBlockStream, BeefyVersionedFinalityProofStream}, + communication::{ + fisherman::Fisherman, + notification::{BeefyBestBlockStream, BeefyVersionedFinalityProofStream}, + }, tests::{ create_beefy_keystore, get_beefy_streams, make_beefy_ids, BeefyPeer, BeefyTestNet, TestApi, @@ -1044,10 +1053,12 @@ pub(crate) mod tests { use sp_api::HeaderT; use sp_blockchain::Backend as BlockchainBackendT; use sp_consensus_beefy::{ - generate_equivocation_proof, known_payloads, known_payloads::MMR_ROOT_ID, - mmr::MmrRootProvider, Keyring, Payload, SignedCommitment, + generate_fork_equivocation_proof_vote, generate_vote_equivocation_proof, known_payloads, + known_payloads::MMR_ROOT_ID, mmr::MmrRootProvider, ForkEquivocationProof, Keyring, Payload, + SignedCommitment, }; use sp_runtime::traits::One; + use std::marker::PhantomData; use substrate_test_runtime_client::{ runtime::{Block, Digest, DigestItem, Header}, Backend, @@ -1082,14 +1093,16 @@ pub(crate) mod tests { key: &Keyring, min_block_delta: u32, genesis_validator_set: ValidatorSet, + runtime_api: Option>, ) -> BeefyWorker< Block, Backend, MmrRootProvider, TestApi, Arc>, + Fisherman>, > { - let keystore = create_beefy_keystore(*key); + let key_store: Arc = Arc::new(Some(create_beefy_keystore(*key)).into()); let (to_rpc_justif_sender, from_voter_justif_stream) = BeefyVersionedFinalityProofStream::::channel(); @@ -1110,11 +1123,21 @@ pub(crate) mod tests { let backend = peer.client().as_backend(); let beefy_genesis = 1; - let api = Arc::new(TestApi::with_validator_set(&genesis_validator_set)); + let api = runtime_api + .unwrap_or_else(|| Arc::new(TestApi::with_validator_set(&genesis_validator_set))); let network = peer.network_service().clone(); let sync = peer.sync_service().clone(); + let payload_provider = MmrRootProvider::new(api.clone()); + let fisherman = Fisherman { + backend: backend.clone(), + runtime: api.clone(), + key_store: key_store.clone(), + payload_provider: payload_provider.clone(), + _phantom: PhantomData, + }; let known_peers = Arc::new(Mutex::new(KnownPeers::new())); - let (gossip_validator, gossip_report_stream) = GossipValidator::new(known_peers.clone()); + let (gossip_validator, gossip_report_stream) = + GossipValidator::new(known_peers.clone(), fisherman); let gossip_validator = Arc::new(gossip_validator); let gossip_engine = GossipEngine::new( network.clone(), @@ -1130,9 +1153,11 @@ pub(crate) mod tests { known_peers, None, ); - // Push 1 block - will start first session. - let hashes = peer.push_blocks(1, false); - backend.finalize_block(hashes[0], None).unwrap(); + // If chain's still at genesis, push 1 block to start first session. + if backend.blockchain().info().best_hash == backend.blockchain().info().genesis_hash { + 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) @@ -1145,12 +1170,11 @@ pub(crate) mod tests { beefy_genesis, ) .unwrap(); - let payload_provider = MmrRootProvider::new(api.clone()); BeefyWorker { backend, payload_provider, runtime: api, - key_store: Some(keystore).into(), + key_store, links, gossip_engine, gossip_validator, @@ -1428,7 +1452,7 @@ pub(crate) mod tests { let keys = &[Keyring::Alice]; 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 mut worker = create_beefy_worker(net.peer(0), &keys[0], 1, validator_set.clone(), None); // keystore doesn't contain other keys than validators' assert_eq!(worker.verify_validator_set(&1, &validator_set), Ok(())); @@ -1441,7 +1465,7 @@ pub(crate) mod tests { assert_eq!(worker.verify_validator_set(&1, &validator_set), expected); // worker has no keystore - worker.key_store = None.into(); + worker.key_store = Arc::new(None.into()); let expected_err = Err(Error::Keystore("no Keystore".into())); assert_eq!(worker.verify_validator_set(&1, &validator_set), expected_err); } @@ -1452,7 +1476,7 @@ pub(crate) mod tests { 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()); + let mut worker = create_beefy_worker(net.peer(0), &keys[0], 1, validator_set.clone(), None); // remove default session, will manually add custom one. worker.persisted_state.voting_oracle.sessions.clear(); @@ -1556,7 +1580,7 @@ pub(crate) mod tests { 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 mut worker = create_beefy_worker(net.peer(0), &keys[0], 1, validator_set.clone(), None); let worker_rounds = worker.active_rounds().unwrap(); assert_eq!(worker_rounds.session_start(), 1); @@ -1583,7 +1607,7 @@ pub(crate) mod tests { } #[tokio::test] - async fn should_not_report_bad_old_or_self_equivocations() { + async fn should_not_report_bad_old_or_self_vote_equivocations() { let block_num = 1; let set_id = 1; let keys = [Keyring::Alice]; @@ -1594,8 +1618,13 @@ pub(crate) mod tests { 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 worker = create_beefy_worker( + net.peer(0), + &keys[0], + 1, + validator_set.clone(), + Some(api_alice.clone()), + ); // let there be a block with num = 1: let _ = net.peer(0).push_blocks(1, false); @@ -1604,45 +1633,133 @@ pub(crate) mod tests { 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( + let good_proof = generate_vote_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(())); + assert_eq!(worker.report_vote_equivocation(good_proof.clone()), Ok(())); // verify Alice reports Bob equivocation to runtime - let reported = api_alice.reported_equivocations.as_ref().unwrap().lock(); + let reported = api_alice.reported_vote_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(); + api_alice.reported_vote_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(())); + assert_eq!(worker.report_vote_equivocation(bad_proof), Ok(())); // verify nothing reported to runtime - assert!(api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty()); + assert!(api_alice.reported_vote_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(())); + assert_eq!(worker.report_vote_equivocation(old_proof), Ok(())); // verify nothing reported to runtime - assert!(api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty()); + assert!(api_alice.reported_vote_equivocations.as_ref().unwrap().lock().is_empty()); // now let's try reporting a self-equivocation - let self_proof = generate_equivocation_proof( + let self_proof = generate_vote_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(())); + assert_eq!(worker.report_vote_equivocation(self_proof), Ok(())); // verify nothing reported to runtime - assert!(api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty()); + assert!(api_alice.reported_vote_equivocations.as_ref().unwrap().lock().is_empty()); + } + + #[tokio::test] + async fn should_report_valid_fork_equivocations() { + let peers = [Keyring::Alice, Keyring::Bob, Keyring::Charlie]; + let validator_set = ValidatorSet::new(make_beefy_ids(&peers), 0).unwrap(); + let mut api_alice = TestApi::with_validator_set(&validator_set); + api_alice.allow_equivocations(); + let api_alice = Arc::new(api_alice); + + // instantiate network with Alice and Bob running full voters. + let mut net = BeefyTestNet::new(3); + + let session_len = 10; + let hashes = net.generate_blocks_and_sync(50, session_len, &validator_set, true).await; + let alice_worker = + create_beefy_worker(net.peer(0), &peers[0], 1, validator_set.clone(), Some(api_alice)); + + let block_number = 1; + let header = net + .peer(1) + .client() + .as_backend() + .blockchain() + .header(hashes[block_number as usize]) + .unwrap() + .unwrap(); + let payload = Payload::from_single_entry(MMR_ROOT_ID, "amievil".encode()); + + let votes: Vec<_> = peers + .iter() + .map(|k| (block_number as u64, payload.clone(), validator_set.id(), k)) + .collect(); + + // verify: Alice reports Bob + let proof = generate_fork_equivocation_proof_vote(votes[1].clone(), header.clone()); + { + // expect fisher (Alice) to successfully process it + assert_eq!( + alice_worker.gossip_validator.fisherman.report_fork_equivocation(proof.clone()), + Ok(()) + ); + // verify Alice reports Bob's equivocation to runtime + let reported = + alice_worker.runtime.reported_fork_equivocations.as_ref().unwrap().lock(); + assert_eq!(reported.len(), 1); + assert_eq!(*reported.get(0).unwrap(), proof); + } + + // verify: Alice does not self-report + let proof = generate_fork_equivocation_proof_vote(votes[0].clone(), header.clone()); + { + // expect fisher (Alice) to successfully process it + assert_eq!( + alice_worker.gossip_validator.fisherman.report_fork_equivocation(proof.clone()), + Ok(()) + ); + // verify Alice does *not* report her own equivocation to runtime + let reported = + alice_worker.runtime.reported_fork_equivocations.as_ref().unwrap().lock(); + assert_eq!(reported.len(), 1); + assert!(*reported.get(0).unwrap() != proof); + } + + // verify: Alice reports VersionedFinalityProof equivocation + let commitment = Commitment { + payload: payload.clone(), + block_number: block_number as u64, + validator_set_id: validator_set.id(), + }; + // only Bob and Charlie sign + let signatories: Vec<_> = vec![Keyring::Bob, Keyring::Charlie] + .iter() + .map(|k| (k.public(), k.sign(&commitment.encode()))) + .collect(); + let proof = ForkEquivocationProof { commitment, signatories, correct_header: header }; + { + // expect fisher (Alice) to successfully process it + assert_eq!( + alice_worker.gossip_validator.fisherman.report_fork_equivocation(proof.clone()), + Ok(()) + ); + // verify Alice report Bob's and Charlie's equivocation to runtime + let reported = + alice_worker.runtime.reported_fork_equivocations.as_ref().unwrap().lock(); + assert_eq!(reported.len(), 2); + assert_eq!(*reported.get(1).unwrap(), proof); + } } } diff --git a/frame/beefy/src/equivocation.rs b/frame/beefy/src/equivocation.rs index 0a7ede327c9e6..625d12ec21324 100644 --- a/frame/beefy/src/equivocation.rs +++ b/frame/beefy/src/equivocation.rs @@ -36,9 +36,11 @@ use codec::{self as codec, Decode, Encode}; use frame_support::traits::{Get, KeyOwnerProofSystem}; -use frame_system::pallet_prelude::BlockNumberFor; +use frame_system::pallet_prelude::{BlockNumberFor, HeaderFor}; use log::{error, info}; -use sp_consensus_beefy::{EquivocationProof, ValidatorSetId, KEY_TYPE as BEEFY_KEY_TYPE}; +use sp_consensus_beefy::{ + ForkEquivocationProof, ValidatorSetId, VoteEquivocationProof, KEY_TYPE as BEEFY_KEY_TYPE, +}; use sp_runtime::{ transaction_validity::{ InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, @@ -76,8 +78,8 @@ where pub session_index: SessionIndex, /// The size of the validator set at the time of the offence. pub validator_set_count: u32, - /// The authority which produced this equivocation. - pub offender: Offender, + /// The authorities which produced this equivocation. + pub offenders: Vec, } impl Offence for EquivocationOffence @@ -88,7 +90,7 @@ where type TimeSlot = TimeSlot; fn offenders(&self) -> Vec { - vec![self.offender.clone()] + self.offenders.clone() } fn session_index(&self) -> SessionIndex { @@ -122,14 +124,25 @@ where pub struct EquivocationReportSystem(sp_std::marker::PhantomData<(T, R, P, L)>); /// Equivocation evidence convenience alias. -pub type EquivocationEvidenceFor = ( - EquivocationProof< - BlockNumberFor, - ::BeefyId, - <::BeefyId as RuntimeAppPublic>::Signature, - >, - ::KeyOwnerProof, -); +pub enum EquivocationEvidenceFor { + VoteEquivocationProof( + VoteEquivocationProof< + BlockNumberFor, + ::BeefyId, + <::BeefyId as RuntimeAppPublic>::Signature, + >, + ::KeyOwnerProof, + ), + ForkEquivocationProof( + ForkEquivocationProof< + BlockNumberFor, + ::BeefyId, + <::BeefyId as RuntimeAppPublic>::Signature, + HeaderFor, + >, + Vec<::KeyOwnerProof>, + ), +} impl OffenceReportSystem, EquivocationEvidenceFor> for EquivocationReportSystem @@ -148,11 +161,20 @@ where fn publish_evidence(evidence: EquivocationEvidenceFor) -> Result<(), ()> { use frame_system::offchain::SubmitTransaction; - let (equivocation_proof, key_owner_proof) = evidence; - let call = Call::report_equivocation_unsigned { - equivocation_proof: Box::new(equivocation_proof), - key_owner_proof, + let call = match evidence { + EquivocationEvidenceFor::VoteEquivocationProof(equivocation_proof, key_owner_proof) => + Call::report_vote_equivocation_unsigned { + equivocation_proof: Box::new(equivocation_proof), + key_owner_proof, + }, + EquivocationEvidenceFor::ForkEquivocationProof( + equivocation_proof, + key_owner_proofs, + ) => Call::report_fork_equivocation_unsigned { + equivocation_proof: Box::new(equivocation_proof), + key_owner_proofs, + }, }; let res = SubmitTransaction::>::submit_unsigned_transaction(call.into()); @@ -166,19 +188,42 @@ where fn check_evidence( evidence: EquivocationEvidenceFor, ) -> Result<(), TransactionValidityError> { - let (equivocation_proof, key_owner_proof) = evidence; - - // Check the membership proof to extract the offender's id - let key = (BEEFY_KEY_TYPE, equivocation_proof.offender_id().clone()); - let offender = P::check_proof(key, key_owner_proof).ok_or(InvalidTransaction::BadProof)?; - - // Check if the offence has already been reported, and if so then we can discard the report. - let time_slot = TimeSlot { - set_id: equivocation_proof.set_id(), - round: *equivocation_proof.round_number(), + let (offenders, key_owner_proofs, time_slot) = match &evidence { + EquivocationEvidenceFor::VoteEquivocationProof(equivocation_proof, key_owner_proof) => { + // Check if the offence has already been reported, and if so then we can discard the + // report. + let time_slot = TimeSlot { + set_id: equivocation_proof.set_id(), + round: *equivocation_proof.round_number(), + }; + (vec![equivocation_proof.offender_id()], vec![key_owner_proof.clone()], time_slot) + }, + EquivocationEvidenceFor::ForkEquivocationProof( + equivocation_proof, + key_owner_proofs, + ) => { + // Check if the offence has already been reported, and if so then we can discard the + // report. + let time_slot = TimeSlot { + set_id: equivocation_proof.set_id(), + round: *equivocation_proof.round_number(), + }; + let offenders = equivocation_proof.offender_ids(); // clone data here + (offenders, key_owner_proofs.to_owned(), time_slot) + }, }; - if R::is_known_offence(&[offender], &time_slot) { + // Validate the key ownership proof extracting the id of the offender. + let offenders = offenders + .into_iter() + .zip(key_owner_proofs.iter()) + .map(|(key, key_owner_proof)| { + P::check_proof((BEEFY_KEY_TYPE, key.clone()), key_owner_proof.clone()) + }) + .collect::>>() + .ok_or(InvalidTransaction::BadProof)?; + + if R::is_known_offence(&offenders, &time_slot) { Err(InvalidTransaction::Stale.into()) } else { Ok(()) @@ -189,41 +234,82 @@ where reporter: Option, evidence: EquivocationEvidenceFor, ) -> Result<(), DispatchError> { - let (equivocation_proof, key_owner_proof) = evidence; let reporter = reporter.or_else(|| >::author()); - let offender = equivocation_proof.offender_id().clone(); + + let (offenders, key_owner_proofs, set_id, round) = match &evidence { + EquivocationEvidenceFor::VoteEquivocationProof(equivocation_proof, key_owner_proof) => + ( + vec![equivocation_proof.offender_id()], + vec![key_owner_proof.clone()], + equivocation_proof.set_id(), + *equivocation_proof.round_number(), + ), + EquivocationEvidenceFor::ForkEquivocationProof( + equivocation_proof, + key_owner_proofs, + ) => { + let offenders = equivocation_proof.offender_ids(); // clone data here + ( + offenders, + key_owner_proofs.to_owned(), + equivocation_proof.set_id(), + *equivocation_proof.round_number(), + ) + }, + }; // We check the equivocation within the context of its set id (and // associated session) and round. We also need to know the validator // set count at the time of the offence since it is required to calculate // the slash amount. - let set_id = equivocation_proof.set_id(); - let round = *equivocation_proof.round_number(); - let session_index = key_owner_proof.session(); - let validator_set_count = key_owner_proof.validator_count(); - - // Validate the key ownership proof extracting the id of the offender. - let offender = P::check_proof((BEEFY_KEY_TYPE, offender), key_owner_proof) + let session_index = key_owner_proofs[0].session(); + let validator_set_count = key_owner_proofs[0].validator_count(); + + // Validate the key ownership proof extracting the ids of the offenders. + let offenders = offenders + .into_iter() + .zip(key_owner_proofs.iter()) + .map(|(key, key_owner_proof)| { + P::check_proof((BEEFY_KEY_TYPE, key.clone()), key_owner_proof.clone()) + }) + .collect::>>() .ok_or(Error::::InvalidKeyOwnershipProof)?; - // Validate equivocation proof (check votes are different and signatures are valid). - if !sp_consensus_beefy::check_equivocation_proof(&equivocation_proof) { - return Err(Error::::InvalidEquivocationProof.into()) + match &evidence { + EquivocationEvidenceFor::VoteEquivocationProof(equivocation_proof, _) => { + // Validate equivocation proof (check votes are different and signatures are valid). + if !sp_consensus_beefy::check_vote_equivocation_proof(&equivocation_proof) { + return Err(Error::::InvalidVoteEquivocationProof.into()) + } + }, + EquivocationEvidenceFor::ForkEquivocationProof(equivocation_proof, _) => { + let block_number = equivocation_proof.commitment.block_number; + let expected_block_hash = >::block_hash(block_number); + + // Validate equivocation proof (check commitment is to unexpected payload and + // signatures are valid). + if !sp_consensus_beefy::check_fork_equivocation_proof( + &equivocation_proof, + &expected_block_hash, + ) { + return Err(Error::::InvalidForkEquivocationProof.into()) + } + }, } // Check that the session id for the membership proof is within the // bounds of the set id reported in the equivocation. - let set_id_session_index = - crate::SetIdSession::::get(set_id).ok_or(Error::::InvalidEquivocationProof)?; + let set_id_session_index = crate::SetIdSession::::get(set_id) + .ok_or(Error::::InvalidVoteEquivocationProof)?; if session_index != set_id_session_index { - return Err(Error::::InvalidEquivocationProof.into()) + return Err(Error::::InvalidVoteEquivocationProof.into()) } let offence = EquivocationOffence { time_slot: TimeSlot { set_id, round }, session_index, validator_set_count, - offender, + offenders, }; R::report_offence(reporter.into_iter().collect(), offence) @@ -234,12 +320,14 @@ where } /// Methods for the `ValidateUnsigned` implementation: -/// It restricts calls to `report_equivocation_unsigned` to local calls (i.e. extrinsics generated -/// on this node) or that already in a block. This guarantees that only block authors can include -/// unsigned equivocation reports. +/// It restricts calls to `report_vote_equivocation_unsigned` to local calls (i.e. extrinsics +/// generated on this node) or that already in a block. This guarantees that only block authors can +/// include unsigned equivocation reports. impl Pallet { pub fn validate_unsigned(source: TransactionSource, call: &Call) -> TransactionValidity { - if let Call::report_equivocation_unsigned { equivocation_proof, key_owner_proof } = call { + if let Call::report_vote_equivocation_unsigned { equivocation_proof, key_owner_proof } = + call + { // discard equivocation report not coming from the local node match source { TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ }, @@ -252,7 +340,10 @@ impl Pallet { }, } - let evidence = (*equivocation_proof.clone(), key_owner_proof.clone()); + let evidence = EquivocationEvidenceFor::::VoteEquivocationProof( + *equivocation_proof.clone(), + key_owner_proof.clone(), + ); T::EquivocationReportSystem::check_evidence(evidence)?; let longevity = @@ -277,8 +368,13 @@ impl Pallet { } pub fn pre_dispatch(call: &Call) -> Result<(), TransactionValidityError> { - if let Call::report_equivocation_unsigned { equivocation_proof, key_owner_proof } = call { - let evidence = (*equivocation_proof.clone(), key_owner_proof.clone()); + if let Call::report_vote_equivocation_unsigned { equivocation_proof, key_owner_proof } = + call + { + let evidence = EquivocationEvidenceFor::::VoteEquivocationProof( + *equivocation_proof.clone(), + key_owner_proof.clone(), + ); T::EquivocationReportSystem::check_evidence(evidence) } else { Err(InvalidTransaction::Call.into()) diff --git a/frame/beefy/src/lib.rs b/frame/beefy/src/lib.rs index 77e74436dd671..e54098f99e35d 100644 --- a/frame/beefy/src/lib.rs +++ b/frame/beefy/src/lib.rs @@ -28,7 +28,7 @@ use frame_support::{ }; use frame_system::{ ensure_none, ensure_signed, - pallet_prelude::{BlockNumberFor, OriginFor}, + pallet_prelude::{BlockNumberFor, HeaderFor, OriginFor}, }; use log; use sp_runtime::{ @@ -41,8 +41,8 @@ use sp_staking::{offence::OffenceReportSystem, SessionIndex}; use sp_std::prelude::*; use sp_consensus_beefy::{ - AuthorityIndex, BeefyAuthorityId, ConsensusLog, EquivocationProof, OnNewValidatorSet, - ValidatorSet, BEEFY_ENGINE_ID, GENESIS_AUTHORITY_SET_ID, + AuthorityIndex, BeefyAuthorityId, ConsensusLog, OnNewValidatorSet, ValidatorSet, + VoteEquivocationProof, BEEFY_ENGINE_ID, GENESIS_AUTHORITY_SET_ID, }; mod default_weights; @@ -63,6 +63,7 @@ const LOG_TARGET: &str = "runtime::beefy"; pub mod pallet { use super::*; use frame_system::pallet_prelude::BlockNumberFor; + use sp_consensus_beefy::ForkEquivocationProof; #[pallet::config] pub trait Config: frame_system::Config { @@ -194,8 +195,10 @@ pub mod pallet { pub enum Error { /// A key ownership proof provided as part of an equivocation report is invalid. InvalidKeyOwnershipProof, - /// An equivocation proof provided as part of an equivocation report is invalid. - InvalidEquivocationProof, + /// An equivocation proof provided as part of a voter equivocation report is invalid. + InvalidVoteEquivocationProof, + /// An equivocation proof provided as part of a fork equivocation report is invalid. + InvalidForkEquivocationProof, /// A given equivocation report is valid but already previously reported. DuplicateOffenceReport, } @@ -214,7 +217,7 @@ pub mod pallet { pub fn report_equivocation( origin: OriginFor, equivocation_proof: Box< - EquivocationProof< + VoteEquivocationProof< BlockNumberFor, T::BeefyId, ::Signature, @@ -226,7 +229,10 @@ pub mod pallet { T::EquivocationReportSystem::process_evidence( Some(reporter), - (*equivocation_proof, key_owner_proof), + EquivocationEvidenceFor::VoteEquivocationProof( + *equivocation_proof, + key_owner_proof, + ), )?; // Waive the fee since the report is valid and beneficial Ok(Pays::No.into()) @@ -246,10 +252,10 @@ pub mod pallet { key_owner_proof.validator_count(), T::MaxNominators::get(), ))] - pub fn report_equivocation_unsigned( + pub fn report_vote_equivocation_unsigned( origin: OriginFor, equivocation_proof: Box< - EquivocationProof< + VoteEquivocationProof< BlockNumberFor, T::BeefyId, ::Signature, @@ -261,10 +267,82 @@ pub mod pallet { T::EquivocationReportSystem::process_evidence( None, - (*equivocation_proof, key_owner_proof), + EquivocationEvidenceFor::::VoteEquivocationProof( + *equivocation_proof, + key_owner_proof, + ), )?; Ok(Pays::No.into()) } + + /// Report voter voting on invalid fork. This method will verify the + /// invalid fork proof and validate the given key ownership proof + /// against the extracted offender. If both are valid, the offence + /// will be reported. + // TODO: fix key_owner_proofs[0].validator_count() + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::report_equivocation( + key_owner_proofs[0].validator_count(), + T::MaxNominators::get(), + ))] + pub fn report_fork_equivocation( + origin: OriginFor, + equivocation_proof: Box< + ForkEquivocationProof< + BlockNumberFor, + T::BeefyId, + ::Signature, + HeaderFor, + >, + >, + key_owner_proofs: Vec, + ) -> DispatchResultWithPostInfo { + let reporter = ensure_signed(origin)?; + + T::EquivocationReportSystem::process_evidence( + Some(reporter), + EquivocationEvidenceFor::ForkEquivocationProof( + *equivocation_proof, + key_owner_proofs, + ), + )?; + // Waive the fee since the report is valid and beneficial + Ok(Pays::No.into()) + } + + /// Report commitment on invalid fork. This method will verify the + /// invalid fork proof and validate the given key ownership proof + /// against the extracted offenders. If both are valid, the offence + /// will be reported. + /// + /// This extrinsic must be called unsigned and it is expected that only + /// block authors will call it (validated in `ValidateUnsigned`), as such + /// if the block author is defined it will be defined as the equivocation + /// reporter. + // TODO: fix key_owner_proofs[0].validator_count() + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::report_equivocation(key_owner_proofs[0].validator_count(), T::MaxNominators::get(),))] + pub fn report_fork_equivocation_unsigned( + origin: OriginFor, + equivocation_proof: Box< + ForkEquivocationProof< + BlockNumberFor, + T::BeefyId, + ::Signature, + HeaderFor, + >, + >, + key_owner_proofs: Vec, + ) -> DispatchResultWithPostInfo { + ensure_none(origin)?; + + // TODO: + // T::EquivocationReportSystem::process_evidence( + // None, + // (*fork_equivocation_proof, key_owner_proof), + // )?; + Ok(Pays::No.into()) + } } #[pallet::validate_unsigned] @@ -292,15 +370,43 @@ impl Pallet { /// Submits an extrinsic to report an equivocation. This method will create /// an unsigned extrinsic with a call to `report_equivocation_unsigned` and /// will push the transaction to the pool. Only useful in an offchain context. - pub fn submit_unsigned_equivocation_report( - equivocation_proof: EquivocationProof< + pub fn submit_unsigned_vote_equivocation_report( + equivocation_proof: VoteEquivocationProof< BlockNumberFor, T::BeefyId, ::Signature, >, key_owner_proof: T::KeyOwnerProof, ) -> Option<()> { - T::EquivocationReportSystem::publish_evidence((equivocation_proof, key_owner_proof)).ok() + T::EquivocationReportSystem::publish_evidence( + EquivocationEvidenceFor::::VoteEquivocationProof( + equivocation_proof, + key_owner_proof, + ), + ) + .ok() + } + + /// Submits an extrinsic to report an invalid fork signed by potentially + /// multiple signatories. This method will create an unsigned extrinsic with + /// a call to `report_fork_equivocation_unsigned` and will push the transaction + /// to the pool. Only useful in an offchain context. + pub fn submit_unsigned_fork_equivocation_report( + fork_equivocation_proof: sp_consensus_beefy::ForkEquivocationProof< + BlockNumberFor, + T::BeefyId, + ::Signature, + HeaderFor, + >, + key_owner_proofs: Vec, + ) -> Option<()> { + T::EquivocationReportSystem::publish_evidence( + EquivocationEvidenceFor::::ForkEquivocationProof( + fork_equivocation_proof, + key_owner_proofs, + ), + ) + .ok() } fn change_authorities( diff --git a/frame/beefy/src/mock.rs b/frame/beefy/src/mock.rs index b55a65dbd73af..d0abdf52c388f 100644 --- a/frame/beefy/src/mock.rs +++ b/frame/beefy/src/mock.rs @@ -43,7 +43,7 @@ use crate as pallet_beefy; pub use sp_consensus_beefy::{ ecdsa_crypto::{AuthorityId as BeefyId, AuthoritySignature as BeefySignature}, - ConsensusLog, EquivocationProof, BEEFY_ENGINE_ID, + ConsensusLog, VoteEquivocationProof, BEEFY_ENGINE_ID, }; impl_opaque_keys! { diff --git a/frame/beefy/src/tests.rs b/frame/beefy/src/tests.rs index e04dc330d0c07..fd99343c77460 100644 --- a/frame/beefy/src/tests.rs +++ b/frame/beefy/src/tests.rs @@ -19,7 +19,7 @@ use std::vec; use codec::Encode; use sp_consensus_beefy::{ - check_equivocation_proof, generate_equivocation_proof, known_payloads::MMR_ROOT_ID, + check_vote_equivocation_proof, generate_vote_equivocation_proof, known_payloads::MMR_ROOT_ID, Keyring as BeefyKeyring, Payload, ValidatorSet, KEY_TYPE as BEEFY_KEY_TYPE, }; @@ -212,47 +212,47 @@ fn should_sign_and_verify() { // generate an equivocation proof, with two votes in the same round for // same payload signed by the same key - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_vote_equivocation_proof( (1, payload1.clone(), set_id, &BeefyKeyring::Bob), (1, payload1.clone(), set_id, &BeefyKeyring::Bob), ); // expect invalid equivocation proof - assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); + assert!(!check_vote_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); // generate an equivocation proof, with two votes in different rounds for // different payloads signed by the same key - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_vote_equivocation_proof( (1, payload1.clone(), set_id, &BeefyKeyring::Bob), (2, payload2.clone(), set_id, &BeefyKeyring::Bob), ); // expect invalid equivocation proof - assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); + assert!(!check_vote_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); // generate an equivocation proof, with two votes by different authorities - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_vote_equivocation_proof( (1, payload1.clone(), set_id, &BeefyKeyring::Alice), (1, payload2.clone(), set_id, &BeefyKeyring::Bob), ); // expect invalid equivocation proof - assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); + assert!(!check_vote_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); // generate an equivocation proof, with two votes in different set ids - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_vote_equivocation_proof( (1, payload1.clone(), set_id, &BeefyKeyring::Bob), (1, payload2.clone(), set_id + 1, &BeefyKeyring::Bob), ); // expect invalid equivocation proof - assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); + assert!(!check_vote_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); // generate an equivocation proof, with two votes in the same round for // different payloads signed by the same key let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]); - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_vote_equivocation_proof( (1, payload1, set_id, &BeefyKeyring::Bob), (1, payload2, set_id, &BeefyKeyring::Bob), ); // expect valid equivocation proof - assert!(check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); + assert!(check_vote_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); } #[test] @@ -291,7 +291,7 @@ fn report_equivocation_current_set_works() { let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]); // generate an equivocation proof, with two votes in the same round for // different payloads signed by the same key - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_vote_equivocation_proof( (block_num, payload1, set_id, &equivocation_keyring), (block_num, payload2, set_id, &equivocation_keyring), ); @@ -300,7 +300,7 @@ fn report_equivocation_current_set_works() { let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); // report the equivocation and the tx should be dispatched successfully - assert_ok!(Beefy::report_equivocation_unsigned( + assert_ok!(Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), key_owner_proof, @@ -377,13 +377,13 @@ fn report_equivocation_old_set_works() { 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 for the old set, - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_vote_equivocation_proof( (block_num, payload1, old_set_id, &equivocation_keyring), (block_num, payload2, old_set_id, &equivocation_keyring), ); // report the equivocation and the tx should be dispatched successfully - assert_ok!(Beefy::report_equivocation_unsigned( + assert_ok!(Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), key_owner_proof, @@ -439,19 +439,19 @@ fn report_equivocation_invalid_set_id() { 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 for a future set - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_vote_equivocation_proof( (block_num, payload1, set_id + 1, &equivocation_keyring), (block_num, payload2, set_id + 1, &equivocation_keyring), ); // the call for reporting the equivocation should error assert_err!( - Beefy::report_equivocation_unsigned( + Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), key_owner_proof, ), - Error::::InvalidEquivocationProof, + Error::::InvalidVoteEquivocationProof, ); }); } @@ -481,7 +481,7 @@ fn report_equivocation_invalid_session() { 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 at following era set id = 2 - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_vote_equivocation_proof( (block_num, payload1, set_id, &equivocation_keyring), (block_num, payload2, set_id, &equivocation_keyring), ); @@ -489,12 +489,12 @@ fn report_equivocation_invalid_session() { // report an equivocation for the current set using an key ownership // proof from the previous set, the session should be invalid. assert_err!( - Beefy::report_equivocation_unsigned( + Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), key_owner_proof, ), - Error::::InvalidEquivocationProof, + Error::::InvalidVoteEquivocationProof, ); }); } @@ -525,7 +525,7 @@ fn report_equivocation_invalid_key_owner_proof() { 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 for the authority at index 0 - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_vote_equivocation_proof( (block_num, payload1, set_id + 1, &equivocation_keyring), (block_num, payload2, set_id + 1, &equivocation_keyring), ); @@ -537,7 +537,7 @@ fn report_equivocation_invalid_key_owner_proof() { // report an equivocation for the current set using a key ownership // proof for a different key than the one in the equivocation proof. assert_err!( - Beefy::report_equivocation_unsigned( + Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), invalid_key_owner_proof, @@ -568,12 +568,12 @@ fn report_equivocation_invalid_equivocation_proof() { let assert_invalid_equivocation_proof = |equivocation_proof| { assert_err!( - Beefy::report_equivocation_unsigned( + Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), key_owner_proof.clone(), ), - Error::::InvalidEquivocationProof, + Error::::InvalidVoteEquivocationProof, ); }; @@ -584,31 +584,31 @@ fn report_equivocation_invalid_equivocation_proof() { // both votes target the same block number and payload, // there is no equivocation. - assert_invalid_equivocation_proof(generate_equivocation_proof( + assert_invalid_equivocation_proof(generate_vote_equivocation_proof( (block_num, payload1.clone(), set_id, &equivocation_keyring), (block_num, payload1.clone(), set_id, &equivocation_keyring), )); // votes targeting different rounds, there is no equivocation. - assert_invalid_equivocation_proof(generate_equivocation_proof( + assert_invalid_equivocation_proof(generate_vote_equivocation_proof( (block_num, payload1.clone(), set_id, &equivocation_keyring), (block_num + 1, payload2.clone(), set_id, &equivocation_keyring), )); // votes signed with different authority keys - assert_invalid_equivocation_proof(generate_equivocation_proof( + assert_invalid_equivocation_proof(generate_vote_equivocation_proof( (block_num, payload1.clone(), set_id, &equivocation_keyring), (block_num, payload1.clone(), set_id, &BeefyKeyring::Charlie), )); // votes signed with a key that isn't part of the authority set - assert_invalid_equivocation_proof(generate_equivocation_proof( + assert_invalid_equivocation_proof(generate_vote_equivocation_proof( (block_num, payload1.clone(), set_id, &equivocation_keyring), (block_num, payload1.clone(), set_id, &BeefyKeyring::Dave), )); // votes targeting different set ids - assert_invalid_equivocation_proof(generate_equivocation_proof( + assert_invalid_equivocation_proof(generate_vote_equivocation_proof( (block_num, payload1, set_id, &equivocation_keyring), (block_num, payload2, set_id + 1, &equivocation_keyring), )); @@ -639,14 +639,14 @@ fn report_equivocation_validate_unsigned_prevents_duplicates() { let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]); - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_vote_equivocation_proof( (block_num, payload1, set_id, &equivocation_keyring), (block_num, payload2, set_id, &equivocation_keyring), ); let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); - let call = Call::report_equivocation_unsigned { + let call = Call::report_vote_equivocation_unsigned { equivocation_proof: Box::new(equivocation_proof.clone()), key_owner_proof: key_owner_proof.clone(), }; @@ -681,7 +681,7 @@ fn report_equivocation_validate_unsigned_prevents_duplicates() { assert_ok!(::pre_dispatch(&call)); // we submit the report - Beefy::report_equivocation_unsigned( + Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), key_owner_proof, @@ -743,7 +743,7 @@ fn valid_equivocation_reports_dont_pay_fees() { // generate equivocation proof let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]); - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_vote_equivocation_proof( (block_num, payload1, set_id, &equivocation_keyring), (block_num, payload2, set_id, &equivocation_keyring), ); @@ -752,7 +752,7 @@ fn valid_equivocation_reports_dont_pay_fees() { let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); // check the dispatch info for the call. - let info = Call::::report_equivocation_unsigned { + let info = Call::::report_vote_equivocation_unsigned { equivocation_proof: Box::new(equivocation_proof.clone()), key_owner_proof: key_owner_proof.clone(), } @@ -763,7 +763,7 @@ fn valid_equivocation_reports_dont_pay_fees() { assert_eq!(info.pays_fee, Pays::Yes); // report the equivocation. - let post_info = Beefy::report_equivocation_unsigned( + let post_info = Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof.clone()), key_owner_proof.clone(), @@ -777,7 +777,7 @@ fn valid_equivocation_reports_dont_pay_fees() { // report the equivocation again which is invalid now since it is // duplicate. - let post_info = Beefy::report_equivocation_unsigned( + let post_info = Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), key_owner_proof, diff --git a/primitives/consensus/beefy/src/commitment.rs b/primitives/consensus/beefy/src/commitment.rs index 5b6ef9ae5ab36..3c451b70afb06 100644 --- a/primitives/consensus/beefy/src/commitment.rs +++ b/primitives/consensus/beefy/src/commitment.rs @@ -81,7 +81,7 @@ where } } -/// A commitment with matching GRANDPA validators' signatures. +/// A commitment with matching BEEFY validators' signatures. /// /// Note that SCALE-encoding of the structure is optimized for size efficiency over the wire, /// please take a look at custom [`Encode`] and [`Decode`] implementations and @@ -90,7 +90,7 @@ where pub struct SignedCommitment { /// The commitment signatures are collected for. pub commitment: Commitment, - /// GRANDPA validators' signatures for the commitment. + /// BEEFY validators' signatures for the commitment. /// /// The length of this `Vec` must match number of validators in the current set (see /// [Commitment::validator_set_id]). @@ -247,6 +247,21 @@ impl From> for VersionedFinalityProof { } } +impl VersionedFinalityProof { + /// Provide reference to inner `Payload`. + pub fn payload(&self) -> &Payload { + match self { + VersionedFinalityProof::V1(inner) => &inner.commitment.payload, + } + } + /// Block number this proof is for. + pub fn number(&self) -> &N { + match self { + VersionedFinalityProof::V1(inner) => &inner.commitment.block_number, + } + } +} + #[cfg(test)] mod tests { diff --git a/primitives/consensus/beefy/src/lib.rs b/primitives/consensus/beefy/src/lib.rs index c69e26bf574d8..09da46cdf113f 100644 --- a/primitives/consensus/beefy/src/lib.rs +++ b/primitives/consensus/beefy/src/lib.rs @@ -34,20 +34,20 @@ mod commitment; pub mod mmr; mod payload; -#[cfg(feature = "std")] +#[cfg(all(test, feature = "std"))] mod test_utils; pub mod witness; pub use commitment::{Commitment, SignedCommitment, VersionedFinalityProof}; pub use payload::{known_payloads, BeefyPayloadId, Payload, PayloadProvider}; -#[cfg(feature = "std")] +#[cfg(all(test, feature = "std"))] pub use test_utils::*; use codec::{Codec, Decode, Encode}; use scale_info::TypeInfo; use sp_application_crypto::RuntimeAppPublic; use sp_core::H256; -use sp_runtime::traits::{Hash, Keccak256, NumberFor}; +use sp_runtime::traits::{Hash, Header, Keccak256, NumberFor}; use sp_std::prelude::*; /// Key type for BEEFY module. @@ -229,14 +229,14 @@ pub struct VoteMessage { /// BEEFY happens when a voter votes on the same round/block for different payloads. /// Proving is achieved by collecting the signed commitments of conflicting votes. #[derive(Clone, Debug, Decode, Encode, PartialEq, TypeInfo)] -pub struct EquivocationProof { +pub struct VoteEquivocationProof { /// The first vote in the equivocation. pub first: VoteMessage, /// The second vote in the equivocation. pub second: VoteMessage, } -impl EquivocationProof { +impl VoteEquivocationProof { /// Returns the authority id of the equivocator. pub fn offender_id(&self) -> &Id { &self.first.id @@ -251,6 +251,41 @@ impl EquivocationProof { } } +/// Proof of authority misbehavior on a given set id. +/// This proof shows commitment signed on a different fork. +#[derive(Clone, Debug, Decode, Encode, PartialEq, TypeInfo)] +pub struct ForkEquivocationProof { + /// Commitment for a block on different fork than one at the same height in + /// this client's chain. + /// TODO: maybe replace {commitment, signatories} with SignedCommitment + /// (tradeoff: SignedCommitment not ideal since sigs optional, but fewer + /// types to juggle around) - check once usage pattern is clear + pub commitment: Commitment, + /// Signatures on this block + /// TODO: maybe change to HashMap - check once usage pattern is clear + pub signatories: Vec<(Id, Signature)>, + /// The proof is valid if + /// 1. the header is in our chain + /// 2. its digest's payload != commitment.payload + /// 3. commitment is signed by signatories + pub correct_header: Header, +} + +impl ForkEquivocationProof { + /// Returns the authority id of the misbehaving voter. + pub fn offender_ids(&self) -> Vec<&Id> { + self.signatories.iter().map(|(id, _)| id).collect() + } + /// Returns the round number at which the infringement occurred. + pub fn round_number(&self) -> &Number { + &self.commitment.block_number + } + /// Returns the set id at which the infringement occurred. + pub fn set_id(&self) -> ValidatorSetId { + self.commitment.validator_set_id + } +} + /// Check a commitment signature by encoding the commitment and /// verifying the provided signature using the expected authority id. pub fn check_commitment_signature( @@ -267,10 +302,10 @@ where BeefyAuthorityId::::verify(authority_id, signature, &encoded_commitment) } -/// Verifies the equivocation proof by making sure that both votes target +/// Verifies the vote equivocation proof by making sure that both votes target /// different blocks and that its signatures are valid. -pub fn check_equivocation_proof( - report: &EquivocationProof::Signature>, +pub fn check_vote_equivocation_proof( + report: &VoteEquivocationProof::Signature>, ) -> bool where Id: BeefyAuthorityId + PartialEq, @@ -302,6 +337,55 @@ where return valid_first && valid_second } +/// Validates [ForkEquivocationProof] by checking: +/// 1. `commitment` is signed, +/// 2. `correct_header` is valid and matches `commitment.block_number`. +/// 2. `commitment.payload` != `expected_payload(correct_header)`. +/// NOTE: GRANDPA finalization proof is not checked, which leads to slashing on forks. +/// This is fine since honest validators will not be slashed on the chain finalized +/// by GRANDPA, which is the only chain that ultimately matters. +/// The only material difference not checking GRANDPA proofs makes is that validators +/// are not slashed for signing BEEFY commitments prior to the blocks committed to being +/// finalized by GRANDPA. This is fine too, since the slashing risk of committing to +/// an incorrect block implies validators will only sign blocks they *know* will be +/// finalized by GRANDPA. +pub fn check_fork_equivocation_proof( + proof: &ForkEquivocationProof::Signature, Header>, + expected_header_hash: &Header::Hash, +) -> bool +where + Id: BeefyAuthorityId + PartialEq, + Number: Clone + Encode + PartialEq, + MsgHash: Hash, + Header: sp_api::HeaderT, +{ + let ForkEquivocationProof { commitment, signatories, correct_header } = proof; + + if correct_header.hash() != *expected_header_hash { + return false + } + + let expected_mmr_root_digest = mmr::find_mmr_root_digest::
(correct_header); + let expected_payload = expected_mmr_root_digest + .map(|mmr_root| Payload::from_single_entry(known_payloads::MMR_ROOT_ID, mmr_root.encode())); + + // cheap failfasts: + // 1. check that `payload` on the `vote` is different that the `expected_payload` + // 2. if the signatories signed a payload when there should be none (for + // instance for a block prior to BEEFY activation), then expected_payload = + // None, and they will likewise be slashed + if Some(&commitment.payload) != expected_payload.as_ref() { + // check check each signatory's signature on the commitment. + // if any are invalid, equivocation report is invalid + // TODO: refactor check_commitment_signature to take a slice of signatories + return signatories.iter().all(|(authority_id, signature)| { + check_commitment_signature(&commitment, authority_id, signature) + }) + } else { + false + } +} + /// New BEEFY validator set notification hook. pub trait OnNewValidatorSet { /// Function called by the pallet when BEEFY validator set changes. @@ -322,7 +406,7 @@ impl OnNewValidatorSet for () { /// the runtime API boundary this type is unknown and as such we keep this /// opaque representation, implementors of the runtime API will have to make /// sure that all usages of `OpaqueKeyOwnershipProof` refer to the same type. -#[derive(Decode, Encode, PartialEq, TypeInfo)] +#[derive(Decode, Encode, PartialEq, TypeInfo, Clone)] pub struct OpaqueKeyOwnershipProof(Vec); impl OpaqueKeyOwnershipProof { /// Create a new `OpaqueKeyOwnershipProof` using the given encoded @@ -339,7 +423,8 @@ impl OpaqueKeyOwnershipProof { } sp_api::decl_runtime_apis! { - /// API necessary for BEEFY voters. + /// API necessary for BEEFY voters. Due to the significant conceptual + /// overlap, in large part, this is lifted from the GRANDPA API. #[api_version(3)] pub trait BeefyApi where AuthorityId : Codec + RuntimeAppPublic, @@ -358,12 +443,26 @@ sp_api::decl_runtime_apis! { /// `None` when creation of the extrinsic fails, e.g. if equivocation /// reporting is disabled for the given runtime (i.e. this method is /// hardcoded to return `None`). Only useful in an offchain context. - fn submit_report_equivocation_unsigned_extrinsic( - equivocation_proof: - EquivocationProof, AuthorityId, ::Signature>, + fn submit_report_vote_equivocation_unsigned_extrinsic( + vote_equivocation_proof: + VoteEquivocationProof, AuthorityId, ::Signature>, key_owner_proof: OpaqueKeyOwnershipProof, ) -> Option<()>; + /// Submits an unsigned extrinsic to report commitments to an invalid fork. + /// The caller must provide the invalid commitments proof and key ownership proofs + /// (should be obtained using `generate_key_ownership_proof`) for the offenders. The + /// extrinsic will be unsigned and should only be accepted for local + /// authorship (not to be broadcast to the network). This method returns + /// `None` when creation of the extrinsic fails, e.g. if equivocation + /// reporting is disabled for the given runtime (i.e. this method is + /// hardcoded to return `None`). Only useful in an offchain context. + fn submit_report_fork_equivocation_unsigned_extrinsic( + fork_equivocation_proof: + ForkEquivocationProof, AuthorityId, ::Signature, Block::Header>, + key_owner_proofs: Vec, + ) -> Option<()>; + /// Generates a proof of key ownership for the given authority in the /// given set. An example usage of this module is coupled with the /// session historical module to prove that a given authority key is diff --git a/primitives/consensus/beefy/src/mmr.rs b/primitives/consensus/beefy/src/mmr.rs index 660506b8763f1..6df5925acc195 100644 --- a/primitives/consensus/beefy/src/mmr.rs +++ b/primitives/consensus/beefy/src/mmr.rs @@ -134,7 +134,7 @@ pub struct BeefyAuthoritySet { pub type BeefyNextAuthoritySet = BeefyAuthoritySet; /// Extract the MMR root hash from a digest in the given header, if it exists. -pub fn find_mmr_root_digest(header: &B::Header) -> Option { +pub fn find_mmr_root_digest(header: &H) -> Option { let id = OpaqueDigestItemId::Consensus(&BEEFY_ENGINE_ID); let filter = |log: ConsensusLog| match log { @@ -181,7 +181,7 @@ mod mmr_root_provider { /// Simple wrapper that gets MMR root from header digests or from client state. fn mmr_root_from_digest_or_runtime(&self, header: &B::Header) -> Option { - find_mmr_root_digest::(header).or_else(|| { + find_mmr_root_digest::(header).or_else(|| { self.runtime.runtime_api().mmr_root(header.hash()).ok().and_then(|r| r.ok()) }) } @@ -190,7 +190,7 @@ mod mmr_root_provider { impl PayloadProvider for MmrRootProvider where B: Block, - R: ProvideRuntimeApi, + R: ProvideRuntimeApi + Send + Sync + 'static, R::Api: MmrApi>, { fn payload(&self, header: &B::Header) -> Option { @@ -205,7 +205,7 @@ mod mmr_root_provider { mod tests { use super::*; use crate::H256; - use sp_runtime::{traits::BlakeTwo256, Digest, DigestItem, OpaqueExtrinsic}; + use sp_runtime::{traits::BlakeTwo256, Digest, DigestItem}; #[test] fn should_construct_version_correctly() { @@ -233,7 +233,6 @@ mod tests { #[test] fn extract_mmr_root_digest() { type Header = sp_runtime::generic::Header; - type Block = sp_runtime::generic::Block; let mut header = Header::new( 1u64, Default::default(), @@ -243,7 +242,7 @@ mod tests { ); // verify empty digest shows nothing - assert!(find_mmr_root_digest::(&header).is_none()); + assert!(find_mmr_root_digest::
(&header).is_none()); let mmr_root_hash = H256::random(); header.digest_mut().push(DigestItem::Consensus( @@ -252,7 +251,7 @@ mod tests { )); // verify validator set is correctly extracted from digest - let extracted = find_mmr_root_digest::(&header); + let extracted = find_mmr_root_digest::
(&header); assert_eq!(extracted, Some(mmr_root_hash)); } } diff --git a/primitives/consensus/beefy/src/payload.rs b/primitives/consensus/beefy/src/payload.rs index d520de445c95a..43b83911dd629 100644 --- a/primitives/consensus/beefy/src/payload.rs +++ b/primitives/consensus/beefy/src/payload.rs @@ -43,7 +43,7 @@ pub mod known_payloads { pub struct Payload(Vec<(BeefyPayloadId, Vec)>); impl Payload { - /// Construct a new payload given an initial vallue + /// Construct a new payload given an initial value pub fn from_single_entry(id: BeefyPayloadId, value: Vec) -> Self { Self(vec![(id, value)]) } @@ -75,7 +75,7 @@ impl Payload { } /// Trait for custom BEEFY payload providers. -pub trait PayloadProvider { +pub trait PayloadProvider: Clone + Send + Sync + 'static { /// Provide BEEFY payload if available for `header`. fn payload(&self, header: &B::Header) -> Option; } diff --git a/primitives/consensus/beefy/src/test_utils.rs b/primitives/consensus/beefy/src/test_utils.rs index b83f657af38e3..d52148073efdd 100644 --- a/primitives/consensus/beefy/src/test_utils.rs +++ b/primitives/consensus/beefy/src/test_utils.rs @@ -17,7 +17,10 @@ #![cfg(feature = "std")] -use crate::{ecdsa_crypto, Commitment, EquivocationProof, Payload, ValidatorSetId, VoteMessage}; +use crate::{ + ecdsa_crypto, Commitment, ForkEquivocationProof, Payload, SignedCommitment, ValidatorSetId, + VoteEquivocationProof, VoteMessage, +}; use codec::Encode; use sp_core::{ecdsa, keccak_256, Pair}; use std::collections::HashMap; @@ -91,20 +94,34 @@ impl From for ecdsa_crypto::Public { } } -/// Create a new `EquivocationProof` based on given arguments. -pub fn generate_equivocation_proof( +/// Create a new `VoteMessage` from commitment primitives and keyring +fn signed_vote( + block_number: u64, + payload: Payload, + validator_set_id: ValidatorSetId, + keyring: &Keyring, +) -> VoteMessage { + let commitment = Commitment { validator_set_id, block_number, payload }; + let signature = keyring.sign(&commitment.encode()); + VoteMessage { commitment, id: keyring.public(), signature } +} + +/// Create a new `VoteEquivocationProof` based on given arguments. +pub fn generate_vote_equivocation_proof( vote1: (u64, Payload, ValidatorSetId, &Keyring), vote2: (u64, Payload, ValidatorSetId, &Keyring), -) -> EquivocationProof { - let signed_vote = |block_number: u64, - payload: Payload, - validator_set_id: ValidatorSetId, - keyring: &Keyring| { - let commitment = Commitment { validator_set_id, block_number, payload }; - let signature = keyring.sign(&commitment.encode()); - VoteMessage { commitment, id: keyring.public(), signature } - }; +) -> VoteEquivocationProof { let first = signed_vote(vote1.0, vote1.1, vote1.2, vote1.3); let second = signed_vote(vote2.0, vote2.1, vote2.2, vote2.3); - EquivocationProof { first, second } + VoteEquivocationProof { first, second } +} + +/// Create a new `ForkEquivocationProof` based on vote & correct header. +pub fn generate_fork_equivocation_proof_vote
( + vote: (u64, Payload, ValidatorSetId, &Keyring), + correct_header: Header, +) -> ForkEquivocationProof { + let signed_vote = signed_vote(vote.0, vote.1, vote.2, vote.3); + let signatories = vec![(signed_vote.id, signed_vote.signature)]; + ForkEquivocationProof { commitment: signed_vote.commitment, signatories, correct_header } }