-
Notifications
You must be signed in to change notification settings - Fork 1.2k
BEEFY: add support for slashing validators signing forking commitments (without ancestry proofs) #1329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
BEEFY: add support for slashing validators signing forking commitments (without ancestry proofs) #1329
Changes from all commits
d8964df
a2678a0
17398d8
df31624
938cde1
fb08553
39799b2
4aa602a
e0e13d9
165d7fc
c7df83b
754ae80
6ed4e99
1e41551
de2ae90
273f34b
eb1e549
3369783
7abbddc
8acea22
103fc53
1509a26
a62cc7f
5445977
d35a97c
4e3e1cc
f27a876
f641759
5c4104c
a0d8b87
9126904
02da07e
a3b4d3f
d44e3c8
c27579c
5516837
60a71c7
ec99b1a
ea7dd35
3185776
59c1c2a
ab3eb02
844ed67
6ebebed
efe4b2a
d0dba3b
b438866
bd856ac
9e1e7b1
f5b1ec5
7504b96
036fa2f
53e8ae0
8a14649
4074a27
e5537dd
c815af6
35330bb
bb2e791
936406b
16c12eb
8b48fec
7be7ba9
0cd4a75
e1953eb
26fc473
d7f553f
d2bf211
11a16cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <https://www.gnu.org/licenses/>. | ||
|
|
||
| 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<B: Block>: Send + Sync { | ||
| /// Check `vote` for contained block against canonical payload. | ||
| fn check_vote( | ||
| &self, | ||
| vote: VoteMessage<NumberFor<B>, AuthorityId, Signature>, | ||
| ) -> Result<(), Error>; | ||
|
|
||
| /// Check `signed_commitment` for contained block against canonical payload. | ||
| fn check_signed_commitment( | ||
| &self, | ||
| signed_commitment: SignedCommitment<NumberFor<B>, Signature>, | ||
| ) -> Result<(), Error>; | ||
|
|
||
| /// Check `proof` for contained block against canonical payload. | ||
| fn check_proof(&self, proof: BeefyVersionedFinalityProof<B>) -> Result<(), Error>; | ||
| } | ||
|
|
||
| /// Helper wrapper used to check gossiped votes for (historical) equivocations, | ||
| /// and report any such protocol infringements. | ||
| pub(crate) struct Fisherman<B: Block, BE, R, P> { | ||
| pub backend: Arc<BE>, | ||
| pub runtime: Arc<R>, | ||
| pub key_store: Arc<BeefyKeystore>, | ||
| pub payload_provider: P, | ||
| pub _phantom: PhantomData<B>, | ||
| } | ||
|
|
||
| impl<B, BE, R, P> Fisherman<B, BE, R, P> | ||
| where | ||
| B: Block, | ||
| BE: Backend<B>, | ||
| P: PayloadProvider<B>, | ||
| R: ProvideRuntimeApi<B> + Send + Sync, | ||
| R::Api: BeefyApi<B, AuthorityId>, | ||
| { | ||
| fn canonical_header_and_payload( | ||
| &self, | ||
| number: NumberFor<B>, | ||
| ) -> 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<ValidatorSet<AuthorityId>, Error> { | ||
| self.runtime | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we maybe use
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #2689 (comment) - maybe this suggestion is no longer relevant
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion is still relevant, just that we need to use some non-blocking version of the function
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good catch - I've add a non-blocking version of the function in 816b726. Note this will simply error if a header's parent header cannot be found. |
||
| .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<NumberFor<B>, 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<B>, | ||
| 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 | ||
serban300 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }, | ||
| Err(e) => Some(Err(Error::RuntimeApi(e))), | ||
| } | ||
| }) | ||
| .collect::<Result<_, _>>()?; | ||
|
|
||
| // 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<B, BE, R, P> BeefyFisherman<B> for Fisherman<B, BE, R, P> | ||
| where | ||
| B: Block, | ||
| BE: Backend<B>, | ||
| P: PayloadProvider<B>, | ||
| R: ProvideRuntimeApi<B> + Send + Sync, | ||
| R::Api: BeefyApi<B, AuthorityId>, | ||
| { | ||
| /// Check `vote` for contained block against canonical payload. | ||
| fn check_vote( | ||
Lederstrumpf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| &self, | ||
| vote: VoteMessage<NumberFor<B>, 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<NumberFor<B>, 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<B>) -> Result<(), Error> { | ||
| match proof { | ||
| BeefyVersionedFinalityProof::<B>::V1(signed_commitment) => | ||
| self.check_signed_commitment(signed_commitment), | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.