diff --git a/client/consensus/beefy/src/communication/fisherman.rs b/client/consensus/beefy/src/communication/fisherman.rs
new file mode 100644
index 0000000000000..c4780bb5f939b
--- /dev/null
+++ b/client/consensus/beefy/src/communication/fisherman.rs
@@ -0,0 +1,296 @@
+// 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::BeefySignatureHasher,
+ LOG_TARGET,
+};
+use log::debug;
+use sc_client_api::Backend;
+use sp_api::ProvideRuntimeApi;
+use sp_blockchain::HeaderBackend;
+use sp_consensus_beefy::{
+ check_invalid_fork_proof,
+ crypto::{AuthorityId, Signature},
+ BeefyApi,
+ Commitment,
+ InvalidForkCommitmentProof,
+ OpaqueKeyOwnershipProof,
+ 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 finalized block against expected payload.
+ ///
+ /// Note: this fn expects `vote.commitment.block_number` to be finalized.
+ fn check_vote(
+ &self,
+ vote: VoteMessage, AuthorityId, Signature>,
+ ) -> Result<(), Error>;
+
+ fn check_signed_commitment(
+ &self,
+ signed_commitment: SignedCommitment, Signature>,
+ ) -> Result<(), Error>;
+
+ /// Check `proof` for contained finalized block against expected payload.
+ ///
+ /// Note: this fn expects block referenced in `proof` to be finalized.
+ 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 payload_provider: P,
+ pub _phantom: PhantomData,
+}
+
+impl Fisherman
+where
+ B: Block,
+ BE: Backend,
+ P: PayloadProvider,
+ R: ProvideRuntimeApi + Send + Sync,
+ R::Api: BeefyApi,
+{
+ fn expected_header_and_payload(
+ &self,
+ number: NumberFor,
+ ) -> Result<(B::Header, Payload), Error> {
+ // This should be un-ambiguous since `number` is finalized.
+ let hash = self
+ .backend
+ .blockchain()
+ .expect_block_hash_from_id(&BlockId::Number(number))
+ .map_err(|e| Error::Backend(e.to_string()))?;
+ let header = self
+ .backend
+ .blockchain()
+ .expect_header(hash)
+ .map_err(|e| Error::Backend(e.to_string()))?;
+ self.payload_provider
+ .payload(&header)
+ .map(|payload| (header, payload))
+ .ok_or_else(|| Error::Backend("BEEFY Payload not found".into()))
+ }
+
+ fn active_validator_set_at(
+ &self,
+ header: &B::Header,
+ ) -> Result, Error> {
+ self.runtime
+ .runtime_api()
+ .validator_set(header.hash())
+ .map_err(Error::RuntimeApi)?
+ .ok_or_else(|| Error::Backend("could not get BEEFY validator set".into()))
+ }
+
+ fn report_invalid_payload(
+ &self,
+ signed_commitment: SignedCommitment, Signature>,
+ correct_payload: &Payload,
+ correct_header: &B::Header,
+ ) -> Result<(), Error> {
+ let validator_set = self.active_validator_set_at(correct_header)?;
+ let set_id = validator_set.id();
+
+ let proof = InvalidForkCommitmentProof {
+ commitment: signed_commitment.commitment.clone(),
+ signatories: vec![],
+ expected_payload: correct_payload.clone(),
+ };
+
+ if signed_commitment.commitment.validator_set_id != set_id ||
+ signed_commitment.commitment.payload != *correct_payload ||
+ !check_invalid_fork_proof::, AuthorityId, BeefySignatureHasher>(&proof)
+ {
+ debug!(target: LOG_TARGET, "🥩 Skip report for bad invalid fork proof {:?}", proof);
+ return Ok(())
+ }
+
+ let offender_ids =
+ proof.signatories.iter().cloned().map(|(id, _sig)| id).collect::>();
+ let runtime_api = self.runtime.runtime_api();
+
+ // generate key ownership proof at that block
+ let key_owner_proofs = offender_ids
+ .iter()
+ .filter_map(|id| {
+ match runtime_api.generate_key_ownership_proof(
+ correct_header.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_invalid_fork_unsigned_extrinsic(best_block_hash, proof, key_owner_proofs)
+ .map_err(Error::RuntimeApi)?;
+
+ Ok(())
+ }
+
+ fn report_invalid_fork_commitments(
+ &self,
+ proof: InvalidForkCommitmentProof, AuthorityId, Signature>,
+ correct_header: &B::Header,
+ ) -> Result<(), Error> {
+ let validator_set = self.active_validator_set_at(correct_header)?;
+ let set_id = validator_set.id();
+
+ if proof.commitment.validator_set_id != set_id ||
+ !check_invalid_fork_proof::, AuthorityId, BeefySignatureHasher>(&proof)
+ {
+ debug!(target: LOG_TARGET, "🥩 Skip report for bad invalid fork proof {:?}", proof);
+ return Ok(())
+ }
+
+ let hash = correct_header.hash();
+ let offender_ids =
+ proof.signatories.iter().cloned().map(|(id, _sig)| id).collect::>();
+ let runtime_api = self.runtime.runtime_api();
+
+ // generate key ownership proof at that block
+ let key_owner_proofs = offender_ids
+ .iter()
+ .filter_map(|id| {
+ match runtime_api.generate_key_ownership_proof(hash, set_id, id.clone()) {
+ Ok(Some(proof)) => Some(Ok(proof)),
+ Ok(None) => {
+ debug!(
+ target: LOG_TARGET,
+ "🥩 Invalid fork vote offender not part of the authority set."
+ );
+ None
+ },
+ Err(e) => Some(Err(Error::RuntimeApi(e))),
+ }
+ })
+ .collect::>()?;
+
+ // submit invalid fork vote report at **best** block
+ let best_block_hash = self.backend.blockchain().info().best_hash;
+ runtime_api
+ .submit_report_invalid_fork_unsigned_extrinsic(best_block_hash, proof, key_owner_proofs)
+ .map_err(Error::RuntimeApi)?;
+
+ Ok(())
+ }
+}
+
+impl BeefyFisherman for Fisherman
+where
+ B: Block,
+ BE: Backend,
+ P: PayloadProvider,
+ R: ProvideRuntimeApi + Send + Sync,
+ R::Api: BeefyApi,
+{
+ /// Check `vote` for contained block against expected payload.
+ ///
+ /// Note: this fn expects `vote.commitment.block_number` to be finalized.
+ fn check_vote(
+ &self,
+ vote: VoteMessage, AuthorityId, Signature>,
+ ) -> Result<(), Error> {
+ let number = vote.commitment.block_number;
+ let (header, expected_payload) = self.expected_header_and_payload(number)?;
+ if vote.commitment.payload != expected_payload {
+ let validator_set = self.active_validator_set_at(&header)?;
+ let proof = InvalidForkCommitmentProof {
+ commitment: vote.commitment,
+ signatories: vec![(vote.id, vote.signature)],
+ expected_payload,
+ };
+ self.report_invalid_fork_commitments(proof, &header)?;
+ }
+ Ok(())
+ }
+
+ /// Check `commitment` for contained block against expected payload.
+ fn check_signed_commitment(
+ &self,
+ signed_commitment: SignedCommitment, Signature>,
+ ) -> Result<(), Error> {
+ let number = signed_commitment.commitment.block_number;
+ let (header, expected_payload) = self.expected_header_and_payload(number)?;
+ if signed_commitment.commitment.payload != expected_payload {
+ let validator_set = self.active_validator_set_at(&header)?;
+ self.report_invalid_payload(signed_commitment, &expected_payload, &header)?;
+ }
+ Ok(())
+ }
+
+ /// Check `proof` for contained block against expected payload.
+ ///
+ /// Note: this fn expects block referenced in `proof` to be finalized.
+ fn check_proof(&self, proof: BeefyVersionedFinalityProof) -> Result<(), Error> {
+ let (commitment, signatures) = match proof {
+ BeefyVersionedFinalityProof::::V1(inner) => (inner.commitment, inner.signatures),
+ };
+ let number = commitment.block_number;
+ let (header, expected_payload) = self.expected_header_and_payload(number)?;
+ if commitment.payload != expected_payload {
+ // TODO: create/get grandpa proof for block number
+ let validator_set = self.active_validator_set_at(&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 = InvalidForkCommitmentProof { commitment, signatories, expected_payload };
+ self.report_invalid_fork_commitments(
+ proof, &header,
+ )?;
+ }
+ Ok(())
+ }
+}
diff --git a/client/consensus/beefy/src/communication/gossip.rs b/client/consensus/beefy/src/communication/gossip.rs
index d60622b03eece..992c4417e86d2 100644
--- a/client/consensus/beefy/src/communication/gossip.rs
+++ b/client/consensus/beefy/src/communication/gossip.rs
@@ -32,6 +32,7 @@ use wasm_timer::Instant;
use crate::{
communication::{
benefit, cost,
+ fisherman::BeefyFisherman,
peers::{KnownPeers, PeerReport},
},
justification::{
@@ -225,26 +226,29 @@ impl Filter {
/// Allows messages for 'rounds >= last concluded' to flow, everything else gets
/// rejected/expired.
///
+/// Messages for active and expired rounds are validated for expected payloads and attempts
+/// to create forks before head of GRANDPA are reported.
+///
///All messaging is handled in a single BEEFY global topic.
-pub(crate) struct GossipValidator
-where
- B: Block,
-{
+pub(crate) struct GossipValidator {
votes_topic: B::Hash,
justifs_topic: B::Hash,
gossip_filter: RwLock>,
next_rebroadcast: Mutex,
known_peers: Arc>>,
report_sender: TracingUnboundedSender,
+ fisherman: F,
}
-impl GossipValidator
+impl GossipValidator
where
B: Block,
+ F: BeefyFisherman,
{
pub(crate) fn new(
known_peers: Arc>>,
- ) -> (GossipValidator, TracingUnboundedReceiver) {
+ fisherman: F,
+ ) -> (GossipValidator, TracingUnboundedReceiver) {
let (tx, rx) = tracing_unbounded("mpsc_beefy_gossip_validator", 10_000);
let val = GossipValidator {
votes_topic: votes_topic::(),
@@ -253,6 +257,7 @@ where
next_rebroadcast: Mutex::new(Instant::now() + REBROADCAST_AFTER),
known_peers,
report_sender: tx,
+ fisherman,
};
(val, rx)
}
@@ -287,9 +292,18 @@ where
let filter = self.gossip_filter.read();
match filter.consider_vote(round, set_id) {
- Consider::RejectPast => return Action::Discard(cost::OUTDATED_MESSAGE),
Consider::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE),
Consider::RejectOutOfScope => return Action::Discard(cost::OUT_OF_SCOPE_MESSAGE),
+ Consider::RejectPast => {
+ // We know `vote` is for some past (finalized) block. Have fisherman check
+ // for equivocations. Best-effort, ignore errors such as state pruned.
+ let _ = self.fisherman.check_vote(vote);
+ // TODO: maybe raise cost reputation when seeing votes that are intentional
+ // spam: votes that trigger fisherman reports, but don't go through either
+ // because signer is/was not authority or similar reasons.
+ // The idea is to more quickly disconnect neighbors which are attempting DoS.
+ return Action::Discard(cost::OUTDATED_MESSAGE)
+ },
Consider::Accept => {},
}
@@ -331,9 +345,18 @@ where
let guard = self.gossip_filter.read();
// Verify general usefulness of the justification.
match guard.consider_finality_proof(round, set_id) {
- Consider::RejectPast => return Action::Discard(cost::OUTDATED_MESSAGE),
Consider::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE),
Consider::RejectOutOfScope => return Action::Discard(cost::OUT_OF_SCOPE_MESSAGE),
+ Consider::RejectPast => {
+ // We know `proof` is for some past (finalized) block. Have fisherman check
+ // for equivocations. Best-effort, ignore errors such as state pruned.
+ let _ = self.fisherman.check_proof(proof);
+ // TODO: maybe raise cost reputation when seeing votes that are intentional
+ // spam: votes that trigger fisherman reports, but don't go through either because
+ // signer is/was not authority or similar reasons.
+ // The idea is to more quickly disconnect neighbors which are attempting DoS.
+ return Action::Discard(cost::OUTDATED_MESSAGE)
+ },
Consider::Accept => {},
}
// Verify justification signatures.
@@ -359,9 +382,10 @@ where
}
}
-impl Validator for GossipValidator
+impl Validator for GossipValidator
where
B: Block,
+ F: BeefyFisherman,
{
fn peer_disconnected(&self, _context: &mut dyn ValidatorContext, who: &PeerId) {
self.known_peers.lock().remove(who);
@@ -474,13 +498,14 @@ 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_consensus_beefy::{
crypto::Signature, known_payloads, Commitment, Keyring, MmrRootHash, Payload,
SignedCommitment, VoteMessage, KEY_TYPE,
};
use sp_keystore::{testing::MemoryKeystore, Keystore};
+ use std::marker::PhantomData;
#[test]
fn known_votes_insert_remove() {
@@ -576,8 +601,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;
@@ -704,7 +730,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();
@@ -781,7 +808,8 @@ pub(crate) mod tests {
fn messages_rebroadcast() {
let keys = vec![Keyring::Alice.public()];
let validator_set = ValidatorSet::::new(keys.clone(), 0).unwrap();
- let (gv, _) = GossipValidator::::new(Arc::new(Mutex::new(KnownPeers::new())));
+ let fisherman = DummyFisherman { _phantom: PhantomData:: };
+ let (gv, _) = GossipValidator::new(Arc::new(Mutex::new(KnownPeers::new())), fisherman);
gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set });
let sender = sc_network::PeerId::random();
let topic = Default::default();
diff --git a/client/consensus/beefy/src/communication/mod.rs b/client/consensus/beefy/src/communication/mod.rs
index 7f9535bfc23f1..c31447d08e565 100644
--- a/client/consensus/beefy/src/communication/mod.rs
+++ b/client/consensus/beefy/src/communication/mod.rs
@@ -21,6 +21,7 @@
pub mod notification;
pub mod request_response;
+pub(crate) mod fisherman;
pub(crate) mod gossip;
pub(crate) mod peers;
diff --git a/client/consensus/beefy/src/lib.rs b/client/consensus/beefy/src/lib.rs
index c55849ff7722c..bcacd6eeb0193 100644
--- a/client/consensus/beefy/src/lib.rs
+++ b/client/consensus/beefy/src/lib.rs
@@ -18,6 +18,7 @@
use crate::{
communication::{
+ fisherman::Fisherman,
notification::{
BeefyBestBlockSender, BeefyBestBlockStream, BeefyVersionedFinalityProofSender,
BeefyVersionedFinalityProofStream,
@@ -220,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,
- R: ProvideRuntimeApi,
+ R: ProvideRuntimeApi + Send + Sync + 'static,
R::Api: BeefyApi + MmrApi>,
N: GossipNetwork + NetworkRequest + Send + Sync + 'static,
S: GossipSyncing + SyncOracle + 'static,
@@ -250,10 +251,16 @@ pub async fn start_beefy_gadget(
} = network_params;
let known_peers = Arc::new(Mutex::new(KnownPeers::new()));
+ let fisherman = Fisherman {
+ backend: backend.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 mut gossip_engine = GossipEngine::new(
network.clone(),
diff --git a/client/consensus/beefy/src/tests.rs b/client/consensus/beefy/src/tests.rs
index 6bb6b1e548557..a936011e0e109 100644
--- a/client/consensus/beefy/src/tests.rs
+++ b/client/consensus/beefy/src/tests.rs
@@ -22,12 +22,14 @@ use crate::{
aux_schema::{load_persistent, tests::verify_persisted_version},
beefy_block_import_and_links,
communication::{
+ fisherman::BeefyFisherman,
gossip::{
proofs_topic, tests::sign_commitment, votes_topic, GossipFilterCfg, GossipMessage,
GossipValidator,
},
request_response::{on_demand_justifications_protocol_config, BeefyJustifsRequestHandler},
},
+ error::Error,
gossip_protocol_name,
justification::*,
load_or_init_voter_state, wait_for_runtime_pallet, BeefyRPCLinks, BeefyVoterLinks, KnownPeers,
@@ -47,7 +49,7 @@ use sc_network_test::{
};
use sc_utils::notification::NotificationReceiver;
use serde::{Deserialize, Serialize};
-use sp_api::{ApiRef, ProvideRuntimeApi};
+use sp_api::{ApiRef, BlockT, ProvideRuntimeApi};
use sp_consensus::BlockOrigin;
use sp_consensus_beefy::{
crypto::{AuthorityId, Signature},
@@ -243,6 +245,28 @@ 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,
@@ -363,8 +387,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(),
@@ -385,7 +410,7 @@ fn initialize_beefy(
min_block_delta: u32,
) -> impl Future