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
@@ -244,8 +244,8 @@ mod tests {
use crate::keystore::BeefyKeystore;
use beefy_primitives::{
crypto::Signature, keyring::Keyring, known_payloads, Commitment, MmrRootHash, Payload,
VoteMessage, KEY_TYPE,
crypto::Signature, known_payloads, Commitment, Keyring, MmrRootHash, Payload, VoteMessage,
KEY_TYPE,
};
use super::*;
+17 -1
View File
@@ -22,14 +22,30 @@
use std::fmt::Debug;
#[derive(Debug, thiserror::Error, PartialEq)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Backend: {0}")]
Backend(String),
#[error("Keystore error: {0}")]
Keystore(String),
#[error("Runtime api error: {0}")]
RuntimeApi(sp_api::ApiError),
#[error("Signature error: {0}")]
Signature(String),
#[error("Session uninitialized")]
UninitSession,
}
#[cfg(test)]
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Error::Backend(s1), Error::Backend(s2)) => s1 == s2,
(Error::Keystore(s1), Error::Keystore(s2)) => s1 == s2,
(Error::RuntimeApi(_), Error::RuntimeApi(_)) => true,
(Error::Signature(s1), Error::Signature(s2)) => s1 == s2,
(Error::UninitSession, Error::UninitSession) => true,
_ => false,
}
}
}
+1 -2
View File
@@ -81,8 +81,7 @@ fn verify_with_validator_set<Block: BlockT>(
#[cfg(test)]
pub(crate) mod tests {
use beefy_primitives::{
keyring::Keyring, known_payloads, Commitment, Payload, SignedCommitment,
VersionedFinalityProof,
known_payloads, Commitment, Keyring, Payload, SignedCommitment, VersionedFinalityProof,
};
use substrate_test_runtime_client::runtime::Block;
+1 -1
View File
@@ -123,7 +123,7 @@ pub mod tests {
use sc_keystore::LocalKeystore;
use sp_core::{ecdsa, Pair};
use beefy_primitives::{crypto, keyring::Keyring};
use beefy_primitives::{crypto, Keyring};
use super::*;
use crate::error::Error;
+2 -1
View File
@@ -282,6 +282,7 @@ where
let worker_params = worker::WorkerParams {
backend,
payload_provider,
runtime,
network,
key_store: key_store.into(),
gossip_engine,
@@ -292,7 +293,7 @@ where
persisted_state,
};
let worker = worker::BeefyWorker::<_, _, _, _>::new(worker_params);
let worker = worker::BeefyWorker::<_, _, _, _, _>::new(worker_params);
futures::future::join(
worker.run(block_import_justif, finality_notifications),
+33 -18
View File
@@ -46,10 +46,16 @@ pub struct VoterMetrics {
pub beefy_no_authority_found_in_store: Counter<U64>,
/// Number of currently buffered votes
pub beefy_buffered_votes: Gauge<U64>,
/// Number of valid but stale votes received
pub beefy_stale_votes: Counter<U64>,
/// Number of votes dropped due to full buffers
pub beefy_buffered_votes_dropped: Counter<U64>,
/// Number of good votes successfully handled
pub beefy_good_votes_processed: Counter<U64>,
/// Number of equivocation votes received
pub beefy_equivocation_votes: Counter<U64>,
/// Number of invalid votes received
pub beefy_invalid_votes: Counter<U64>,
/// Number of valid but stale votes received
pub beefy_stale_votes: Counter<U64>,
/// Number of currently buffered justifications
pub beefy_buffered_justifications: Gauge<U64>,
/// Number of valid but stale justifications received
@@ -60,8 +66,6 @@ pub struct VoterMetrics {
pub beefy_buffered_justifications_dropped: Counter<U64>,
/// Trying to set Best Beefy block to old block
pub beefy_best_block_set_last_failure: Gauge<U64>,
/// Number of Successful handled votes
pub beefy_successful_handled_votes: Counter<U64>,
}
impl PrometheusRegister for VoterMetrics {
@@ -109,13 +113,6 @@ impl PrometheusRegister for VoterMetrics {
Gauge::new("substrate_beefy_buffered_votes", "Number of currently buffered votes")?,
registry,
)?,
beefy_stale_votes: register(
Counter::new(
"substrate_beefy_stale_votes",
"Number of valid but stale votes received",
)?,
registry,
)?,
beefy_buffered_votes_dropped: register(
Counter::new(
"substrate_beefy_buffered_votes_dropped",
@@ -123,6 +120,31 @@ impl PrometheusRegister for VoterMetrics {
)?,
registry,
)?,
beefy_good_votes_processed: register(
Counter::new(
"substrate_beefy_successful_handled_votes",
"Number of good votes successfully handled",
)?,
registry,
)?,
beefy_equivocation_votes: register(
Counter::new(
"substrate_beefy_stale_votes",
"Number of equivocation votes received",
)?,
registry,
)?,
beefy_invalid_votes: register(
Counter::new("substrate_beefy_stale_votes", "Number of invalid votes received")?,
registry,
)?,
beefy_stale_votes: register(
Counter::new(
"substrate_beefy_stale_votes",
"Number of valid but stale votes received",
)?,
registry,
)?,
beefy_buffered_justifications: register(
Gauge::new(
"substrate_beefy_buffered_justifications",
@@ -158,13 +180,6 @@ impl PrometheusRegister for VoterMetrics {
)?,
registry,
)?,
beefy_successful_handled_votes: register(
Counter::new(
"substrate_beefy_successful_handled_votes",
"Number of Successful handled votes",
)?,
registry,
)?,
})
}
}
+46 -6
View File
@@ -20,7 +20,7 @@ use crate::LOG_TARGET;
use beefy_primitives::{
crypto::{AuthorityId, Public, Signature},
Commitment, SignedCommitment, ValidatorSet, ValidatorSetId, VoteMessage,
Commitment, EquivocationProof, SignedCommitment, ValidatorSet, ValidatorSetId, VoteMessage,
};
use codec::{Decode, Encode};
use log::debug;
@@ -61,7 +61,7 @@ pub fn threshold(authorities: usize) -> usize {
pub enum VoteImportResult<B: Block> {
Ok,
RoundConcluded(SignedCommitment<NumberFor<B>, Signature>),
Equivocation, /* TODO: (EquivocationProof<NumberFor<B>, Public, Signature>) */
Equivocation(EquivocationProof<NumberFor<B>, Public, Signature>),
Invalid,
Stale,
}
@@ -149,8 +149,10 @@ where
target: LOG_TARGET,
"🥩 detected equivocated vote: 1st: {:?}, 2nd: {:?}", previous_vote, vote
);
// TODO: build `EquivocationProof` and return it here.
return VoteImportResult::Equivocation
return VoteImportResult::Equivocation(EquivocationProof {
first: previous_vote.clone(),
second: vote,
})
}
} else {
// this is the first vote sent by `id` for `num`, all good
@@ -197,8 +199,8 @@ mod tests {
use sc_network_test::Block;
use beefy_primitives::{
crypto::Public, keyring::Keyring, known_payloads::MMR_ROOT_ID, Commitment, Payload,
SignedCommitment, ValidatorSet, VoteMessage,
crypto::Public, known_payloads::MMR_ROOT_ID, Commitment, EquivocationProof, Keyring,
Payload, SignedCommitment, ValidatorSet, VoteMessage,
};
use super::{threshold, Block as BlockT, RoundTracker, Rounds};
@@ -452,4 +454,42 @@ mod tests {
rounds.conclude(3);
assert!(rounds.previous_votes.is_empty());
}
#[test]
fn should_provide_equivocation_proof() {
sp_tracing::try_init_simple();
let validators = ValidatorSet::<Public>::new(
vec![Keyring::Alice.public(), Keyring::Bob.public()],
Default::default(),
)
.unwrap();
let validator_set_id = validators.id();
let session_start = 1u64.into();
let mut rounds = Rounds::<Block>::new(session_start, validators);
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![1, 1, 1, 1]);
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![2, 2, 2, 2]);
let commitment1 = Commitment { block_number: 1, payload: payload1, validator_set_id };
let commitment2 = Commitment { block_number: 1, payload: payload2, validator_set_id };
let alice_vote1 = VoteMessage {
id: Keyring::Alice.public(),
commitment: commitment1,
signature: Keyring::Alice.sign(b"I am committed"),
};
let mut alice_vote2 = alice_vote1.clone();
alice_vote2.commitment = commitment2;
let expected_result = VoteImportResult::Equivocation(EquivocationProof {
first: alice_vote1.clone(),
second: alice_vote2.clone(),
});
// vote on one payload - ok
assert_eq!(rounds.add_vote(alice_vote1), VoteImportResult::Ok);
// vote on _another_ commitment/payload -> expected equivocation proof
assert_eq!(rounds.add_vote(alice_vote2), expected_result);
}
}
+96 -31
View File
@@ -31,10 +31,10 @@ use crate::{
};
use beefy_primitives::{
crypto::{AuthorityId, Signature},
keyring::Keyring as BeefyKeyring,
known_payloads,
mmr::MmrRootProvider,
BeefyApi, Commitment, ConsensusLog, MmrRootHash, Payload, SignedCommitment, ValidatorSet,
BeefyApi, Commitment, ConsensusLog, EquivocationProof, Keyring as BeefyKeyring, MmrRootHash,
OpaqueKeyOwnershipProof, Payload, SignedCommitment, ValidatorSet, ValidatorSetId,
VersionedFinalityProof, BEEFY_ENGINE_ID, KEY_TYPE as BeefyKeyType,
};
use futures::{future, stream::FuturesUnordered, Future, StreamExt};
@@ -55,7 +55,7 @@ use sp_api::{ApiRef, ProvideRuntimeApi};
use sp_consensus::BlockOrigin;
use sp_core::H256;
use sp_keystore::{testing::KeyStore as TestKeystore, SyncCryptoStore, SyncCryptoStorePtr};
use sp_mmr_primitives::{EncodableOpaqueLeaf, Error as MmrError, MmrApi, Proof};
use sp_mmr_primitives::{Error as MmrError, MmrApi};
use sp_runtime::{
codec::Encode,
generic::BlockId,
@@ -73,6 +73,7 @@ fn beefy_gossip_proto_name() -> ProtocolName {
const GOOD_MMR_ROOT: MmrRootHash = MmrRootHash::repeat_byte(0xbf);
const BAD_MMR_ROOT: MmrRootHash = MmrRootHash::repeat_byte(0x42);
const ALTERNATE_BAD_MMR_ROOT: MmrRootHash = MmrRootHash::repeat_byte(0x13);
type BeefyBlockImport = crate::BeefyBlockImport<
Block,
@@ -236,6 +237,8 @@ pub(crate) struct TestApi {
pub beefy_genesis: u64,
pub validator_set: BeefyValidatorSet,
pub mmr_root_hash: MmrRootHash,
pub reported_equivocations:
Option<Arc<Mutex<Vec<EquivocationProof<NumberFor<Block>, AuthorityId, Signature>>>>>,
}
impl TestApi {
@@ -244,7 +247,12 @@ impl TestApi {
validator_set: &BeefyValidatorSet,
mmr_root_hash: MmrRootHash,
) -> Self {
TestApi { beefy_genesis, validator_set: validator_set.clone(), mmr_root_hash }
TestApi {
beefy_genesis,
validator_set: validator_set.clone(),
mmr_root_hash,
reported_equivocations: None,
}
}
pub fn with_validator_set(validator_set: &BeefyValidatorSet) -> Self {
@@ -252,8 +260,13 @@ impl TestApi {
beefy_genesis: 1,
validator_set: validator_set.clone(),
mmr_root_hash: GOOD_MMR_ROOT,
reported_equivocations: None,
}
}
pub fn allow_equivocations(&mut self) {
self.reported_equivocations = Some(Arc::new(Mutex::new(vec![])));
}
}
// compiler gets confused and warns us about unused inner
@@ -277,31 +290,29 @@ sp_api::mock_impl_runtime_apis! {
fn validator_set() -> Option<BeefyValidatorSet> {
Some(self.inner.validator_set.clone())
}
fn submit_report_equivocation_unsigned_extrinsic(
proof: EquivocationProof<NumberFor<Block>, AuthorityId, Signature>,
_dummy: OpaqueKeyOwnershipProof,
) -> Option<()> {
if let Some(equivocations_buf) = self.inner.reported_equivocations.as_ref() {
equivocations_buf.lock().push(proof);
None
} else {
panic!("Equivocations not expected, but following proof was reported: {:?}", proof);
}
}
fn generate_key_ownership_proof(
_dummy1: ValidatorSetId,
_dummy2: AuthorityId,
) -> Option<OpaqueKeyOwnershipProof> { Some(OpaqueKeyOwnershipProof::new(vec![])) }
}
impl MmrApi<Block, MmrRootHash, NumberFor<Block>> for RuntimeApi {
fn mmr_root() -> Result<MmrRootHash, MmrError> {
Ok(self.inner.mmr_root_hash)
}
fn generate_proof(
_block_numbers: Vec<u64>,
_best_known_block_number: Option<u64>
) -> Result<(Vec<EncodableOpaqueLeaf>, Proof<MmrRootHash>), MmrError> {
unimplemented!()
}
fn verify_proof(_leaves: Vec<EncodableOpaqueLeaf>, _proof: Proof<MmrRootHash>) -> Result<(), MmrError> {
unimplemented!()
}
fn verify_proof_stateless(
_root: MmrRootHash,
_leaves: Vec<EncodableOpaqueLeaf>,
_proof: Proof<MmrRootHash>
) -> Result<(), MmrError> {
unimplemented!()
}
}
}
@@ -330,7 +341,7 @@ pub(crate) fn create_beefy_keystore(authority: BeefyKeyring) -> SyncCryptoStoreP
keystore
}
fn voter_init_setup(
async fn voter_init_setup(
net: &mut BeefyTestNet,
finality: &mut futures::stream::Fuse<FinalityNotifications<Block>>,
api: &TestApi,
@@ -345,9 +356,7 @@ fn voter_init_setup(
gossip_validator,
None,
);
let best_grandpa =
futures::executor::block_on(wait_for_runtime_pallet(api, &mut gossip_engine, finality))
.unwrap();
let best_grandpa = wait_for_runtime_pallet(api, &mut gossip_engine, finality).await.unwrap();
load_or_init_voter_state(&*backend, api, best_grandpa, 1)
}
@@ -980,7 +989,7 @@ async fn should_initialize_voter_at_genesis() {
let api = TestApi::with_validator_set(&validator_set);
// load persistent state - nothing in DB, should init at genesis
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).unwrap();
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).await.unwrap();
// Test initialization at session boundary.
// verify voter initialized with two sessions starting at blocks 1 and 10
@@ -1029,7 +1038,7 @@ async fn should_initialize_voter_at_custom_genesis() {
net.peer(0).client().as_client().finalize_block(hashes[8], None).unwrap();
// load persistent state - nothing in DB, should init at genesis
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).unwrap();
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).await.unwrap();
// Test initialization at session boundary.
// verify voter initialized with single session starting at block `custom_pallet_genesis` (7)
@@ -1090,7 +1099,7 @@ async fn should_initialize_voter_when_last_final_is_session_boundary() {
let api = TestApi::with_validator_set(&validator_set);
// load persistent state - nothing in DB, should init at session boundary
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).unwrap();
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).await.unwrap();
// verify voter initialized with single session starting at block 10
assert_eq!(persisted_state.voting_oracle().sessions().len(), 1);
@@ -1148,7 +1157,7 @@ async fn should_initialize_voter_at_latest_finalized() {
let api = TestApi::with_validator_set(&validator_set);
// load persistent state - nothing in DB, should init at last BEEFY finalized
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).unwrap();
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).await.unwrap();
// verify voter initialized with single session starting at block 12
assert_eq!(persisted_state.voting_oracle().sessions().len(), 1);
@@ -1205,3 +1214,59 @@ async fn beefy_finalizing_after_pallet_genesis() {
// GRANDPA finalize #21 -> BEEFY finalize #20 (mandatory) and #21
finalize_block_and_wait_for_beefy(&net, peers.clone(), &[hashes[21]], &[20, 21]).await;
}
#[tokio::test]
async fn beefy_reports_equivocations() {
sp_tracing::try_init_simple();
let peers = [BeefyKeyring::Alice, BeefyKeyring::Bob, BeefyKeyring::Charlie];
let validator_set = ValidatorSet::new(make_beefy_ids(&peers), 0).unwrap();
let session_len = 10;
let min_block_delta = 4;
let mut net = BeefyTestNet::new(3);
// 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 alice = (0, &peers[0], api_alice.clone());
tokio::spawn(initialize_beefy(&mut net, vec![alice], min_block_delta));
// Bob votes on bad MMR roots, equivocations are allowed/expected.
let mut api_bob = TestApi::new(1, &validator_set, BAD_MMR_ROOT);
api_bob.allow_equivocations();
let api_bob = Arc::new(api_bob);
let bob = (1, &peers[1], api_bob.clone());
tokio::spawn(initialize_beefy(&mut net, vec![bob], min_block_delta));
// We spawn another node voting with Bob key, on alternate bad MMR roots (equivocating).
// Equivocations are allowed/expected.
let mut api_bob_prime = TestApi::new(1, &validator_set, ALTERNATE_BAD_MMR_ROOT);
api_bob_prime.allow_equivocations();
let api_bob_prime = Arc::new(api_bob_prime);
let bob_prime = (2, &BeefyKeyring::Bob, api_bob_prime.clone());
tokio::spawn(initialize_beefy(&mut net, vec![bob_prime], min_block_delta));
// push 42 blocks including `AuthorityChange` digests every 10 blocks.
let hashes = net.generate_blocks_and_sync(42, session_len, &validator_set, false).await;
let net = Arc::new(Mutex::new(net));
// Minimum BEEFY block delta is 4.
let peers = peers.into_iter().enumerate();
// finalize block #1 -> BEEFY should not finalize anything (each node votes on different MMR).
finalize_block_and_wait_for_beefy(&net, peers, &[hashes[1]], &[]).await;
// Verify neither Bob or Bob_Prime report themselves as equivocating.
assert!(api_bob.reported_equivocations.as_ref().unwrap().lock().is_empty());
assert!(api_bob_prime.reported_equivocations.as_ref().unwrap().lock().is_empty());
// Verify Alice reports Bob/Bob_Prime equivocation.
let alice_reported_equivocations = api_alice.reported_equivocations.as_ref().unwrap().lock();
assert_eq!(alice_reported_equivocations.len(), 1);
let equivocation_proof = alice_reported_equivocations.get(0).unwrap();
assert_eq!(equivocation_proof.first.id, BeefyKeyring::Bob.public());
assert_eq!(equivocation_proof.first.commitment.block_number, 1);
}
+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());
}
}