Skip to content

Commit

Permalink
Define basic fisherman structure
Browse files Browse the repository at this point in the history
Define basic fisherman structure containing 2 methods:

- report_equivocation() was just moved from BeefyWorker.

- prove_offenders() is an adaptation of the previous logic for
generating key ownership proofs that was part of
BeefyWorker::report_equivocation() in oreder to make it more generic.
It is used by report_equivocation() now and it will be also needed for
reporting signed commitment equivocations in the future.
  • Loading branch information
serban300 committed Apr 29, 2024
1 parent 1d6545e commit a2c97d9
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 66 deletions.
162 changes: 162 additions & 0 deletions substrate/client/consensus/beefy/src/fisherman.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// 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, keystore::BeefyKeystore, round::Rounds, LOG_TARGET};
use log::{debug, error, warn};
use sc_client_api::Backend;
use sp_api::ProvideRuntimeApi;
use sp_blockchain::HeaderBackend;
use sp_consensus_beefy::{
check_equivocation_proof,
ecdsa_crypto::{AuthorityId, Signature},
BeefyApi, BeefySignatureHasher, EquivocationProof, OpaqueKeyOwnershipProof, ValidatorSetId,
};
use sp_runtime::{
generic::BlockId,
traits::{Block, NumberFor},
};
use std::{marker::PhantomData, sync::Arc};

/// Helper struct containing the id and the key ownership proof for a validator.
pub struct ProvedValidator<'a> {
pub id: &'a AuthorityId,
pub key_owner_proof: OpaqueKeyOwnershipProof,
}

/// Helper used to check and report equivocations.
pub struct Fisherman<B, BE, RuntimeApi> {
backend: Arc<BE>,
runtime: Arc<RuntimeApi>,
key_store: Arc<BeefyKeystore<AuthorityId>>,

_phantom: PhantomData<B>,
}

impl<B: Block, BE: Backend<B>, RuntimeApi: ProvideRuntimeApi<B>> Fisherman<B, BE, RuntimeApi>
where
RuntimeApi::Api: BeefyApi<B, AuthorityId>,
{
pub fn new(
backend: Arc<BE>,
runtime: Arc<RuntimeApi>,
keystore: Arc<BeefyKeystore<AuthorityId>>,
) -> Self {
Self { backend, runtime, key_store: keystore, _phantom: Default::default() }
}

fn prove_offenders<'a>(
&self,
at: BlockId<B>,
offender_ids: impl Iterator<Item = &'a AuthorityId>,
validator_set_id: ValidatorSetId,
) -> Result<Vec<ProvedValidator<'a>>, Error> {
let hash = match at {
BlockId::Hash(hash) => hash,
BlockId::Number(number) => self
.backend
.blockchain()
.expect_block_hash_from_id(&BlockId::Number(number))
.map_err(|err| {
Error::Backend(format!(
"Couldn't get hash for block #{:?} (error: {:?}). \
Skipping report for equivocation",
at, err
))
})?,
};

let runtime_api = self.runtime.runtime_api();
let mut proved_offenders = vec![];
for offender_id in offender_ids {
match runtime_api.generate_key_ownership_proof(
hash,
validator_set_id,
offender_id.clone(),
) {
Ok(Some(key_owner_proof)) => {
proved_offenders.push(ProvedValidator { id: offender_id, key_owner_proof });
},
Ok(None) => {
debug!(
target: LOG_TARGET,
"🥩 Equivocation offender {} not part of the authority set {}.",
offender_id, validator_set_id
);
},
Err(e) => {
error!(
target: LOG_TARGET,
"🥩 Error generating key ownership proof for equivocation offender {} \
in authority set {}: {}",
offender_id, validator_set_id, e
);
},
};
}

Ok(proved_offenders)
}

/// Report the given equivocation to the BEEFY runtime module. This method
/// generates a session membership proof of the offender and then submits an
/// 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 fn report_equivocation(
&self,
proof: EquivocationProof<NumberFor<B>, AuthorityId, Signature>,
active_rounds: &Rounds<B>,
) -> Result<(), Error> {
let (validators, validator_set_id) =
(active_rounds.validators(), active_rounds.validator_set_id());
let offender_id = proof.offender_id();

if !check_equivocation_proof::<_, _, BeefySignatureHasher>(&proof) {
debug!(target: LOG_TARGET, "🥩 Skipping report for bad equivocation {:?}", proof);
return Ok(())
}

if let Some(local_id) = self.key_store.authority_id(validators) {
if offender_id == &local_id {
warn!(target: LOG_TARGET, "🥩 Skipping report for own equivocation");
return Ok(())
}
}

let key_owner_proofs = self.prove_offenders(
BlockId::Number(*proof.round_number()),
vec![offender_id].into_iter(),
validator_set_id,
)?;

// submit equivocation report at **best** block
let best_block_hash = self.backend.blockchain().info().best_hash;
for ProvedValidator { key_owner_proof, .. } in key_owner_proofs {
self.runtime
.runtime_api()
.submit_report_equivocation_unsigned_extrinsic(
best_block_hash,
proof.clone(),
key_owner_proof,
)
.map_err(Error::RuntimeApi)?;
}

Ok(())
}
}
14 changes: 9 additions & 5 deletions substrate/client/consensus/beefy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ pub mod justification;

