Skip to content
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

Closed
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
d8964df
sc-consensus-beefy: add BEEFY fisherman to gossip network
acatangiu Jul 5, 2023
a2678a0
sp-consensus-beefy: add invalid fork vote proof and equivalent BeefyApi
acatangiu Jul 5, 2023
17398d8
pallet-beefy: add stubs for reporting invalid fork votes
acatangiu Jul 5, 2023
df31624
sc-consensus-beefy: fisherman reports invalid votes and justifications
acatangiu Jul 5, 2023
938cde1
don't check GRANDPA finality
Lederstrumpf Jul 20, 2023
fb08553
change primitive: vote -> commitment
Lederstrumpf Jul 20, 2023
39799b2
add check_signed_commitment/report_invalid_payload
Lederstrumpf Jul 27, 2023
4aa602a
update comments
Lederstrumpf Aug 2, 2023
e0e13d9
add dummy for report of invalid fork commitments
Lederstrumpf Aug 2, 2023
165d7fc
account for #14471
Lederstrumpf Aug 1, 2023
c7df83b
account for #14373
Lederstrumpf Aug 7, 2023
754ae80
EquivocationOffence.{offender->offenders}
Lederstrumpf Aug 7, 2023
6ed4e99
EquivocationProof->VoteEquivocationProof
Lederstrumpf Aug 7, 2023
1e41551
Invalid{""->Vote}EquivocationProof
Lederstrumpf Aug 7, 2023
de2ae90
check_{""->vote}_equivocation_proof
Lederstrumpf Aug 7, 2023
273f34b
InvalidForkCommitmentProof->ForkEquivocationProof
Lederstrumpf Aug 7, 2023
eb1e549
renames across submit_report_...
Lederstrumpf Aug 7, 2023
3369783
convert EquivocationEvidenceFor to enum (minimal)
Lederstrumpf Aug 7, 2023
7abbddc
handle ForkEquivocationProof enum variant
Lederstrumpf Aug 7, 2023
8acea22
reduce find_mmr_root_digest trait constraint
Lederstrumpf Aug 8, 2023
103fc53
fix fork equiv. call interfaces (vecs of sigs)
Lederstrumpf Aug 8, 2023
1509a26
check proof's payload against correct header's
Lederstrumpf Aug 8, 2023
a62cc7f
rm superfluous report_fork_equiv.correct_header
Lederstrumpf Aug 8, 2023
5445977
remove duplic. in check_{signed_commitment, proof}
Lederstrumpf Aug 8, 2023
d35a97c
update outdated comments
Lederstrumpf Aug 8, 2023
4e3e1cc
remove report_invalid_payload
Lederstrumpf Aug 8, 2023
f27a876
.+report.+{""->_vote}_equivocations
Lederstrumpf Aug 8, 2023
f641759
move correct_header hash check into primitives
Lederstrumpf Aug 9, 2023
5c4104c
create_beefy_worker: only push block if at genesis
Lederstrumpf Aug 9, 2023
a0d8b87
create_beefy_worker: opt. instantiate with TestApi
Lederstrumpf Aug 9, 2023
9126904
push to reported_fork_equivocations
Lederstrumpf Aug 9, 2023
02da07e
add generate_fork_equivocation_proof_vote to tests
Lederstrumpf Aug 9, 2023
a3b4d3f
test: Alice snitches on Bob's vote equivocation
Lederstrumpf Aug 9, 2023
d44e3c8
store reference to key_store in fisherman
Lederstrumpf Aug 10, 2023
c27579c
test: Alice doesn't snitch *own* vote equivocation
Lederstrumpf Aug 10, 2023
5516837
un-stub submit_unsigned_fork_equivocation_report
Lederstrumpf Aug 10, 2023
60a71c7
Alice reports Bob & Charlie's signed commitment
Aug 24, 2023
ec99b1a
cleanup
Lederstrumpf Aug 24, 2023
ea7dd35
remove superfluous None check
Lederstrumpf Aug 24, 2023
3185776
fixup! check proof's payload against correct header's
Lederstrumpf Aug 28, 2023
59c1c2a
fmt
Lederstrumpf Aug 28, 2023
ab3eb02
missing keystore
Lederstrumpf Aug 29, 2023
844ed67
graft https://github.com/paritytech/polkadot/pull/7634
Lederstrumpf Aug 29, 2023
6ebebed
impl equiv. runtime interfaces kusama/westend
Lederstrumpf Aug 29, 2023
efe4b2a
clone beefy pallet equivocation tests
Lederstrumpf Aug 31, 2023
d0dba3b
test renames
Lederstrumpf Aug 31, 2023
b438866
test fork equivocation (via vote) reports
Lederstrumpf Aug 31, 2023
bd856ac
handle fork equivocations in validate_unsigned & pre_dispatch
Lederstrumpf Aug 31, 2023
9e1e7b1
clone tests for fork equivocations via commitments
Lederstrumpf Aug 31, 2023
f5b1ec5
bump number of beefy authorities in tests
Lederstrumpf Sep 1, 2023
7504b96
test fork equivocation (via commitment) reports
Lederstrumpf Sep 1, 2023
036fa2f
cleanup & TODOs
Lederstrumpf Sep 3, 2023
53e8ae0
test that overlapping reports stack correctly
Lederstrumpf Sep 3, 2023
8a14649
note re. temporary equivocation proof structure
Lederstrumpf Sep 4, 2023
4074a27
Merge remote-tracking branch 'origin/master' into rhmb/beefy-slashing…
Lederstrumpf Sep 5, 2023
e5537dd
fmt
Lederstrumpf Sep 5, 2023
c815af6
cleanup & remove TODO
Lederstrumpf Sep 5, 2023
35330bb
Merge remote-tracking branch 'origin/master' into rhmb/beefy-slashing…
Lederstrumpf Sep 5, 2023
bb2e791
add reference to #1441
Lederstrumpf Sep 7, 2023
936406b
Merge remote-tracking branch 'origin/master' into rhmb/beefy-slashing…
Lederstrumpf Sep 7, 2023
16c12eb
Merge remote-tracking branch 'origin/master' into rhmb/beefy-slashing…
Lederstrumpf Oct 17, 2023
8b48fec
fixup! Merge remote-tracking branch 'origin/master' into rhmb/beefy-s…
Lederstrumpf Oct 17, 2023
7be7ba9
Merge remote-tracking branch 'origin/master' into rhmb/beefy-slashing…
Lederstrumpf Nov 28, 2023
0cd4a75
account for #2446
Lederstrumpf Nov 28, 2023
e1953eb
{correct->canonical}_header
Lederstrumpf Dec 14, 2023
26fc473
remove old logging
Lederstrumpf Dec 14, 2023
d7f553f
more {expected->canonical}_...
Lederstrumpf Dec 14, 2023
d2bf211
linearize failfast flow
Lederstrumpf Dec 14, 2023
11a16cc
debug->warn
Lederstrumpf Jan 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions polkadot/node/service/src/fake_runtime_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -251,6 +251,13 @@ sp_api::impl_runtime_apis! {
unimplemented!()
}

