mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-31 13:21:01 +00:00
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:
@@ -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::*;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user