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
+8
View File
@@ -5500,9 +5500,16 @@ dependencies = [
name = "pallet-beefy"
version = "4.0.0-dev"
dependencies = [
"frame-election-provider-support",
"frame-support",
"frame-system",
"pallet-authorship",
"pallet-balances",
"pallet-offences",
"pallet-session",
"pallet-staking",
"pallet-staking-reward-curve",
"pallet-timestamp",
"parity-scale-codec",
"scale-info",
"serde",
@@ -5510,6 +5517,7 @@ dependencies = [
"sp-core",
"sp-io",
"sp-runtime",
"sp-session",
"sp-staking",
"sp-std",
]
@@ -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());
}
}
+1 -1
View File
@@ -1086,7 +1086,7 @@ where
.map_err(|e| Error::Client(e.into()))?;
// generate a key ownership proof. we start by trying to generate the
// key owernship proof at the parent of the equivocating header, this
// key ownership proof at the parent of the equivocating header, this
// will make sure that proof generation is successful since it happens
// during the on-going session (i.e. session keys are available in the
// state to be able to generate the proof). this might fail if the
+2 -2
View File
@@ -23,8 +23,8 @@
//! While both BEEFY and Merkle Mountain Range (MMR) can be used separately,
//! these tools were designed to work together in unison.
//!
//! The pallet provides a standardized MMR Leaf format that is can be used
//! to bridge BEEFY+MMR-based networks (both standalone and polkadot-like).
//! The pallet provides a standardized MMR Leaf format that can be used
//! to bridge BEEFY+MMR-based networks (both standalone and Polkadot-like).
//!
//! The MMR leaf contains:
//! 1. Block number and parent block hash.
+11 -2
View File
@@ -22,10 +22,10 @@ use codec::Encode;
use frame_support::{
construct_runtime, parameter_types,
sp_io::TestExternalities,
traits::{ConstU16, ConstU32, ConstU64, GenesisBuild},
traits::{ConstU16, ConstU32, ConstU64, GenesisBuild, KeyOwnerProofSystem},
BasicExternalities,
};
use sp_core::{Hasher, H256};
use sp_core::{crypto::KeyTypeId, Hasher, H256};
use sp_runtime::{
app_crypto::ecdsa::Public,
impl_opaque_keys,
@@ -124,8 +124,17 @@ impl pallet_mmr::Config for Test {
impl pallet_beefy::Config for Test {
type BeefyId = BeefyId;
type KeyOwnerProofSystem = ();
type KeyOwnerProof =
<Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(KeyTypeId, BeefyId)>>::Proof;
type KeyOwnerIdentification = <Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(
KeyTypeId,
BeefyId,
)>>::IdentificationTuple;
type HandleEquivocation = ();
type MaxAuthorities = ConstU32<100>;
type OnNewValidatorSet = BeefyMmr;
type WeightInfo = ();
}
parameter_types! {
+12
View File
@@ -15,11 +15,20 @@ serde = { version = "1.0.136", optional = true }
beefy-primitives = { version = "4.0.0-dev", default-features = false, path = "../../primitives/beefy", package = "sp-beefy" }
frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" }
frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" }
pallet-authorship = { version = "4.0.0-dev", default-features = false, path = "../authorship" }
pallet-session = { version = "4.0.0-dev", default-features = false, path = "../session" }
sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" }
sp-session = { version = "4.0.0-dev", default-features = false, path = "../../primitives/session" }
sp-staking = { version = "4.0.0-dev", default-features = false, path = "../../primitives/staking" }
sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" }
[dev-dependencies]
frame-election-provider-support = { version = "4.0.0-dev", path = "../election-provider-support" }
pallet-balances = { version = "4.0.0-dev", path = "../balances" }
pallet-offences = { version = "4.0.0-dev", path = "../offences" }
pallet-staking = { version = "4.0.0-dev", path = "../staking" }
pallet-staking-reward-curve = { version = "4.0.0-dev", path = "../staking/reward-curve" }
pallet-timestamp = { version = "4.0.0-dev", path = "../timestamp" }
sp-core = { version = "7.0.0", path = "../../primitives/core" }
sp-io = { version = "7.0.0", path = "../../primitives/io" }
sp-staking = { version = "4.0.0-dev", path = "../../primitives/staking" }
@@ -31,10 +40,13 @@ std = [
"codec/std",
"frame-support/std",
"frame-system/std",
"pallet-authorship/std",
"pallet-session/std",
"scale-info/std",
"serde",
"sp-runtime/std",
"sp-session/std",
"sp-staking/std",
"sp-std/std",
]
try-runtime = ["frame-support/try-runtime"]
@@ -0,0 +1,54 @@
// This file is part of Substrate.
// Copyright (C) 2020-2023 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Default weights for the BEEFY Pallet
//! This file was not auto-generated.
use frame_support::weights::{
constants::{RocksDbWeight as DbWeight, WEIGHT_REF_TIME_PER_MICROS, WEIGHT_REF_TIME_PER_NANOS},
Weight,
};
impl crate::WeightInfo for () {
fn report_equivocation(validator_count: u32) -> Weight {
// we take the validator set count from the membership proof to
// calculate the weight but we set a floor of 100 validators.
let validator_count = validator_count.max(100) as u64;
// worst case we are considering is that the given offender is backed by 200 nominators
const MAX_NOMINATORS: u64 = 200;
// checking membership proof
Weight::from_ref_time(35u64 * WEIGHT_REF_TIME_PER_MICROS)
.saturating_add(
Weight::from_ref_time(175u64 * WEIGHT_REF_TIME_PER_NANOS)
.saturating_mul(validator_count),
)
.saturating_add(DbWeight::get().reads(5))
// check equivocation proof
.saturating_add(Weight::from_ref_time(95u64 * WEIGHT_REF_TIME_PER_MICROS))
// report offence
.saturating_add(Weight::from_ref_time(110u64 * WEIGHT_REF_TIME_PER_MICROS))
.saturating_add(Weight::from_ref_time(
25u64 * WEIGHT_REF_TIME_PER_MICROS * MAX_NOMINATORS,
))
.saturating_add(DbWeight::get().reads(14 + 3 * MAX_NOMINATORS))
.saturating_add(DbWeight::get().writes(10 + 3 * MAX_NOMINATORS))
// fetching set id -> session index mappings
.saturating_add(DbWeight::get().reads(2))
}
}
+385
View File
@@ -0,0 +1,385 @@
// This file is part of Substrate.
// Copyright (C) 2017-2023 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! An opt-in utility module for reporting equivocations.
//!
//! This module defines an offence type for BEEFY equivocations
//! and some utility traits to wire together:
//! - a key ownership proof system (e.g. to prove that a given authority was part of a session);
//! - a system for reporting offences;
//! - a system for signing and submitting transactions;
//! - a way to get the current block author;
//!
//! These can be used in an offchain context in order to submit equivocation
//! reporting extrinsics (from the client that's running the BEEFY protocol).
//! And in a runtime context, so that the BEEFY pallet can validate the
//! equivocation proofs in the extrinsic and report the offences.
//!
//! IMPORTANT:
//! When using this module for enabling equivocation reporting it is required
//! that the `ValidateUnsigned` for the BEEFY pallet is used in the runtime
//! definition.
use sp_std::prelude::*;
use beefy_primitives::{EquivocationProof, ValidatorSetId};
use codec::{self as codec, Decode, Encode};
use frame_support::{
log,
traits::{Get, KeyOwnerProofSystem},
};
use frame_system::pallet_prelude::BlockNumberFor;
use sp_runtime::{
transaction_validity::{
InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
TransactionValidityError, ValidTransaction,
},
DispatchResult, Perbill, RuntimeAppPublic,
};
use sp_staking::{
offence::{Kind, Offence, OffenceError, ReportOffence},
SessionIndex,
};
use super::{Call, Config, Pallet, LOG_TARGET};
/// A trait with utility methods for handling equivocation reports in BEEFY.
/// The offence type is generic, and the trait provides, reporting an offence
/// triggered by a valid equivocation report, and also for creating and
/// submitting equivocation report extrinsics (useful only in offchain context).
pub trait HandleEquivocation<T: Config> {
/// The offence type used for reporting offences on valid equivocation reports.
type Offence: BeefyOffence<BlockNumberFor<T>, T::KeyOwnerIdentification>;
/// The longevity, in blocks, that the equivocation report is valid for. When using the staking
/// pallet this should be equal to the bonding duration (in blocks, not eras).
type ReportLongevity: Get<u64>;
/// Report an offence proved by the given reporters.
fn report_offence(
reporters: Vec<T::AccountId>,
offence: Self::Offence,
) -> Result<(), OffenceError>;
/// Returns true if all of the offenders at the given time slot have already been reported.
fn is_known_offence(
offenders: &[T::KeyOwnerIdentification],
time_slot: &<Self::Offence as Offence<T::KeyOwnerIdentification>>::TimeSlot,
) -> bool;
/// Create and dispatch an equivocation report extrinsic.
fn submit_unsigned_equivocation_report(
equivocation_proof: EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
key_owner_proof: T::KeyOwnerProof,
) -> DispatchResult;
/// Fetch the current block author id, if defined.
fn block_author() -> Option<T::AccountId>;
}
impl<T: Config> HandleEquivocation<T> for () {
type Offence = BeefyEquivocationOffence<BlockNumberFor<T>, T::KeyOwnerIdentification>;
type ReportLongevity = ();
fn report_offence(
_reporters: Vec<T::AccountId>,
_offence: BeefyEquivocationOffence<BlockNumberFor<T>, T::KeyOwnerIdentification>,
) -> Result<(), OffenceError> {
Ok(())
}
fn is_known_offence(
_offenders: &[T::KeyOwnerIdentification],
_time_slot: &BeefyTimeSlot<BlockNumberFor<T>>,
) -> bool {
true
}
fn submit_unsigned_equivocation_report(
_equivocation_proof: EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
_key_owner_proof: T::KeyOwnerProof,
) -> DispatchResult {
Ok(())
}
fn block_author() -> Option<T::AccountId> {
None
}
}
/// Generic equivocation handler. This type implements `HandleEquivocation`
/// using existing subsystems that are part of frame (type bounds described
/// below) and will dispatch to them directly, it's only purpose is to wire all
/// subsystems together.
pub struct EquivocationHandler<N, I, R, L, O = BeefyEquivocationOffence<N, I>> {
_phantom: sp_std::marker::PhantomData<(N, I, R, L, O)>,
}
impl<N, I, R, L, O> Default for EquivocationHandler<N, I, R, L, O> {
fn default() -> Self {
Self { _phantom: Default::default() }
}
}
impl<T, R, L, O> HandleEquivocation<T>
for EquivocationHandler<BlockNumberFor<T>, T::KeyOwnerIdentification, R, L, O>
where
// We use the authorship pallet to fetch the current block author and use
// `offchain::SendTransactionTypes` for unsigned extrinsic creation and
// submission.
T: Config + pallet_authorship::Config + frame_system::offchain::SendTransactionTypes<Call<T>>,
// A system for reporting offences after valid equivocation reports are
// processed.
R: ReportOffence<T::AccountId, T::KeyOwnerIdentification, O>,
// The longevity (in blocks) that the equivocation report is valid for. When using the staking
// pallet this should be the bonding duration.
L: Get<u64>,
// The offence type that should be used when reporting.
O: BeefyOffence<BlockNumberFor<T>, T::KeyOwnerIdentification>,
{
type Offence = O;
type ReportLongevity = L;
fn report_offence(reporters: Vec<T::AccountId>, offence: O) -> Result<(), OffenceError> {
R::report_offence(reporters, offence)
}
fn is_known_offence(offenders: &[T::KeyOwnerIdentification], time_slot: &O::TimeSlot) -> bool {
R::is_known_offence(offenders, time_slot)
}
fn submit_unsigned_equivocation_report(
equivocation_proof: EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
key_owner_proof: T::KeyOwnerProof,
) -> DispatchResult {
use frame_system::offchain::SubmitTransaction;
let call = Call::report_equivocation_unsigned {
equivocation_proof: Box::new(equivocation_proof),
key_owner_proof,
};
match SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call.into()) {
Ok(()) => log::info!(target: LOG_TARGET, "Submitted BEEFY equivocation report.",),
Err(e) =>
log::error!(target: LOG_TARGET, "Error submitting equivocation report: {:?}", e,),
}
Ok(())
}
fn block_author() -> Option<T::AccountId> {
<pallet_authorship::Pallet<T>>::author()
}
}
/// A round number and set id which point on the time of an offence.
#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Encode, Decode)]
pub struct BeefyTimeSlot<N: Copy + Clone + PartialOrd + Ord + Eq + PartialEq + Encode + Decode> {
// The order of these matters for `derive(Ord)`.
/// BEEFY Set ID.
pub set_id: ValidatorSetId,
/// Round number.
pub round: N,
}
/// Methods for the `ValidateUnsigned` implementation:
/// It restricts calls to `report_equivocation_unsigned` to local calls (i.e. extrinsics generated
/// on this node) or that already in a block. This guarantees that only block authors can include
/// unsigned equivocation reports.
impl<T: Config> Pallet<T> {
pub fn validate_unsigned(source: TransactionSource, call: &Call<T>) -> TransactionValidity {
if let Call::report_equivocation_unsigned { equivocation_proof, key_owner_proof } = call {
// discard equivocation report not coming from the local node
match source {
TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ },
_ => {
log::warn!(
target: LOG_TARGET,
"rejecting unsigned report equivocation transaction because it is not local/in-block."
);
return InvalidTransaction::Call.into()
},
}
// check report staleness
is_known_offence::<T>(equivocation_proof, key_owner_proof)?;
let longevity =
<T::HandleEquivocation as HandleEquivocation<T>>::ReportLongevity::get();
ValidTransaction::with_tag_prefix("BeefyEquivocation")
// We assign the maximum priority for any equivocation report.
.priority(TransactionPriority::MAX)
// Only one equivocation report for the same offender at the same slot.
.and_provides((
equivocation_proof.offender_id().clone(),
equivocation_proof.set_id(),
*equivocation_proof.round_number(),
))
.longevity(longevity)
// We don't propagate this. This can never be included on a remote node.
.propagate(false)
.build()
} else {
InvalidTransaction::Call.into()
}
}
pub fn pre_dispatch(call: &Call<T>) -> Result<(), TransactionValidityError> {
if let Call::report_equivocation_unsigned { equivocation_proof, key_owner_proof } = call {
is_known_offence::<T>(equivocation_proof, key_owner_proof)
} else {
Err(InvalidTransaction::Call.into())
}
}
}
fn is_known_offence<T: Config>(
equivocation_proof: &EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
key_owner_proof: &T::KeyOwnerProof,
) -> Result<(), TransactionValidityError> {
// check the membership proof to extract the offender's id,
// equivocation validity will be fully checked during the call.
let key = (beefy_primitives::KEY_TYPE, equivocation_proof.offender_id().clone());
let offender = T::KeyOwnerProofSystem::check_proof(key, key_owner_proof.clone())
.ok_or(InvalidTransaction::BadProof)?;
// check if the offence has already been reported,
// and if so then we can discard the report.
let time_slot = <T::HandleEquivocation as HandleEquivocation<T>>::Offence::new_time_slot(
equivocation_proof.set_id(),
*equivocation_proof.round_number(),
);
let is_known_offence = T::HandleEquivocation::is_known_offence(&[offender], &time_slot);
if is_known_offence {
Err(InvalidTransaction::Stale.into())
} else {
Ok(())
}
}
/// A BEEFY equivocation offence report.
pub struct BeefyEquivocationOffence<N, FullIdentification>
where
N: Copy + Clone + PartialOrd + Ord + Eq + PartialEq + Encode + Decode,
{
/// Time slot at which this incident happened.
pub time_slot: BeefyTimeSlot<N>,
/// The session index in which the incident happened.
pub session_index: SessionIndex,
/// The size of the validator set at the time of the offence.
pub validator_set_count: u32,
/// The authority which produced this equivocation.
pub offender: FullIdentification,
}
/// An interface for types that will be used as BEEFY offences and must also
/// implement the `Offence` trait. This trait provides a constructor that is
/// provided all available data during processing of BEEFY equivocations.
pub trait BeefyOffence<N, FullIdentification>: Offence<FullIdentification>
where
N: Copy + Clone + PartialOrd + Ord + Eq + PartialEq + Encode + Decode,
{
/// Create a new BEEFY offence using the given equivocation details.
fn new(
session_index: SessionIndex,
validator_set_count: u32,
offender: FullIdentification,
set_id: ValidatorSetId,
round: N,
) -> Self;
/// Create a new BEEFY offence time slot.
fn new_time_slot(set_id: ValidatorSetId, round: N) -> Self::TimeSlot;
}
impl<N, FullIdentification: Clone> BeefyOffence<N, FullIdentification>
for BeefyEquivocationOffence<N, FullIdentification>
where
N: Copy + Clone + PartialOrd + Ord + Eq + PartialEq + Encode + Decode,
{
fn new(
session_index: SessionIndex,
validator_set_count: u32,
offender: FullIdentification,
set_id: ValidatorSetId,
round: N,
) -> Self {
BeefyEquivocationOffence {
session_index,
validator_set_count,
offender,
time_slot: BeefyTimeSlot { set_id, round },
}
}
fn new_time_slot(set_id: ValidatorSetId, round: N) -> Self::TimeSlot {
BeefyTimeSlot { set_id, round }
}
}
impl<N, FullIdentification: Clone> Offence<FullIdentification>
for BeefyEquivocationOffence<N, FullIdentification>
where
N: Copy + Clone + PartialOrd + Ord + Eq + PartialEq + Encode + Decode,
{
const ID: Kind = *b"beefy:equivocati";
type TimeSlot = BeefyTimeSlot<N>;
fn offenders(&self) -> Vec<FullIdentification> {
vec![self.offender.clone()]
}
fn session_index(&self) -> SessionIndex {
self.session_index
}
fn validator_set_count(&self) -> u32 {
self.validator_set_count
}
fn time_slot(&self) -> Self::TimeSlot {
self.time_slot
}
fn slash_fraction(&self, offenders_count: u32) -> Perbill {
// the formula is min((3k / n)^2, 1)
let x = Perbill::from_rational(3 * offenders_count, self.validator_set_count);
// _ ^ 2
x.square()
}
}
+236 -16
View File
@@ -20,35 +20,48 @@
use codec::{Encode, MaxEncodedLen};
use frame_support::{
dispatch::{DispatchResultWithPostInfo, Pays},
log,
traits::{Get, OneSessionHandler},
pallet_prelude::*,
traits::{Get, KeyOwnerProofSystem, OneSessionHandler},
weights::Weight,
BoundedSlice, BoundedVec, Parameter,
};
use frame_system::{
ensure_none, ensure_signed,
pallet_prelude::{BlockNumberFor, OriginFor},
};
use sp_runtime::{
generic::DigestItem,
traits::{IsMember, Member},
RuntimeAppPublic,
KeyTypeId, RuntimeAppPublic,
};
use sp_session::{GetSessionNumber, GetValidatorCount};
use sp_staking::SessionIndex;
use sp_std::prelude::*;
use beefy_primitives::{
AuthorityIndex, ConsensusLog, OnNewValidatorSet, ValidatorSet, BEEFY_ENGINE_ID,
GENESIS_AUTHORITY_SET_ID,
AuthorityIndex, BeefyAuthorityId, ConsensusLog, EquivocationProof, OnNewValidatorSet,
ValidatorSet, BEEFY_ENGINE_ID, GENESIS_AUTHORITY_SET_ID,
};
mod default_weights;
mod equivocation;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub use crate::equivocation::{
BeefyEquivocationOffence, BeefyOffence, BeefyTimeSlot, EquivocationHandler, HandleEquivocation,
};
pub use pallet::*;
const LOG_TARGET: &str = "runtime::beefy";
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::BlockNumberFor;
#[pallet::config]
@@ -56,10 +69,35 @@ pub mod pallet {
/// Authority identifier type
type BeefyId: Member
+ Parameter
+ RuntimeAppPublic
// todo: use custom signature hashing type instead of hardcoded `Keccak256`
+ BeefyAuthorityId<sp_runtime::traits::Keccak256>
+ MaybeSerializeDeserialize
+ MaxEncodedLen;
/// A system for proving ownership of keys, i.e. that a given key was part
/// of a validator set, needed for validating equivocation reports.
type KeyOwnerProofSystem: KeyOwnerProofSystem<
(KeyTypeId, Self::BeefyId),
Proof = Self::KeyOwnerProof,
IdentificationTuple = Self::KeyOwnerIdentification,
>;
/// The proof of key ownership, used for validating equivocation reports
/// The proof must include the session index and validator count of the
/// session at which the equivocation occurred.
type KeyOwnerProof: Parameter + GetSessionNumber + GetValidatorCount;
/// The identification of a key owner, used when reporting equivocations.
type KeyOwnerIdentification: Parameter;
/// The equivocation handling subsystem, defines methods to report an
/// offence (after the equivocation has been validated) and for submitting a
/// transaction to report an equivocation (from an offchain context).
/// NOTE: when enabling equivocation handling (i.e. this type isn't set to
/// `()`) you must use this pallet's `ValidateUnsigned` in the runtime
/// definition.
type HandleEquivocation: HandleEquivocation<Self>;
/// The maximum number of authorities that can be added.
type MaxAuthorities: Get<u32>;
@@ -69,6 +107,9 @@ pub mod pallet {
/// externally apart from having it in the storage. For instance you might cache a light
/// weight MMR root over validators and make it available for Light Clients.
type OnNewValidatorSet: OnNewValidatorSet<<Self as Config>::BeefyId>;
/// Weights for this pallet.
type WeightInfo: WeightInfo;
}
#[pallet::pallet]
@@ -92,6 +133,15 @@ pub mod pallet {
pub(super) type NextAuthorities<T: Config> =
StorageValue<_, BoundedVec<T::BeefyId, T::MaxAuthorities>, ValueQuery>;
/// A mapping from BEEFY set ID to the index of the *most recent* session for which its
/// members were responsible.
///
/// TWOX-NOTE: `ValidatorSetId` is not under user control.
#[pallet::storage]
#[pallet::getter(fn session_for_set)]
pub(super) type SetIdSession<T: Config> =
StorageMap<_, Twox64Concat, beefy_primitives::ValidatorSetId, SessionIndex>;
/// Block number where BEEFY consensus is enabled/started.
/// If changing this, make sure `Self::ValidatorSetId` is also reset to
/// `GENESIS_AUTHORITY_SET_ID` in the state of the new block number configured here.
@@ -124,13 +174,90 @@ pub mod pallet {
#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
fn build(&self) {
Pallet::<T>::initialize_authorities(&self.authorities)
Pallet::<T>::initialize(&self.authorities)
// we panic here as runtime maintainers can simply reconfigure genesis and restart
// the chain easily
.expect("Authorities vec too big");
<GenesisBlock<T>>::put(&self.genesis_block);
}
}
#[pallet::error]
pub enum Error<T> {
/// A key ownership proof provided as part of an equivocation report is invalid.
InvalidKeyOwnershipProof,
/// An equivocation proof provided as part of an equivocation report is invalid.
InvalidEquivocationProof,
/// A given equivocation report is valid but already previously reported.
DuplicateOffenceReport,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Report voter equivocation/misbehavior. This method will verify the
/// equivocation proof and validate the given key ownership proof
/// against the extracted offender. If both are valid, the offence
/// will be reported.
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::report_equivocation(key_owner_proof.validator_count()))]
pub fn report_equivocation(
origin: OriginFor<T>,
equivocation_proof: Box<
EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
>,
key_owner_proof: T::KeyOwnerProof,
) -> DispatchResultWithPostInfo {
let reporter = ensure_signed(origin)?;
Self::do_report_equivocation(Some(reporter), *equivocation_proof, key_owner_proof)
}
/// Report voter equivocation/misbehavior. This method will verify the
/// equivocation proof and validate the given key ownership proof
/// against the extracted offender. If both are valid, the offence
/// will be reported.
///
/// This extrinsic must be called unsigned and it is expected that only
/// block authors will call it (validated in `ValidateUnsigned`), as such
/// if the block author is defined it will be defined as the equivocation
/// reporter.
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::report_equivocation(key_owner_proof.validator_count()))]
pub fn report_equivocation_unsigned(
origin: OriginFor<T>,
equivocation_proof: Box<
EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
>,
key_owner_proof: T::KeyOwnerProof,
) -> DispatchResultWithPostInfo {
ensure_none(origin)?;
Self::do_report_equivocation(
T::HandleEquivocation::block_author(),
*equivocation_proof,
key_owner_proof,
)
}
}
#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
Self::pre_dispatch(call)
}
fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
Self::validate_unsigned(source, call)
}
}
}
impl<T: Config> Pallet<T> {
@@ -141,6 +268,24 @@ impl<T: Config> Pallet<T> {
ValidatorSet::<T::BeefyId>::new(validators, id)
}
/// Submits an extrinsic to report an equivocation. This method will create
/// an unsigned extrinsic with a call to `report_equivocation_unsigned` and
/// will push the transaction to the pool. Only useful in an offchain context.
pub fn submit_unsigned_equivocation_report(
equivocation_proof: EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
key_owner_proof: T::KeyOwnerProof,
) -> Option<()> {
T::HandleEquivocation::submit_unsigned_equivocation_report(
equivocation_proof,
key_owner_proof,
)
.ok()
}
fn change_authorities(
new: BoundedVec<T::BeefyId, T::MaxAuthorities>,
queued: BoundedVec<T::BeefyId, T::MaxAuthorities>,
@@ -169,7 +314,7 @@ impl<T: Config> Pallet<T> {
}
}
fn initialize_authorities(authorities: &Vec<T::BeefyId>) -> Result<(), ()> {
fn initialize(authorities: &Vec<T::BeefyId>) -> Result<(), ()> {
if authorities.is_empty() {
return Ok(())
}
@@ -199,15 +344,80 @@ impl<T: Config> Pallet<T> {
);
}
}
// NOTE: initialize first session of first set. this is necessary for
// the genesis set and session since we only update the set -> session
// mapping whenever a new session starts, i.e. through `on_new_session`.
SetIdSession::<T>::insert(0, 0);
Ok(())
}
fn do_report_equivocation(
reporter: Option<T::AccountId>,
equivocation_proof: EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
key_owner_proof: T::KeyOwnerProof,
) -> DispatchResultWithPostInfo {
// We check the equivocation within the context of its set id (and
// associated session) and round. We also need to know the validator
// set count at the time of the offence since it is required to calculate
// the slash amount.
let set_id = equivocation_proof.set_id();
let round = *equivocation_proof.round_number();
let offender_id = equivocation_proof.offender_id().clone();
let session_index = key_owner_proof.session();
let validator_count = key_owner_proof.validator_count();
// validate the key ownership proof extracting the id of the offender.
let offender = T::KeyOwnerProofSystem::check_proof(
(beefy_primitives::KEY_TYPE, offender_id),
key_owner_proof,
)
.ok_or(Error::<T>::InvalidKeyOwnershipProof)?;
// validate equivocation proof (check votes are different and signatures are valid).
if !beefy_primitives::check_equivocation_proof(&equivocation_proof) {
return Err(Error::<T>::InvalidEquivocationProof.into())
}
// check that the session id for the membership proof is within the
// bounds of the set id reported in the equivocation.
let set_id_session_index =
Self::session_for_set(set_id).ok_or(Error::<T>::InvalidEquivocationProof)?;
if session_index != set_id_session_index {
return Err(Error::<T>::InvalidEquivocationProof.into())
}
// report to the offences module rewarding the sender.
T::HandleEquivocation::report_offence(
reporter.into_iter().collect(),
<T::HandleEquivocation as HandleEquivocation<T>>::Offence::new(
session_index,
validator_count,
offender,
set_id,
round,
),
)
.map_err(|_| Error::<T>::DuplicateOffenceReport)?;
// waive the fee since the report is valid and beneficial
Ok(Pays::No.into())
}
}
impl<T: Config> sp_runtime::BoundToRuntimeAppPublic for Pallet<T> {
type Public = T::BeefyId;
}
impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T>
where
T: pallet_session::Config,
{
type Key = T::BeefyId;
fn on_genesis_session<'a, I: 'a>(validators: I)
@@ -217,7 +427,7 @@ impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
let authorities = validators.map(|(_, k)| k).collect::<Vec<_>>();
// we panic here as runtime maintainers can simply reconfigure genesis and restart the
// chain easily
Self::initialize_authorities(&authorities).expect("Authorities vec too big");
Self::initialize(&authorities).expect("Authorities vec too big");
}
fn on_new_session<'a, I: 'a>(_changed: bool, validators: I, queued_validators: I)
@@ -227,9 +437,10 @@ impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
let next_authorities = validators.map(|(_, k)| k).collect::<Vec<_>>();
if next_authorities.len() as u32 > T::MaxAuthorities::get() {
log::error!(
target: "runtime::beefy",
target: LOG_TARGET,
"authorities list {:?} truncated to length {}",
next_authorities, T::MaxAuthorities::get(),
next_authorities,
T::MaxAuthorities::get(),
);
}
let bounded_next_authorities =
@@ -238,9 +449,10 @@ impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
let next_queued_authorities = queued_validators.map(|(_, k)| k).collect::<Vec<_>>();
if next_queued_authorities.len() as u32 > T::MaxAuthorities::get() {
log::error!(
target: "runtime::beefy",
target: LOG_TARGET,
"queued authorities list {:?} truncated to length {}",
next_queued_authorities, T::MaxAuthorities::get(),
next_queued_authorities,
T::MaxAuthorities::get(),
);
}
let bounded_next_queued_authorities =
@@ -249,6 +461,10 @@ impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
// Always issue a change on each `session`, even if validator set hasn't changed.
// We want to have at least one BEEFY mandatory block per session.
Self::change_authorities(bounded_next_authorities, bounded_next_queued_authorities);
// Update the mapping for the new set id that corresponds to the latest session (i.e. now).
let session_index = <pallet_session::Pallet<T>>::current_index();
SetIdSession::<T>::insert(Self::validator_set_id(), &session_index);
}
fn on_disabled(i: u32) {
@@ -266,3 +482,7 @@ impl<T: Config> IsMember<T::BeefyId> for Pallet<T> {
Self::authorities().iter().any(|id| id == authority_id)
}
}
pub trait WeightInfo {
fn report_equivocation(validator_count: u32) -> Weight;
}
+203 -28
View File
@@ -17,24 +17,33 @@
use std::vec;
use frame_election_provider_support::{onchain, SequentialPhragmen};
use frame_support::{
construct_runtime, parameter_types,
sp_io::TestExternalities,
traits::{ConstU16, ConstU32, ConstU64, GenesisBuild},
traits::{
ConstU16, ConstU32, ConstU64, GenesisBuild, KeyOwnerProofSystem, OnFinalize, OnInitialize,
},
BasicExternalities,
};
use sp_core::H256;
use pallet_session::historical as pallet_session_historical;
use sp_core::{crypto::KeyTypeId, ConstU128, H256};
use sp_runtime::{
app_crypto::ecdsa::Public,
curve::PiecewiseLinear,
impl_opaque_keys,
testing::Header,
traits::{BlakeTwo256, ConvertInto, IdentityLookup, OpaqueKeys},
testing::{Header, TestXt},
traits::{BlakeTwo256, IdentityLookup, OpaqueKeys},
Perbill,
};
use sp_staking::{EraIndex, SessionIndex};
use crate as pallet_beefy;
pub use beefy_primitives::{crypto::AuthorityId as BeefyId, ConsensusLog, BEEFY_ENGINE_ID};
pub use beefy_primitives::{
crypto::{AuthorityId as BeefyId, AuthoritySignature as BeefySignature},
ConsensusLog, EquivocationProof, BEEFY_ENGINE_ID,
};
impl_opaque_keys! {
pub struct MockSessionKeys {
@@ -51,9 +60,15 @@ construct_runtime!(
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
Beefy: pallet_beefy::{Pallet, Config<T>, Storage},
Session: pallet_session::{Pallet, Call, Storage, Event, Config<T>},
System: frame_system,
Authorship: pallet_authorship,
Timestamp: pallet_timestamp,
Balances: pallet_balances,
Beefy: pallet_beefy,
Staking: pallet_staking,
Session: pallet_session,
Offences: pallet_offences,
Historical: pallet_session_historical,
}
);
@@ -75,7 +90,7 @@ impl frame_system::Config for Test {
type BlockHashCount = ConstU64<250>;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = ();
type AccountData = pallet_balances::AccountData<u128>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
@@ -84,10 +99,34 @@ impl frame_system::Config for Test {
type MaxConsumers = ConstU32<16>;
}
impl<C> frame_system::offchain::SendTransactionTypes<C> for Test
where
RuntimeCall: From<C>,
{
type OverarchingCall = RuntimeCall;
type Extrinsic = TestXt<RuntimeCall, ()>;
}
parameter_types! {
pub const Period: u64 = 1;
pub const ReportLongevity: u64 =
BondingDuration::get() as u64 * SessionsPerEra::get() as u64 * Period::get();
}
impl pallet_beefy::Config for Test {
type BeefyId = BeefyId;
type KeyOwnerProofSystem = Historical;
type KeyOwnerProof =
<Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(KeyTypeId, BeefyId)>>::Proof;
type KeyOwnerIdentification = <Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(
KeyTypeId,
BeefyId,
)>>::IdentificationTuple;
type HandleEquivocation =
super::EquivocationHandler<u64, Self::KeyOwnerIdentification, Offences, ReportLongevity>;
type MaxAuthorities = ConstU32<100>;
type OnNewValidatorSet = ();
type WeightInfo = ();
}
parameter_types! {
@@ -97,29 +136,107 @@ parameter_types! {
impl pallet_session::Config for Test {
type RuntimeEvent = RuntimeEvent;
type ValidatorId = u64;
type ValidatorIdOf = ConvertInto;
type ValidatorIdOf = pallet_staking::StashOf<Self>;
type ShouldEndSession = pallet_session::PeriodicSessions<ConstU64<1>, ConstU64<0>>;
type NextSessionRotation = pallet_session::PeriodicSessions<ConstU64<1>, ConstU64<0>>;
type SessionManager = MockSessionManager;
type SessionManager = pallet_session::historical::NoteHistoricalRoot<Self, Staking>;
type SessionHandler = <MockSessionKeys as OpaqueKeys>::KeyTypeIdProviders;
type Keys = MockSessionKeys;
type WeightInfo = ();
}
pub struct MockSessionManager;
impl pallet_session::historical::Config for Test {
type FullIdentification = pallet_staking::Exposure<u64, u128>;
type FullIdentificationOf = pallet_staking::ExposureOf<Self>;
}
impl pallet_session::SessionManager<u64> for MockSessionManager {
fn end_session(_: sp_staking::SessionIndex) {}
fn start_session(_: sp_staking::SessionIndex) {}
fn new_session(idx: sp_staking::SessionIndex) -> Option<Vec<u64>> {
if idx == 0 || idx == 1 {
Some(vec![1, 2])
} else if idx == 2 {
Some(vec![3, 4])
} else {
None
}
}
impl pallet_authorship::Config for Test {
type FindAuthor = ();
type EventHandler = ();
}
impl pallet_balances::Config for Test {
type MaxLocks = ();
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = u128;
type DustRemoval = ();
type RuntimeEvent = RuntimeEvent;
type ExistentialDeposit = ConstU128<1>;
type AccountStore = System;
type WeightInfo = ();
}
impl pallet_timestamp::Config for Test {
type Moment = u64;
type OnTimestampSet = ();
type MinimumPeriod = ConstU64<3>;
type WeightInfo = ();
}
pallet_staking_reward_curve::build! {
const REWARD_CURVE: PiecewiseLinear<'static> = curve!(
min_inflation: 0_025_000u64,
max_inflation: 0_100_000,
ideal_stake: 0_500_000,
falloff: 0_050_000,
max_piece_count: 40,
test_precision: 0_005_000,
);
}
parameter_types! {
pub const SessionsPerEra: SessionIndex = 3;
pub const BondingDuration: EraIndex = 3;
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
}
pub struct OnChainSeqPhragmen;
impl onchain::Config for OnChainSeqPhragmen {
type System = Test;
type Solver = SequentialPhragmen<u64, Perbill>;
type DataProvider = Staking;
type WeightInfo = ();
type MaxWinners = ConstU32<100>;
type VotersBound = ConstU32<{ u32::MAX }>;
type TargetsBound = ConstU32<{ u32::MAX }>;
}
impl pallet_staking::Config for Test {
type MaxNominations = ConstU32<16>;
type RewardRemainder = ();
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
type Slash = ();
type Reward = ();
type SessionsPerEra = SessionsPerEra;
type BondingDuration = BondingDuration;
type SlashDeferDuration = ();
type AdminOrigin = frame_system::EnsureRoot<Self::AccountId>;
type SessionInterface = Self;
type UnixTime = pallet_timestamp::Pallet<Test>;
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
type MaxNominatorRewardedPerValidator = ConstU32<64>;
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
type NextNewSession = Session;
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type TargetList = pallet_staking::UseValidatorsMap<Self>;
type MaxUnlockingChunks = ConstU32<32>;
type HistoryDepth = ConstU32<84>;
type OnStakerSlash = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
}
impl pallet_offences::Config for Test {
type RuntimeEvent = RuntimeEvent;
type IdentificationTuple = pallet_session::historical::IdentificationTuple<Self>;
type OnOffenceHandler = Staking;
}
// Note, that we can't use `UintAuthorityId` here. Reason is that the implementation
@@ -134,20 +251,27 @@ pub fn mock_beefy_id(id: u8) -> BeefyId {
BeefyId::from(pk)
}
pub fn mock_authorities(vec: Vec<u8>) -> Vec<(u64, BeefyId)> {
vec.into_iter().map(|id| ((id as u64), mock_beefy_id(id))).collect()
pub fn mock_authorities(vec: Vec<u8>) -> Vec<BeefyId> {
vec.into_iter().map(|id| mock_beefy_id(id)).collect()
}
pub fn new_test_ext(ids: Vec<u8>) -> TestExternalities {
new_test_ext_raw_authorities(mock_authorities(ids))
}
pub fn new_test_ext_raw_authorities(authorities: Vec<(u64, BeefyId)>) -> TestExternalities {
pub fn new_test_ext_raw_authorities(authorities: Vec<BeefyId>) -> TestExternalities {
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
let balances: Vec<_> = (0..authorities.len()).map(|i| (i as u64, 10_000_000)).collect();
pallet_balances::GenesisConfig::<Test> { balances }
.assimilate_storage(&mut t)
.unwrap();
let session_keys: Vec<_> = authorities
.iter()
.map(|id| (id.0 as u64, id.0 as u64, MockSessionKeys { dummy: id.1.clone() }))
.enumerate()
.map(|(i, k)| (i as u64, i as u64, MockSessionKeys { dummy: k.clone() }))
.collect();
BasicExternalities::execute_with_storage(&mut t, || {
@@ -160,5 +284,56 @@ pub fn new_test_ext_raw_authorities(authorities: Vec<(u64, BeefyId)>) -> TestExt
.assimilate_storage(&mut t)
.unwrap();
// controllers are the index + 1000
let stakers: Vec<_> = (0..authorities.len())
.map(|i| {
(i as u64, i as u64 + 1000, 10_000, pallet_staking::StakerStatus::<u64>::Validator)
})
.collect();
let staking_config = pallet_staking::GenesisConfig::<Test> {
stakers,
validator_count: 2,
force_era: pallet_staking::Forcing::ForceNew,
minimum_validator_count: 0,
invulnerables: vec![],
..Default::default()
};
staking_config.assimilate_storage(&mut t).unwrap();
t.into()
}
pub fn start_session(session_index: SessionIndex) {
for i in Session::current_index()..session_index {
System::on_finalize(System::block_number());
Session::on_finalize(System::block_number());
Staking::on_finalize(System::block_number());
Beefy::on_finalize(System::block_number());
let parent_hash = if System::block_number() > 1 {
let hdr = System::finalize();
hdr.hash()
} else {
System::parent_hash()
};
System::reset_events();
System::initialize(&(i as u64 + 1), &parent_hash, &Default::default());
System::set_block_number((i + 1).into());
Timestamp::set_timestamp(System::block_number() * 6000);
System::on_initialize(System::block_number());
Session::on_initialize(System::block_number());
Staking::on_initialize(System::block_number());
Beefy::on_initialize(System::block_number());
}
assert_eq!(Session::current_index(), session_index);
}
pub fn start_era(era_index: EraIndex) {
start_session((era_index * 3).into());
assert_eq!(Staking::current_era(), Some(era_index));
}
+634 -13
View File
@@ -17,14 +17,21 @@
use std::vec;
use beefy_primitives::ValidatorSet;
use beefy_primitives::{
check_equivocation_proof, generate_equivocation_proof, known_payloads::MMR_ROOT_ID,
Keyring as BeefyKeyring, Payload, ValidatorSet,
};
use codec::Encode;
use sp_runtime::DigestItem;
use frame_support::traits::OnInitialize;
use frame_support::{
assert_err, assert_ok,
dispatch::{GetDispatchInfo, Pays},
traits::{Currency, KeyOwnerProofSystem, OnInitialize},
};
use crate::mock::*;
use crate::{mock::*, Call, Config, Error, Weight, WeightInfo};
fn init_block(block: u64) {
System::set_block_number(block);
@@ -37,12 +44,13 @@ pub fn beefy_log(log: ConsensusLog<BeefyId>) -> DigestItem {
#[test]
fn genesis_session_initializes_authorities() {
let want = vec![mock_beefy_id(1), mock_beefy_id(2), mock_beefy_id(3), mock_beefy_id(4)];
let authorities = mock_authorities(vec![1, 2, 3, 4]);
let want = authorities.clone();
new_test_ext(vec![1, 2, 3, 4]).execute_with(|| {
new_test_ext_raw_authorities(authorities).execute_with(|| {
let authorities = Beefy::authorities();
assert!(authorities.len() == 2);
assert_eq!(authorities.len(), 4);
assert_eq!(want[0], authorities[0]);
assert_eq!(want[1], authorities[1]);
@@ -50,7 +58,7 @@ fn genesis_session_initializes_authorities() {
let next_authorities = Beefy::next_authorities();
assert!(next_authorities.len() == 2);
assert_eq!(next_authorities.len(), 4);
assert_eq!(want[0], next_authorities[0]);
assert_eq!(want[1], next_authorities[1]);
});
@@ -58,6 +66,9 @@ fn genesis_session_initializes_authorities() {
#[test]
fn session_change_updates_authorities() {
let authorities = mock_authorities(vec![1, 2, 3, 4]);
let want_validators = authorities.clone();
new_test_ext(vec![1, 2, 3, 4]).execute_with(|| {
assert!(0 == Beefy::validator_set_id());
@@ -66,7 +77,7 @@ fn session_change_updates_authorities() {
assert!(1 == Beefy::validator_set_id());
let want = beefy_log(ConsensusLog::AuthoritiesChange(
ValidatorSet::new(vec![mock_beefy_id(1), mock_beefy_id(2)], 1).unwrap(),
ValidatorSet::new(want_validators, 1).unwrap(),
));
let log = System::digest().logs[0].clone();
@@ -77,7 +88,7 @@ fn session_change_updates_authorities() {
assert!(2 == Beefy::validator_set_id());
let want = beefy_log(ConsensusLog::AuthoritiesChange(
ValidatorSet::new(vec![mock_beefy_id(3), mock_beefy_id(4)], 2).unwrap(),
ValidatorSet::new(vec![mock_beefy_id(2), mock_beefy_id(4)], 2).unwrap(),
));
let log = System::digest().logs[1].clone();
@@ -92,16 +103,18 @@ fn session_change_updates_next_authorities() {
new_test_ext(vec![1, 2, 3, 4]).execute_with(|| {
let next_authorities = Beefy::next_authorities();
assert!(next_authorities.len() == 2);
assert_eq!(next_authorities.len(), 4);
assert_eq!(want[0], next_authorities[0]);
assert_eq!(want[1], next_authorities[1]);
assert_eq!(want[2], next_authorities[2]);
assert_eq!(want[3], next_authorities[3]);
init_block(1);
let next_authorities = Beefy::next_authorities();
assert!(next_authorities.len() == 2);
assert_eq!(want[2], next_authorities[0]);
assert_eq!(next_authorities.len(), 2);
assert_eq!(want[1], next_authorities[0]);
assert_eq!(want[3], next_authorities[1]);
});
}
@@ -126,6 +139,10 @@ fn validator_set_updates_work() {
new_test_ext(vec![1, 2, 3, 4]).execute_with(|| {
let vs = Beefy::validator_set().unwrap();
assert_eq!(vs.id(), 0u64);
assert_eq!(want[0], vs.validators()[0]);
assert_eq!(want[1], vs.validators()[1]);
assert_eq!(want[2], vs.validators()[2]);
assert_eq!(want[3], vs.validators()[3]);
init_block(1);
@@ -140,7 +157,611 @@ fn validator_set_updates_work() {
let vs = Beefy::validator_set().unwrap();
assert_eq!(vs.id(), 2u64);
assert_eq!(want[2], vs.validators()[0]);
assert_eq!(want[1], vs.validators()[0]);
assert_eq!(want[3], vs.validators()[1]);
});
}
/// Returns a list with 3 authorities with known keys:
/// Alice, Bob and Charlie.
pub fn test_authorities() -> Vec<BeefyId> {
let authorities = vec![BeefyKeyring::Alice, BeefyKeyring::Bob, BeefyKeyring::Charlie];
authorities.into_iter().map(|id| id.public()).collect()
}
#[test]
fn should_sign_and_verify() {
use sp_runtime::traits::Keccak256;
let set_id = 3;
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 two votes in the same round for
// same payload signed by the same key
let equivocation_proof = generate_equivocation_proof(
(1, payload1.clone(), set_id, &BeefyKeyring::Bob),
(1, payload1.clone(), set_id, &BeefyKeyring::Bob),
);
// expect invalid equivocation proof
assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof));
// generate an equivocation proof, with two votes in different rounds for
// different payloads signed by the same key
let equivocation_proof = generate_equivocation_proof(
(1, payload1.clone(), set_id, &BeefyKeyring::Bob),
(2, payload2.clone(), set_id, &BeefyKeyring::Bob),
);
// expect invalid equivocation proof
assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof));
// generate an equivocation proof, with two votes by different authorities
let equivocation_proof = generate_equivocation_proof(
(1, payload1.clone(), set_id, &BeefyKeyring::Alice),
(1, payload2.clone(), set_id, &BeefyKeyring::Bob),
);
// expect invalid equivocation proof
assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof));
// generate an equivocation proof, with two votes in different set ids
let equivocation_proof = generate_equivocation_proof(
(1, payload1.clone(), set_id, &BeefyKeyring::Bob),
(1, payload2.clone(), set_id + 1, &BeefyKeyring::Bob),
);
// expect invalid equivocation proof
assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof));
// generate an equivocation proof, with two votes in the same round for
// different payloads signed by the same key
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
let equivocation_proof = generate_equivocation_proof(
(1, payload1, set_id, &BeefyKeyring::Bob),
(1, payload2, set_id, &BeefyKeyring::Bob),
);
// expect valid equivocation proof
assert!(check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof));
}
#[test]
fn report_equivocation_current_set_works() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
assert_eq!(Staking::current_era(), Some(0));
assert_eq!(Session::current_index(), 0);
start_era(1);
let block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let set_id = validator_set.id();
let validators = Session::validators();
// make sure that all validators have the same balance
for validator in &validators {
assert_eq!(Balances::total_balance(validator), 10_000_000);
assert_eq!(Staking::slashable_balance_of(validator), 10_000);
assert_eq!(
Staking::eras_stakers(1, validator),
pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
);
}
assert_eq!(authorities.len(), 2);
let equivocation_authority_index = 1;
let equivocation_key = &authorities[equivocation_authority_index];
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
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 two votes in the same round for
// different payloads signed by the same key
let equivocation_proof = generate_equivocation_proof(
(block_num, payload1, set_id, &equivocation_keyring),
(block_num, payload2, set_id, &equivocation_keyring),
);
// create the key ownership proof
let key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &equivocation_key)).unwrap();
// report the equivocation and the tx should be dispatched successfully
assert_ok!(Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
key_owner_proof,
),);
start_era(2);
// check that the balance of 0-th validator is slashed 100%.
let equivocation_validator_id = validators[equivocation_authority_index];
assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000);
assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0);
assert_eq!(
Staking::eras_stakers(2, equivocation_validator_id),
pallet_staking::Exposure { total: 0, own: 0, others: vec![] },
);
// check that the balances of all other validators are left intact.
for validator in &validators {
if *validator == equivocation_validator_id {
continue
}
assert_eq!(Balances::total_balance(validator), 10_000_000);
assert_eq!(Staking::slashable_balance_of(validator), 10_000);
assert_eq!(
Staking::eras_stakers(2, validator),
pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
);
}
});
}
#[test]
fn report_equivocation_old_set_works() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let validators = Session::validators();
let old_set_id = validator_set.id();
assert_eq!(authorities.len(), 2);
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index];
// create the key ownership proof in the "old" set
let key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &equivocation_key)).unwrap();
start_era(2);
// make sure that all authorities have the same balance
for validator in &validators {
assert_eq!(Balances::total_balance(validator), 10_000_000);
assert_eq!(Staking::slashable_balance_of(validator), 10_000);
assert_eq!(
Staking::eras_stakers(2, validator),
pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
);
}
let validator_set = Beefy::validator_set().unwrap();
let new_set_id = validator_set.id();
assert_eq!(old_set_id + 3, new_set_id);
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
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 for the old set,
let equivocation_proof = generate_equivocation_proof(
(block_num, payload1, old_set_id, &equivocation_keyring),
(block_num, payload2, old_set_id, &equivocation_keyring),
);
// report the equivocation and the tx should be dispatched successfully
assert_ok!(Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
key_owner_proof,
),);
start_era(3);
// check that the balance of 0-th validator is slashed 100%.
let equivocation_validator_id = validators[equivocation_authority_index];
assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000);
assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0);
assert_eq!(
Staking::eras_stakers(3, equivocation_validator_id),
pallet_staking::Exposure { total: 0, own: 0, others: vec![] },
);
// check that the balances of all other validators are left intact.
for validator in &validators {
if *validator == equivocation_validator_id {
continue
}
assert_eq!(Balances::total_balance(validator), 10_000_000);
assert_eq!(Staking::slashable_balance_of(validator), 10_000);
assert_eq!(
Staking::eras_stakers(3, validator),
pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
);
}
});
}
#[test]
fn report_equivocation_invalid_set_id() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let set_id = validator_set.id();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index];
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
let key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &equivocation_key)).unwrap();
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 for a future set
let equivocation_proof = generate_equivocation_proof(
(block_num, payload1, set_id + 1, &equivocation_keyring),
(block_num, payload2, set_id + 1, &equivocation_keyring),
);
// the call for reporting the equivocation should error
assert_err!(
Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
key_owner_proof,
),
Error::<Test>::InvalidEquivocationProof,
);
});
}
#[test]
fn report_equivocation_invalid_session() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index];
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
// generate a key ownership proof at current era set id
let key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &equivocation_key)).unwrap();
start_era(2);
let set_id = Beefy::validator_set().unwrap().id();
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 at following era set id = 2
let equivocation_proof = generate_equivocation_proof(
(block_num, payload1, set_id, &equivocation_keyring),
(block_num, payload2, set_id, &equivocation_keyring),
);
// report an equivocation for the current set using an key ownership
// proof from the previous set, the session should be invalid.
assert_err!(
Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
key_owner_proof,
),
Error::<Test>::InvalidEquivocationProof,
);
});
}
#[test]
fn report_equivocation_invalid_key_owner_proof() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let set_id = validator_set.id();
let invalid_owner_authority_index = 1;
let invalid_owner_key = &authorities[invalid_owner_authority_index];
// generate a key ownership proof for the authority at index 1
let invalid_key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &invalid_owner_key)).unwrap();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index];
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
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 for the authority at index 0
let equivocation_proof = generate_equivocation_proof(
(block_num, payload1, set_id + 1, &equivocation_keyring),
(block_num, payload2, set_id + 1, &equivocation_keyring),
);
// we need to start a new era otherwise the key ownership proof won't be
// checked since the authorities are part of the current session
start_era(2);
// report an equivocation for the current set using a key ownership
// proof for a different key than the one in the equivocation proof.
assert_err!(
Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
invalid_key_owner_proof,
),
Error::<Test>::InvalidKeyOwnershipProof,
);
});
}
#[test]
fn report_equivocation_invalid_equivocation_proof() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let set_id = validator_set.id();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index];
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
// generate a key ownership proof at set id in era 1
let key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &equivocation_key)).unwrap();
let assert_invalid_equivocation_proof = |equivocation_proof| {
assert_err!(
Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
key_owner_proof.clone(),
),
Error::<Test>::InvalidEquivocationProof,
);
};
start_era(2);
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]);
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
// both votes target the same block number and payload,
// there is no equivocation.
assert_invalid_equivocation_proof(generate_equivocation_proof(
(block_num, payload1.clone(), set_id, &equivocation_keyring),
(block_num, payload1.clone(), set_id, &equivocation_keyring),
));
// votes targeting different rounds, there is no equivocation.
assert_invalid_equivocation_proof(generate_equivocation_proof(
(block_num, payload1.clone(), set_id, &equivocation_keyring),
(block_num + 1, payload2.clone(), set_id, &equivocation_keyring),
));
// votes signed with different authority keys
assert_invalid_equivocation_proof(generate_equivocation_proof(
(block_num, payload1.clone(), set_id, &equivocation_keyring),
(block_num, payload1.clone(), set_id, &BeefyKeyring::Charlie),
));
// votes signed with a key that isn't part of the authority set
assert_invalid_equivocation_proof(generate_equivocation_proof(
(block_num, payload1.clone(), set_id, &equivocation_keyring),
(block_num, payload1.clone(), set_id, &BeefyKeyring::Dave),
));
// votes targeting different set ids
assert_invalid_equivocation_proof(generate_equivocation_proof(
(block_num, payload1, set_id, &equivocation_keyring),
(block_num, payload2, set_id + 1, &equivocation_keyring),
));
});
}
#[test]
fn report_equivocation_validate_unsigned_prevents_duplicates() {
use sp_runtime::transaction_validity::{
InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
ValidTransaction,
};
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let set_id = validator_set.id();
// generate and report an equivocation for the validator at index 0
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index];
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]);
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
let equivocation_proof = generate_equivocation_proof(
(block_num, payload1, set_id, &equivocation_keyring),
(block_num, payload2, set_id, &equivocation_keyring),
);
let key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &equivocation_key)).unwrap();
let call = Call::report_equivocation_unsigned {
equivocation_proof: Box::new(equivocation_proof.clone()),
key_owner_proof: key_owner_proof.clone(),
};
// only local/inblock reports are allowed
assert_eq!(
<Beefy as sp_runtime::traits::ValidateUnsigned>::validate_unsigned(
TransactionSource::External,
&call,
),
InvalidTransaction::Call.into(),
);
// the transaction is valid when passed as local
let tx_tag = (equivocation_key, set_id, 3u64);
assert_eq!(
<Beefy as sp_runtime::traits::ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call,
),
TransactionValidity::Ok(ValidTransaction {
priority: TransactionPriority::max_value(),
requires: vec![],
provides: vec![("BeefyEquivocation", tx_tag).encode()],
longevity: ReportLongevity::get(),
propagate: false,
})
);
// the pre dispatch checks should also pass
assert_ok!(<Beefy as sp_runtime::traits::ValidateUnsigned>::pre_dispatch(&call));
// we submit the report
Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
key_owner_proof,
)
.unwrap();
// the report should now be considered stale and the transaction is invalid
// the check for staleness should be done on both `validate_unsigned` and on `pre_dispatch`
assert_err!(
<Beefy as sp_runtime::traits::ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call,
),
InvalidTransaction::Stale,
);
assert_err!(
<Beefy as sp_runtime::traits::ValidateUnsigned>::pre_dispatch(&call),
InvalidTransaction::Stale,
);
});
}
#[test]
fn report_equivocation_has_valid_weight() {
// the weight depends on the size of the validator set,
// but there's a lower bound of 100 validators.
assert!((1..=100)
.map(<Test as Config>::WeightInfo::report_equivocation)
.collect::<Vec<_>>()
.windows(2)
.all(|w| w[0] == w[1]));
// after 100 validators the weight should keep increasing
// with every extra validator.
assert!((100..=1000)
.map(<Test as Config>::WeightInfo::report_equivocation)
.collect::<Vec<_>>()
.windows(2)
.all(|w| w[0].ref_time() < w[1].ref_time()));
}
#[test]
fn valid_equivocation_reports_dont_pay_fees() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let set_id = validator_set.id();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index];
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
// generate equivocation proof
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]);
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
let equivocation_proof = generate_equivocation_proof(
(block_num, payload1, set_id, &equivocation_keyring),
(block_num, payload2, set_id, &equivocation_keyring),
);
// create the key ownership proof.
let key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &equivocation_key)).unwrap();
// check the dispatch info for the call.
let info = Call::<Test>::report_equivocation_unsigned {
equivocation_proof: Box::new(equivocation_proof.clone()),
key_owner_proof: key_owner_proof.clone(),
}
.get_dispatch_info();
// it should have non-zero weight and the fee has to be paid.
assert!(info.weight.any_gt(Weight::zero()));
assert_eq!(info.pays_fee, Pays::Yes);
// report the equivocation.
let post_info = Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof.clone()),
key_owner_proof.clone(),
)
.unwrap();
// the original weight should be kept, but given that the report
// is valid the fee is waived.
assert!(post_info.actual_weight.is_none());
assert_eq!(post_info.pays_fee, Pays::No);
// report the equivocation again which is invalid now since it is
// duplicate.
let post_info = Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
key_owner_proof,
)
.err()
.unwrap()
.post_info;
// the fee is not waived and the original weight is kept.
assert!(post_info.actual_weight.is_none());
assert_eq!(post_info.pays_fee, Pays::Yes);
})
}
+131 -75
View File
@@ -34,10 +34,14 @@
mod commitment;
pub mod mmr;
mod payload;
#[cfg(feature = "std")]
mod test_utils;
pub mod witness;
pub use commitment::{Commitment, SignedCommitment, VersionedFinalityProof};
pub use payload::{known_payloads, BeefyPayloadId, Payload, PayloadProvider};
#[cfg(feature = "std")]
pub use test_utils::*;
use codec::{Codec, Decode, Encode};
use scale_info::TypeInfo;
@@ -183,6 +187,83 @@ pub struct VoteMessage<Number, Id, Signature> {
pub signature: Signature,
}
/// Proof of voter misbehavior on a given set id. Misbehavior/equivocation in
/// BEEFY happens when a voter votes on the same round/block for different payloads.
/// Proving is achieved by collecting the signed commitments of conflicting votes.
#[derive(Clone, Debug, Decode, Encode, PartialEq, TypeInfo)]
pub struct EquivocationProof<Number, Id, Signature> {
/// The first vote in the equivocation.
pub first: VoteMessage<Number, Id, Signature>,
/// The second vote in the equivocation.
pub second: VoteMessage<Number, Id, Signature>,
}
impl<Number, Id, Signature> EquivocationProof<Number, Id, Signature> {
/// Returns the authority id of the equivocator.
pub fn offender_id(&self) -> &Id {
&self.first.id
}
/// Returns the round number at which the equivocation occurred.
pub fn round_number(&self) -> &Number {
&self.first.commitment.block_number
}
/// Returns the set id at which the equivocation occurred.
pub fn set_id(&self) -> ValidatorSetId {
self.first.commitment.validator_set_id
}
}
/// Check a commitment signature by encoding the commitment and
/// verifying the provided signature using the expected authority id.
pub fn check_commitment_signature<Number, Id, MsgHash>(
commitment: &Commitment<Number>,
authority_id: &Id,
signature: &<Id as RuntimeAppPublic>::Signature,
) -> bool
where
Id: BeefyAuthorityId<MsgHash>,
Number: Clone + Encode + PartialEq,
MsgHash: Hash,
{
let encoded_commitment = commitment.encode();
BeefyAuthorityId::<MsgHash>::verify(authority_id, signature, &encoded_commitment)
}
/// Verifies the equivocation proof by making sure that both votes target
/// different blocks and that its signatures are valid.
pub fn check_equivocation_proof<Number, Id, MsgHash>(
report: &EquivocationProof<Number, Id, <Id as RuntimeAppPublic>::Signature>,
) -> bool
where
Id: BeefyAuthorityId<MsgHash> + PartialEq,
Number: Clone + Encode + PartialEq,
MsgHash: Hash,
{
let first = &report.first;
let second = &report.second;
// if votes
// come from different authorities,
// are for different rounds,
// have different validator set ids,
// or both votes have the same commitment,
// --> the equivocation is invalid.
if first.id != second.id ||
first.commitment.block_number != second.commitment.block_number ||
first.commitment.validator_set_id != second.commitment.validator_set_id ||
first.commitment.payload == second.commitment.payload
{
return false
}
// check signatures on both votes are valid
let valid_first = check_commitment_signature(&first.commitment, &first.id, &first.signature);
let valid_second =
check_commitment_signature(&second.commitment, &second.id, &second.signature);
return valid_first && valid_second
}
/// New BEEFY validator set notification hook.
pub trait OnNewValidatorSet<AuthorityId> {
/// Function called by the pallet when BEEFY validator set changes.
@@ -197,6 +278,28 @@ impl<AuthorityId> OnNewValidatorSet<AuthorityId> for () {
fn on_new_validator_set(_: &ValidatorSet<AuthorityId>, _: &ValidatorSet<AuthorityId>) {}
}
/// An opaque type used to represent the key ownership proof at the runtime API
/// boundary. The inner value is an encoded representation of the actual key
/// ownership proof which will be parameterized when defining the runtime. At
/// the runtime API boundary this type is unknown and as such we keep this
/// opaque representation, implementors of the runtime API will have to make
/// sure that all usages of `OpaqueKeyOwnershipProof` refer to the same type.
#[derive(Decode, Encode, PartialEq)]
pub struct OpaqueKeyOwnershipProof(Vec<u8>);
impl OpaqueKeyOwnershipProof {
/// Create a new `OpaqueKeyOwnershipProof` using the given encoded
/// representation.
pub fn new(inner: Vec<u8>) -> OpaqueKeyOwnershipProof {
OpaqueKeyOwnershipProof(inner)
}
/// Try to decode this `OpaqueKeyOwnershipProof` into the given concrete key
/// ownership proof type.
pub fn decode<T: Decode>(self) -> Option<T> {
codec::Decode::decode(&mut &self.0[..]).ok()
}
}
sp_api::decl_runtime_apis! {
/// API necessary for BEEFY voters.
pub trait BeefyApi
@@ -206,83 +309,36 @@ sp_api::decl_runtime_apis! {
/// Return the current active BEEFY validator set
fn validator_set() -> Option<ValidatorSet<crypto::AuthorityId>>;
}
}
#[cfg(feature = "std")]
/// Test accounts using [`crate::crypto`] types.
pub mod keyring {
use super::*;
use sp_core::{ecdsa, keccak_256, Pair};
use std::collections::HashMap;
use strum::IntoEnumIterator;
/// Submits an unsigned extrinsic to report an equivocation. The caller
/// must provide the equivocation proof and a key ownership proof
/// (should be obtained using `generate_key_ownership_proof`). The
/// extrinsic will be unsigned and should only be accepted for local
/// authorship (not to be broadcast to the network). This method returns
/// `None` when creation of the extrinsic fails, e.g. if equivocation
/// reporting is disabled for the given runtime (i.e. this method is
/// hardcoded to return `None`). Only useful in an offchain context.
fn submit_report_equivocation_unsigned_extrinsic(
equivocation_proof:
EquivocationProof<NumberFor<Block>, crypto::AuthorityId, crypto::Signature>,
key_owner_proof: OpaqueKeyOwnershipProof,
) -> Option<()>;
/// Set of test accounts using [`crate::crypto`] types.
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, strum::EnumIter)]
pub enum Keyring {
Alice,
Bob,
Charlie,
Dave,
Eve,
Ferdie,
One,
Two,
}
impl Keyring {
/// Sign `msg`.
pub fn sign(self, msg: &[u8]) -> crypto::Signature {
// todo: use custom signature hashing type
let msg = keccak_256(msg);
ecdsa::Pair::from(self).sign_prehashed(&msg).into()
}
/// Return key pair.
pub fn pair(self) -> crypto::Pair {
ecdsa::Pair::from_string(self.to_seed().as_str(), None).unwrap().into()
}
/// Return public key.
pub fn public(self) -> crypto::Public {
self.pair().public()
}
/// Return seed string.
pub fn to_seed(self) -> String {
format!("//{}", self)
}
/// Get Keyring from public key.
pub fn from_public(who: &crypto::Public) -> Option<Keyring> {
Self::iter().find(|&k| &crypto::Public::from(k) == who)
}
}
lazy_static::lazy_static! {
static ref PRIVATE_KEYS: HashMap<Keyring, crypto::Pair> =
Keyring::iter().map(|i| (i, i.pair())).collect();
static ref PUBLIC_KEYS: HashMap<Keyring, crypto::Public> =
PRIVATE_KEYS.iter().map(|(&name, pair)| (name, pair.public())).collect();
}
impl From<Keyring> for crypto::Pair {
fn from(k: Keyring) -> Self {
k.pair()
}
}
impl From<Keyring> for ecdsa::Pair {
fn from(k: Keyring) -> Self {
k.pair().into()
}
}
impl From<Keyring> for crypto::Public {
fn from(k: Keyring) -> Self {
(*PUBLIC_KEYS).get(&k).cloned().unwrap()
}
/// Generates a proof of key ownership for the given authority in the
/// given set. An example usage of this module is coupled with the
/// session historical module to prove that a given authority key is
/// tied to a given staking identity during a specific session. Proofs
/// of key ownership are necessary for submitting equivocation reports.
/// NOTE: even though the API takes a `set_id` as parameter the current
/// implementations ignores this parameter and instead relies on this
/// method being called at the correct block height, i.e. any point at
/// which the given set id is live on-chain. Future implementations will
/// instead use indexed data through an offchain worker, not requiring
/// older states to be available.
fn generate_key_ownership_proof(
set_id: ValidatorSetId,
authority_id: crypto::AuthorityId,
) -> Option<OpaqueKeyOwnershipProof>;
}
}
@@ -0,0 +1,110 @@
// This file is part of Substrate.
// Copyright (C) 2021-2023 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![cfg(feature = "std")]
use crate::{crypto, Commitment, EquivocationProof, Payload, ValidatorSetId, VoteMessage};
use codec::Encode;
use sp_core::{ecdsa, keccak_256, Pair};
use std::collections::HashMap;
use strum::IntoEnumIterator;
/// Set of test accounts using [`crate::crypto`] types.
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, strum::EnumIter)]
pub enum Keyring {
Alice,
Bob,
Charlie,
Dave,
Eve,
Ferdie,
One,
Two,
}
impl Keyring {
/// Sign `msg`.
pub fn sign(self, msg: &[u8]) -> crypto::Signature {
// todo: use custom signature hashing type
let msg = keccak_256(msg);
ecdsa::Pair::from(self).sign_prehashed(&msg).into()
}
/// Return key pair.
pub fn pair(self) -> crypto::Pair {
ecdsa::Pair::from_string(self.to_seed().as_str(), None).unwrap().into()
}
/// Return public key.
pub fn public(self) -> crypto::Public {
self.pair().public()
}
/// Return seed string.
pub fn to_seed(self) -> String {
format!("//{}", self)
}
/// Get Keyring from public key.
pub fn from_public(who: &crypto::Public) -> Option<Keyring> {
Self::iter().find(|&k| &crypto::Public::from(k) == who)
}
}
lazy_static::lazy_static! {
static ref PRIVATE_KEYS: HashMap<Keyring, crypto::Pair> =
Keyring::iter().map(|i| (i, i.pair())).collect();
static ref PUBLIC_KEYS: HashMap<Keyring, crypto::Public> =
PRIVATE_KEYS.iter().map(|(&name, pair)| (name, pair.public())).collect();
}
impl From<Keyring> for crypto::Pair {
fn from(k: Keyring) -> Self {
k.pair()
}
}
impl From<Keyring> for ecdsa::Pair {
fn from(k: Keyring) -> Self {
k.pair().into()
}
}
impl From<Keyring> for crypto::Public {
fn from(k: Keyring) -> Self {
(*PUBLIC_KEYS).get(&k).cloned().unwrap()
}
}
/// Create a new `EquivocationProof` based on given arguments.
pub fn generate_equivocation_proof(
vote1: (u64, Payload, ValidatorSetId, &Keyring),
vote2: (u64, Payload, ValidatorSetId, &Keyring),
) -> EquivocationProof<u64, crypto::Public, crypto::Signature> {
let signed_vote = |block_number: u64,
payload: Payload,
validator_set_id: ValidatorSetId,
keyring: &Keyring| {
let commitment = Commitment { validator_set_id, block_number, payload };
let signature = keyring.sign(&commitment.encode());
VoteMessage { commitment, id: keyring.public(), signature }
};
let first = signed_vote(vote1.0, vote1.1, vote1.2, vote1.3);
let second = signed_vote(vote2.0, vote2.1, vote2.2, vote2.3);
EquivocationProof { first, second }
}
+14
View File
@@ -980,6 +980,20 @@ cfg_if! {
fn validator_set() -> Option<beefy_primitives::ValidatorSet<beefy_primitives::crypto::AuthorityId>> {
None
}
fn submit_report_equivocation_unsigned_extrinsic(
_equivocation_proof: beefy_primitives::EquivocationProof<
NumberFor<Block>,
beefy_primitives::crypto::AuthorityId,
beefy_primitives::crypto::Signature
>,
_key_owner_proof: beefy_primitives::OpaqueKeyOwnershipProof,
) -> Option<()> { None }
fn generate_key_ownership_proof(
_set_id: beefy_primitives::ValidatorSetId,
_authority_id: beefy_primitives::crypto::AuthorityId,
) -> Option<beefy_primitives::OpaqueKeyOwnershipProof> { None }
}
impl pallet_beefy_mmr::BeefyMmrApi<Block, beefy_primitives::MmrRootHash> for Runtime {