fn submit_report_fork_equivocation_unsigned_extrinsic(
_: beefy_primitives::ForkEquivocationProof<BlockNumber, BeefyId, BeefySignature, <Block as BlockT>::Header>,
_: Vec<beefy_primitives::OpaqueKeyOwnershipProof>,
) -> Option<()> {
unimplemented!()
}

fn generate_key_ownership_proof(
_: beefy_primitives::ValidatorSetId,
_: BeefyId,
Expand Down
20 changes: 16 additions & 4 deletions polkadot/runtime/rococo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<BlockNumber, BeefyId, BeefySignature, Header>,
key_owner_proofs: Vec<beefy_primitives::OpaqueKeyOwnershipProof>,
) -> Option<()> {
let key_owner_proofs = key_owner_proofs.iter().cloned().map(|p| p.decode()).collect::<Option<Vec<_>>>()?;

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,
Expand Down
11 changes: 9 additions & 2 deletions polkadot/runtime/test-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -977,6 +977,13 @@ sp_api::impl_runtime_apis! {
None
}

fn submit_report_fork_equivocation_unsigned_extrinsic(
_fork_equivocation_proof: beefy_primitives::ForkEquivocationProof<BlockNumber, BeefyId, BeefySignature, Header>,
_key_owner_proofs: Vec<beefy_primitives::OpaqueKeyOwnershipProof>,
) -> Option<()> {
None
}

fn generate_key_ownership_proof(
_set_id: beefy_primitives::ValidatorSetId,
_authority_id: BeefyId,
Expand Down
20 changes: 16 additions & 4 deletions polkadot/runtime/westend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<BlockNumber, BeefyId, BeefySignature, Header>,
key_owner_proofs: Vec<beefy_primitives::OpaqueKeyOwnershipProof>,
) -> Option<()> {
let key_owner_proofs = key_owner_proofs.iter().cloned().map(|p| p.decode()).collect::<Option<Vec<_>>>()?;

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,
Expand Down
244 changes: 244 additions & 0 deletions substrate/client/consensus/beefy/src/communication/fisherman.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
// This file is part of Substrate.

// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <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 {
Lederstrumpf marked this conversation as resolved.
Show resolved Hide resolved
/// Check `vote` for contained block against expected payload.
fn check_vote(
&self,
vote: VoteMessage<NumberFor<B>, AuthorityId, Signature>,
) -> Result<(), Error>;

/// Check `signed_commitment` for contained block against expected payload.
fn check_signed_commitment(
&self,
signed_commitment: SignedCommitment<NumberFor<B>, Signature>,
) -> Result<(), Error>;

/// Check `proof` for contained block against expected 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 expected_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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we maybe use sc_consensus_beefy::expect_validator_set, which falls back to headers traversal if state is already discarded for the header? I expect that when this method is called (if RejectPast is returned from consider_*), then there's a big chance that we may hit StateDiscarded here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #2689 (comment) - maybe this suggestion is no longer relevant

Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.correct_header)?;
let set_id = validator_set.id();

let expected_header_hash = self
.backend
.blockchain()
.expect_block_hash_from_id(&BlockId::Number(proof.commitment.block_number))
.map_err(|e| Error::Backend(e.to_string()))?;

if proof.commitment.validator_set_id != set_id ||
!check_fork_equivocation_proof::<
NumberFor<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) {
debug!(target: LOG_TARGET, "🥩 Skip equivocation report for own equivocation");
// TODO: maybe error here instead?
Lederstrumpf marked this conversation as resolved.
Show resolved Hide resolved
return Ok(())
}
}

