mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-12 22:51:13 +00:00
BEEFY: implement equivocations detection, reporting and slashing (#13121)
* client/beefy: simplify self_vote logic * client/beefy: migrate to new state version * client/beefy: detect equivocated votes * fix typos * sp-beefy: add equivocation primitives * client/beefy: refactor vote processing * fix version migration for new rounds struct * client/beefy: track equivocations and create proofs * client/beefy: adjust tests for new voting logic * sp-beefy: fix commitment ordering and equality * client/beefy: simplify handle_vote() a bit * client/beefy: add simple equivocation test * client/beefy: submit equivocation proof - WIP * frame/beefy: add equivocation report runtime api - part 1 * frame/beefy: report equivocation logic - part 2 * frame/beefy: add pluggable Equivocation handler - part 3 * frame/beefy: impl ValidateUnsigned for equivocations reporting * client/beefy: submit report equivocation unsigned extrinsic * primitives/beefy: fix tests * frame/beefy: add default weights * frame/beefy: fix tests * client/beefy: fix tests * frame/beefy-mmr: fix tests * frame/beefy: cross-check session index with equivocation report * sp-beefy: make test Keyring useable in pallet * frame/beefy: add basic equivocation test * frame/beefy: test verify equivocation results in slashing * frame/beefy: test report_equivocation_old_set * frame/beefy: add more equivocation tests * sp-beefy: fix docs * beefy: simplify equivocations and fix tests * client/beefy: address review comments * frame/beefy: add ValidateUnsigned to test/mock runtime * client/beefy: fixes after merge master * fix missed merge damage * client/beefy: add test for reporting equivocations Also validated there's no unexpected equivocations reported in the other tests. Signed-off-by: acatangiu <adrian@parity.io> * sp-beefy: move test utils to their own file * client/beefy: add negative test for equivocation reports * sp-beefy: move back MmrRootProvider - used in polkadot-service * impl review suggestions * client/beefy: add equivocation metrics --------- Signed-off-by: acatangiu <adrian@parity.io> Co-authored-by: parity-processbot <>
This commit is contained in:
Generated
+8
@@ -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::*;
|
||||
|
||||
@@ -22,14 +22,30 @@
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Backend: {0}")]
|
||||
Backend(String),
|
||||
#[error("Keystore error: {0}")]
|
||||
Keystore(String),
|
||||
#[error("Runtime api error: {0}")]
|
||||
RuntimeApi(sp_api::ApiError),
|
||||
#[error("Signature error: {0}")]
|
||||
Signature(String),
|
||||
#[error("Session uninitialized")]
|
||||
UninitSession,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl PartialEq for Error {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Error::Backend(s1), Error::Backend(s2)) => s1 == s2,
|
||||
(Error::Keystore(s1), Error::Keystore(s2)) => s1 == s2,
|
||||
(Error::RuntimeApi(_), Error::RuntimeApi(_)) => true,
|
||||
(Error::Signature(s1), Error::Signature(s2)) => s1 == s2,
|
||||
(Error::UninitSession, Error::UninitSession) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +81,7 @@ fn verify_with_validator_set<Block: BlockT>(
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use beefy_primitives::{
|
||||
keyring::Keyring, known_payloads, Commitment, Payload, SignedCommitment,
|
||||
VersionedFinalityProof,
|
||||
known_payloads, Commitment, Keyring, Payload, SignedCommitment, VersionedFinalityProof,
|
||||
};
|
||||
use substrate_test_runtime_client::runtime::Block;
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ pub mod tests {
|
||||
use sc_keystore::LocalKeystore;
|
||||
use sp_core::{ecdsa, Pair};
|
||||
|
||||
use beefy_primitives::{crypto, keyring::Keyring};
|
||||
use beefy_primitives::{crypto, Keyring};
|
||||
|
||||
use super::*;
|
||||
use crate::error::Error;
|
||||
|
||||
@@ -282,6 +282,7 @@ where
|
||||
let worker_params = worker::WorkerParams {
|
||||
backend,
|
||||
payload_provider,
|
||||
runtime,
|
||||
network,
|
||||
key_store: key_store.into(),
|
||||
gossip_engine,
|
||||
@@ -292,7 +293,7 @@ where
|
||||
persisted_state,
|
||||
};
|
||||
|
||||
let worker = worker::BeefyWorker::<_, _, _, _>::new(worker_params);
|
||||
let worker = worker::BeefyWorker::<_, _, _, _, _>::new(worker_params);
|
||||
|
||||
futures::future::join(
|
||||
worker.run(block_import_justif, finality_notifications),
|
||||
|
||||
@@ -46,10 +46,16 @@ pub struct VoterMetrics {
|
||||
pub beefy_no_authority_found_in_store: Counter<U64>,
|
||||
/// Number of currently buffered votes
|
||||
pub beefy_buffered_votes: Gauge<U64>,
|
||||
/// Number of valid but stale votes received
|
||||
pub beefy_stale_votes: Counter<U64>,
|
||||
/// Number of votes dropped due to full buffers
|
||||
pub beefy_buffered_votes_dropped: Counter<U64>,
|
||||
/// Number of good votes successfully handled
|
||||
pub beefy_good_votes_processed: Counter<U64>,
|
||||
/// Number of equivocation votes received
|
||||
pub beefy_equivocation_votes: Counter<U64>,
|
||||
/// Number of invalid votes received
|
||||
pub beefy_invalid_votes: Counter<U64>,
|
||||
/// Number of valid but stale votes received
|
||||
pub beefy_stale_votes: Counter<U64>,
|
||||
/// Number of currently buffered justifications
|
||||
pub beefy_buffered_justifications: Gauge<U64>,
|
||||
/// Number of valid but stale justifications received
|
||||
@@ -60,8 +66,6 @@ pub struct VoterMetrics {
|
||||
pub beefy_buffered_justifications_dropped: Counter<U64>,
|
||||
/// Trying to set Best Beefy block to old block
|
||||
pub beefy_best_block_set_last_failure: Gauge<U64>,
|
||||
/// Number of Successful handled votes
|
||||
pub beefy_successful_handled_votes: Counter<U64>,
|
||||
}
|
||||
|
||||
impl PrometheusRegister for VoterMetrics {
|
||||
@@ -109,13 +113,6 @@ impl PrometheusRegister for VoterMetrics {
|
||||
Gauge::new("substrate_beefy_buffered_votes", "Number of currently buffered votes")?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_stale_votes: register(
|
||||
Counter::new(
|
||||
"substrate_beefy_stale_votes",
|
||||
"Number of valid but stale votes received",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_buffered_votes_dropped: register(
|
||||
Counter::new(
|
||||
"substrate_beefy_buffered_votes_dropped",
|
||||
@@ -123,6 +120,31 @@ impl PrometheusRegister for VoterMetrics {
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_good_votes_processed: register(
|
||||
Counter::new(
|
||||
"substrate_beefy_successful_handled_votes",
|
||||
"Number of good votes successfully handled",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_equivocation_votes: register(
|
||||
Counter::new(
|
||||
"substrate_beefy_stale_votes",
|
||||
"Number of equivocation votes received",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_invalid_votes: register(
|
||||
Counter::new("substrate_beefy_stale_votes", "Number of invalid votes received")?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_stale_votes: register(
|
||||
Counter::new(
|
||||
"substrate_beefy_stale_votes",
|
||||
"Number of valid but stale votes received",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_buffered_justifications: register(
|
||||
Gauge::new(
|
||||
"substrate_beefy_buffered_justifications",
|
||||
@@ -158,13 +180,6 @@ impl PrometheusRegister for VoterMetrics {
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
beefy_successful_handled_votes: register(
|
||||
Counter::new(
|
||||
"substrate_beefy_successful_handled_votes",
|
||||
"Number of Successful handled votes",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::LOG_TARGET;
|
||||
|
||||
use beefy_primitives::{
|
||||
crypto::{AuthorityId, Public, Signature},
|
||||
Commitment, SignedCommitment, ValidatorSet, ValidatorSetId, VoteMessage,
|
||||
Commitment, EquivocationProof, SignedCommitment, ValidatorSet, ValidatorSetId, VoteMessage,
|
||||
};
|
||||
use codec::{Decode, Encode};
|
||||
use log::debug;
|
||||
@@ -61,7 +61,7 @@ pub fn threshold(authorities: usize) -> usize {
|
||||
pub enum VoteImportResult<B: Block> {
|
||||
Ok,
|
||||
RoundConcluded(SignedCommitment<NumberFor<B>, Signature>),
|
||||
Equivocation, /* TODO: (EquivocationProof<NumberFor<B>, Public, Signature>) */
|
||||
Equivocation(EquivocationProof<NumberFor<B>, Public, Signature>),
|
||||
Invalid,
|
||||
Stale,
|
||||
}
|
||||
@@ -149,8 +149,10 @@ where
|
||||
target: LOG_TARGET,
|
||||
"🥩 detected equivocated vote: 1st: {:?}, 2nd: {:?}", previous_vote, vote
|
||||
);
|
||||
// TODO: build `EquivocationProof` and return it here.
|
||||
return VoteImportResult::Equivocation
|
||||
return VoteImportResult::Equivocation(EquivocationProof {
|
||||
first: previous_vote.clone(),
|
||||
second: vote,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// this is the first vote sent by `id` for `num`, all good
|
||||
@@ -197,8 +199,8 @@ mod tests {
|
||||
use sc_network_test::Block;
|
||||
|
||||
use beefy_primitives::{
|
||||
crypto::Public, keyring::Keyring, known_payloads::MMR_ROOT_ID, Commitment, Payload,
|
||||
SignedCommitment, ValidatorSet, VoteMessage,
|
||||
crypto::Public, known_payloads::MMR_ROOT_ID, Commitment, EquivocationProof, Keyring,
|
||||
Payload, SignedCommitment, ValidatorSet, VoteMessage,
|
||||
};
|
||||
|
||||
use super::{threshold, Block as BlockT, RoundTracker, Rounds};
|
||||
@@ -452,4 +454,42 @@ mod tests {
|
||||
rounds.conclude(3);
|
||||
assert!(rounds.previous_votes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_provide_equivocation_proof() {
|
||||
sp_tracing::try_init_simple();
|
||||
|
||||
let validators = ValidatorSet::<Public>::new(
|
||||
vec![Keyring::Alice.public(), Keyring::Bob.public()],
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let validator_set_id = validators.id();
|
||||
let session_start = 1u64.into();
|
||||
let mut rounds = Rounds::<Block>::new(session_start, validators);
|
||||
|
||||
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![1, 1, 1, 1]);
|
||||
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![2, 2, 2, 2]);
|
||||
let commitment1 = Commitment { block_number: 1, payload: payload1, validator_set_id };
|
||||
let commitment2 = Commitment { block_number: 1, payload: payload2, validator_set_id };
|
||||
|
||||
let alice_vote1 = VoteMessage {
|
||||
id: Keyring::Alice.public(),
|
||||
commitment: commitment1,
|
||||
signature: Keyring::Alice.sign(b"I am committed"),
|
||||
};
|
||||
let mut alice_vote2 = alice_vote1.clone();
|
||||
alice_vote2.commitment = commitment2;
|
||||
|
||||
let expected_result = VoteImportResult::Equivocation(EquivocationProof {
|
||||
first: alice_vote1.clone(),
|
||||
second: alice_vote2.clone(),
|
||||
});
|
||||
|
||||
// vote on one payload - ok
|
||||
assert_eq!(rounds.add_vote(alice_vote1), VoteImportResult::Ok);
|
||||
|
||||
// vote on _another_ commitment/payload -> expected equivocation proof
|
||||
assert_eq!(rounds.add_vote(alice_vote2), expected_result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ use crate::{
|
||||
};
|
||||
use beefy_primitives::{
|
||||
crypto::{AuthorityId, Signature},
|
||||
keyring::Keyring as BeefyKeyring,
|
||||
known_payloads,
|
||||
mmr::MmrRootProvider,
|
||||
BeefyApi, Commitment, ConsensusLog, MmrRootHash, Payload, SignedCommitment, ValidatorSet,
|
||||
BeefyApi, Commitment, ConsensusLog, EquivocationProof, Keyring as BeefyKeyring, MmrRootHash,
|
||||
OpaqueKeyOwnershipProof, Payload, SignedCommitment, ValidatorSet, ValidatorSetId,
|
||||
VersionedFinalityProof, BEEFY_ENGINE_ID, KEY_TYPE as BeefyKeyType,
|
||||
};
|
||||
use futures::{future, stream::FuturesUnordered, Future, StreamExt};
|
||||
@@ -55,7 +55,7 @@ use sp_api::{ApiRef, ProvideRuntimeApi};
|
||||
use sp_consensus::BlockOrigin;
|
||||
use sp_core::H256;
|
||||
use sp_keystore::{testing::KeyStore as TestKeystore, SyncCryptoStore, SyncCryptoStorePtr};
|
||||
use sp_mmr_primitives::{EncodableOpaqueLeaf, Error as MmrError, MmrApi, Proof};
|
||||
use sp_mmr_primitives::{Error as MmrError, MmrApi};
|
||||
use sp_runtime::{
|
||||
codec::Encode,
|
||||
generic::BlockId,
|
||||
@@ -73,6 +73,7 @@ fn beefy_gossip_proto_name() -> ProtocolName {
|
||||
|
||||
const GOOD_MMR_ROOT: MmrRootHash = MmrRootHash::repeat_byte(0xbf);
|
||||
const BAD_MMR_ROOT: MmrRootHash = MmrRootHash::repeat_byte(0x42);
|
||||
const ALTERNATE_BAD_MMR_ROOT: MmrRootHash = MmrRootHash::repeat_byte(0x13);
|
||||
|
||||
type BeefyBlockImport = crate::BeefyBlockImport<
|
||||
Block,
|
||||
@@ -236,6 +237,8 @@ pub(crate) struct TestApi {
|
||||
pub beefy_genesis: u64,
|
||||
pub validator_set: BeefyValidatorSet,
|
||||
pub mmr_root_hash: MmrRootHash,
|
||||
pub reported_equivocations:
|
||||
Option<Arc<Mutex<Vec<EquivocationProof<NumberFor<Block>, AuthorityId, Signature>>>>>,
|
||||
}
|
||||
|
||||
impl TestApi {
|
||||
@@ -244,7 +247,12 @@ impl TestApi {
|
||||
validator_set: &BeefyValidatorSet,
|
||||
mmr_root_hash: MmrRootHash,
|
||||
) -> Self {
|
||||
TestApi { beefy_genesis, validator_set: validator_set.clone(), mmr_root_hash }
|
||||
TestApi {
|
||||
beefy_genesis,
|
||||
validator_set: validator_set.clone(),
|
||||
mmr_root_hash,
|
||||
reported_equivocations: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_validator_set(validator_set: &BeefyValidatorSet) -> Self {
|
||||
@@ -252,8 +260,13 @@ impl TestApi {
|
||||
beefy_genesis: 1,
|
||||
validator_set: validator_set.clone(),
|
||||
mmr_root_hash: GOOD_MMR_ROOT,
|
||||
reported_equivocations: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allow_equivocations(&mut self) {
|
||||
self.reported_equivocations = Some(Arc::new(Mutex::new(vec![])));
|
||||
}
|
||||
}
|
||||
|
||||
// compiler gets confused and warns us about unused inner
|
||||
@@ -277,31 +290,29 @@ sp_api::mock_impl_runtime_apis! {
|
||||
fn validator_set() -> Option<BeefyValidatorSet> {
|
||||
Some(self.inner.validator_set.clone())
|
||||
}
|
||||
|
||||
fn submit_report_equivocation_unsigned_extrinsic(
|
||||
proof: EquivocationProof<NumberFor<Block>, AuthorityId, Signature>,
|
||||
_dummy: OpaqueKeyOwnershipProof,
|
||||
) -> Option<()> {
|
||||
if let Some(equivocations_buf) = self.inner.reported_equivocations.as_ref() {
|
||||
equivocations_buf.lock().push(proof);
|
||||
None
|
||||
} else {
|
||||
panic!("Equivocations not expected, but following proof was reported: {:?}", proof);
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_key_ownership_proof(
|
||||
_dummy1: ValidatorSetId,
|
||||
_dummy2: AuthorityId,
|
||||
) -> Option<OpaqueKeyOwnershipProof> { Some(OpaqueKeyOwnershipProof::new(vec![])) }
|
||||
}
|
||||
|
||||
impl MmrApi<Block, MmrRootHash, NumberFor<Block>> for RuntimeApi {
|
||||
fn mmr_root() -> Result<MmrRootHash, MmrError> {
|
||||
Ok(self.inner.mmr_root_hash)
|
||||
}
|
||||
|
||||
fn generate_proof(
|
||||
_block_numbers: Vec<u64>,
|
||||
_best_known_block_number: Option<u64>
|
||||
) -> Result<(Vec<EncodableOpaqueLeaf>, Proof<MmrRootHash>), MmrError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn verify_proof(_leaves: Vec<EncodableOpaqueLeaf>, _proof: Proof<MmrRootHash>) -> Result<(), MmrError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn verify_proof_stateless(
|
||||
_root: MmrRootHash,
|
||||
_leaves: Vec<EncodableOpaqueLeaf>,
|
||||
_proof: Proof<MmrRootHash>
|
||||
) -> Result<(), MmrError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +341,7 @@ pub(crate) fn create_beefy_keystore(authority: BeefyKeyring) -> SyncCryptoStoreP
|
||||
keystore
|
||||
}
|
||||
|
||||
fn voter_init_setup(
|
||||
async fn voter_init_setup(
|
||||
net: &mut BeefyTestNet,
|
||||
finality: &mut futures::stream::Fuse<FinalityNotifications<Block>>,
|
||||
api: &TestApi,
|
||||
@@ -345,9 +356,7 @@ fn voter_init_setup(
|
||||
gossip_validator,
|
||||
None,
|
||||
);
|
||||
let best_grandpa =
|
||||
futures::executor::block_on(wait_for_runtime_pallet(api, &mut gossip_engine, finality))
|
||||
.unwrap();
|
||||
let best_grandpa = wait_for_runtime_pallet(api, &mut gossip_engine, finality).await.unwrap();
|
||||
load_or_init_voter_state(&*backend, api, best_grandpa, 1)
|
||||
}
|
||||
|
||||
@@ -980,7 +989,7 @@ async fn should_initialize_voter_at_genesis() {
|
||||
|
||||
let api = TestApi::with_validator_set(&validator_set);
|
||||
// load persistent state - nothing in DB, should init at genesis
|
||||
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).unwrap();
|
||||
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).await.unwrap();
|
||||
|
||||
// Test initialization at session boundary.
|
||||
// verify voter initialized with two sessions starting at blocks 1 and 10
|
||||
@@ -1029,7 +1038,7 @@ async fn should_initialize_voter_at_custom_genesis() {
|
||||
net.peer(0).client().as_client().finalize_block(hashes[8], None).unwrap();
|
||||
|
||||
// load persistent state - nothing in DB, should init at genesis
|
||||
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).unwrap();
|
||||
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).await.unwrap();
|
||||
|
||||
// Test initialization at session boundary.
|
||||
// verify voter initialized with single session starting at block `custom_pallet_genesis` (7)
|
||||
@@ -1090,7 +1099,7 @@ async fn should_initialize_voter_when_last_final_is_session_boundary() {
|
||||
|
||||
let api = TestApi::with_validator_set(&validator_set);
|
||||
// load persistent state - nothing in DB, should init at session boundary
|
||||
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).unwrap();
|
||||
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).await.unwrap();
|
||||
|
||||
// verify voter initialized with single session starting at block 10
|
||||
assert_eq!(persisted_state.voting_oracle().sessions().len(), 1);
|
||||
@@ -1148,7 +1157,7 @@ async fn should_initialize_voter_at_latest_finalized() {
|
||||
|
||||
let api = TestApi::with_validator_set(&validator_set);
|
||||
// load persistent state - nothing in DB, should init at last BEEFY finalized
|
||||
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).unwrap();
|
||||
let persisted_state = voter_init_setup(&mut net, &mut finality, &api).await.unwrap();
|
||||
|
||||
// verify voter initialized with single session starting at block 12
|
||||
assert_eq!(persisted_state.voting_oracle().sessions().len(), 1);
|
||||
@@ -1205,3 +1214,59 @@ async fn beefy_finalizing_after_pallet_genesis() {
|
||||
// GRANDPA finalize #21 -> BEEFY finalize #20 (mandatory) and #21
|
||||
finalize_block_and_wait_for_beefy(&net, peers.clone(), &[hashes[21]], &[20, 21]).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn beefy_reports_equivocations() {
|
||||
sp_tracing::try_init_simple();
|
||||
|
||||
let peers = [BeefyKeyring::Alice, BeefyKeyring::Bob, BeefyKeyring::Charlie];
|
||||
let validator_set = ValidatorSet::new(make_beefy_ids(&peers), 0).unwrap();
|
||||
let session_len = 10;
|
||||
let min_block_delta = 4;
|
||||
|
||||
let mut net = BeefyTestNet::new(3);
|
||||
|
||||
// Alice votes on good MMR roots, equivocations are allowed/expected.
|
||||
let mut api_alice = TestApi::with_validator_set(&validator_set);
|
||||
api_alice.allow_equivocations();
|
||||
let api_alice = Arc::new(api_alice);
|
||||
let alice = (0, &peers[0], api_alice.clone());
|
||||
tokio::spawn(initialize_beefy(&mut net, vec![alice], min_block_delta));
|
||||
|
||||
// Bob votes on bad MMR roots, equivocations are allowed/expected.
|
||||
let mut api_bob = TestApi::new(1, &validator_set, BAD_MMR_ROOT);
|
||||
api_bob.allow_equivocations();
|
||||
let api_bob = Arc::new(api_bob);
|
||||
let bob = (1, &peers[1], api_bob.clone());
|
||||
tokio::spawn(initialize_beefy(&mut net, vec![bob], min_block_delta));
|
||||
|
||||
// We spawn another node voting with Bob key, on alternate bad MMR roots (equivocating).
|
||||
// Equivocations are allowed/expected.
|
||||
let mut api_bob_prime = TestApi::new(1, &validator_set, ALTERNATE_BAD_MMR_ROOT);
|
||||
api_bob_prime.allow_equivocations();
|
||||
let api_bob_prime = Arc::new(api_bob_prime);
|
||||
let bob_prime = (2, &BeefyKeyring::Bob, api_bob_prime.clone());
|
||||
tokio::spawn(initialize_beefy(&mut net, vec![bob_prime], min_block_delta));
|
||||
|
||||
// push 42 blocks including `AuthorityChange` digests every 10 blocks.
|
||||
let hashes = net.generate_blocks_and_sync(42, session_len, &validator_set, false).await;
|
||||
|
||||
let net = Arc::new(Mutex::new(net));
|
||||
|
||||
// Minimum BEEFY block delta is 4.
|
||||
|
||||
let peers = peers.into_iter().enumerate();
|
||||
// finalize block #1 -> BEEFY should not finalize anything (each node votes on different MMR).
|
||||
finalize_block_and_wait_for_beefy(&net, peers, &[hashes[1]], &[]).await;
|
||||
|
||||
// Verify neither Bob or Bob_Prime report themselves as equivocating.
|
||||
assert!(api_bob.reported_equivocations.as_ref().unwrap().lock().is_empty());
|
||||
assert!(api_bob_prime.reported_equivocations.as_ref().unwrap().lock().is_empty());
|
||||
|
||||
// Verify Alice reports Bob/Bob_Prime equivocation.
|
||||
let alice_reported_equivocations = api_alice.reported_equivocations.as_ref().unwrap().lock();
|
||||
assert_eq!(alice_reported_equivocations.len(), 1);
|
||||
let equivocation_proof = alice_reported_equivocations.get(0).unwrap();
|
||||
assert_eq!(equivocation_proof.first.id, BeefyKeyring::Bob.public());
|
||||
assert_eq!(equivocation_proof.first.commitment.block_number, 1);
|
||||
}
|
||||
|
||||
@@ -23,16 +23,17 @@ use crate::{
|
||||
},
|
||||
error::Error,
|
||||
justification::BeefyVersionedFinalityProof,
|
||||
keystore::BeefyKeystore,
|
||||
keystore::{BeefyKeystore, BeefySignatureHasher},
|
||||
metric_get, metric_inc, metric_set,
|
||||
metrics::VoterMetrics,
|
||||
round::{Rounds, VoteImportResult},
|
||||
BeefyVoterLinks, LOG_TARGET,
|
||||
};
|
||||
use beefy_primitives::{
|
||||
check_equivocation_proof,
|
||||
crypto::{AuthorityId, Signature},
|
||||
Commitment, ConsensusLog, PayloadProvider, ValidatorSet, VersionedFinalityProof, VoteMessage,
|
||||
BEEFY_ENGINE_ID,
|
||||
BeefyApi, Commitment, ConsensusLog, EquivocationProof, PayloadProvider, ValidatorSet,
|
||||
VersionedFinalityProof, VoteMessage, BEEFY_ENGINE_ID,
|
||||
};
|
||||
use codec::{Codec, Decode, Encode};
|
||||
use futures::{stream::Fuse, FutureExt, StreamExt};
|
||||
@@ -41,7 +42,7 @@ use sc_client_api::{Backend, FinalityNotification, FinalityNotifications, Header
|
||||
use sc_network_common::service::{NetworkEventStream, NetworkRequest};
|
||||
use sc_network_gossip::GossipEngine;
|
||||
use sc_utils::notification::NotificationReceiver;
|
||||
use sp_api::BlockId;
|
||||
use sp_api::{BlockId, ProvideRuntimeApi};
|
||||
use sp_arithmetic::traits::{AtLeast32Bit, Saturating};
|
||||
use sp_consensus::SyncOracle;
|
||||
use sp_runtime::{
|
||||
@@ -243,9 +244,10 @@ impl<B: Block> VoterOracle<B> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WorkerParams<B: Block, BE, P, N> {
|
||||
pub(crate) struct WorkerParams<B: Block, BE, P, R, N> {
|
||||
pub backend: Arc<BE>,
|
||||
pub payload_provider: P,
|
||||
pub runtime: Arc<R>,
|
||||
pub network: N,
|
||||
pub key_store: BeefyKeystore,
|
||||
pub gossip_engine: GossipEngine<B>,
|
||||
@@ -294,10 +296,11 @@ impl<B: Block> PersistedState<B> {
|
||||
}
|
||||
|
||||
/// A BEEFY worker plays the BEEFY protocol
|
||||
pub(crate) struct BeefyWorker<B: Block, BE, P, N> {
|
||||
pub(crate) struct BeefyWorker<B: Block, BE, P, RuntimeApi, N> {
|
||||
// utilities
|
||||
backend: Arc<BE>,
|
||||
payload_provider: P,
|
||||
runtime: Arc<RuntimeApi>,
|
||||
network: N,
|
||||
key_store: BeefyKeystore,
|
||||
|
||||
@@ -327,11 +330,13 @@ pub(crate) struct BeefyWorker<B: Block, BE, P, N> {
|
||||
persisted_state: PersistedState<B>,
|
||||
}
|
||||
|
||||
impl<B, BE, P, N> BeefyWorker<B, BE, P, N>
|
||||
impl<B, BE, P, R, N> BeefyWorker<B, BE, P, R, N>
|
||||
where
|
||||
B: Block + Codec,
|
||||
BE: Backend<B>,
|
||||
P: PayloadProvider<B>,
|
||||
R: ProvideRuntimeApi<B>,
|
||||
R::Api: BeefyApi<B>,
|
||||
N: NetworkEventStream + NetworkRequest + SyncOracle + Send + Sync + Clone + 'static,
|
||||
{
|
||||
/// Return a new BEEFY worker instance.
|
||||
@@ -340,10 +345,11 @@ where
|
||||
/// BEEFY pallet has been deployed on-chain.
|
||||
///
|
||||
/// The BEEFY pallet is needed in order to keep track of the BEEFY authority set.
|
||||
pub(crate) fn new(worker_params: WorkerParams<B, BE, P, N>) -> Self {
|
||||
pub(crate) fn new(worker_params: WorkerParams<B, BE, P, R, N>) -> Self {
|
||||
let WorkerParams {
|
||||
backend,
|
||||
payload_provider,
|
||||
runtime,
|
||||
key_store,
|
||||
network,
|
||||
gossip_engine,
|
||||
@@ -357,6 +363,7 @@ where
|
||||
BeefyWorker {
|
||||
backend,
|
||||
payload_provider,
|
||||
runtime,
|
||||
network,
|
||||
key_store,
|
||||
gossip_engine,
|
||||
@@ -571,6 +578,7 @@ where
|
||||
// We created the `finality_proof` and know to be valid.
|
||||
// New state is persisted after finalization.
|
||||
self.finalize(finality_proof)?;
|
||||
metric_inc!(self, beefy_good_votes_processed);
|
||||
},
|
||||
VoteImportResult::Ok => {
|
||||
// Persist state after handling mandatory block vote.
|
||||
@@ -583,14 +591,15 @@ where
|
||||
crate::aux_schema::write_voter_state(&*self.backend, &self.persisted_state)
|
||||
.map_err(|e| Error::Backend(e.to_string()))?;
|
||||
}
|
||||
metric_inc!(self, beefy_good_votes_processed);
|
||||
},
|
||||
VoteImportResult::Equivocation => {
|
||||
// TODO: report returned `EquivocationProof` to chain through `pallet-beefy`.
|
||||
()
|
||||
VoteImportResult::Equivocation(proof) => {
|
||||
metric_inc!(self, beefy_equivocation_votes);
|
||||
self.report_equivocation(proof)?;
|
||||
},
|
||||
VoteImportResult::Invalid | VoteImportResult::Stale => (),
|
||||
VoteImportResult::Invalid => metric_inc!(self, beefy_invalid_votes),
|
||||
VoteImportResult::Stale => metric_inc!(self, beefy_stale_votes),
|
||||
};
|
||||
metric_inc!(self, beefy_successful_handled_votes);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -928,6 +937,60 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Report the given equivocation to the BEEFY runtime module. This method
|
||||
/// generates a session membership proof of the offender and then submits an
|
||||
/// extrinsic to report the equivocation. In particular, the session membership
|
||||
/// proof must be generated at the block at which the given set was active which
|
||||
/// isn't necessarily the best block if there are pending authority set changes.
|
||||
pub(crate) fn report_equivocation(
|
||||
&self,
|
||||
proof: EquivocationProof<NumberFor<B>, AuthorityId, Signature>,
|
||||
) -> Result<(), Error> {
|
||||
let rounds =
|
||||
self.persisted_state.voting_oracle.active_rounds().ok_or(Error::UninitSession)?;
|
||||
let (validators, validator_set_id) = (rounds.validators(), rounds.validator_set_id());
|
||||
let offender_id = proof.offender_id().clone();
|
||||
|
||||
if !check_equivocation_proof::<_, _, BeefySignatureHasher>(&proof) {
|
||||
debug!(target: LOG_TARGET, "🥩 Skip report for bad equivocation {:?}", proof);
|
||||
return Ok(())
|
||||
} else if let Some(local_id) = self.key_store.authority_id(validators) {
|
||||
if offender_id == local_id {
|
||||
debug!(target: LOG_TARGET, "🥩 Skip equivocation report for own equivocation");
|
||||
return Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let number = *proof.round_number();
|
||||
let runtime_api = self.runtime.runtime_api();
|
||||
// generate key ownership proof at that block
|
||||
let key_owner_proof = match runtime_api
|
||||
.generate_key_ownership_proof(&BlockId::Number(number), validator_set_id, offender_id)
|
||||
.map_err(Error::RuntimeApi)?
|
||||
{
|
||||
Some(proof) => proof,
|
||||
None => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Equivocation offender not part of the authority set."
|
||||
);
|
||||
return Ok(())
|
||||
},
|
||||
};
|
||||
|
||||
// submit equivocation report at **best** block
|
||||
let best_block_hash = self.backend.blockchain().info().best_hash;
|
||||
runtime_api
|
||||
.submit_report_equivocation_unsigned_extrinsic(
|
||||
&BlockId::Hash(best_block_hash),
|
||||
proof,
|
||||
key_owner_proof,
|
||||
)
|
||||
.map_err(Error::RuntimeApi)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan the `header` digest log for a BEEFY validator set change. Return either the new
|
||||
@@ -993,7 +1056,8 @@ pub(crate) mod tests {
|
||||
BeefyRPCLinks, KnownPeers,
|
||||
};
|
||||
use beefy_primitives::{
|
||||
keyring::Keyring, known_payloads, mmr::MmrRootProvider, Payload, SignedCommitment,
|
||||
generate_equivocation_proof, known_payloads, known_payloads::MMR_ROOT_ID,
|
||||
mmr::MmrRootProvider, Keyring, Payload, SignedCommitment,
|
||||
};
|
||||
use futures::{future::poll_fn, task::Poll};
|
||||
use parking_lot::Mutex;
|
||||
@@ -1041,6 +1105,7 @@ pub(crate) mod tests {
|
||||
Block,
|
||||
Backend,
|
||||
MmrRootProvider<Block, TestApi>,
|
||||
TestApi,
|
||||
Arc<NetworkService<Block, H256>>,
|
||||
> {
|
||||
let keystore = create_beefy_keystore(*key);
|
||||
@@ -1091,6 +1156,7 @@ pub(crate) mod tests {
|
||||
let worker_params = crate::worker::WorkerParams {
|
||||
backend,
|
||||
payload_provider,
|
||||
runtime: api,
|
||||
key_store: Some(keystore).into(),
|
||||
links,
|
||||
gossip_engine,
|
||||
@@ -1100,7 +1166,7 @@ pub(crate) mod tests {
|
||||
on_demand_justifications,
|
||||
persisted_state,
|
||||
};
|
||||
BeefyWorker::<_, _, _, _>::new(worker_params)
|
||||
BeefyWorker::<_, _, _, _, _>::new(worker_params)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1546,4 +1612,65 @@ pub(crate) mod tests {
|
||||
assert_eq!(votes.next().unwrap().first().unwrap().commitment.block_number, 21);
|
||||
assert_eq!(votes.next().unwrap().first().unwrap().commitment.block_number, 22);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_not_report_bad_old_or_self_equivocations() {
|
||||
let block_num = 1;
|
||||
let set_id = 1;
|
||||
let keys = [Keyring::Alice];
|
||||
let validator_set = ValidatorSet::new(make_beefy_ids(&keys), set_id).unwrap();
|
||||
// Alice votes on good MMR roots, equivocations are allowed/expected
|
||||
let mut api_alice = TestApi::with_validator_set(&validator_set);
|
||||
api_alice.allow_equivocations();
|
||||
let api_alice = Arc::new(api_alice);
|
||||
|
||||
let mut net = BeefyTestNet::new(1);
|
||||
let mut worker = create_beefy_worker(&net.peer(0), &keys[0], 1, validator_set.clone());
|
||||
worker.runtime = api_alice.clone();
|
||||
|
||||
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]);
|
||||
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
|
||||
|
||||
// generate an equivocation proof, with Bob as perpetrator
|
||||
let good_proof = generate_equivocation_proof(
|
||||
(block_num, payload1.clone(), set_id, &Keyring::Bob),
|
||||
(block_num, payload2.clone(), set_id, &Keyring::Bob),
|
||||
);
|
||||
{
|
||||
// expect voter (Alice) to successfully report it
|
||||
assert_eq!(worker.report_equivocation(good_proof.clone()), Ok(()));
|
||||
// verify Alice reports Bob equivocation to runtime
|
||||
let reported = api_alice.reported_equivocations.as_ref().unwrap().lock();
|
||||
assert_eq!(reported.len(), 1);
|
||||
assert_eq!(*reported.get(0).unwrap(), good_proof);
|
||||
}
|
||||
api_alice.reported_equivocations.as_ref().unwrap().lock().clear();
|
||||
|
||||
// now let's try with a bad proof
|
||||
let mut bad_proof = good_proof.clone();
|
||||
bad_proof.first.id = Keyring::Charlie.public();
|
||||
// bad proofs are simply ignored
|
||||
assert_eq!(worker.report_equivocation(bad_proof), Ok(()));
|
||||
// verify nothing reported to runtime
|
||||
assert!(api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty());
|
||||
|
||||
// now let's try with old set it
|
||||
let mut old_proof = good_proof.clone();
|
||||
old_proof.first.commitment.validator_set_id = 0;
|
||||
old_proof.second.commitment.validator_set_id = 0;
|
||||
// old proofs are simply ignored
|
||||
assert_eq!(worker.report_equivocation(old_proof), Ok(()));
|
||||
// verify nothing reported to runtime
|
||||
assert!(api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty());
|
||||
|
||||
// now let's try reporting a self-equivocation
|
||||
let self_proof = generate_equivocation_proof(
|
||||
(block_num, payload1.clone(), set_id, &Keyring::Alice),
|
||||
(block_num, payload2.clone(), set_id, &Keyring::Alice),
|
||||
);
|
||||
// equivocations done by 'self' are simply ignored (not reported)
|
||||
assert_eq!(worker.report_equivocation(self_proof), Ok(()));
|
||||
// verify nothing reported to runtime
|
||||
assert!(api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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! {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user