BEEFY: implement equivocations detection, reporting and slashing (#13121)

* client/beefy: simplify self_vote logic

* client/beefy: migrate to new state version

* client/beefy: detect equivocated votes

* fix typos

* sp-beefy: add equivocation primitives

* client/beefy: refactor vote processing

* fix version migration for new rounds struct

* client/beefy: track equivocations and create proofs

* client/beefy: adjust tests for new voting logic

* sp-beefy: fix commitment ordering and equality

* client/beefy: simplify handle_vote() a bit

* client/beefy: add simple equivocation test

* client/beefy: submit equivocation proof - WIP

* frame/beefy: add equivocation report runtime api - part 1

* frame/beefy: report equivocation logic - part 2

* frame/beefy: add pluggable Equivocation handler - part 3

* frame/beefy: impl ValidateUnsigned for equivocations reporting

* client/beefy: submit report equivocation unsigned extrinsic

* primitives/beefy: fix tests

* frame/beefy: add default weights

* frame/beefy: fix tests

* client/beefy: fix tests

* frame/beefy-mmr: fix tests

* frame/beefy: cross-check session index with equivocation report

* sp-beefy: make test Keyring useable in pallet

* frame/beefy: add basic equivocation test

* frame/beefy: test verify equivocation results in slashing

* frame/beefy: test report_equivocation_old_set

* frame/beefy: add more equivocation tests

* sp-beefy: fix docs

* beefy: simplify equivocations and fix tests

* client/beefy: address review comments

* frame/beefy: add ValidateUnsigned to test/mock runtime

* client/beefy: fixes after merge master

* fix missed merge damage

* client/beefy: add test for reporting equivocations

Also validated there's no unexpected equivocations reported in the
other tests.

Signed-off-by: acatangiu <adrian@parity.io>

* sp-beefy: move test utils to their own file

* client/beefy: add negative test for equivocation reports

* sp-beefy: move back MmrRootProvider - used in polkadot-service

* impl review suggestions

* client/beefy: add equivocation metrics

---------

Signed-off-by: acatangiu <adrian@parity.io>
Co-authored-by: parity-processbot <>
This commit is contained in:
Adrian Catangiu
2023-02-17 11:45:00 +02:00
committed by GitHub
parent 36480b158d
commit c21f292a02
22 changed files with 2141 additions and 214 deletions
+142 -15
View File
@@ -23,16 +23,17 @@ use crate::{
},
error::Error,
justification::BeefyVersionedFinalityProof,
keystore::BeefyKeystore,
keystore::{BeefyKeystore, BeefySignatureHasher},
metric_get, metric_inc, metric_set,
metrics::VoterMetrics,
round::{Rounds, VoteImportResult},
BeefyVoterLinks, LOG_TARGET,
};
use beefy_primitives::{
check_equivocation_proof,
crypto::{AuthorityId, Signature},
Commitment, ConsensusLog, PayloadProvider, ValidatorSet, VersionedFinalityProof, VoteMessage,
BEEFY_ENGINE_ID,
BeefyApi, Commitment, ConsensusLog, EquivocationProof, PayloadProvider, ValidatorSet,
VersionedFinalityProof, VoteMessage, BEEFY_ENGINE_ID,
};
use codec::{Codec, Decode, Encode};
use futures::{stream::Fuse, FutureExt, StreamExt};
@@ -41,7 +42,7 @@ use sc_client_api::{Backend, FinalityNotification, FinalityNotifications, Header
use sc_network_common::service::{NetworkEventStream, NetworkRequest};
use sc_network_gossip::GossipEngine;
use sc_utils::notification::NotificationReceiver;
use sp_api::BlockId;
use sp_api::{BlockId, ProvideRuntimeApi};
use sp_arithmetic::traits::{AtLeast32Bit, Saturating};
use sp_consensus::SyncOracle;
use sp_runtime::{
@@ -243,9 +244,10 @@ impl<B: Block> VoterOracle<B> {
}
}
pub(crate) struct WorkerParams<B: Block, BE, P, N> {
pub(crate) struct WorkerParams<B: Block, BE, P, R, N> {
pub backend: Arc<BE>,
pub payload_provider: P,
pub runtime: Arc<R>,
pub network: N,
pub key_store: BeefyKeystore,
pub gossip_engine: GossipEngine<B>,
@@ -294,10 +296,11 @@ impl<B: Block> PersistedState<B> {
}
/// A BEEFY worker plays the BEEFY protocol
pub(crate) struct BeefyWorker<B: Block, BE, P, N> {
pub(crate) struct BeefyWorker<B: Block, BE, P, RuntimeApi, N> {
// utilities
backend: Arc<BE>,
payload_provider: P,
runtime: Arc<RuntimeApi>,
network: N,
key_store: BeefyKeystore,
@@ -327,11 +330,13 @@ pub(crate) struct BeefyWorker<B: Block, BE, P, N> {
persisted_state: PersistedState<B>,
}
impl<B, BE, P, N> BeefyWorker<B, BE, P, N>
impl<B, BE, P, R, N> BeefyWorker<B, BE, P, R, N>
where
B: Block + Codec,
BE: Backend<B>,
P: PayloadProvider<B>,
R: ProvideRuntimeApi<B>,
R::Api: BeefyApi<B>,
N: NetworkEventStream + NetworkRequest + SyncOracle + Send + Sync + Clone + 'static,
{
/// Return a new BEEFY worker instance.
@@ -340,10 +345,11 @@ where
/// BEEFY pallet has been deployed on-chain.
///
/// The BEEFY pallet is needed in order to keep track of the BEEFY authority set.
pub(crate) fn new(worker_params: WorkerParams<B, BE, P, N>) -> Self {
pub(crate) fn new(worker_params: WorkerParams<B, BE, P, R, N>) -> Self {
let WorkerParams {
backend,
payload_provider,
runtime,
key_store,
network,
gossip_engine,
@@ -357,6 +363,7 @@ where
BeefyWorker {
backend,
payload_provider,
runtime,
network,
key_store,
gossip_engine,
@@ -571,6 +578,7 @@ where
// We created the `finality_proof` and know to be valid.
// New state is persisted after finalization.
self.finalize(finality_proof)?;
metric_inc!(self, beefy_good_votes_processed);
},
VoteImportResult::Ok => {
// Persist state after handling mandatory block vote.
@@ -583,14 +591,15 @@ where
crate::aux_schema::write_voter_state(&*self.backend, &self.persisted_state)
.map_err(|e| Error::Backend(e.to_string()))?;
}
metric_inc!(self, beefy_good_votes_processed);
},
VoteImportResult::Equivocation => {
// TODO: report returned `EquivocationProof` to chain through `pallet-beefy`.
()
VoteImportResult::Equivocation(proof) => {
metric_inc!(self, beefy_equivocation_votes);
self.report_equivocation(proof)?;
},
VoteImportResult::Invalid | VoteImportResult::Stale => (),
VoteImportResult::Invalid => metric_inc!(self, beefy_invalid_votes),
VoteImportResult::Stale => metric_inc!(self, beefy_stale_votes),
};
metric_inc!(self, beefy_successful_handled_votes);
Ok(())
}
@@ -928,6 +937,60 @@ where
}
}
}
/// 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(
&self,
proof: EquivocationProof<NumberFor<B>, AuthorityId, Signature>,
) -> Result<(), Error> {
let rounds =
self.persisted_state.voting_oracle.active_rounds().ok_or(Error::UninitSession)?;
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 {
debug!(target: LOG_TARGET, "🥩 Skip equivocation report for own equivocation");
return Ok(())
}
}
let number = *proof.round_number();
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(&BlockId::Number(number), 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(
&BlockId::Hash(best_block_hash),
proof,
key_owner_proof,
)
.map_err(Error::RuntimeApi)?;
Ok(())
}
}
/// Scan the `header` digest log for a BEEFY validator set change. Return either the new
@@ -993,7 +1056,8 @@ pub(crate) mod tests {
BeefyRPCLinks, KnownPeers,
};
use beefy_primitives::{
keyring::Keyring, known_payloads, mmr::MmrRootProvider, Payload, SignedCommitment,
generate_equivocation_proof, known_payloads, known_payloads::MMR_ROOT_ID,
mmr::MmrRootProvider, Keyring, Payload, SignedCommitment,
};
use futures::{future::poll_fn, task::Poll};
use parking_lot::Mutex;
@@ -1041,6 +1105,7 @@ pub(crate) mod tests {
Block,
Backend,
MmrRootProvider<Block, TestApi>,
TestApi,
Arc<NetworkService<Block, H256>>,
> {
let keystore = create_beefy_keystore(*key);
@@ -1091,6 +1156,7 @@ pub(crate) mod tests {
let worker_params = crate::worker::WorkerParams {
backend,
payload_provider,
runtime: api,
key_store: Some(keystore).into(),
links,
gossip_engine,
@@ -1100,7 +1166,7 @@ pub(crate) mod tests {
on_demand_justifications,
persisted_state,
};
BeefyWorker::<_, _, _, _>::new(worker_params)
BeefyWorker::<_, _, _, _, _>::new(worker_params)
}
#[test]
@@ -1546,4 +1612,65 @@ pub(crate) mod tests {
assert_eq!(votes.next().unwrap().first().unwrap().commitment.block_number, 21);
assert_eq!(votes.next().unwrap().first().unwrap().commitment.block_number, 22);
}
#[tokio::test]
async fn should_not_report_bad_old_or_self_equivocations() {
let block_num = 1;
let set_id = 1;
let keys = [Keyring::Alice];
let validator_set = ValidatorSet::new(make_beefy_ids(&keys), set_id).unwrap();
// Alice votes on good MMR roots, equivocations are allowed/expected
let mut api_alice = TestApi::with_validator_set(&validator_set);
api_alice.allow_equivocations();
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 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, with Bob as perpetrator
let good_proof = generate_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(()));
// verify Alice reports Bob equivocation to runtime
let reported = api_alice.reported_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();
// 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(()));
// verify nothing reported to runtime
assert!(api_alice.reported_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(()));
// verify nothing reported to runtime
assert!(api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty());
// now let's try reporting a self-equivocation
let self_proof = generate_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(()));
// verify nothing reported to runtime
assert!(api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty());
}
}