use crate::{
communication::gossip::GossipValidator,
fisherman::Fisherman,
justification::BeefyVersionedFinalityProof,
keystore::BeefyKeystore,
metrics::VoterMetrics,
Expand All @@ -79,6 +80,7 @@ pub use communication::beefy_protocol_name::{
};
use sp_runtime::generic::OpaqueDigestItemId;

mod fisherman;
#[cfg(test)]
mod tests;

Expand Down Expand Up @@ -304,14 +306,16 @@ where
pending_justifications: BTreeMap<NumberFor<B>, BeefyVersionedFinalityProof<B>>,
is_authority: bool,
) -> BeefyWorker<B, BE, P, R, S, N> {
let key_store = Arc::new(self.key_store);
BeefyWorker {
backend: self.backend,
runtime: self.runtime,
key_store: self.key_store,
metrics: self.metrics,
persisted_state: self.persisted_state,
backend: self.backend.clone(),
runtime: self.runtime.clone(),
key_store: key_store.clone(),
payload_provider,
sync,
fisherman: Arc::new(Fisherman::new(self.backend, self.runtime, key_store)),
metrics: self.metrics,
persisted_state: self.persisted_state,
comms,
links,
pending_justifications,
Expand Down
79 changes: 18 additions & 61 deletions substrate/client/consensus/beefy/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use crate::{
},
error::Error,
find_authorities_change,
fisherman::Fisherman,
justification::BeefyVersionedFinalityProof,
keystore::BeefyKeystore,
metric_inc, metric_set,
Expand All @@ -39,10 +40,9 @@ use sp_api::ProvideRuntimeApi;
use sp_arithmetic::traits::{AtLeast32Bit, Saturating};
use sp_consensus::SyncOracle;
use sp_consensus_beefy::{
check_equivocation_proof,
ecdsa_crypto::{AuthorityId, Signature},
BeefyApi, BeefySignatureHasher, Commitment, EquivocationProof, PayloadProvider, ValidatorSet,
VersionedFinalityProof, VoteMessage, BEEFY_ENGINE_ID,
BeefyApi, Commitment, EquivocationProof, PayloadProvider, ValidatorSet, VersionedFinalityProof,
VoteMessage, BEEFY_ENGINE_ID,
};
use sp_runtime::{
generic::BlockId,
Expand Down Expand Up @@ -377,9 +377,10 @@ pub(crate) struct BeefyWorker<B: Block, BE, P, RuntimeApi, S, N> {
// utilities
pub backend: Arc<BE>,
pub runtime: Arc<RuntimeApi>,
pub key_store: BeefyKeystore<AuthorityId>,
pub key_store: Arc<BeefyKeystore<AuthorityId>>,
pub payload_provider: P,
pub sync: Arc<S>,
pub fisherman: Arc<Fisherman<B, BE, RuntimeApi>>,

// communication (created once, but returned and reused if worker is restarted/reinitialized)
pub comms: BeefyComms<B, N>,
Expand Down Expand Up @@ -941,64 +942,13 @@ where
(error, self.comms)
}

/// Report the given equivocation to the BEEFY runtime module. This method
/// generates a session membership proof of the offender and then submits an
/// 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(
/// Report the given equivocation to the BEEFY runtime module.
fn report_equivocation(
&self,
proof: EquivocationProof<NumberFor<B>, 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) {
debug!(target: LOG_TARGET, "🥩 Skip report for bad equivocation {:?}", proof);
return Ok(())
} else if let Some(local_id) = self.key_store.authority_id(validators) {
if offender_id == local_id {
warn!(target: LOG_TARGET, "🥩 Skip equivocation report for own equivocation");
return Ok(())
}
}

let number = *proof.round_number();
let hash = self
.backend
.blockchain()
.expect_block_hash_from_id(&BlockId::Number(number))
.map_err(|err| {
let err_msg = format!(
"Couldn't get hash for block #{:?} (error: {:?}), skipping report for equivocation",
number, err
);
Error::Backend(err_msg)
})?;
let runtime_api = self.runtime.runtime_api();
// generate key ownership proof at that block
let key_owner_proof = match runtime_api
.generate_key_ownership_proof(hash, validator_set_id, offender_id)
.map_err(Error::RuntimeApi)?
{
Some(proof) => proof,
None => {
debug!(
target: LOG_TARGET,
"🥩 Equivocation offender not part of the authority set."
);
return Ok(())
},
};

// 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)
.map_err(Error::RuntimeApi)?;

Ok(())
self.fisherman.report_equivocation(proof, rounds)
}
}

Expand Down Expand Up @@ -1165,13 +1115,15 @@ pub(crate) mod tests {
.unwrap();
let payload_provider = MmrRootProvider::new(api.clone());
let comms = BeefyComms { gossip_engine, gossip_validator, on_demand_justifications };
let key_store: Arc<BeefyKeystore<AuthorityId>> = Arc::new(Some(keystore).into());
BeefyWorker {
backend,
runtime: api,
key_store: Some(keystore).into(),
backend: backend.clone(),
runtime: api.clone(),
key_store: key_store.clone(),
metrics,
payload_provider,
sync: Arc::new(sync),
fisherman: Arc::new(Fisherman::new(backend, api, key_store)),
links,
comms,
pending_justifications: BTreeMap::new(),
Expand Down Expand Up @@ -1590,6 +1542,11 @@ pub(crate) mod tests {
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();
worker.fisherman = Arc::new(Fisherman::new(
worker.backend.clone(),
worker.runtime.clone(),
worker.key_store.clone(),
));

// let there be a block with num = 1:
let _ = net.peer(0).push_blocks(1, false);
Expand Down

0 comments on commit a2c97d9

Please sign in to comment.