let hash = proof.correct_header.hash();
let runtime_api = self.runtime.runtime_api();

// generate key ownership proof at that block
let key_owner_proofs = offender_ids
.iter()
.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 expected 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 (correct_header, expected_payload) = self.expected_header_and_payload(number)?;
if vote.commitment.payload != expected_payload {
let proof = ForkEquivocationProof {
commitment: vote.commitment,
signatories: vec![(vote.id, vote.signature)],
correct_header: correct_header.clone(),
};
self.report_fork_equivocation(proof)?;
}
Ok(())
}

/// Check `signed_commitment` for contained block against expected payload.
fn check_signed_commitment(
&self,
signed_commitment: SignedCommitment<NumberFor<B>, Signature>,
) -> Result<(), Error> {
let SignedCommitment { commitment, signatures } = signed_commitment;
let number = commitment.block_number;
let (correct_header, expected_payload) = self.expected_header_and_payload(number)?;
if commitment.payload != expected_payload {
let validator_set = self.active_validator_set_at(&correct_header)?;
if signatures.len() != validator_set.validators().len() {
// invalid proof
return Ok(())
}
// report every signer of the bad justification
let signatories = validator_set
.validators()
.iter()
.cloned()
.zip(signatures.into_iter())
.filter_map(|(id, signature)| signature.map(|sig| (id, sig)))
.collect();

let proof = ForkEquivocationProof {
commitment,
signatories,
correct_header: correct_header.clone(),
};
self.report_fork_equivocation(proof)?;
}
Ok(())
}

/// Check `proof` for contained block against expected payload.
fn check_proof(&self, proof: BeefyVersionedFinalityProof<B>) -> Result<(), Error> {
match proof {
BeefyVersionedFinalityProof::<B>::V1(signed_commitment) =>
self.check_signed_commitment(signed_commitment),
}
}
}
Loading