diff --git a/polkadot/node/service/src/fake_runtime_api.rs b/polkadot/node/service/src/fake_runtime_api.rs index d9553afa024b4..418217997385a 100644 --- a/polkadot/node/service/src/fake_runtime_api.rs +++ b/polkadot/node/service/src/fake_runtime_api.rs @@ -240,8 +240,8 @@ sp_api::impl_runtime_apis! { unimplemented!() } - fn submit_report_equivocation_unsigned_extrinsic( - _: beefy_primitives::EquivocationProof< + fn submit_report_vote_equivocation_unsigned_extrinsic( + _: beefy_primitives::VoteEquivocationProof< BlockNumber, BeefyId, BeefySignature, @@ -251,6 +251,13 @@ sp_api::impl_runtime_apis! { unimplemented!() } + fn submit_report_fork_equivocation_unsigned_extrinsic( + _: beefy_primitives::ForkEquivocationProof::Header>, + _: Vec, + ) -> Option<()> { + unimplemented!() + } + fn generate_key_ownership_proof( _: beefy_primitives::ValidatorSetId, _: BeefyId, diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 12388da1868dc..66dc49db11d98 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -1852,8 +1852,8 @@ sp_api::impl_runtime_apis! { Beefy::validator_set() } - fn submit_report_equivocation_unsigned_extrinsic( - equivocation_proof: beefy_primitives::EquivocationProof< + fn submit_report_vote_equivocation_unsigned_extrinsic( + vote_equivocation_proof: beefy_primitives::VoteEquivocationProof< BlockNumber, BeefyId, BeefySignature, @@ -1862,12 +1862,24 @@ sp_api::impl_runtime_apis! { ) -> Option<()> { let key_owner_proof = key_owner_proof.decode()?; - Beefy::submit_unsigned_equivocation_report( - equivocation_proof, + Beefy::submit_unsigned_vote_equivocation_report( + vote_equivocation_proof, key_owner_proof, ) } + fn submit_report_fork_equivocation_unsigned_extrinsic( + fork_equivocation_proof: beefy_primitives::ForkEquivocationProof, + key_owner_proofs: Vec, + ) -> Option<()> { + let key_owner_proofs = key_owner_proofs.iter().cloned().map(|p| p.decode()).collect::>>()?; + + Beefy::submit_unsigned_fork_equivocation_report( + fork_equivocation_proof, + key_owner_proofs, + ) + } + fn generate_key_ownership_proof( _set_id: beefy_primitives::ValidatorSetId, authority_id: BeefyId, diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index 596e65eca0680..816a5fd5baa7f 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -966,8 +966,8 @@ sp_api::impl_runtime_apis! { None } - fn submit_report_equivocation_unsigned_extrinsic( - _equivocation_proof: beefy_primitives::EquivocationProof< + fn submit_report_vote_equivocation_unsigned_extrinsic( + _vote_equivocation_proof: beefy_primitives::VoteEquivocationProof< BlockNumber, BeefyId, BeefySignature, @@ -977,6 +977,13 @@ sp_api::impl_runtime_apis! { None } + fn submit_report_fork_equivocation_unsigned_extrinsic( + _fork_equivocation_proof: beefy_primitives::ForkEquivocationProof, + _key_owner_proofs: Vec, + ) -> Option<()> { + None + } + fn generate_key_ownership_proof( _set_id: beefy_primitives::ValidatorSetId, _authority_id: BeefyId, diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index bb6331e953456..fad630abe3068 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -1840,8 +1840,8 @@ sp_api::impl_runtime_apis! { Beefy::validator_set() } - fn submit_report_equivocation_unsigned_extrinsic( - equivocation_proof: beefy_primitives::EquivocationProof< + fn submit_report_vote_equivocation_unsigned_extrinsic( + vote_equivocation_proof: beefy_primitives::VoteEquivocationProof< BlockNumber, BeefyId, BeefySignature, @@ -1850,12 +1850,24 @@ sp_api::impl_runtime_apis! { ) -> Option<()> { let key_owner_proof = key_owner_proof.decode()?; - Beefy::submit_unsigned_equivocation_report( - equivocation_proof, + Beefy::submit_unsigned_vote_equivocation_report( + vote_equivocation_proof, key_owner_proof, ) } + fn submit_report_fork_equivocation_unsigned_extrinsic( + fork_equivocation_proof: beefy_primitives::ForkEquivocationProof, + key_owner_proofs: Vec, + ) -> Option<()> { + let key_owner_proofs = key_owner_proofs.iter().cloned().map(|p| p.decode()).collect::>>()?; + + Beefy::submit_unsigned_fork_equivocation_report( + fork_equivocation_proof, + key_owner_proofs, + ) + } + fn generate_key_ownership_proof( _set_id: beefy_primitives::ValidatorSetId, authority_id: BeefyId, diff --git a/substrate/client/consensus/beefy/src/communication/fisherman.rs b/substrate/client/consensus/beefy/src/communication/fisherman.rs new file mode 100644 index 0000000000000..d0fb8954d9fd5 --- /dev/null +++ b/substrate/client/consensus/beefy/src/communication/fisherman.rs @@ -0,0 +1,243 @@ +// 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 canonical payload. + fn check_vote( + &self, + vote: VoteMessage, AuthorityId, Signature>, + ) -> Result<(), Error>; + + /// Check `signed_commitment` for contained block against canonical payload. + fn check_signed_commitment( + &self, + signed_commitment: SignedCommitment, Signature>, + ) -> Result<(), Error>; + + /// Check `proof` for contained block against canonical 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 canonical_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.canonical_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.offender_ids(); + if let Some(local_id) = self.key_store.authority_id(validator_set.validators()) { + if offender_ids.contains(&&local_id) { + warn!(target: LOG_TARGET, "🥩 Skip equivocation report for own equivocation"); + return Ok(()) + } + } + + let hash = proof.canonical_header.hash(); + let runtime_api = self.runtime.runtime_api(); + + // generate key ownership proof at that block + let key_owner_proofs = offender_ids + .iter() + .cloned() + .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 canonical payload. + fn check_vote( + &self, + vote: VoteMessage, AuthorityId, Signature>, + ) -> Result<(), Error> { + let number = vote.commitment.block_number; + let (canonical_header, canonical_payload) = self.canonical_header_and_payload(number)?; + if vote.commitment.payload != canonical_payload { + let proof = ForkEquivocationProof { + commitment: vote.commitment, + signatories: vec![(vote.id, vote.signature)], + canonical_header: canonical_header.clone(), + }; + self.report_fork_equivocation(proof)?; + } + Ok(()) + } + + /// Check `signed_commitment` for contained block against canonical payload. + fn check_signed_commitment( + &self, + signed_commitment: SignedCommitment, Signature>, + ) -> Result<(), Error> { + let SignedCommitment { commitment, signatures } = signed_commitment; + let number = commitment.block_number; + let (canonical_header, canonical_payload) = self.canonical_header_and_payload(number)?; + if commitment.payload != canonical_payload { + let validator_set = self.active_validator_set_at(&canonical_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, + canonical_header: canonical_header.clone(), + }; + self.report_fork_equivocation(proof)?; + } + Ok(()) + } + + /// Check `proof` for contained block against canonical payload. + fn check_proof(&self, proof: BeefyVersionedFinalityProof) -> Result<(), Error> { + match proof { + BeefyVersionedFinalityProof::::V1(signed_commitment) => + self.check_signed_commitment(signed_commitment), + } + } +} diff --git a/substrate/client/consensus/beefy/src/communication/gossip.rs b/substrate/client/consensus/beefy/src/communication/gossip.rs index 342cd0511a511..56b1f561e7a1f 100644 --- a/substrate/client/consensus/beefy/src/communication/gossip.rs +++ b/substrate/client/consensus/beefy/src/communication/gossip.rs @@ -31,6 +31,7 @@ use wasm_timer::Instant; use crate::{ communication::{ benefit, cost, + fisherman::BeefyFisherman, peers::{KnownPeers, PeerReport}, }, justification::{ @@ -224,26 +225,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::(), @@ -252,6 +256,7 @@ where next_rebroadcast: Mutex::new(Instant::now() + REBROADCAST_AFTER), known_peers, report_sender: tx, + fisherman, }; (val, rx) } @@ -283,9 +288,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 => {}, } @@ -324,7 +338,16 @@ where // Verify general usefulness of the justification. match guard.consider_finality_proof(round, set_id) { - Consider::RejectPast => return Action::Discard(cost::OUTDATED_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::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE), Consider::RejectOutOfScope => return Action::Discard(cost::OUT_OF_SCOPE_MESSAGE), Consider::Accept => {}, @@ -362,9 +385,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); @@ -477,7 +501,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::{ @@ -485,6 +509,7 @@ pub(crate) mod tests { SignedCommitment, VoteMessage, }; use sp_keystore::{testing::MemoryKeystore, Keystore}; + use std::marker::PhantomData; struct TestContext; impl ValidatorContext for TestContext { @@ -545,8 +570,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; @@ -659,7 +685,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(); @@ -736,7 +763,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/substrate/client/consensus/beefy/src/communication/mod.rs b/substrate/client/consensus/beefy/src/communication/mod.rs index 10a6071aae658..d834c0f15a910 100644 --- a/substrate/client/consensus/beefy/src/communication/mod.rs +++ b/substrate/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/substrate/client/consensus/beefy/src/lib.rs b/substrate/client/consensus/beefy/src/lib.rs index 3d104f132508d..1d37de913e429 100644 --- a/substrate/client/consensus/beefy/src/lib.rs +++ b/substrate/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, + 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, @@ -256,10 +260,17 @@ pub async fn start_beefy_gadget( let mut block_import_justif = links.from_block_import_justif_stream.subscribe(100_000).fuse(); let known_peers = Arc::new(Mutex::new(KnownPeers::new())); + let fisherman = Fisherman { + backend: backend.clone(), + key_store: key_store.clone(), + runtime: runtime.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 gossip_engine = GossipEngine::new( network.clone(), diff --git a/substrate/client/consensus/beefy/src/round.rs b/substrate/client/consensus/beefy/src/round.rs index 6f400ce47843c..3abe662fc27fd 100644 --- a/substrate/client/consensus/beefy/src/round.rs +++ b/substrate/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/substrate/client/consensus/beefy/src/tests.rs b/substrate/client/consensus/beefy/src/tests.rs index 3aaa59cbfa1c1..4b84abf6ade90 100644 --- a/substrate/client/consensus/beefy/src/tests.rs +++ b/substrate/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, @@ -43,7 +45,7 @@ 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; @@ -55,16 +57,16 @@ 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}; use sp_mmr_primitives::{Error as MmrError, MmrApi}; use sp_runtime::{ codec::{Decode, Encode}, - traits::{Header as HeaderT, NumberFor}, + traits::{Block as BlockT, Header as HeaderT, NumberFor}, BuildStorage, DigestItem, EncodedJustification, Justifications, Storage, }; use std::{marker::PhantomData, sync::Arc, task::Poll}; @@ -245,13 +247,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: Option, 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 { @@ -264,7 +291,8 @@ impl TestApi { beefy_genesis, validator_set: Some(validator_set.clone()), mmr_root_hash, - reported_equivocations: None, + reported_vote_equivocations: None, + reported_fork_equivocations: None, } } @@ -273,12 +301,14 @@ impl TestApi { beefy_genesis: 1, validator_set: Some(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![]))); } } @@ -304,11 +334,23 @@ sp_api::mock_impl_runtime_apis! { 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 { @@ -365,8 +407,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(), @@ -387,7 +430,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(); @@ -1285,7 +1328,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]; @@ -1335,21 +1378,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; @@ -1372,9 +1416,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, @@ -1439,7 +1484,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/substrate/client/consensus/beefy/src/worker.rs b/substrate/client/consensus/beefy/src/worker.rs index 0eea5647e51cf..2ee95710b6b23 100644 --- a/substrate/client/consensus/beefy/src/worker.rs +++ b/substrate/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::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::{BlockId, OpaqueDigestItemId}, @@ -316,24 +317,24 @@ impl PersistedState { /// Helper object holding BEEFY worker communication/gossip components. /// /// These are created once, but will be reused if worker is restarted/reinitialized. -pub(crate) struct BeefyComms { +pub(crate) struct BeefyComms> { pub gossip_engine: GossipEngine, - pub gossip_validator: Arc>, + pub gossip_validator: Arc>, pub gossip_report_stream: TracingUnboundedReceiver, pub on_demand_justifications: OnDemandJustificationsEngine, } /// 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 (created once, but returned and reused if worker is restarted/reinitialized) - pub comms: BeefyComms, + pub comms: BeefyComms, // channels /// Links between the block importer, the background voter and the RPC layer. @@ -348,7 +349,7 @@ pub(crate) struct BeefyWorker { pub persisted_state: PersistedState, } -impl BeefyWorker +impl BeefyWorker where B: Block + Codec, BE: Backend, @@ -356,6 +357,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() @@ -591,7 +593,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), @@ -821,7 +823,7 @@ where mut self, block_import_justif: &mut Fuse>>, finality_notifications: &mut Fuse>, - ) -> (Error, BeefyComms) { + ) -> (Error, BeefyComms) { info!( target: LOG_TARGET, "🥩 run BEEFY worker, best grandpa: #{:?}.", @@ -951,15 +953,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) { @@ -1000,7 +1002,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(()) @@ -1062,7 +1068,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, @@ -1076,10 +1085,12 @@ pub(crate) mod tests { use sc_network_test::TestNetFactory; use sp_blockchain::Backend as BlockchainBackendT; use sp_consensus_beefy::{ - generate_equivocation_proof, known_payloads, known_payloads::MMR_ROOT_ID, + generate_fork_equivocation_proof_sc, generate_fork_equivocation_proof_vote, + generate_vote_equivocation_proof, known_payloads, known_payloads::MMR_ROOT_ID, mmr::MmrRootProvider, Keyring, Payload, SignedCommitment, }; use sp_runtime::traits::{Header as HeaderT, One}; + use std::marker::PhantomData; use substrate_test_runtime_client::{ runtime::{Block, Digest, DigestItem, Header}, Backend, @@ -1114,14 +1125,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(); @@ -1142,11 +1155,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(), @@ -1162,9 +1185,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) @@ -1177,7 +1202,6 @@ pub(crate) mod tests { beefy_genesis, ) .unwrap(); - let payload_provider = MmrRootProvider::new(api.clone()); let comms = BeefyComms { gossip_engine, gossip_validator, @@ -1188,7 +1212,7 @@ pub(crate) mod tests { backend, payload_provider, runtime: api, - key_store: Some(keystore).into(), + key_store, links, comms, metrics, @@ -1463,7 +1487,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(())); @@ -1476,7 +1500,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); } @@ -1487,7 +1511,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(); @@ -1591,7 +1615,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); @@ -1618,7 +1642,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]; @@ -1629,8 +1653,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); @@ -1639,45 +1668,147 @@ 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| { + // signed_vote(block_number as u64, payload.clone(), validator_set.id(), 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 + .comms + .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 + .comms + .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 proof = generate_fork_equivocation_proof_sc( + commitment, + vec![Keyring::Bob, Keyring::Charlie], + header, + ); + { + // expect fisher (Alice) to successfully process it + assert_eq!( + alice_worker + .comms + .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/substrate/frame/beefy/src/equivocation.rs b/substrate/frame/beefy/src/equivocation.rs index 0a7ede327c9e6..7fd038ca9b078 100644 --- a/substrate/frame/beefy/src/equivocation.rs +++ b/substrate/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,88 @@ 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). + // NOTE: Fork equivocation proof currently only prevents attacks + // assuming 2/3rds of validators honestly participate in BEEFY + // finalization and at least one honest relayer can update the + // beefy light client at least once every 4096 blocks. See + // https://github.com/paritytech/polkadot-sdk/issues/1441 for + // replacement solution. + 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::::InvalidEquivocationProofSession)?; if session_index != set_id_session_index { - return Err(Error::::InvalidEquivocationProof.into()) + return Err(Error::::InvalidEquivocationProofSession.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,54 +326,92 @@ 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 { - // discard equivocation report not coming from the local node - match source { - TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ }, - _ => { - log::warn!( - target: LOG_TARGET, - "rejecting unsigned report equivocation transaction because it is not local/in-block." - ); - return InvalidTransaction::Call.into() - }, - } - - let evidence = (*equivocation_proof.clone(), key_owner_proof.clone()); - T::EquivocationReportSystem::check_evidence(evidence)?; - - let longevity = - >::Longevity::get(); - - ValidTransaction::with_tag_prefix("BeefyEquivocation") - // We assign the maximum priority for any equivocation report. - .priority(TransactionPriority::MAX) - // Only one equivocation report for the same offender at the same slot. - .and_provides(( - equivocation_proof.offender_id().clone(), - equivocation_proof.set_id(), - *equivocation_proof.round_number(), - )) - .longevity(longevity) - // We don't propagate this. This can never be included on a remote node. - .propagate(false) - .build() - } else { - InvalidTransaction::Call.into() + // discard equivocation report not coming from the local node + match source { + TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ }, + _ => { + log::warn!( + target: LOG_TARGET, + "rejecting unsigned report equivocation transaction because it is not local/in-block." + ); + return InvalidTransaction::Call.into() + }, + } + match call { + Call::report_vote_equivocation_unsigned { equivocation_proof, key_owner_proof } => { + let evidence = EquivocationEvidenceFor::::VoteEquivocationProof( + *equivocation_proof.clone(), + key_owner_proof.clone(), + ); + T::EquivocationReportSystem::check_evidence(evidence)?; + + let longevity = + >::Longevity::get(); + + ValidTransaction::with_tag_prefix("BeefyEquivocation") + // We assign the maximum priority for any equivocation report. + .priority(TransactionPriority::MAX) + // Only one equivocation report for the same offender at the same slot. + .and_provides(( + equivocation_proof.offender_id().clone(), + equivocation_proof.set_id(), + *equivocation_proof.round_number(), + )) + .longevity(longevity) + // We don't propagate this. This can never be included on a remote node. + .propagate(false) + .build() + }, + Call::report_fork_equivocation_unsigned { equivocation_proof, key_owner_proofs } => { + let evidence = EquivocationEvidenceFor::::ForkEquivocationProof( + *equivocation_proof.clone(), + key_owner_proofs.clone(), + ); + T::EquivocationReportSystem::check_evidence(evidence)?; + + let longevity = + >::Longevity::get(); + + ValidTransaction::with_tag_prefix("BeefyEquivocation") + // We assign the maximum priority for any equivocation report. + .priority(TransactionPriority::MAX) + // Only one equivocation report for the same offender at the same slot. + .and_provides(( + equivocation_proof.offender_ids().clone(), + equivocation_proof.set_id(), + *equivocation_proof.round_number(), + )) + .longevity(longevity) + // We don't propagate this. This can never be included on a remote node. + .propagate(false) + .build() + }, + _ => InvalidTransaction::Call.into(), } } 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()); - T::EquivocationReportSystem::check_evidence(evidence) - } else { - Err(InvalidTransaction::Call.into()) + match call { + Call::report_vote_equivocation_unsigned { equivocation_proof, key_owner_proof } => { + let evidence = EquivocationEvidenceFor::::VoteEquivocationProof( + *equivocation_proof.clone(), + key_owner_proof.clone(), + ); + T::EquivocationReportSystem::check_evidence(evidence) + }, + Call::report_fork_equivocation_unsigned { equivocation_proof, key_owner_proofs } => { + let evidence = EquivocationEvidenceFor::::ForkEquivocationProof( + *equivocation_proof.clone(), + key_owner_proofs.clone(), + ); + T::EquivocationReportSystem::check_evidence(evidence) + }, + _ => Err(InvalidTransaction::Call.into()), } } } diff --git a/substrate/frame/beefy/src/lib.rs b/substrate/frame/beefy/src/lib.rs index 0760446753a68..a12bf587a7e0a 100644 --- a/substrate/frame/beefy/src/lib.rs +++ b/substrate/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::{ensure_root, pallet_prelude::BlockNumberFor}; + use sp_consensus_beefy::ForkEquivocationProof; #[pallet::config] pub trait Config: frame_system::Config { @@ -194,8 +195,12 @@ 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, + /// The session of the equivocation proof is invalid + InvalidEquivocationProofSession, /// A given equivocation report is valid but already previously reported. DuplicateOffenceReport, /// Submitted configuration is invalid. @@ -216,7 +221,7 @@ pub mod pallet { pub fn report_equivocation( origin: OriginFor, equivocation_proof: Box< - EquivocationProof< + VoteEquivocationProof< BlockNumberFor, T::BeefyId, ::Signature, @@ -228,7 +233,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()) @@ -248,10 +256,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, @@ -263,7 +271,10 @@ pub mod pallet { T::EquivocationReportSystem::process_evidence( None, - (*equivocation_proof, key_owner_proof), + EquivocationEvidenceFor::::VoteEquivocationProof( + *equivocation_proof, + key_owner_proof, + ), )?; Ok(Pays::No.into()) } @@ -284,6 +295,78 @@ pub mod pallet { GenesisBlock::::put(Some(genesis_block)); Ok(()) } + + /// 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(3)] + #[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(4)] + #[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)?; + + T::EquivocationReportSystem::process_evidence( + None, + EquivocationEvidenceFor::ForkEquivocationProof( + *equivocation_proof, + key_owner_proofs, + ), + )?; + // Waive the fee since the report is valid and beneficial + Ok(Pays::No.into()) + } } #[pallet::validate_unsigned] @@ -311,15 +394,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( @@ -470,6 +581,7 @@ impl IsMember for Pallet { } pub trait WeightInfo { + // TODO: distinguish for fork equivocation proofs fn report_equivocation(validator_count: u32, max_nominators_per_validator: u32) -> Weight; fn set_new_genesis() -> Weight; } diff --git a/substrate/frame/beefy/src/mock.rs b/substrate/frame/beefy/src/mock.rs index 53d523cf724d9..069fd6dbdc6ea 100644 --- a/substrate/frame/beefy/src/mock.rs +++ b/substrate/frame/beefy/src/mock.rs @@ -39,7 +39,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! { @@ -265,7 +265,7 @@ pub fn new_test_ext_raw_authorities(authorities: Vec) -> TestExternalit let staking_config = pallet_staking::GenesisConfig:: { stakers, - validator_count: 2, + validator_count: authorities.len() as u32 - 1, force_era: pallet_staking::Forcing::ForceNew, minimum_validator_count: 0, invulnerables: vec![], diff --git a/substrate/frame/beefy/src/tests.rs b/substrate/frame/beefy/src/tests.rs index bf5ae19510ce9..8e599e266b047 100644 --- a/substrate/frame/beefy/src/tests.rs +++ b/substrate/frame/beefy/src/tests.rs @@ -19,8 +19,10 @@ use std::vec; use codec::Encode; use sp_consensus_beefy::{ - check_equivocation_proof, generate_equivocation_proof, known_payloads::MMR_ROOT_ID, - Keyring as BeefyKeyring, Payload, ValidatorSet, KEY_TYPE as BEEFY_KEY_TYPE, + check_vote_equivocation_proof, generate_fork_equivocation_proof_sc, + generate_fork_equivocation_proof_vote, generate_vote_equivocation_proof, + known_payloads::MMR_ROOT_ID, Commitment, Keyring as BeefyKeyring, Payload, ValidatorSet, + KEY_TYPE as BEEFY_KEY_TYPE, }; use sp_runtime::DigestItem; @@ -88,7 +90,8 @@ fn session_change_updates_authorities() { assert!(2 == Beefy::validator_set_id()); let want = beefy_log(ConsensusLog::AuthoritiesChange( - ValidatorSet::new(vec![mock_beefy_id(2), mock_beefy_id(4)], 2).unwrap(), + ValidatorSet::new(vec![mock_beefy_id(2), mock_beefy_id(3), mock_beefy_id(4)], 2) + .unwrap(), )); let log = System::digest().logs[1].clone(); @@ -113,9 +116,9 @@ fn session_change_updates_next_authorities() { let next_authorities = Beefy::next_authorities(); - assert_eq!(next_authorities.len(), 2); + assert_eq!(next_authorities.len(), 3); assert_eq!(want[1], next_authorities[0]); - assert_eq!(want[3], next_authorities[1]); + assert_eq!(want[3], next_authorities[2]); }); } @@ -158,7 +161,7 @@ fn validator_set_updates_work() { assert_eq!(vs.id(), 2u64); assert_eq!(want[1], vs.validators()[0]); - assert_eq!(want[3], vs.validators()[1]); + assert_eq!(want[3], vs.validators()[2]); }); } @@ -198,7 +201,8 @@ fn cleans_up_old_set_id_session_mappings() { /// Returns a list with 3 authorities with known keys: /// Alice, Bob and Charlie. pub fn test_authorities() -> Vec { - let authorities = vec![BeefyKeyring::Alice, BeefyKeyring::Bob, BeefyKeyring::Charlie]; + let authorities = + vec![BeefyKeyring::Alice, BeefyKeyring::Bob, BeefyKeyring::Charlie, BeefyKeyring::Dave]; authorities.into_iter().map(|id| id.public()).collect() } @@ -212,51 +216,53 @@ 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)); } +// vote equivocation report tests +// TODO: deduplicate by extracting common test structure of equivocation classes #[test] -fn report_equivocation_current_set_works() { +fn report_vote_equivocation_current_set_works() { let authorities = test_authorities(); new_test_ext_raw_authorities(authorities).execute_with(|| { @@ -282,7 +288,7 @@ fn report_equivocation_current_set_works() { ); } - assert_eq!(authorities.len(), 2); + assert_eq!(authorities.len(), 3); let equivocation_authority_index = 1; let equivocation_key = &authorities[equivocation_authority_index]; let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); @@ -291,7 +297,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 +306,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, @@ -336,7 +342,7 @@ fn report_equivocation_current_set_works() { } #[test] -fn report_equivocation_old_set_works() { +fn report_vote_equivocation_old_set_works() { let authorities = test_authorities(); new_test_ext_raw_authorities(authorities).execute_with(|| { @@ -348,7 +354,7 @@ fn report_equivocation_old_set_works() { let validators = Session::validators(); let old_set_id = validator_set.id(); - assert_eq!(authorities.len(), 2); + assert_eq!(authorities.len(), 3); let equivocation_authority_index = 0; let equivocation_key = &authorities[equivocation_authority_index]; @@ -377,13 +383,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, @@ -419,7 +425,7 @@ fn report_equivocation_old_set_works() { } #[test] -fn report_equivocation_invalid_set_id() { +fn report_vote_equivocation_invalid_set_id() { let authorities = test_authorities(); new_test_ext_raw_authorities(authorities).execute_with(|| { @@ -439,25 +445,25 @@ 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::::InvalidEquivocationProofSession, ); }); } #[test] -fn report_equivocation_invalid_session() { +fn report_vote_equivocation_invalid_session() { let authorities = test_authorities(); new_test_ext_raw_authorities(authorities).execute_with(|| { @@ -481,7 +487,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,18 +495,18 @@ 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::::InvalidEquivocationProofSession, ); }); } #[test] -fn report_equivocation_invalid_key_owner_proof() { +fn report_vote_equivocation_invalid_key_owner_proof() { let authorities = test_authorities(); new_test_ext_raw_authorities(authorities).execute_with(|| { @@ -525,7 +531,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 +543,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, @@ -548,7 +554,7 @@ fn report_equivocation_invalid_key_owner_proof() { } #[test] -fn report_equivocation_invalid_equivocation_proof() { +fn report_vote_equivocation_invalid_equivocation_proof() { let authorities = test_authorities(); new_test_ext_raw_authorities(authorities).execute_with(|| { @@ -568,12 +574,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 +590,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), )); @@ -616,7 +622,7 @@ fn report_equivocation_invalid_equivocation_proof() { } #[test] -fn report_equivocation_validate_unsigned_prevents_duplicates() { +fn report_vote_equivocation_validate_unsigned_prevents_duplicates() { use sp_runtime::transaction_validity::{ InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, ValidTransaction, @@ -639,14 +645,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 +687,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, @@ -706,7 +712,7 @@ fn report_equivocation_validate_unsigned_prevents_duplicates() { } #[test] -fn report_equivocation_has_valid_weight() { +fn report_vote_equivocation_has_valid_weight() { // the weight depends on the size of the validator set, // but there's a lower bound of 100 validators. assert!((1..=100) @@ -725,7 +731,7 @@ fn report_equivocation_has_valid_weight() { } #[test] -fn valid_equivocation_reports_dont_pay_fees() { +fn valid_vote_equivocation_reports_dont_pay_fees() { let authorities = test_authorities(); new_test_ext_raw_authorities(authorities).execute_with(|| { @@ -743,7 +749,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 +758,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 +769,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 +783,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, @@ -792,6 +798,1340 @@ fn valid_equivocation_reports_dont_pay_fees() { }) } +// fork equivocation (via vote) report tests +// TODO: deduplicate by extracting common test structure of equivocation classes +#[test] +fn report_fork_equivocation_vote_current_set_works() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + assert_eq!(Staking::current_era(), Some(0)); + assert_eq!(Session::current_index(), 0); + + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + let validators = Session::validators(); + + // make sure that all validators have the same balance + for validator in &validators { + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + + assert_eq!(authorities.len(), 3); + let equivocation_authority_index = 1; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + // generate an fork equivocation proof, with a vote in the same round for a + // different payload than finalized + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id, &equivocation_keyring), + header, + ); + + // create the key ownership proof + 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_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof], + ),); + + era += 1; + start_era(era); + + // check that the balance of 0-th validator is slashed 100%. + let equivocation_validator_id = validators[equivocation_authority_index]; + + assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0); + assert_eq!( + Staking::eras_stakers(era, equivocation_validator_id), + pallet_staking::Exposure { total: 0, own: 0, others: vec![] }, + ); + + // check that the balances of all other validators are left intact. + for validator in &validators { + if *validator == equivocation_validator_id { + continue + } + + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + }); +} + +#[test] +fn report_fork_equivocation_vote_old_set_works() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let validators = Session::validators(); + let old_set_id = validator_set.id(); + + assert_eq!(authorities.len(), 3); + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + + // create the key ownership proof in the "old" set + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + era += 1; + start_era(era); + + // make sure that all authorities have the same balance + for validator in &validators { + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(2, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + + let validator_set = Beefy::validator_set().unwrap(); + let new_set_id = validator_set.id(); + assert_eq!(old_set_id + 3, new_set_id); + + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + // generate an fork equivocation proof, with a vote in the same round for a + // different payload than finalized + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, old_set_id, &equivocation_keyring), + header, + ); + + // report the equivocation and the tx should be dispatched successfully + assert_ok!(Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof], + ),); + + era += 1; + start_era(era); + + // check that the balance of 0-th validator is slashed 100%. + let equivocation_validator_id = validators[equivocation_authority_index]; + + assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0); + assert_eq!( + Staking::eras_stakers(era, equivocation_validator_id), + pallet_staking::Exposure { total: 0, own: 0, others: vec![] }, + ); + + // check that the balances of all other validators are left intact. + for validator in &validators { + if *validator == equivocation_validator_id { + continue + } + + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(3, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + }); +} + +#[test] +fn report_fork_equivocation_vote_invalid_set_id() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + // generate an equivocation for a future set + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id + 1, &equivocation_keyring), + header, + ); + + // the call for reporting the equivocation should error + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof], + ), + Error::::InvalidEquivocationProofSession, + ); + }); +} + +#[test] +fn report_fork_equivocation_vote_invalid_session() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + // generate a key ownership proof at current era set id + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + era += 1; + start_era(era); + + let set_id = Beefy::validator_set().unwrap().id(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + // generate an equivocation proof at following era set id = 3 + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id, &equivocation_keyring), + header, + ); + + // 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_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof], + ), + Error::::InvalidEquivocationProofSession, + ); + }); +} + +#[test] +fn report_fork_equivocation_vote_invalid_key_owner_proof() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let invalid_owner_authority_index = 1; + let invalid_owner_key = &authorities[invalid_owner_authority_index]; + + // generate a key ownership proof for the authority at index 1 + let invalid_key_owner_proof = + Historical::prove((BEEFY_KEY_TYPE, &invalid_owner_key)).unwrap(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + // generate an equivocation for a future set + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id + 1, &equivocation_keyring), + header, + ); + + // we need to start a new era otherwise the key ownership proof won't be + // checked since the authorities are part of the current session + era += 1; + start_era(era); + + // 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_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![invalid_key_owner_proof], + ), + Error::::InvalidKeyOwnershipProof, + ); + }); +} + +#[test] +fn report_fork_equivocation_vote_invalid_equivocation_proof() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + start_era(1); + + let block_num = System::block_number(); + let header = System::finalize(); + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + // generate a key ownership proof at set id in era 1 + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + start_era(2); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + + // vote targets different round than finalized payload, there is no equivocation. + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num + 1, payload.clone(), set_id, &equivocation_keyring), + header.clone(), + ); + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof.clone()], + ), + Error::::InvalidForkEquivocationProof, + ); + + // vote signed with a key that isn't part of the authority set + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload.clone(), set_id, &BeefyKeyring::Dave), + header.clone(), + ); + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof.clone()], + ), + Error::::InvalidKeyOwnershipProof, + ); + + // vote targets future set id + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload.clone(), set_id + 1, &equivocation_keyring), + header.clone(), + ); + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof.clone()], + ), + Error::::InvalidEquivocationProofSession, + ); + }); +} + +#[test] +fn report_fork_equivocation_vote_validate_unsigned_prevents_duplicates() { + use sp_runtime::transaction_validity::{ + InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, + ValidTransaction, + }; + + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + // generate and report an equivocation for the validator at index 0 + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id, &equivocation_keyring), + header, + ); + + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + let call = Call::report_fork_equivocation_unsigned { + equivocation_proof: Box::new(equivocation_proof.clone()), + key_owner_proofs: vec![key_owner_proof.clone()], + }; + + // only local/inblock reports are allowed + assert_eq!( + ::validate_unsigned( + TransactionSource::External, + &call, + ), + InvalidTransaction::Call.into(), + ); + + // the transaction is valid when passed as local + let tx_tag = (vec![equivocation_key], set_id, 3u64); + + let call_result = ::validate_unsigned( + TransactionSource::Local, + &call, + ); + + assert_eq!( + call_result, + TransactionValidity::Ok(ValidTransaction { + priority: TransactionPriority::max_value(), + requires: vec![], + provides: vec![("BeefyEquivocation", tx_tag.clone()).encode()], + longevity: ReportLongevity::get(), + propagate: false, + }) + ); + + assert_ok!(::pre_dispatch(&call)); + + // we submit the report + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof], + ) + .unwrap(); + + // the report should now be considered stale and the transaction is invalid + // the check for staleness should be done on both `validate_unsigned` and on `pre_dispatch` + assert_err!( + ::validate_unsigned( + TransactionSource::Local, + &call, + ), + InvalidTransaction::Stale, + ); + + assert_err!( + ::pre_dispatch(&call), + InvalidTransaction::Stale, + ); + }); +} + +#[test] +fn valid_fork_equivocation_vote_reports_dont_pay_fees() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + // generate equivocation proof + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id, &equivocation_keyring), + header, + ); + + // create the key ownership proof. + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + // check the dispatch info for the call. + let info = Call::::report_fork_equivocation_unsigned { + equivocation_proof: Box::new(equivocation_proof.clone()), + key_owner_proofs: vec![key_owner_proof.clone()], + } + .get_dispatch_info(); + + // it should have non-zero weight and the fee has to be paid. + assert!(info.weight.any_gt(Weight::zero())); + assert_eq!(info.pays_fee, Pays::Yes); + + // report the equivocation. + let post_info = Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof.clone()), + vec![key_owner_proof.clone()], + ) + .unwrap(); + + // the original weight should be kept, but given that the report + // is valid the fee is waived. + assert!(post_info.actual_weight.is_none()); + assert_eq!(post_info.pays_fee, Pays::No); + + // report the equivocation again which is invalid now since it is + // duplicate. + let post_info = Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof.clone()), + vec![key_owner_proof.clone()], + ) + .err() + .unwrap() + .post_info; + + // the fee is not waived and the original weight is kept. + assert!(post_info.actual_weight.is_none()); + assert_eq!(post_info.pays_fee, Pays::Yes); + }) +} + +// fork equivocation (via signed commitment) report tests +// TODO: deduplicate by extracting common test structure of equivocation classes +#[test] +fn report_fork_equivocation_sc_current_set_works() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + assert_eq!(Staking::current_era(), Some(0)); + assert_eq!(Session::current_index(), 0); + + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + let validators = Session::validators(); + + // make sure that all validators have the same balance + for validator in &validators { + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + + assert_eq!(authorities.len(), 3); + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| &authorities[*i]) + .collect::>(); + let equivocation_keyrings = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k).unwrap()) + .collect(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let commitment = Commitment { validator_set_id: set_id, block_number: block_num, payload }; + // generate an fork equivocation proof, with a vote in the same round for a + // different payload than finalized + let equivocation_proof = + generate_fork_equivocation_proof_sc(commitment, equivocation_keyrings, header); + + // create the key ownership proof + let key_owner_proofs = equivocation_keys + .iter() + .map(|k| Historical::prove((BEEFY_KEY_TYPE, &k)).unwrap()) + .collect(); + + // report the equivocation and the tx should be dispatched successfully + assert_ok!(Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs, + ),); + + era += 1; + start_era(era); + + // check that the balance of equivocating validators is slashed 100%. + let equivocation_validator_ids = equivocation_authority_indices + .iter() + .map(|i| validators[*i]) + .collect::>(); + + for equivocation_validator_id in &equivocation_validator_ids { + assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0); + assert_eq!( + Staking::eras_stakers(era, equivocation_validator_id), + pallet_staking::Exposure { total: 0, own: 0, others: vec![] }, + ); + } + + // check that the balances of all other validators are left intact. + for validator in &validators { + if equivocation_validator_ids.contains(&validator) { + continue + } + + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + }); +} + +#[test] +fn report_fork_equivocation_sc_old_set_works() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let validators = Session::validators(); + let old_set_id = validator_set.id(); + + assert_eq!(authorities.len(), 3); + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| &authorities[*i]) + .collect::>(); + + // create the key ownership proofs in the "old" set + let key_owner_proofs = equivocation_keys + .iter() + .map(|k| Historical::prove((BEEFY_KEY_TYPE, &k)).unwrap()) + .collect(); + + era += 1; + start_era(era); + + // make sure that all authorities have the same balance + for validator in &validators { + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(2, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + + let validator_set = Beefy::validator_set().unwrap(); + let new_set_id = validator_set.id(); + assert_eq!(old_set_id + 3, new_set_id); + + let equivocation_keyrings = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k).unwrap()) + .collect(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + // generate an fork equivocation proof, with a vote in the same round for a + // different payload than finalized + let commitment = + Commitment { validator_set_id: old_set_id, block_number: block_num, payload }; + // generate an fork equivocation proof, with a vote in the same round for a + // different payload than finalized + let equivocation_proof = + generate_fork_equivocation_proof_sc(commitment, equivocation_keyrings, header); + + // report the equivocation and the tx should be dispatched successfully + assert_ok!(Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs, + ),); + + era += 1; + start_era(era); + + // check that the balance of equivocating validators is slashed 100%. + let equivocation_validator_ids = equivocation_authority_indices + .iter() + .map(|i| validators[*i]) + .collect::>(); + + for equivocation_validator_id in &equivocation_validator_ids { + assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0); + assert_eq!( + Staking::eras_stakers(era, equivocation_validator_id), + pallet_staking::Exposure { total: 0, own: 0, others: vec![] }, + ); + } + + // check that the balances of all other validators are left intact. + for validator in &validators { + if equivocation_validator_ids.contains(&validator) { + continue + } + + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(3, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + }); +} + +#[test] +fn report_fork_equivocation_sc_invalid_set_id() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| &authorities[*i]) + .collect::>(); + let equivocation_keyrings = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k).unwrap()) + .collect(); + + let key_owner_proofs = equivocation_keys + .iter() + .map(|k| Historical::prove((BEEFY_KEY_TYPE, &k)).unwrap()) + .collect(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + // generate an equivocation for a future set + let commitment = + Commitment { validator_set_id: set_id + 1, block_number: block_num, payload }; + let equivocation_proof = + generate_fork_equivocation_proof_sc(commitment, equivocation_keyrings, header); + + // the call for reporting the equivocation should error + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs, + ), + Error::::InvalidEquivocationProofSession, + ); + }); +} + +#[test] +fn report_fork_equivocation_sc_invalid_session() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| &authorities[*i]) + .collect::>(); + let equivocation_keyrings = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k).unwrap()) + .collect(); + + // generate key ownership proofs at current era set id + let key_owner_proofs = equivocation_keys + .iter() + .map(|k| Historical::prove((BEEFY_KEY_TYPE, &k)).unwrap()) + .collect(); + + era += 1; + start_era(era); + + let set_id = Beefy::validator_set().unwrap().id(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + // generate an equivocation proof at following era set id = 3 + let commitment = Commitment { validator_set_id: set_id, block_number: block_num, payload }; + let equivocation_proof = + generate_fork_equivocation_proof_sc(commitment, equivocation_keyrings, header); + + // 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_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs, + ), + Error::::InvalidEquivocationProofSession, + ); + }); +} + +#[test] +fn report_fork_equivocation_sc_invalid_key_owner_proof() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let invalid_owner_authority_index = 1; + let invalid_owner_key = &authorities[invalid_owner_authority_index]; + let valid_owner_authority_index = 0; + let valid_owner_key = &authorities[valid_owner_authority_index]; + + // generate a key ownership proof for the authority at index 1 + let invalid_key_owner_proof = + Historical::prove((BEEFY_KEY_TYPE, &invalid_owner_key)).unwrap(); + // generate a key ownership proof for the authority at index 1 + let valid_key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &valid_owner_key)).unwrap(); + + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| &authorities[*i]) + .collect::>(); + let equivocation_keyrings = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k).unwrap()) + .collect(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + // generate an equivocation proof for the authorities at indices [0, 2] + let commitment = + Commitment { validator_set_id: set_id + 1, block_number: block_num, payload }; + let equivocation_proof = + generate_fork_equivocation_proof_sc(commitment, equivocation_keyrings, header); + + // we need to start a new era otherwise the key ownership proof won't be + // checked since the authorities are part of the current session + era += 1; + start_era(era); + + // report an equivocation for the current set using a key ownership + // proof for a different key than the ones in the equivocation proof. + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![valid_key_owner_proof, invalid_key_owner_proof], + ), + Error::::InvalidKeyOwnershipProof, + ); + }); +} + +#[test] +fn report_fork_equivocation_sc_invalid_equivocation_proof() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + start_era(1); + + let block_num = System::block_number(); + let header = System::finalize(); + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| &authorities[*i]) + .collect::>(); + let equivocation_keyrings: Vec<_> = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k).unwrap()) + .collect(); + + // generate a key ownership proof at set id in era 1 + let key_owner_proofs: Vec<_> = equivocation_keys + .iter() + .map(|k| Historical::prove((BEEFY_KEY_TYPE, &k)).unwrap()) + .collect(); + + start_era(2); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + + // commitment targets different round than finalized payload, there is no equivocation. + let equivocation_proof = generate_fork_equivocation_proof_sc( + Commitment { + validator_set_id: set_id, + block_number: block_num + 1, + payload: payload.clone(), + }, + equivocation_keyrings.clone(), + header.clone(), + ); + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs.clone(), + ), + Error::::InvalidForkEquivocationProof, + ); + + // commitment signed with a key that isn't part of the authority set + let equivocation_proof = generate_fork_equivocation_proof_sc( + Commitment { + validator_set_id: set_id, + block_number: block_num, + payload: payload.clone(), + }, + vec![BeefyKeyring::Eve], + header.clone(), + ); + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs.clone(), + ), + Error::::InvalidKeyOwnershipProof, + ); + + // commitment targets future set id + let equivocation_proof = generate_fork_equivocation_proof_sc( + Commitment { + validator_set_id: set_id + 1, + block_number: block_num, + payload: payload.clone(), + }, + equivocation_keyrings, + header, + ); + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs, + ), + Error::::InvalidEquivocationProofSession, + ); + }); +} + +#[test] +fn report_fork_equivocation_sc_validate_unsigned_prevents_duplicates() { + use sp_runtime::transaction_validity::{ + InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, + ValidTransaction, + }; + + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + // generate and report an equivocation for the validator at index 0 + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id, &equivocation_keyring), + header, + ); + + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + let call = Call::report_fork_equivocation_unsigned { + equivocation_proof: Box::new(equivocation_proof.clone()), + key_owner_proofs: vec![key_owner_proof.clone()], + }; + + // only local/inblock reports are allowed + assert_eq!( + ::validate_unsigned( + TransactionSource::External, + &call, + ), + InvalidTransaction::Call.into(), + ); + + // the transaction is valid when passed as local + let tx_tag = (vec![equivocation_key], set_id, 3u64); + + let call_result = ::validate_unsigned( + TransactionSource::Local, + &call, + ); + + assert_eq!( + call_result, + TransactionValidity::Ok(ValidTransaction { + priority: TransactionPriority::max_value(), + requires: vec![], + provides: vec![("BeefyEquivocation", tx_tag.clone()).encode()], + longevity: ReportLongevity::get(), + propagate: false, + }) + ); + + assert_ok!(::pre_dispatch(&call)); + + // we submit the report + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof], + ) + .unwrap(); + + // the report should now be considered stale and the transaction is invalid + // the check for staleness should be done on both `validate_unsigned` and on `pre_dispatch` + assert_err!( + ::validate_unsigned( + TransactionSource::Local, + &call, + ), + InvalidTransaction::Stale, + ); + + assert_err!( + ::pre_dispatch(&call), + InvalidTransaction::Stale, + ); + }); +} + +#[test] +fn valid_fork_equivocation_sc_reports_dont_pay_fees() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + // generate equivocation proof + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id, &equivocation_keyring), + header, + ); + + // create the key ownership proof. + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + // check the dispatch info for the call. + let info = Call::::report_fork_equivocation_unsigned { + equivocation_proof: Box::new(equivocation_proof.clone()), + key_owner_proofs: vec![key_owner_proof.clone()], + } + .get_dispatch_info(); + + // it should have non-zero weight and the fee has to be paid. + assert!(info.weight.any_gt(Weight::zero())); + assert_eq!(info.pays_fee, Pays::Yes); + + // report the equivocation. + let post_info = Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof.clone()), + vec![key_owner_proof.clone()], + ) + .unwrap(); + + // the original weight should be kept, but given that the report + // is valid the fee is waived. + assert!(post_info.actual_weight.is_none()); + assert_eq!(post_info.pays_fee, Pays::No); + + // report the equivocation again which is invalid now since it is + // duplicate. + let post_info = Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof.clone()), + vec![key_owner_proof.clone()], + ) + .err() + .unwrap() + .post_info; + + // the fee is not waived and the original weight is kept. + assert!(post_info.actual_weight.is_none()); + assert_eq!(post_info.pays_fee, Pays::Yes); + }) +} + +#[test] +fn report_fork_equivocation_sc_stacked_reports_stack_correctly() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + assert_eq!(Staking::current_era(), Some(0)); + assert_eq!(Session::current_index(), 0); + + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + let header = System::finalize(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + let validators = Session::validators(); + + // make sure that all validators have the same balance + for validator in &validators { + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + + assert_eq!(authorities.len(), 3); + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| &authorities[*i]) + .collect::>(); + let equivocation_keyrings: Vec<_> = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k).unwrap()) + .collect(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let commitment = Commitment { validator_set_id: set_id, block_number: block_num, payload }; + // generate two fork equivocation proofs with a signed commitment in the same round for a + // different payload than finalized + // 1. the first equivocation proof is only for Alice + // 2. the second equivocation proof is for all equivocators + let equivocation_proof_singleton = generate_fork_equivocation_proof_sc( + commitment.clone(), + vec![equivocation_keyrings[0]], + header.clone(), + ); + let equivocation_proof_full = + generate_fork_equivocation_proof_sc(commitment, equivocation_keyrings, header); + + // create the key ownership proof + let key_owner_proofs: Vec<_> = equivocation_keys + .iter() + .map(|k| Historical::prove((BEEFY_KEY_TYPE, &k)).unwrap()) + .collect(); + + // only report a single equivocator and the tx should be dispatched successfully + assert_ok!(Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof_singleton), + vec![key_owner_proofs[0].clone()], + ),); + + era += 1; + start_era(era); + + // check that the balance of the reported equivocating validator is slashed 100%. + let equivocation_validator_ids = equivocation_authority_indices + .iter() + .map(|i| validators[*i]) + .collect::>(); + + assert_eq!(Balances::total_balance(&equivocation_validator_ids[0]), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&equivocation_validator_ids[0]), 0); + assert_eq!( + Staking::eras_stakers(era, equivocation_validator_ids[0]), + pallet_staking::Exposure { total: 0, own: 0, others: vec![] }, + ); + + // check that the balances of all other validators are left intact. + for validator in &validators { + if equivocation_validator_ids[0] == *validator { + continue + } + + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + + // report the full equivocation and the tx should be dispatched successfully + assert_ok!(Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof_full), + key_owner_proofs, + ),); + + era += 1; + start_era(era); + + let equivocation_validator_ids = equivocation_authority_indices + .iter() + .map(|i| validators[*i]) + .collect::>(); + + // check that the balance of equivocating validators is slashed 100%, and the validator + // already reported isn't slashed again + for equivocation_validator_id in &equivocation_validator_ids { + assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0); + assert_eq!( + Staking::eras_stakers(era, equivocation_validator_id), + pallet_staking::Exposure { total: 0, own: 0, others: vec![] }, + ); + } + + // check that the balances of all other validators are left intact. + for validator in &validators { + if equivocation_validator_ids.contains(&validator) { + continue + } + + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + }); +} + #[test] fn set_new_genesis_works() { let authorities = test_authorities(); diff --git a/substrate/primitives/consensus/beefy/src/commitment.rs b/substrate/primitives/consensus/beefy/src/commitment.rs index 5b6ef9ae5ab36..3c451b70afb06 100644 --- a/substrate/primitives/consensus/beefy/src/commitment.rs +++ b/substrate/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/substrate/primitives/consensus/beefy/src/lib.rs b/substrate/primitives/consensus/beefy/src/lib.rs index e31c53237be24..65465b64d01c0 100644 --- a/substrate/primitives/consensus/beefy/src/lib.rs +++ b/substrate/primitives/consensus/beefy/src/lib.rs @@ -47,7 +47,7 @@ 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 as HeaderT, Keccak256, NumberFor}; use sp_std::prelude::*; /// Key type for BEEFY module. @@ -279,14 +279,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 @@ -301,6 +301,37 @@ 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. + pub commitment: Commitment, + /// Signatures on this block + 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 canonical_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( @@ -317,10 +348,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, @@ -352,6 +383,52 @@ where return valid_first && valid_second } +/// Validates [ForkEquivocationProof] by checking: +/// 1. `commitment` is signed, +/// 2. `canonical_header` is valid and matches `commitment.block_number`. +/// 2. `commitment.payload` != `canonical_payload(canonical_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>, + canonical_header_hash: &Header::Hash, +) -> bool +where + Id: BeefyAuthorityId + PartialEq, + Number: Clone + Encode + PartialEq, + MsgHash: Hash, + Header: HeaderT, +{ + let ForkEquivocationProof { commitment, signatories, canonical_header } = proof; + + if canonical_header.hash() != *canonical_header_hash { + return false + } + + let canonical_mmr_root_digest = mmr::find_mmr_root_digest::
(canonical_header); + let canonical_payload = canonical_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 `canonical_payload` + // 2. if the signatories signed a payload when there should be none (for + // instance for a block prior to BEEFY activation), then canonical_payload = + // None, and they will likewise be slashed + if Some(&commitment.payload) == canonical_payload.as_ref() { + return false + } + + signatories.iter().all(|(authority_id, signature)| { + check_commitment_signature(&commitment, authority_id, signature) + }) +} + /// New BEEFY validator set notification hook. pub trait OnNewValidatorSet { /// Function called by the pallet when BEEFY validator set changes. @@ -372,7 +449,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 @@ -389,7 +466,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, @@ -408,12 +486,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/substrate/primitives/consensus/beefy/src/mmr.rs b/substrate/primitives/consensus/beefy/src/mmr.rs index 9ac1624ca752c..addb09ce993fa 100644 --- a/substrate/primitives/consensus/beefy/src/mmr.rs +++ b/substrate/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 { @@ -182,7 +182,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()) }) } @@ -191,7 +191,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 { @@ -206,7 +206,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() { @@ -234,7 +234,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(), @@ -244,7 +243,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( @@ -253,7 +252,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/substrate/primitives/consensus/beefy/src/payload.rs b/substrate/primitives/consensus/beefy/src/payload.rs index d520de445c95a..43b83911dd629 100644 --- a/substrate/primitives/consensus/beefy/src/payload.rs +++ b/substrate/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/substrate/primitives/consensus/beefy/src/test_utils.rs b/substrate/primitives/consensus/beefy/src/test_utils.rs index b83f657af38e3..d6b0e0a82eb36 100644 --- a/substrate/primitives/consensus/beefy/src/test_utils.rs +++ b/substrate/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, ValidatorSetId, + VoteEquivocationProof, VoteMessage, +}; use codec::Encode; use sp_core::{ecdsa, keccak_256, Pair}; use std::collections::HashMap; @@ -91,20 +94,47 @@ 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 & canonical header. +pub fn generate_fork_equivocation_proof_vote
( + vote: (u64, Payload, ValidatorSetId, &Keyring), + canonical_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, canonical_header } +} + +/// Create a new `ForkEquivocationProof` based on signed commitment & correct header. +pub fn generate_fork_equivocation_proof_sc
( + commitment: Commitment, + keyrings: Vec, + canonical_header: Header, +) -> ForkEquivocationProof { + let signatories = keyrings + .into_iter() + .map(|k| (k.public(), k.sign(&commitment.encode()))) + .collect::>(); + ForkEquivocationProof { commitment, signatories, canonical_header } } diff --git a/substrate/primitives/runtime/src/traits.rs b/substrate/primitives/runtime/src/traits.rs index ec79f43cabdc3..5ab12206b338e 100644 --- a/substrate/primitives/runtime/src/traits.rs +++ b/substrate/primitives/runtime/src/traits.rs @@ -1763,7 +1763,7 @@ pub trait ValidateUnsigned { /// this code before the unsigned extrinsic enters the transaction pool and also periodically /// afterwards to ensure the validity. To prevent dos-ing a network with unsigned /// extrinsics, these validity checks should include some checks around uniqueness, for example, - /// like checking that the unsigned extrinsic was send by an authority in the active set. + /// like checking that the unsigned extrinsic was sent by an authority in the active set. /// /// Changes made to storage should be discarded by caller. fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity;