mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-12 17:01:09 +00:00
babe: report equivocations (#6362)
* slots: create primitives crate for consensus slots * offences: add method to check if an offence is unknown * babe: initial equivocation reporting implementation * babe: organize imports * babe: working equivocation reporting * babe: add slot number to equivocation proof * session: move duplicate traits to session primitives * babe: move equivocation stuff to its own file * offences: fix test * session: don't have primitives depend on frame_support * babe: use opaque type for key owner proof * babe: cleanup client equivocation reporting * babe: cleanup equivocation code in pallet * babe: allow sending signed equivocation reports * node: fix compilation * fix test compilation * babe: return bool on check_equivocation_proof * babe: add test for equivocation reporting * babe: add more tests * babe: add test for validate unsigned * babe: take slot number in generate_key_ownership_proof API * babe: add benchmark for equivocation proof checking * session: add benchmark for membership proof checking * offences: fix babe benchmark * babe: add weights based on benchmark results * babe: adjust weights after benchmarking on reference hardware * babe: reorder checks in check_and_report_equivocation
This commit is contained in:
Generated
+19
@@ -4009,9 +4009,15 @@ dependencies = [
|
||||
name = "pallet-babe"
|
||||
version = "2.0.0-rc4"
|
||||
dependencies = [
|
||||
"frame-benchmarking",
|
||||
"frame-support",
|
||||
"frame-system",
|
||||
"pallet-authorship",
|
||||
"pallet-balances",
|
||||
"pallet-offences",
|
||||
"pallet-session",
|
||||
"pallet-staking",
|
||||
"pallet-staking-reward-curve",
|
||||
"pallet-timestamp",
|
||||
"parity-scale-codec",
|
||||
"serde",
|
||||
@@ -4022,6 +4028,7 @@ dependencies = [
|
||||
"sp-inherents",
|
||||
"sp-io",
|
||||
"sp-runtime",
|
||||
"sp-session",
|
||||
"sp-staking",
|
||||
"sp-std",
|
||||
"sp-timestamp",
|
||||
@@ -4554,10 +4561,12 @@ dependencies = [
|
||||
"pallet-staking-reward-curve",
|
||||
"pallet-timestamp",
|
||||
"parity-scale-codec",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"sp-core",
|
||||
"sp-io",
|
||||
"sp-runtime",
|
||||
"sp-session",
|
||||
"sp-std",
|
||||
]
|
||||
|
||||
@@ -6343,6 +6352,7 @@ dependencies = [
|
||||
"sp-application-crypto",
|
||||
"sp-blockchain",
|
||||
"sp-consensus",
|
||||
"sp-consensus-slots",
|
||||
"sp-core",
|
||||
"sp-inherents",
|
||||
"sp-runtime",
|
||||
@@ -7568,6 +7578,7 @@ dependencies = [
|
||||
"sp-api",
|
||||
"sp-application-crypto",
|
||||
"sp-consensus",
|
||||
"sp-consensus-slots",
|
||||
"sp-consensus-vrf",
|
||||
"sp-core",
|
||||
"sp-inherents",
|
||||
@@ -7587,6 +7598,14 @@ dependencies = [
|
||||
"sp-std",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sp-consensus-slots"
|
||||
version = "0.8.0-rc4"
|
||||
dependencies = [
|
||||
"parity-scale-codec",
|
||||
"sp-runtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sp-consensus-vrf"
|
||||
version = "0.8.0-rc4"
|
||||
|
||||
@@ -83,7 +83,7 @@ macro_rules! new_full_start {
|
||||
let (grandpa_block_import, grandpa_link) = grandpa::block_import(
|
||||
client.clone(),
|
||||
&(client.clone() as Arc<_>),
|
||||
select_chain,
|
||||
select_chain.clone(),
|
||||
)?;
|
||||
let justification_import = grandpa_block_import.clone();
|
||||
|
||||
@@ -99,6 +99,7 @@ macro_rules! new_full_start {
|
||||
Some(Box::new(justification_import)),
|
||||
None,
|
||||
client,
|
||||
select_chain,
|
||||
inherent_data_providers.clone(),
|
||||
spawn_task_handle,
|
||||
prometheus_registry,
|
||||
@@ -367,14 +368,18 @@ pub fn new_light_base(config: Configuration) -> Result<(
|
||||
client,
|
||||
backend,
|
||||
fetcher,
|
||||
_select_chain,
|
||||
mut select_chain,
|
||||
_tx_pool,
|
||||
spawn_task_handle,
|
||||
registry,
|
||||
| {
|
||||
let select_chain = select_chain.take()
|
||||
.ok_or_else(|| sc_service::Error::SelectChainRequired)?;
|
||||
|
||||
let fetch_checker = fetcher
|
||||
.map(|fetcher| fetcher.checker().clone())
|
||||
.ok_or_else(|| "Trying to start light import queue without active fetch checker")?;
|
||||
|
||||
let grandpa_block_import = grandpa::light_block_import(
|
||||
client.clone(),
|
||||
backend,
|
||||
@@ -398,6 +403,7 @@ pub fn new_light_base(config: Configuration) -> Result<(
|
||||
None,
|
||||
Some(Box::new(finality_proof_import)),
|
||||
client.clone(),
|
||||
select_chain,
|
||||
inherent_data_providers.clone(),
|
||||
spawn_task_handle,
|
||||
registry,
|
||||
|
||||
@@ -149,6 +149,7 @@ runtime-benchmarks = [
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
"pallet-babe/runtime-benchmarks",
|
||||
"pallet-balances/runtime-benchmarks",
|
||||
"pallet-collective/runtime-benchmarks",
|
||||
"pallet-democracy/runtime-benchmarks",
|
||||
|
||||
@@ -270,6 +270,21 @@ impl pallet_babe::Trait for Runtime {
|
||||
type EpochDuration = EpochDuration;
|
||||
type ExpectedBlockTime = ExpectedBlockTime;
|
||||
type EpochChangeTrigger = pallet_babe::ExternalTrigger;
|
||||
|
||||
type KeyOwnerProofSystem = Historical;
|
||||
|
||||
type KeyOwnerProof = <Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(
|
||||
KeyTypeId,
|
||||
pallet_babe::AuthorityId,
|
||||
)>>::Proof;
|
||||
|
||||
type KeyOwnerIdentification = <Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(
|
||||
KeyTypeId,
|
||||
pallet_babe::AuthorityId,
|
||||
)>>::IdentificationTuple;
|
||||
|
||||
type HandleEquivocation =
|
||||
pallet_babe::EquivocationHandler<Self::KeyOwnerIdentification, Offences>;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
@@ -808,7 +823,7 @@ construct_runtime!(
|
||||
{
|
||||
System: frame_system::{Module, Call, Config, Storage, Event<T>},
|
||||
Utility: pallet_utility::{Module, Call, Event},
|
||||
Babe: pallet_babe::{Module, Call, Storage, Config, Inherent(Timestamp)},
|
||||
Babe: pallet_babe::{Module, Call, Storage, Config, Inherent(Timestamp), ValidateUnsigned},
|
||||
Timestamp: pallet_timestamp::{Module, Call, Storage, Inherent},
|
||||
Authorship: pallet_authorship::{Module, Call, Storage, Inherent},
|
||||
Indices: pallet_indices::{Module, Call, Storage, Config<T>, Event<T>},
|
||||
@@ -985,6 +1000,29 @@ impl_runtime_apis! {
|
||||
fn current_epoch_start() -> sp_consensus_babe::SlotNumber {
|
||||
Babe::current_epoch_start()
|
||||
}
|
||||
|
||||
fn generate_key_ownership_proof(
|
||||
_slot_number: sp_consensus_babe::SlotNumber,
|
||||
authority_id: sp_consensus_babe::AuthorityId,
|
||||
) -> Option<sp_consensus_babe::OpaqueKeyOwnershipProof> {
|
||||
use codec::Encode;
|
||||
|
||||
Historical::prove((sp_consensus_babe::KEY_TYPE, authority_id))
|
||||
.map(|p| p.encode())
|
||||
.map(sp_consensus_babe::OpaqueKeyOwnershipProof::new)
|
||||
}
|
||||
|
||||
fn submit_report_equivocation_unsigned_extrinsic(
|
||||
equivocation_proof: sp_consensus_babe::EquivocationProof<<Block as BlockT>::Header>,
|
||||
key_owner_proof: sp_consensus_babe::OpaqueKeyOwnershipProof,
|
||||
) -> Option<()> {
|
||||
let key_owner_proof = key_owner_proof.decode()?;
|
||||
|
||||
Babe::submit_unsigned_equivocation_report(
|
||||
equivocation_proof,
|
||||
key_owner_proof,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl sp_authority_discovery::AuthorityDiscoveryApi<Block> for Runtime {
|
||||
@@ -1099,6 +1137,7 @@ impl_runtime_apis! {
|
||||
let mut batches = Vec::<BenchmarkBatch>::new();
|
||||
let params = (&pallet, &benchmark, &lowest_range_values, &highest_range_values, &steps, repeat, &whitelist);
|
||||
|
||||
add_benchmark!(params, batches, b"babe", Babe);
|
||||
add_benchmark!(params, batches, b"balances", Balances);
|
||||
add_benchmark!(params, batches, b"collective", Council);
|
||||
add_benchmark!(params, batches, b"democracy", Democracy);
|
||||
|
||||
@@ -479,8 +479,8 @@ fn check_header<C, B: BlockT, P: Pair>(
|
||||
info!(
|
||||
"Slot author is equivocating at slot {} with headers {:?} and {:?}",
|
||||
slot_num,
|
||||
equivocation_proof.fst_header().hash(),
|
||||
equivocation_proof.snd_header().hash(),
|
||||
equivocation_proof.first_header.hash(),
|
||||
equivocation_proof.second_header.hash(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -720,27 +720,29 @@ impl<Block: BlockT> BabeLink<Block> {
|
||||
}
|
||||
|
||||
/// A verifier for Babe blocks.
|
||||
pub struct BabeVerifier<Block: BlockT, Client> {
|
||||
pub struct BabeVerifier<Block: BlockT, Client, SelectChain> {
|
||||
client: Arc<Client>,
|
||||
select_chain: SelectChain,
|
||||
inherent_data_providers: sp_inherents::InherentDataProviders,
|
||||
config: Config,
|
||||
epoch_changes: SharedEpochChanges<Block, Epoch>,
|
||||
time_source: TimeSource,
|
||||
}
|
||||
|
||||
impl<Block, Client> BabeVerifier<Block, Client>
|
||||
where
|
||||
Block: BlockT,
|
||||
Client: HeaderBackend<Block> + HeaderMetadata<Block> + ProvideRuntimeApi<Block>,
|
||||
Client::Api: BlockBuilderApi<Block, Error = sp_blockchain::Error>,
|
||||
impl<Block, Client, SelectChain> BabeVerifier<Block, Client, SelectChain>
|
||||
where
|
||||
Block: BlockT,
|
||||
Client: AuxStore + HeaderBackend<Block> + HeaderMetadata<Block> + ProvideRuntimeApi<Block>,
|
||||
Client::Api: BlockBuilderApi<Block, Error = sp_blockchain::Error>
|
||||
+ BabeApi<Block, Error = sp_blockchain::Error>,
|
||||
SelectChain: sp_consensus::SelectChain<Block>,
|
||||
{
|
||||
fn check_inherents(
|
||||
&self,
|
||||
block: Block,
|
||||
block_id: BlockId<Block>,
|
||||
inherent_data: InherentData,
|
||||
) -> Result<(), Error<Block>>
|
||||
{
|
||||
) -> Result<(), Error<Block>> {
|
||||
let inherent_res = self.client.runtime_api().check_inherents(
|
||||
&block_id,
|
||||
block,
|
||||
@@ -757,13 +759,95 @@ impl<Block, Client> BabeVerifier<Block, Client>
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn check_and_report_equivocation(
|
||||
&self,
|
||||
slot_now: SlotNumber,
|
||||
slot: SlotNumber,
|
||||
header: &Block::Header,
|
||||
author: &AuthorityId,
|
||||
origin: &BlockOrigin,
|
||||
) -> Result<(), Error<Block>> {
|
||||
// don't report any equivocations during initial sync
|
||||
// as they are most likely stale.
|
||||
if *origin == BlockOrigin::NetworkInitialSync {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// check if authorship of this header is an equivocation and return a proof if so.
|
||||
let equivocation_proof =
|
||||
match check_equivocation(&*self.client, slot_now, slot, header, author)
|
||||
.map_err(Error::Client)?
|
||||
{
|
||||
Some(proof) => proof,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
info!(
|
||||
"Slot author {:?} is equivocating at slot {} with headers {:?} and {:?}",
|
||||
author,
|
||||
slot,
|
||||
equivocation_proof.first_header.hash(),
|
||||
equivocation_proof.second_header.hash(),
|
||||
);
|
||||
|
||||
// get the best block on which we will build and send the equivocation report.
|
||||
let best_id = self
|
||||
.select_chain
|
||||
.best_chain()
|
||||
.map(|h| BlockId::Hash(h.hash()))
|
||||
.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
|
||||
// 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
|
||||
// equivocation happens on the first block of the session, in which case
|
||||
// its parent would be on the previous session. if generation on the
|
||||
// parent header fails we try with best block as well.
|
||||
let generate_key_owner_proof = |block_id: &BlockId<Block>| {
|
||||
self.client
|
||||
.runtime_api()
|
||||
.generate_key_ownership_proof(block_id, slot, equivocation_proof.offender.clone())
|
||||
.map_err(Error::Client)
|
||||
};
|
||||
|
||||
let parent_id = BlockId::Hash(*header.parent_hash());
|
||||
let key_owner_proof = match generate_key_owner_proof(&parent_id)? {
|
||||
Some(proof) => proof,
|
||||
None => match generate_key_owner_proof(&best_id)? {
|
||||
Some(proof) => proof,
|
||||
None => {
|
||||
debug!(target: "babe", "Equivocation offender is not part of the authority set.");
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// submit equivocation report at best block.
|
||||
self.client
|
||||
.runtime_api()
|
||||
.submit_report_equivocation_unsigned_extrinsic(
|
||||
&best_id,
|
||||
equivocation_proof,
|
||||
key_owner_proof,
|
||||
)
|
||||
.map_err(Error::Client)?;
|
||||
|
||||
info!(target: "babe", "Submitted equivocation report for author {:?}", author);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block, Client> Verifier<Block> for BabeVerifier<Block, Client> where
|
||||
impl<Block, Client, SelectChain> Verifier<Block> for BabeVerifier<Block, Client, SelectChain>
|
||||
where
|
||||
Block: BlockT,
|
||||
Client: HeaderMetadata<Block, Error = sp_blockchain::Error> + HeaderBackend<Block> + ProvideRuntimeApi<Block>
|
||||
+ Send + Sync + AuxStore + ProvideCache<Block>,
|
||||
+ Send + Sync + AuxStore + ProvideCache<Block>,
|
||||
Client::Api: BlockBuilderApi<Block, Error = sp_blockchain::Error> + BabeApi<Block, Error = sp_blockchain::Error>,
|
||||
SelectChain: sp_consensus::SelectChain<Block>,
|
||||
{
|
||||
fn verify(
|
||||
&mut self,
|
||||
@@ -824,28 +908,18 @@ impl<Block, Client> Verifier<Block> for BabeVerifier<Block, Client> where
|
||||
CheckedHeader::Checked(pre_header, verified_info) => {
|
||||
let babe_pre_digest = verified_info.pre_digest.as_babe_pre_digest()
|
||||
.expect("check_header always returns a pre-digest digest item; qed");
|
||||
|
||||
let slot_number = babe_pre_digest.slot_number();
|
||||
|
||||
let author = verified_info.author;
|
||||
|
||||
// the header is valid but let's check if there was something else already
|
||||
// proposed at the same slot by the given author
|
||||
if let Some(equivocation_proof) = check_equivocation(
|
||||
&*self.client,
|
||||
// proposed at the same slot by the given author. if there was, we will
|
||||
// report the equivocation to the runtime.
|
||||
self.check_and_report_equivocation(
|
||||
slot_now,
|
||||
babe_pre_digest.slot_number(),
|
||||
slot_number,
|
||||
&header,
|
||||
&author,
|
||||
).map_err(|e| e.to_string())? {
|
||||
info!(
|
||||
"Slot author {:?} is equivocating at slot {} with headers {:?} and {:?}",
|
||||
author,
|
||||
babe_pre_digest.slot_number(),
|
||||
equivocation_proof.fst_header().hash(),
|
||||
equivocation_proof.snd_header().hash(),
|
||||
);
|
||||
}
|
||||
&verified_info.author,
|
||||
&origin,
|
||||
)?;
|
||||
|
||||
// if the body is passed through, we need to use the runtime
|
||||
// to check that the internally-set timestamp in the inherents
|
||||
@@ -1284,12 +1358,13 @@ pub fn block_import<Client, Block: BlockT, I>(
|
||||
///
|
||||
/// The block import object provided must be the `BabeBlockImport` or a wrapper
|
||||
/// of it, otherwise crucial import logic will be omitted.
|
||||
pub fn import_queue<Block: BlockT, Client, Inner>(
|
||||
pub fn import_queue<Block: BlockT, Client, SelectChain, Inner>(
|
||||
babe_link: BabeLink<Block>,
|
||||
block_import: Inner,
|
||||
justification_import: Option<BoxJustificationImport<Block>>,
|
||||
finality_proof_import: Option<BoxFinalityProofImport<Block>>,
|
||||
client: Arc<Client>,
|
||||
select_chain: SelectChain,
|
||||
inherent_data_providers: InherentDataProviders,
|
||||
spawner: &impl sp_core::traits::SpawnNamed,
|
||||
registry: Option<&Registry>,
|
||||
@@ -1299,11 +1374,13 @@ pub fn import_queue<Block: BlockT, Client, Inner>(
|
||||
Client: ProvideRuntimeApi<Block> + ProvideCache<Block> + Send + Sync + AuxStore + 'static,
|
||||
Client: HeaderBackend<Block> + HeaderMetadata<Block, Error = sp_blockchain::Error>,
|
||||
Client::Api: BlockBuilderApi<Block> + BabeApi<Block> + ApiExt<Block, Error = sp_blockchain::Error>,
|
||||
SelectChain: sp_consensus::SelectChain<Block> + 'static,
|
||||
{
|
||||
register_babe_inherent_data_provider(&inherent_data_providers, babe_link.config.slot_duration)?;
|
||||
|
||||
let verifier = BabeVerifier {
|
||||
client,
|
||||
select_chain,
|
||||
inherent_data_providers,
|
||||
config: babe_link.config,
|
||||
epoch_changes: babe_link.epoch_changes,
|
||||
|
||||
@@ -214,8 +214,13 @@ pub struct BabeTestNet {
|
||||
type TestHeader = <TestBlock as BlockT>::Header;
|
||||
type TestExtrinsic = <TestBlock as BlockT>::Extrinsic;
|
||||
|
||||
type TestSelectChain = substrate_test_runtime_client::LongestChain<
|
||||
substrate_test_runtime_client::Backend,
|
||||
TestBlock,
|
||||
>;
|
||||
|
||||
pub struct TestVerifier {
|
||||
inner: BabeVerifier<TestBlock, PeersFullClient>,
|
||||
inner: BabeVerifier<TestBlock, PeersFullClient, TestSelectChain>,
|
||||
mutator: Mutator,
|
||||
}
|
||||
|
||||
@@ -297,15 +302,20 @@ impl TestNetFactory for BabeTestNet {
|
||||
)
|
||||
-> Self::Verifier
|
||||
{
|
||||
use substrate_test_runtime_client::DefaultTestClientBuilderExt;
|
||||
|
||||
let client = client.as_full().expect("only full clients are used in test");
|
||||
trace!(target: "babe", "Creating a verifier");
|
||||
|
||||
// ensure block import and verifier are linked correctly.
|
||||
let data = maybe_link.as_ref().expect("babe link always provided to verifier instantiation");
|
||||
|
||||
let (_, longest_chain) = TestClientBuilder::new().build_with_longest_chain();
|
||||
|
||||
TestVerifier {
|
||||
inner: BabeVerifier {
|
||||
client: client.clone(),
|
||||
select_chain: longest_chain,
|
||||
inherent_data_providers: data.inherent_data_providers.clone(),
|
||||
config: data.link.config.clone(),
|
||||
epoch_changes: data.link.epoch_changes.clone(),
|
||||
|
||||
@@ -18,6 +18,7 @@ sc-client-api = { version = "2.0.0-rc4", path = "../../api" }
|
||||
sp-core = { version = "2.0.0-rc4", path = "../../../primitives/core" }
|
||||
sp-application-crypto = { version = "2.0.0-rc4", path = "../../../primitives/application-crypto" }
|
||||
sp-blockchain = { version = "2.0.0-rc4", path = "../../../primitives/blockchain" }
|
||||
sp-consensus-slots = { version = "0.8.0-rc4", path = "../../../primitives/consensus/slots" }
|
||||
sp-runtime = { version = "2.0.0-rc4", path = "../../../primitives/runtime" }
|
||||
sp-state-machine = { version = "0.8.0-rc4", path = "../../../primitives/state-machine" }
|
||||
sp-api = { version = "2.0.0-rc4", path = "../../../primitives/api" }
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
use codec::{Encode, Decode};
|
||||
use sc_client_api::backend::AuxStore;
|
||||
use sp_blockchain::{Result as ClientResult, Error as ClientError};
|
||||
use sp_consensus_slots::EquivocationProof;
|
||||
use sp_runtime::traits::Header;
|
||||
|
||||
const SLOT_HEADER_MAP_KEY: &[u8] = b"slot_header_map";
|
||||
@@ -44,31 +45,6 @@ fn load_decode<C, T>(backend: &C, key: &[u8]) -> ClientResult<Option<T>>
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an equivocation proof.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EquivocationProof<H> {
|
||||
slot: u64,
|
||||
fst_header: H,
|
||||
snd_header: H,
|
||||
}
|
||||
|
||||
impl<H> EquivocationProof<H> {
|
||||
/// Get the slot number where the equivocation happened.
|
||||
pub fn slot(&self) -> u64 {
|
||||
self.slot
|
||||
}
|
||||
|
||||
/// Get the first header involved in the equivocation.
|
||||
pub fn fst_header(&self) -> &H {
|
||||
&self.fst_header
|
||||
}
|
||||
|
||||
/// Get the second header involved in the equivocation.
|
||||
pub fn snd_header(&self) -> &H {
|
||||
&self.snd_header
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the header is an equivocation and returns the proof in that case.
|
||||
///
|
||||
/// Note: it detects equivocations only when slot_now - slot <= MAX_SLOT_CAPACITY.
|
||||
@@ -78,7 +54,7 @@ pub fn check_equivocation<C, H, P>(
|
||||
slot: u64,
|
||||
header: &H,
|
||||
signer: &P,
|
||||
) -> ClientResult<Option<EquivocationProof<H>>>
|
||||
) -> ClientResult<Option<EquivocationProof<H, P>>>
|
||||
where
|
||||
H: Header,
|
||||
C: AuxStore,
|
||||
@@ -114,9 +90,10 @@ pub fn check_equivocation<C, H, P>(
|
||||
// 2) with different hash
|
||||
if header.hash() != prev_header.hash() {
|
||||
return Ok(Some(EquivocationProof {
|
||||
slot, // 3) and mentioning the same slot.
|
||||
fst_header: prev_header.clone(),
|
||||
snd_header: header.clone(),
|
||||
slot_number: slot,
|
||||
offender: signer.clone(),
|
||||
first_header: prev_header.clone(),
|
||||
second_header: header.clone(),
|
||||
}));
|
||||
} else {
|
||||
// We don't need to continue in case of duplicated header,
|
||||
|
||||
@@ -13,40 +13,52 @@ targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "1.3.1", default-features = false, features = ["derive"] }
|
||||
serde = { version = "1.0.101", optional = true }
|
||||
sp-inherents = { version = "2.0.0-rc4", default-features = false, path = "../../primitives/inherents" }
|
||||
sp-application-crypto = { version = "2.0.0-rc4", default-features = false, path = "../../primitives/application-crypto" }
|
||||
sp-std = { version = "2.0.0-rc4", default-features = false, path = "../../primitives/std" }
|
||||
sp-runtime = { version = "2.0.0-rc4", default-features = false, path = "../../primitives/runtime" }
|
||||
sp-staking = { version = "2.0.0-rc4", default-features = false, path = "../../primitives/staking" }
|
||||
frame-benchmarking = { version = "2.0.0-rc4", default-features = false, path = "../benchmarking", optional = true }
|
||||
frame-support = { version = "2.0.0-rc4", default-features = false, path = "../support" }
|
||||
frame-system = { version = "2.0.0-rc4", default-features = false, path = "../system" }
|
||||
pallet-timestamp = { version = "2.0.0-rc4", default-features = false, path = "../timestamp" }
|
||||
sp-timestamp = { version = "2.0.0-rc4", default-features = false, path = "../../primitives/timestamp" }
|
||||
pallet-authorship = { version = "2.0.0-rc4", default-features = false, path = "../authorship" }
|
||||
pallet-session = { version = "2.0.0-rc4", default-features = false, path = "../session" }
|
||||
pallet-timestamp = { version = "2.0.0-rc4", default-features = false, path = "../timestamp" }
|
||||
serde = { version = "1.0.101", optional = true }
|
||||
sp-application-crypto = { version = "2.0.0-rc4", default-features = false, path = "../../primitives/application-crypto" }
|
||||
sp-consensus-babe = { version = "0.8.0-rc4", default-features = false, path = "../../primitives/consensus/babe" }
|
||||
sp-consensus-vrf = { version = "0.8.0-rc4", default-features = false, path = "../../primitives/consensus/vrf" }
|
||||
sp-inherents = { version = "2.0.0-rc4", default-features = false, path = "../../primitives/inherents" }
|
||||
sp-io = { version = "2.0.0-rc4", default-features = false, path = "../../primitives/io" }
|
||||
sp-runtime = { version = "2.0.0-rc4", default-features = false, path = "../../primitives/runtime" }
|
||||
sp-session = { version = "2.0.0-rc4", default-features = false, path = "../../primitives/session" }
|
||||
sp-staking = { version = "2.0.0-rc4", default-features = false, path = "../../primitives/staking" }
|
||||
sp-std = { version = "2.0.0-rc4", default-features = false, path = "../../primitives/std" }
|
||||
sp-timestamp = { version = "2.0.0-rc4", default-features = false, path = "../../primitives/timestamp" }
|
||||
|
||||
[dev-dependencies]
|
||||
frame-benchmarking = { version = "2.0.0-rc4", path = "../benchmarking" }
|
||||
pallet-balances = { version = "2.0.0-rc4", path = "../balances" }
|
||||
pallet-offences = { version = "2.0.0-rc4", path = "../offences" }
|
||||
pallet-staking = { version = "2.0.0-rc4", path = "../staking" }
|
||||
pallet-staking-reward-curve = { version = "2.0.0-rc4", path = "../staking/reward-curve" }
|
||||
sp-core = { version = "2.0.0-rc4", path = "../../primitives/core" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"serde",
|
||||
"sp-std/std",
|
||||
"sp-application-crypto/std",
|
||||
"frame-benchmarking/std",
|
||||
"frame-support/std",
|
||||
"sp-runtime/std",
|
||||
"sp-staking/std",
|
||||
"frame-system/std",
|
||||
"pallet-authorship/std",
|
||||
"pallet-session/std",
|
||||
"pallet-timestamp/std",
|
||||
"sp-timestamp/std",
|
||||
"sp-inherents/std",
|
||||
"serde",
|
||||
"sp-application-crypto/std",
|
||||
"sp-consensus-babe/std",
|
||||
"sp-consensus-vrf/std",
|
||||
"pallet-session/std",
|
||||
"sp-inherents/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-session/std",
|
||||
"sp-staking/std",
|
||||
"sp-std/std",
|
||||
"sp-timestamp/std",
|
||||
]
|
||||
runtime-benchmarks = ["frame-benchmarking"]
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2020 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.
|
||||
|
||||
//! Benchmarks for the BABE Pallet.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use super::*;
|
||||
use frame_benchmarking::benchmarks;
|
||||
|
||||
type Header = sp_runtime::generic::Header<u64, sp_runtime::traits::BlakeTwo256>;
|
||||
|
||||
benchmarks! {
|
||||
_ { }
|
||||
|
||||
check_equivocation_proof {
|
||||
let x in 0 .. 1;
|
||||
|
||||
// NOTE: generated with the test below `test_generate_equivocation_report_blob`.
|
||||
// the output is not deterministic since keys are generated randomly (and therefore
|
||||
// signature content changes). it should not affect the benchmark.
|
||||
// with the current benchmark setup it is not possible to generate this programatically
|
||||
// from the benchmark setup.
|
||||
const EQUIVOCATION_PROOF_BLOB: [u8; 416] = [
|
||||
222, 241, 46, 66, 243, 228, 135, 233, 177, 64, 149, 170, 141, 92, 193, 106, 51, 73, 31,
|
||||
27, 80, 218, 220, 248, 129, 29, 20, 128, 243, 250, 134, 39, 11, 0, 0, 0, 0, 0, 0, 0,
|
||||
158, 4, 7, 240, 67, 153, 134, 190, 251, 196, 229, 95, 136, 165, 234, 228, 255, 18, 2,
|
||||
187, 76, 125, 108, 50, 67, 33, 196, 108, 38, 115, 179, 86, 40, 36, 27, 5, 105, 58, 228,
|
||||
94, 198, 65, 212, 218, 213, 61, 170, 21, 51, 249, 182, 121, 101, 91, 204, 25, 31, 87,
|
||||
219, 208, 43, 119, 211, 185, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 66, 65, 66, 69, 52, 2, 0, 0, 0, 0, 11,
|
||||
0, 0, 0, 0, 0, 0, 0, 5, 66, 65, 66, 69, 1, 1, 188, 192, 217, 91, 138, 78, 217, 80, 8,
|
||||
29, 140, 55, 242, 210, 170, 184, 73, 98, 135, 212, 236, 209, 115, 52, 200, 79, 175,
|
||||
172, 242, 161, 199, 47, 236, 93, 101, 95, 43, 34, 141, 16, 247, 220, 33, 59, 31, 197,
|
||||
27, 7, 196, 62, 12, 238, 236, 124, 136, 191, 29, 36, 22, 238, 242, 202, 57, 139, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 40, 23, 175, 153, 83, 6, 33, 65, 123, 51, 80, 223, 126, 186, 226, 225, 240, 105, 28,
|
||||
169, 9, 54, 11, 138, 46, 194, 201, 250, 48, 242, 125, 117, 116, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 66, 65,
|
||||
66, 69, 52, 2, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 5, 66, 65, 66, 69, 1, 1, 142, 12,
|
||||
124, 11, 167, 227, 103, 88, 78, 23, 228, 33, 96, 41, 207, 183, 227, 189, 114, 70, 254,
|
||||
30, 128, 243, 233, 83, 214, 45, 74, 182, 120, 119, 64, 243, 219, 119, 63, 240, 205,
|
||||
123, 231, 82, 205, 174, 143, 70, 2, 86, 182, 20, 16, 141, 145, 91, 116, 195, 58, 223,
|
||||
175, 145, 255, 7, 121, 133
|
||||
];
|
||||
|
||||
let equivocation_proof1: sp_consensus_babe::EquivocationProof<Header> =
|
||||
Decode::decode(&mut &EQUIVOCATION_PROOF_BLOB[..]).unwrap();
|
||||
|
||||
let equivocation_proof2 = equivocation_proof1.clone();
|
||||
}: {
|
||||
sp_consensus_babe::check_equivocation_proof::<Header>(equivocation_proof1);
|
||||
} verify {
|
||||
assert!(sp_consensus_babe::check_equivocation_proof::<Header>(equivocation_proof2));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::*;
|
||||
use frame_support::assert_ok;
|
||||
|
||||
#[test]
|
||||
fn test_benchmarks() {
|
||||
new_test_ext(3).execute_with(|| {
|
||||
assert_ok!(test_benchmark_check_equivocation_proof::<Test>());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_equivocation_report_blob() {
|
||||
let (pairs, mut ext) = new_test_ext_with_pairs(3);
|
||||
|
||||
let offending_authority_index = 0;
|
||||
let offending_authority_pair = &pairs[0];
|
||||
|
||||
ext.execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
let equivocation_proof = generate_equivocation_proof(
|
||||
offending_authority_index,
|
||||
offending_authority_pair,
|
||||
CurrentSlot::get() + 1,
|
||||
);
|
||||
|
||||
println!("equivocation_proof: {:?}", equivocation_proof);
|
||||
println!(
|
||||
"equivocation_proof.encode(): {:?}",
|
||||
equivocation_proof.encode()
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2020 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 BABE equivocations
|
||||
//! and some utility traits to wire together:
|
||||
//! - a system for reporting offences;
|
||||
//! - a system for submitting unsigned 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 import BABE blocks).
|
||||
//! And in a runtime context, so that the BABE 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 BABE pallet is used in the runtime
|
||||
//! definition.
|
||||
//!
|
||||
|
||||
use frame_support::{debug, traits::KeyOwnerProofSystem};
|
||||
use sp_consensus_babe::{EquivocationProof, SlotNumber};
|
||||
use sp_runtime::transaction_validity::{
|
||||
InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
|
||||
TransactionValidityError, ValidTransaction,
|
||||
};
|
||||
use sp_runtime::{DispatchResult, Perbill};
|
||||
use sp_staking::{
|
||||
offence::{Kind, Offence, OffenceError, ReportOffence},
|
||||
SessionIndex,
|
||||
};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
use crate::{Call, Module, Trait};
|
||||
|
||||
/// A trait with utility methods for handling equivocation reports in BABE.
|
||||
/// The trait provides methods for reporting an offence triggered by a valid
|
||||
/// equivocation report, checking the current block author (to declare as the
|
||||
/// reporter), and also for creating and submitting equivocation report
|
||||
/// extrinsics (useful only in offchain context).
|
||||
pub trait HandleEquivocation<T: Trait> {
|
||||
/// Report an offence proved by the given reporters.
|
||||
fn report_offence(
|
||||
reporters: Vec<T::AccountId>,
|
||||
offence: BabeEquivocationOffence<T::KeyOwnerIdentification>,
|
||||
) -> 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: &SlotNumber) -> bool;
|
||||
|
||||
/// Create and dispatch an equivocation report extrinsic.
|
||||
fn submit_unsigned_equivocation_report(
|
||||
equivocation_proof: EquivocationProof<T::Header>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) -> DispatchResult;
|
||||
|
||||
/// Fetch the current block author id, if defined.
|
||||
fn block_author() -> Option<T::AccountId>;
|
||||
}
|
||||
|
||||
impl<T: Trait> HandleEquivocation<T> for () {
|
||||
fn report_offence(
|
||||
_reporters: Vec<T::AccountId>,
|
||||
_offence: BabeEquivocationOffence<T::KeyOwnerIdentification>,
|
||||
) -> Result<(), OffenceError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_known_offence(_offenders: &[T::KeyOwnerIdentification], _time_slot: &SlotNumber) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn submit_unsigned_equivocation_report(
|
||||
_equivocation_proof: EquivocationProof<T::Header>,
|
||||
_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<I, R> {
|
||||
_phantom: sp_std::marker::PhantomData<(I, R)>,
|
||||
}
|
||||
|
||||
impl<I, R> Default for EquivocationHandler<I, R> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R> HandleEquivocation<T> for EquivocationHandler<T::KeyOwnerIdentification, R>
|
||||
where
|
||||
// We use the authorship pallet to fetch the current block author and use
|
||||
// `offchain::SendTransactionTypes` for unsigned extrinsic creation and
|
||||
// submission.
|
||||
T: Trait + pallet_authorship::Trait + frame_system::offchain::SendTransactionTypes<Call<T>>,
|
||||
// A system for reporting offences after valid equivocation reports are
|
||||
// processed.
|
||||
R: ReportOffence<
|
||||
T::AccountId,
|
||||
T::KeyOwnerIdentification,
|
||||
BabeEquivocationOffence<T::KeyOwnerIdentification>,
|
||||
>,
|
||||
{
|
||||
fn report_offence(
|
||||
reporters: Vec<T::AccountId>,
|
||||
offence: BabeEquivocationOffence<T::KeyOwnerIdentification>,
|
||||
) -> Result<(), OffenceError> {
|
||||
R::report_offence(reporters, offence)
|
||||
}
|
||||
|
||||
fn is_known_offence(offenders: &[T::KeyOwnerIdentification], time_slot: &SlotNumber) -> bool {
|
||||
R::is_known_offence(offenders, time_slot)
|
||||
}
|
||||
|
||||
fn submit_unsigned_equivocation_report(
|
||||
equivocation_proof: EquivocationProof<T::Header>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) -> DispatchResult {
|
||||
use frame_system::offchain::SubmitTransaction;
|
||||
|
||||
let call = Call::report_equivocation_unsigned(equivocation_proof, key_owner_proof);
|
||||
|
||||
match SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call.into()) {
|
||||
Ok(()) => debug::info!("Submitted BABE equivocation report."),
|
||||
Err(e) => debug::error!("Error submitting equivocation report: {:?}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn block_author() -> Option<T::AccountId> {
|
||||
Some(<pallet_authorship::Module<T>>::author())
|
||||
}
|
||||
}
|
||||
|
||||
/// A `ValidateUnsigned` implementation that 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: Trait> frame_support::unsigned::ValidateUnsigned for Module<T> {
|
||||
type Call = Call<T>;
|
||||
fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
|
||||
if let Call::report_equivocation_unsigned(equivocation_proof, _) = call {
|
||||
// discard equivocation report not coming from the local node
|
||||
match source {
|
||||
TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ }
|
||||
_ => {
|
||||
debug::warn!(
|
||||
target: "babe",
|
||||
"rejecting unsigned report equivocation transaction because it is not local/in-block."
|
||||
);
|
||||
|
||||
return InvalidTransaction::Call.into();
|
||||
}
|
||||
}
|
||||
|
||||
ValidTransaction::with_tag_prefix("BabeEquivocation")
|
||||
// We assign the maximum priority for any equivocation report.
|
||||
.priority(TransactionPriority::max_value())
|
||||
// Only one equivocation report for the same offender at the same slot.
|
||||
.and_provides((
|
||||
equivocation_proof.offender.clone(),
|
||||
equivocation_proof.slot_number,
|
||||
))
|
||||
// We don't propagate this. This can never be included on a remote node.
|
||||
.propagate(false)
|
||||
.build()
|
||||
} else {
|
||||
InvalidTransaction::Call.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
|
||||
if let Call::report_equivocation_unsigned(equivocation_proof, key_owner_proof) = call {
|
||||
// check the membership proof to extract the offender's id
|
||||
let key = (
|
||||
sp_consensus_babe::KEY_TYPE,
|
||||
equivocation_proof.offender.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 is_known_offence = T::HandleEquivocation::is_known_offence(
|
||||
&[offender],
|
||||
&equivocation_proof.slot_number,
|
||||
);
|
||||
|
||||
if is_known_offence {
|
||||
Err(InvalidTransaction::Stale.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
Err(InvalidTransaction::Call.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A BABE equivocation offence report.
|
||||
///
|
||||
/// When a validator released two or more blocks at the same slot.
|
||||
pub struct BabeEquivocationOffence<FullIdentification> {
|
||||
/// A babe slot number in which this incident happened.
|
||||
pub slot: SlotNumber,
|
||||
/// 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 that produced the equivocation.
|
||||
pub offender: FullIdentification,
|
||||
}
|
||||
|
||||
impl<FullIdentification: Clone> Offence<FullIdentification>
|
||||
for BabeEquivocationOffence<FullIdentification>
|
||||
{
|
||||
const ID: Kind = *b"babe:equivocatio";
|
||||
type TimeSlot = SlotNumber;
|
||||
|
||||
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.slot
|
||||
}
|
||||
|
||||
fn slash_fraction(offenders_count: u32, validator_set_count: u32) -> Perbill {
|
||||
// the formula is min((3k / n)^2, 1)
|
||||
let x = Perbill::from_rational_approximation(3 * offenders_count, validator_set_count);
|
||||
// _ ^ 2
|
||||
x.square()
|
||||
}
|
||||
}
|
||||
+187
-64
@@ -21,37 +21,44 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![warn(unused_must_use, unsafe_code, unused_variables, unused_must_use)]
|
||||
|
||||
use pallet_timestamp;
|
||||
|
||||
use sp_std::{result, prelude::*};
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::{
|
||||
decl_storage, decl_module, traits::{FindAuthor, Get, Randomness as RandomnessT},
|
||||
decl_error, decl_module, decl_storage,
|
||||
traits::{FindAuthor, Get, KeyOwnerProofSystem, Randomness as RandomnessT},
|
||||
weights::Weight,
|
||||
Parameter,
|
||||
};
|
||||
use sp_timestamp::OnTimestampSet;
|
||||
use sp_runtime::{generic::DigestItem, ConsensusEngineId, Perbill};
|
||||
use sp_runtime::traits::{IsMember, SaturatedConversion, Saturating, Hash, One};
|
||||
use sp_staking::{
|
||||
SessionIndex,
|
||||
offence::{Offence, Kind},
|
||||
};
|
||||
use frame_system::{ensure_none, ensure_signed};
|
||||
use sp_application_crypto::Public;
|
||||
use sp_runtime::{
|
||||
generic::DigestItem,
|
||||
traits::{Hash, IsMember, One, SaturatedConversion, Saturating},
|
||||
ConsensusEngineId, KeyTypeId,
|
||||
};
|
||||
use sp_session::{GetSessionNumber, GetValidatorCount};
|
||||
use sp_std::{prelude::*, result};
|
||||
use sp_timestamp::OnTimestampSet;
|
||||
|
||||
use codec::{Encode, Decode};
|
||||
use sp_inherents::{InherentIdentifier, InherentData, ProvideInherent, MakeFatalError};
|
||||
use sp_consensus_babe::{
|
||||
BABE_ENGINE_ID, ConsensusLog, BabeAuthorityWeight, SlotNumber,
|
||||
inherents::{INHERENT_IDENTIFIER, BabeInherentData},
|
||||
digests::{NextEpochDescriptor, NextConfigDescriptor, PreDigest},
|
||||
digests::{NextConfigDescriptor, NextEpochDescriptor, PreDigest},
|
||||
inherents::{BabeInherentData, INHERENT_IDENTIFIER},
|
||||
BabeAuthorityWeight, ConsensusLog, EquivocationProof, SlotNumber, BABE_ENGINE_ID,
|
||||
};
|
||||
use sp_consensus_vrf::schnorrkel;
|
||||
pub use sp_consensus_babe::{AuthorityId, VRF_OUTPUT_LENGTH, RANDOMNESS_LENGTH, PUBLIC_KEY_LENGTH};
|
||||
use sp_inherents::{InherentData, InherentIdentifier, MakeFatalError, ProvideInherent};
|
||||
|
||||
pub use sp_consensus_babe::{AuthorityId, PUBLIC_KEY_LENGTH, RANDOMNESS_LENGTH, VRF_OUTPUT_LENGTH};
|
||||
|
||||
mod equivocation;
|
||||
|
||||
#[cfg(any(feature = "runtime-benchmarks", test))]
|
||||
mod benchmarking;
|
||||
#[cfg(all(feature = "std", test))]
|
||||
mod mock;
|
||||
#[cfg(all(feature = "std", test))]
|
||||
mod tests;
|
||||
|
||||
#[cfg(all(feature = "std", test))]
|
||||
mod mock;
|
||||
pub use equivocation::{BabeEquivocationOffence, EquivocationHandler, HandleEquivocation};
|
||||
|
||||
pub trait Trait: pallet_timestamp::Trait {
|
||||
/// The amount of time, in slots, that each epoch should last.
|
||||
@@ -70,6 +77,30 @@ pub trait Trait: pallet_timestamp::Trait {
|
||||
/// Typically, the `ExternalTrigger` type should be used. An internal trigger should only be used
|
||||
/// when no other module is responsible for changing authority set.
|
||||
type EpochChangeTrigger: EpochChangeTrigger;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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, AuthorityId),
|
||||
Proof = Self::KeyOwnerProof,
|
||||
IdentificationTuple = Self::KeyOwnerIdentification,
|
||||
>;
|
||||
|
||||
/// 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>;
|
||||
}
|
||||
|
||||
/// Trigger an epoch change, if any should take place.
|
||||
@@ -106,6 +137,17 @@ const UNDER_CONSTRUCTION_SEGMENT_LENGTH: usize = 256;
|
||||
|
||||
type MaybeRandomness = Option<schnorrkel::Randomness>;
|
||||
|
||||
decl_error! {
|
||||
pub enum Error for Module<T: Trait> {
|
||||
/// An equivocation proof provided as part of an equivocation report is invalid.
|
||||
InvalidEquivocationProof,
|
||||
/// A key ownership proof provided as part of an equivocation report is invalid.
|
||||
InvalidKeyOwnershipProof,
|
||||
/// A given equivocation report is valid but already previously reported.
|
||||
DuplicateOffenceReport,
|
||||
}
|
||||
}
|
||||
|
||||
decl_storage! {
|
||||
trait Store for Module<T: Trait> as Babe {
|
||||
/// Current epoch index.
|
||||
@@ -208,6 +250,69 @@ decl_module! {
|
||||
// remove temporary "environment" entry from storage
|
||||
Lateness::<T>::kill();
|
||||
}
|
||||
|
||||
/// Report authority 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.
|
||||
#[weight = weight::weight_for_report_equivocation::<T>()]
|
||||
fn report_equivocation(
|
||||
origin,
|
||||
equivocation_proof: EquivocationProof<T::Header>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) {
|
||||
let reporter = ensure_signed(origin)?;
|
||||
|
||||
Self::do_report_equivocation(
|
||||
Some(reporter),
|
||||
equivocation_proof,
|
||||
key_owner_proof,
|
||||
)?;
|
||||
}
|
||||
|
||||
/// Report authority 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.
|
||||
#[weight = weight::weight_for_report_equivocation::<T>()]
|
||||
fn report_equivocation_unsigned(
|
||||
origin,
|
||||
equivocation_proof: EquivocationProof<T::Header>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) {
|
||||
ensure_none(origin)?;
|
||||
|
||||
Self::do_report_equivocation(
|
||||
T::HandleEquivocation::block_author(),
|
||||
equivocation_proof,
|
||||
key_owner_proof,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod weight {
|
||||
use frame_support::{
|
||||
traits::Get,
|
||||
weights::{constants::WEIGHT_PER_MICROS, Weight},
|
||||
};
|
||||
|
||||
pub fn weight_for_report_equivocation<T: super::Trait>() -> Weight {
|
||||
// checking membership proof
|
||||
(35 * WEIGHT_PER_MICROS)
|
||||
.saturating_add(T::DbWeight::get().reads(5))
|
||||
// check equivocation proof
|
||||
.saturating_add(110 * WEIGHT_PER_MICROS)
|
||||
// report offence
|
||||
.saturating_add(110 * WEIGHT_PER_MICROS)
|
||||
// worst case we are considering is that the given offender
|
||||
// is backed by 200 nominators
|
||||
.saturating_add(T::DbWeight::get().reads(14 + 3 * 200))
|
||||
.saturating_add(T::DbWeight::get().writes(10 + 3 * 200))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,51 +379,6 @@ impl<T: Trait> pallet_session::ShouldEndSession<T::BlockNumber> for Module<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A BABE equivocation offence report.
|
||||
///
|
||||
/// When a validator released two or more blocks at the same slot.
|
||||
pub struct BabeEquivocationOffence<FullIdentification> {
|
||||
/// A babe slot number in which this incident happened.
|
||||
pub slot: u64,
|
||||
/// 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 that produced the equivocation.
|
||||
pub offender: FullIdentification,
|
||||
}
|
||||
|
||||
impl<FullIdentification: Clone> Offence<FullIdentification> for BabeEquivocationOffence<FullIdentification> {
|
||||
const ID: Kind = *b"babe:equivocatio";
|
||||
type TimeSlot = u64;
|
||||
|
||||
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.slot
|
||||
}
|
||||
|
||||
fn slash_fraction(
|
||||
offenders_count: u32,
|
||||
validator_set_count: u32,
|
||||
) -> Perbill {
|
||||
// the formula is min((3k / n)^2, 1)
|
||||
let x = Perbill::from_rational_approximation(3 * offenders_count, validator_set_count);
|
||||
// _ ^ 2
|
||||
x.square()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait> Module<T> {
|
||||
/// Determine the BABE slot duration based on the Timestamp module configuration.
|
||||
pub fn slot_duration() -> T::Moment {
|
||||
@@ -561,6 +621,69 @@ impl<T: Trait> Module<T> {
|
||||
Authorities::put(authorities);
|
||||
}
|
||||
}
|
||||
|
||||
fn do_report_equivocation(
|
||||
reporter: Option<T::AccountId>,
|
||||
equivocation_proof: EquivocationProof<T::Header>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) -> Result<(), Error<T>> {
|
||||
let offender = equivocation_proof.offender.clone();
|
||||
let slot_number = equivocation_proof.slot_number;
|
||||
|
||||
// validate the equivocation proof
|
||||
if !sp_consensus_babe::check_equivocation_proof(equivocation_proof) {
|
||||
return Err(Error::InvalidEquivocationProof.into());
|
||||
}
|
||||
|
||||
let validator_set_count = key_owner_proof.validator_count();
|
||||
let session_index = key_owner_proof.session();
|
||||
|
||||
let epoch_index = (slot_number.saturating_sub(GenesisSlot::get()) / T::EpochDuration::get())
|
||||
.saturated_into::<u32>();
|
||||
|
||||
// check that the slot number is consistent with the session index
|
||||
// in the key ownership proof (i.e. slot is for that epoch)
|
||||
if epoch_index != session_index {
|
||||
return Err(Error::InvalidKeyOwnershipProof.into());
|
||||
}
|
||||
|
||||
// check the membership proof and extract the offender's id
|
||||
let key = (sp_consensus_babe::KEY_TYPE, offender);
|
||||
let offender = T::KeyOwnerProofSystem::check_proof(key, key_owner_proof)
|
||||
.ok_or(Error::InvalidKeyOwnershipProof)?;
|
||||
|
||||
let offence = BabeEquivocationOffence {
|
||||
slot: slot_number,
|
||||
validator_set_count,
|
||||
offender,
|
||||
session_index,
|
||||
};
|
||||
|
||||
let reporters = match reporter {
|
||||
Some(id) => vec![id],
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
T::HandleEquivocation::report_offence(reporters, offence)
|
||||
.map_err(|_| Error::DuplicateOffenceReport)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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<T::Header>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) -> Option<()> {
|
||||
T::HandleEquivocation::submit_unsigned_equivocation_report(
|
||||
equivocation_proof,
|
||||
key_owner_proof,
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait> OnTimestampSet<T::Moment> for Module<T> {
|
||||
|
||||
@@ -18,27 +18,37 @@
|
||||
//! Test utilities
|
||||
|
||||
use codec::Encode;
|
||||
use super::{Trait, Module, GenesisConfig, CurrentSlot};
|
||||
use super::{Trait, Module, CurrentSlot};
|
||||
use sp_runtime::{
|
||||
Perbill, impl_opaque_keys,
|
||||
testing::{Header, UintAuthorityId, Digest, DigestItem},
|
||||
traits::IdentityLookup,
|
||||
curve::PiecewiseLinear,
|
||||
testing::{Digest, DigestItem, Header, TestXt,},
|
||||
traits::{Convert, Header as _, IdentityLookup, OpaqueKeys, SaturatedConversion},
|
||||
};
|
||||
use frame_system::InitKind;
|
||||
use frame_support::{
|
||||
impl_outer_origin, parameter_types, StorageValue,
|
||||
traits::OnInitialize,
|
||||
impl_outer_dispatch, impl_outer_origin, parameter_types, StorageValue,
|
||||
traits::{KeyOwnerProofSystem, OnInitialize},
|
||||
weights::Weight,
|
||||
};
|
||||
use sp_io;
|
||||
use sp_core::{H256, U256, crypto::Pair};
|
||||
use sp_consensus_babe::AuthorityPair;
|
||||
use sp_core::{H256, U256, crypto::{KeyTypeId, Pair}};
|
||||
use sp_consensus_babe::{AuthorityId, AuthorityPair, SlotNumber};
|
||||
use sp_consensus_vrf::schnorrkel::{VRFOutput, VRFProof};
|
||||
use sp_staking::SessionIndex;
|
||||
use pallet_staking::EraIndex;
|
||||
|
||||
impl_outer_origin!{
|
||||
pub enum Origin for Test where system = frame_system {}
|
||||
}
|
||||
|
||||
impl_outer_dispatch! {
|
||||
pub enum Call for Test where origin: Origin {
|
||||
babe::Babe,
|
||||
staking::Staking,
|
||||
}
|
||||
}
|
||||
|
||||
type DummyValidatorId = u64;
|
||||
|
||||
// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
|
||||
@@ -50,7 +60,6 @@ parameter_types! {
|
||||
pub const MaximumBlockWeight: Weight = 1024;
|
||||
pub const MaximumBlockLength: u32 = 2 * 1024;
|
||||
pub const AvailableBlockRatio: Perbill = Perbill::one();
|
||||
pub const MinimumPeriod: u64 = 1;
|
||||
pub const EpochDuration: u64 = 3;
|
||||
pub const ExpectedBlockTime: u64 = 1;
|
||||
pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(16);
|
||||
@@ -61,7 +70,7 @@ impl frame_system::Trait for Test {
|
||||
type Origin = Origin;
|
||||
type Index = u64;
|
||||
type BlockNumber = u64;
|
||||
type Call = ();
|
||||
type Call = Call;
|
||||
type Hash = H256;
|
||||
type Version = ();
|
||||
type Hashing = sp_runtime::traits::BlakeTwo256;
|
||||
@@ -78,27 +87,55 @@ impl frame_system::Trait for Test {
|
||||
type AvailableBlockRatio = AvailableBlockRatio;
|
||||
type MaximumBlockLength = MaximumBlockLength;
|
||||
type ModuleToIndex = ();
|
||||
type AccountData = ();
|
||||
type AccountData = pallet_balances::AccountData<u128>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
}
|
||||
|
||||
impl<C> frame_system::offchain::SendTransactionTypes<C> for Test
|
||||
where
|
||||
Call: From<C>,
|
||||
{
|
||||
type OverarchingCall = Call;
|
||||
type Extrinsic = TestXt<Call, ()>;
|
||||
}
|
||||
|
||||
impl_opaque_keys! {
|
||||
pub struct MockSessionKeys {
|
||||
pub dummy: UintAuthorityId,
|
||||
pub babe_authority: super::Module<Test>,
|
||||
}
|
||||
}
|
||||
|
||||
impl pallet_session::Trait for Test {
|
||||
type Event = ();
|
||||
type ValidatorId = <Self as frame_system::Trait>::AccountId;
|
||||
type ValidatorIdOf = pallet_staking::StashOf<Self>;
|
||||
type ShouldEndSession = Babe;
|
||||
type SessionHandler = (Babe,);
|
||||
type SessionManager = ();
|
||||
type ValidatorIdOf = ();
|
||||
type NextSessionRotation = Babe;
|
||||
type SessionManager = pallet_session::historical::NoteHistoricalRoot<Self, Staking>;
|
||||
type SessionHandler = <MockSessionKeys as OpaqueKeys>::KeyTypeIdProviders;
|
||||
type Keys = MockSessionKeys;
|
||||
type DisabledValidatorsThreshold = DisabledValidatorsThreshold;
|
||||
type NextSessionRotation = Babe;
|
||||
}
|
||||
|
||||
impl pallet_session::historical::Trait for Test {
|
||||
type FullIdentification = pallet_staking::Exposure<u64, u128>;
|
||||
type FullIdentificationOf = pallet_staking::ExposureOf<Self>;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const UncleGenerations: u64 = 0;
|
||||
}
|
||||
|
||||
impl pallet_authorship::Trait for Test {
|
||||
type FindAuthor = pallet_session::FindAccountFromAuthorIndex<Self, Babe>;
|
||||
type UncleGenerations = UncleGenerations;
|
||||
type FilterUncle = ();
|
||||
type EventHandler = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const MinimumPeriod: u64 = 1;
|
||||
}
|
||||
|
||||
impl pallet_timestamp::Trait for Test {
|
||||
@@ -107,33 +144,142 @@ impl pallet_timestamp::Trait for Test {
|
||||
type MinimumPeriod = MinimumPeriod;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ExistentialDeposit: u128 = 1;
|
||||
}
|
||||
|
||||
impl pallet_balances::Trait for Test {
|
||||
type Balance = u128;
|
||||
type DustRemoval = ();
|
||||
type Event = ();
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = System;
|
||||
}
|
||||
|
||||
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 SlashDeferDuration: EraIndex = 0;
|
||||
pub const AttestationPeriod: u64 = 100;
|
||||
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
|
||||
pub const MaxNominatorRewardedPerValidator: u32 = 64;
|
||||
pub const ElectionLookahead: u64 = 0;
|
||||
pub const StakingUnsignedPriority: u64 = u64::max_value() / 2;
|
||||
}
|
||||
|
||||
pub struct CurrencyToVoteHandler;
|
||||
|
||||
impl Convert<u128, u128> for CurrencyToVoteHandler {
|
||||
fn convert(x: u128) -> u128 {
|
||||
x
|
||||
}
|
||||
}
|
||||
|
||||
impl Convert<u128, u64> for CurrencyToVoteHandler {
|
||||
fn convert(x: u128) -> u64 {
|
||||
x.saturated_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl pallet_staking::Trait for Test {
|
||||
type RewardRemainder = ();
|
||||
type CurrencyToVote = CurrencyToVoteHandler;
|
||||
type Event = ();
|
||||
type Currency = Balances;
|
||||
type Slash = ();
|
||||
type Reward = ();
|
||||
type SessionsPerEra = SessionsPerEra;
|
||||
type BondingDuration = BondingDuration;
|
||||
type SlashDeferDuration = SlashDeferDuration;
|
||||
type SlashCancelOrigin = frame_system::EnsureRoot<Self::AccountId>;
|
||||
type SessionInterface = Self;
|
||||
type UnixTime = pallet_timestamp::Module<Test>;
|
||||
type RewardCurve = RewardCurve;
|
||||
type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator;
|
||||
type NextNewSession = Session;
|
||||
type ElectionLookahead = ElectionLookahead;
|
||||
type Call = Call;
|
||||
type UnsignedPriority = StakingUnsignedPriority;
|
||||
type MaxIterations = ();
|
||||
type MinSolutionScoreBump = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub OffencesWeightSoftLimit: Weight = Perbill::from_percent(60) * MaximumBlockWeight::get();
|
||||
}
|
||||
|
||||
impl pallet_offences::Trait for Test {
|
||||
type Event = ();
|
||||
type IdentificationTuple = pallet_session::historical::IdentificationTuple<Self>;
|
||||
type OnOffenceHandler = Staking;
|
||||
type WeightSoftLimit = OffencesWeightSoftLimit;
|
||||
}
|
||||
|
||||
impl Trait for Test {
|
||||
type EpochDuration = EpochDuration;
|
||||
type ExpectedBlockTime = ExpectedBlockTime;
|
||||
type EpochChangeTrigger = crate::ExternalTrigger;
|
||||
|
||||
type KeyOwnerProofSystem = Historical;
|
||||
|
||||
type KeyOwnerProof =
|
||||
<Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(KeyTypeId, AuthorityId)>>::Proof;
|
||||
|
||||
type KeyOwnerIdentification = <Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(
|
||||
KeyTypeId,
|
||||
AuthorityId,
|
||||
)>>::IdentificationTuple;
|
||||
|
||||
type HandleEquivocation = super::EquivocationHandler<Self::KeyOwnerIdentification, Offences>;
|
||||
}
|
||||
|
||||
pub fn new_test_ext(authorities_len: usize) -> (Vec<AuthorityPair>, sp_io::TestExternalities) {
|
||||
let pairs = (0..authorities_len).map(|i| {
|
||||
AuthorityPair::from_seed(&U256::from(i).into())
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
|
||||
GenesisConfig {
|
||||
authorities: pairs.iter().map(|a| (a.public(), 1)).collect(),
|
||||
}.assimilate_storage::<Test>(&mut t).unwrap();
|
||||
(pairs, t.into())
|
||||
}
|
||||
pub type Balances = pallet_balances::Module<Test>;
|
||||
pub type Historical = pallet_session::historical::Module<Test>;
|
||||
pub type Offences = pallet_offences::Module<Test>;
|
||||
pub type Session = pallet_session::Module<Test>;
|
||||
pub type Staking = pallet_staking::Module<Test>;
|
||||
pub type System = frame_system::Module<Test>;
|
||||
pub type Timestamp = pallet_timestamp::Module<Test>;
|
||||
pub type Babe = Module<Test>;
|
||||
|
||||
pub fn go_to_block(n: u64, s: u64) {
|
||||
use frame_support::traits::OnFinalize;
|
||||
|
||||
System::on_finalize(System::block_number());
|
||||
Session::on_finalize(System::block_number());
|
||||
Staking::on_finalize(System::block_number());
|
||||
|
||||
let parent_hash = if System::block_number() > 1 {
|
||||
let hdr = System::finalize();
|
||||
hdr.hash()
|
||||
} else {
|
||||
System::parent_hash()
|
||||
};
|
||||
|
||||
let pre_digest = make_secondary_plain_pre_digest(0, s);
|
||||
System::initialize(&n, &Default::default(), &Default::default(), &pre_digest, InitKind::Full);
|
||||
|
||||
System::initialize(&n, &parent_hash, &Default::default(), &pre_digest, InitKind::Full);
|
||||
System::set_block_number(n);
|
||||
Timestamp::set_timestamp(n);
|
||||
|
||||
if s > 1 {
|
||||
CurrentSlot::put(s);
|
||||
}
|
||||
// includes a call into `Babe::do_initialize`.
|
||||
|
||||
System::on_initialize(n);
|
||||
Session::on_initialize(n);
|
||||
Staking::on_initialize(n);
|
||||
}
|
||||
|
||||
/// Slots will grow accordingly to blocks
|
||||
@@ -145,6 +291,19 @@ pub fn progress_to_block(n: u64) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Progress to the first block at the given session
|
||||
pub fn start_session(session_index: SessionIndex) {
|
||||
let missing = (session_index - Session::current_index()) * 3;
|
||||
progress_to_block(System::block_number() + missing as u64 + 1);
|
||||
assert_eq!(Session::current_index(), session_index);
|
||||
}
|
||||
|
||||
/// Progress to the first block at the given era
|
||||
pub fn start_era(era_index: EraIndex) {
|
||||
start_session((era_index * 3).into());
|
||||
assert_eq!(Staking::current_era(), Some(era_index));
|
||||
}
|
||||
|
||||
pub fn make_pre_digest(
|
||||
authority_index: sp_consensus_babe::AuthorityIndex,
|
||||
slot_number: sp_consensus_babe::SlotNumber,
|
||||
@@ -177,6 +336,124 @@ pub fn make_secondary_plain_pre_digest(
|
||||
Digest { logs: vec![log] }
|
||||
}
|
||||
|
||||
pub type System = frame_system::Module<Test>;
|
||||
pub type Babe = Module<Test>;
|
||||
pub type Session = pallet_session::Module<Test>;
|
||||
pub fn new_test_ext(authorities_len: usize) -> sp_io::TestExternalities {
|
||||
new_test_ext_with_pairs(authorities_len).1
|
||||
}
|
||||
|
||||
pub fn new_test_ext_with_pairs(authorities_len: usize) -> (Vec<AuthorityPair>, sp_io::TestExternalities) {
|
||||
let pairs = (0..authorities_len).map(|i| {
|
||||
AuthorityPair::from_seed(&U256::from(i).into())
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
let public = pairs.iter().map(|p| p.public()).collect();
|
||||
|
||||
(pairs, new_test_ext_raw_authorities(public))
|
||||
}
|
||||
|
||||
pub fn new_test_ext_raw_authorities(authorities: Vec<AuthorityId>) -> sp_io::TestExternalities {
|
||||
let mut t = frame_system::GenesisConfig::default()
|
||||
.build_storage::<Test>()
|
||||
.unwrap();
|
||||
|
||||
// stashes are the index.
|
||||
let session_keys: Vec<_> = authorities
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, k)| {
|
||||
(
|
||||
i as u64,
|
||||
i as u64,
|
||||
MockSessionKeys {
|
||||
babe_authority: AuthorityId::from(k.clone()),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 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 balances: Vec<_> = (0..authorities.len())
|
||||
.map(|i| (i as u64, 10_000_000))
|
||||
.collect();
|
||||
|
||||
// NOTE: this will initialize the babe authorities
|
||||
// through OneSessionHandler::on_genesis_session
|
||||
pallet_session::GenesisConfig::<Test> { keys: session_keys }
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
pallet_balances::GenesisConfig::<Test> { balances }
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let staking_config = pallet_staking::GenesisConfig::<Test> {
|
||||
stakers,
|
||||
validator_count: 8,
|
||||
force_era: pallet_staking::Forcing::ForceNew,
|
||||
minimum_validator_count: 0,
|
||||
invulnerables: vec![],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
staking_config.assimilate_storage(&mut t).unwrap();
|
||||
|
||||
t.into()
|
||||
}
|
||||
|
||||
/// Creates an equivocation at the current block, by generating two headers.
|
||||
pub fn generate_equivocation_proof(
|
||||
offender_authority_index: u32,
|
||||
offender_authority_pair: &AuthorityPair,
|
||||
slot_number: SlotNumber,
|
||||
) -> sp_consensus_babe::EquivocationProof<Header> {
|
||||
use sp_consensus_babe::digests::CompatibleDigestItem;
|
||||
|
||||
let current_block = System::block_number();
|
||||
let current_slot = CurrentSlot::get();
|
||||
|
||||
let make_header = || {
|
||||
let parent_hash = System::parent_hash();
|
||||
let pre_digest = make_secondary_plain_pre_digest(offender_authority_index, slot_number);
|
||||
System::initialize(¤t_block, &parent_hash, &Default::default(), &pre_digest, InitKind::Full);
|
||||
System::set_block_number(current_block);
|
||||
Timestamp::set_timestamp(current_block);
|
||||
System::finalize()
|
||||
};
|
||||
|
||||
// sign the header prehash and sign it, adding it to the block as the seal
|
||||
// digest item
|
||||
let seal_header = |header: &mut Header| {
|
||||
let prehash = header.hash();
|
||||
let seal = <DigestItem as CompatibleDigestItem>::babe_seal(
|
||||
offender_authority_pair.sign(prehash.as_ref()),
|
||||
);
|
||||
header.digest_mut().push(seal);
|
||||
};
|
||||
|
||||
// generate two headers at the current block
|
||||
let mut h1 = make_header();
|
||||
let mut h2 = make_header();
|
||||
|
||||
seal_header(&mut h1);
|
||||
seal_header(&mut h2);
|
||||
|
||||
// restore previous runtime state
|
||||
go_to_block(current_block, current_slot);
|
||||
|
||||
sp_consensus_babe::EquivocationProof {
|
||||
slot_number,
|
||||
offender: offender_authority_pair.public(),
|
||||
first_header: h1,
|
||||
second_header: h2,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,16 @@
|
||||
|
||||
//! Consensus extension module tests for BABE consensus.
|
||||
|
||||
use super::*;
|
||||
use super::{Call, *};
|
||||
use frame_support::{
|
||||
assert_err, assert_ok,
|
||||
traits::{Currency, OnFinalize},
|
||||
};
|
||||
use mock::*;
|
||||
use frame_support::traits::OnFinalize;
|
||||
use pallet_session::ShouldEndSession;
|
||||
use sp_core::crypto::IsWrappedBy;
|
||||
use sp_consensus_babe::AllowedSlots;
|
||||
use sp_consensus_vrf::schnorrkel::{VRFOutput, VRFProof};
|
||||
use sp_core::crypto::{IsWrappedBy, Pair};
|
||||
|
||||
const EMPTY_RANDOMNESS: [u8; 32] = [
|
||||
74, 25, 49, 128, 53, 97, 244, 49,
|
||||
@@ -40,14 +43,14 @@ fn empty_randomness_is_correct() {
|
||||
|
||||
#[test]
|
||||
fn initial_values() {
|
||||
new_test_ext(4).1.execute_with(|| {
|
||||
new_test_ext(4).execute_with(|| {
|
||||
assert_eq!(Babe::authorities().len(), 4)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_module() {
|
||||
new_test_ext(4).1.execute_with(|| {
|
||||
new_test_ext(4).execute_with(|| {
|
||||
assert!(!Babe::should_end_session(0), "Genesis does not change sessions");
|
||||
assert!(!Babe::should_end_session(200000),
|
||||
"BABE does not include the block number in epoch calculations");
|
||||
@@ -56,7 +59,7 @@ fn check_module() {
|
||||
|
||||
#[test]
|
||||
fn first_block_epoch_zero_start() {
|
||||
let (pairs, mut ext) = new_test_ext(4);
|
||||
let (pairs, mut ext) = new_test_ext_with_pairs(4);
|
||||
|
||||
ext.execute_with(|| {
|
||||
let genesis_slot = 100;
|
||||
@@ -124,7 +127,7 @@ fn first_block_epoch_zero_start() {
|
||||
|
||||
#[test]
|
||||
fn authority_index() {
|
||||
new_test_ext(4).1.execute_with(|| {
|
||||
new_test_ext(4).execute_with(|| {
|
||||
assert_eq!(
|
||||
Babe::find_author((&[(BABE_ENGINE_ID, &[][..])]).into_iter().cloned()), None,
|
||||
"Trivially invalid authorities are ignored")
|
||||
@@ -133,7 +136,7 @@ fn authority_index() {
|
||||
|
||||
#[test]
|
||||
fn can_predict_next_epoch_change() {
|
||||
new_test_ext(0).1.execute_with(|| {
|
||||
new_test_ext(1).execute_with(|| {
|
||||
assert_eq!(<Test as Trait>::EpochDuration::get(), 3);
|
||||
// this sets the genesis slot to 6;
|
||||
go_to_block(1, 6);
|
||||
@@ -154,7 +157,7 @@ fn can_predict_next_epoch_change() {
|
||||
|
||||
#[test]
|
||||
fn can_enact_next_config() {
|
||||
new_test_ext(0).1.execute_with(|| {
|
||||
new_test_ext(1).execute_with(|| {
|
||||
assert_eq!(<Test as Trait>::EpochDuration::get(), 3);
|
||||
// this sets the genesis slot to 6;
|
||||
go_to_block(1, 6);
|
||||
@@ -183,3 +186,402 @@ fn can_enact_next_config() {
|
||||
assert_eq!(header.digest.logs[2], consensus_digest.clone())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_equivocation_current_session_works() {
|
||||
let (pairs, mut ext) = new_test_ext_with_pairs(3);
|
||||
|
||||
ext.execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
let authorities = Babe::authorities();
|
||||
let validators = Session::validators();
|
||||
|
||||
// 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(1, validator),
|
||||
pallet_staking::Exposure {
|
||||
total: 10_000,
|
||||
own: 10_000,
|
||||
others: vec![],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// we will use the validator at index 0 as the offending authority
|
||||
let offending_validator_index = 0;
|
||||
let offending_validator_id = Session::validators()[offending_validator_index];
|
||||
let offending_authority_pair = pairs
|
||||
.into_iter()
|
||||
.find(|p| p.public() == authorities[offending_validator_index].0)
|
||||
.unwrap();
|
||||
|
||||
// generate an equivocation proof. it creates two headers at the given
|
||||
// slot with different block hashes and signed by the given key
|
||||
let equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
|
||||
// create the key ownership proof
|
||||
let key = (
|
||||
sp_consensus_babe::KEY_TYPE,
|
||||
&offending_authority_pair.public(),
|
||||
);
|
||||
let key_owner_proof = Historical::prove(key).unwrap();
|
||||
|
||||
// report the equivocation
|
||||
Babe::report_equivocation_unsigned(Origin::none(), equivocation_proof, key_owner_proof)
|
||||
.unwrap();
|
||||
|
||||
// start a new era so that the results of the offence report
|
||||
// are applied at era end
|
||||
start_era(2);
|
||||
|
||||
// check that the balance of offending validator is slashed 100%.
|
||||
assert_eq!(
|
||||
Balances::total_balance(&offending_validator_id),
|
||||
10_000_000 - 10_000
|
||||
);
|
||||
assert_eq!(Staking::slashable_balance_of(&offending_validator_id), 0);
|
||||
assert_eq!(
|
||||
Staking::eras_stakers(2, offending_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 == offending_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_session_works() {
|
||||
let (pairs, mut ext) = new_test_ext_with_pairs(3);
|
||||
|
||||
ext.execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
let authorities = Babe::authorities();
|
||||
|
||||
// we will use the validator at index 0 as the offending authority
|
||||
let offending_validator_index = 0;
|
||||
let offending_validator_id = Session::validators()[offending_validator_index];
|
||||
let offending_authority_pair = pairs
|
||||
.into_iter()
|
||||
.find(|p| p.public() == authorities[offending_validator_index].0)
|
||||
.unwrap();
|
||||
|
||||
// generate an equivocation proof at the current slot
|
||||
let equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
|
||||
// create the key ownership proof
|
||||
let key = (
|
||||
sp_consensus_babe::KEY_TYPE,
|
||||
&offending_authority_pair.public(),
|
||||
);
|
||||
let key_owner_proof = Historical::prove(key).unwrap();
|
||||
|
||||
// start a new era and report the equivocation
|
||||
// from the previous era
|
||||
start_era(2);
|
||||
|
||||
// check the balance of the offending validator
|
||||
assert_eq!(Balances::total_balance(&offending_validator_id), 10_000_000);
|
||||
assert_eq!(
|
||||
Staking::slashable_balance_of(&offending_validator_id),
|
||||
10_000
|
||||
);
|
||||
|
||||
// report the equivocation
|
||||
Babe::report_equivocation_unsigned(Origin::none(), equivocation_proof, key_owner_proof)
|
||||
.unwrap();
|
||||
|
||||
// start a new era so that the results of the offence report
|
||||
// are applied at era end
|
||||
start_era(3);
|
||||
|
||||
// check that the balance of offending validator is slashed 100%.
|
||||
assert_eq!(
|
||||
Balances::total_balance(&offending_validator_id),
|
||||
10_000_000 - 10_000
|
||||
);
|
||||
assert_eq!(Staking::slashable_balance_of(&offending_validator_id), 0);
|
||||
assert_eq!(
|
||||
Staking::eras_stakers(3, offending_validator_id),
|
||||
pallet_staking::Exposure {
|
||||
total: 0,
|
||||
own: 0,
|
||||
others: vec![],
|
||||
},
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_equivocation_invalid_key_owner_proof() {
|
||||
let (pairs, mut ext) = new_test_ext_with_pairs(3);
|
||||
|
||||
ext.execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
let authorities = Babe::authorities();
|
||||
|
||||
// we will use the validator at index 0 as the offending authority
|
||||
let offending_validator_index = 0;
|
||||
let offending_authority_pair = pairs
|
||||
.into_iter()
|
||||
.find(|p| p.public() == authorities[offending_validator_index].0)
|
||||
.unwrap();
|
||||
|
||||
// generate an equivocation proof at the current slot
|
||||
let equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
|
||||
// create the key ownership proof
|
||||
let key = (
|
||||
sp_consensus_babe::KEY_TYPE,
|
||||
&offending_authority_pair.public(),
|
||||
);
|
||||
let mut key_owner_proof = Historical::prove(key).unwrap();
|
||||
|
||||
// we change the session index in the key ownership proof
|
||||
// which should make it invalid
|
||||
key_owner_proof.session = 0;
|
||||
assert_err!(
|
||||
Babe::report_equivocation_unsigned(
|
||||
Origin::none(),
|
||||
equivocation_proof.clone(),
|
||||
key_owner_proof
|
||||
),
|
||||
Error::<Test>::InvalidKeyOwnershipProof,
|
||||
);
|
||||
|
||||
// it should fail as well if we create a key owner proof
|
||||
// for a different authority than the offender
|
||||
let key = (sp_consensus_babe::KEY_TYPE, &authorities[1].0);
|
||||
let key_owner_proof = Historical::prove(key).unwrap();
|
||||
|
||||
// we need to progress to a new era to make sure that the key
|
||||
// ownership proof is properly checked, otherwise since the state
|
||||
// is still available the historical module will just check
|
||||
// against current session data.
|
||||
start_era(2);
|
||||
|
||||
assert_err!(
|
||||
Babe::report_equivocation_unsigned(Origin::none(), equivocation_proof, key_owner_proof),
|
||||
Error::<Test>::InvalidKeyOwnershipProof,
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_equivocation_invalid_equivocation_proof() {
|
||||
use sp_runtime::traits::Header;
|
||||
|
||||
let (pairs, mut ext) = new_test_ext_with_pairs(3);
|
||||
|
||||
ext.execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
let authorities = Babe::authorities();
|
||||
|
||||
// we will use the validator at index 0 as the offending authority
|
||||
let offending_validator_index = 0;
|
||||
let offending_authority_pair = pairs
|
||||
.into_iter()
|
||||
.find(|p| p.public() == authorities[offending_validator_index].0)
|
||||
.unwrap();
|
||||
|
||||
// create the key ownership proof
|
||||
let key = (
|
||||
sp_consensus_babe::KEY_TYPE,
|
||||
&offending_authority_pair.public(),
|
||||
);
|
||||
let key_owner_proof = Historical::prove(key).unwrap();
|
||||
|
||||
let assert_invalid_equivocation = |equivocation_proof| {
|
||||
assert_err!(
|
||||
Babe::report_equivocation_unsigned(
|
||||
Origin::none(),
|
||||
equivocation_proof,
|
||||
key_owner_proof.clone(),
|
||||
),
|
||||
Error::<Test>::InvalidEquivocationProof,
|
||||
)
|
||||
};
|
||||
|
||||
// both headers have the same hash, no equivocation.
|
||||
let mut equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
equivocation_proof.second_header = equivocation_proof.first_header.clone();
|
||||
assert_invalid_equivocation(equivocation_proof);
|
||||
|
||||
// missing preruntime digest from one header
|
||||
let mut equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
equivocation_proof.first_header.digest_mut().logs.remove(0);
|
||||
assert_invalid_equivocation(equivocation_proof);
|
||||
|
||||
// missing seal from one header
|
||||
let mut equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
equivocation_proof.first_header.digest_mut().logs.remove(1);
|
||||
assert_invalid_equivocation(equivocation_proof);
|
||||
|
||||
// invalid slot number in proof compared to runtime digest
|
||||
let mut equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
equivocation_proof.slot_number = 0;
|
||||
assert_invalid_equivocation(equivocation_proof.clone());
|
||||
|
||||
// different slot numbers in headers
|
||||
let h1 = equivocation_proof.first_header;
|
||||
let mut equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get() + 1,
|
||||
);
|
||||
|
||||
// use the header from the previous equivocation generated
|
||||
// at the previous slot
|
||||
equivocation_proof.first_header = h1.clone();
|
||||
|
||||
assert_invalid_equivocation(equivocation_proof.clone());
|
||||
|
||||
// invalid seal signature
|
||||
let mut equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get() + 1,
|
||||
);
|
||||
|
||||
// replace the seal digest with the digest from the
|
||||
// previous header at the previous slot
|
||||
equivocation_proof.first_header.digest_mut().pop();
|
||||
equivocation_proof
|
||||
.first_header
|
||||
.digest_mut()
|
||||
.push(h1.digest().logs().last().unwrap().clone());
|
||||
|
||||
assert_invalid_equivocation(equivocation_proof.clone());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_equivocation_validate_unsigned_prevents_duplicates() {
|
||||
use sp_runtime::transaction_validity::{
|
||||
InvalidTransaction, TransactionLongevity, TransactionPriority, TransactionSource,
|
||||
TransactionValidity, ValidTransaction,
|
||||
};
|
||||
|
||||
let (pairs, mut ext) = new_test_ext_with_pairs(3);
|
||||
|
||||
ext.execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
let authorities = Babe::authorities();
|
||||
|
||||
// generate and report an equivocation for the validator at index 0
|
||||
let offending_validator_index = 0;
|
||||
let offending_authority_pair = pairs
|
||||
.into_iter()
|
||||
.find(|p| p.public() == authorities[offending_validator_index].0)
|
||||
.unwrap();
|
||||
|
||||
let equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
|
||||
let key = (
|
||||
sp_consensus_babe::KEY_TYPE,
|
||||
&offending_authority_pair.public(),
|
||||
);
|
||||
let key_owner_proof = Historical::prove(key).unwrap();
|
||||
|
||||
let inner =
|
||||
Call::report_equivocation_unsigned(equivocation_proof.clone(), key_owner_proof.clone());
|
||||
|
||||
// only local/inblock reports are allowed
|
||||
assert_eq!(
|
||||
<Babe as sp_runtime::traits::ValidateUnsigned>::validate_unsigned(
|
||||
TransactionSource::External,
|
||||
&inner,
|
||||
),
|
||||
InvalidTransaction::Call.into(),
|
||||
);
|
||||
|
||||
// the transaction is valid when passed as local
|
||||
let tx_tag = (offending_authority_pair.public(), CurrentSlot::get());
|
||||
assert_eq!(
|
||||
<Babe as sp_runtime::traits::ValidateUnsigned>::validate_unsigned(
|
||||
TransactionSource::Local,
|
||||
&inner,
|
||||
),
|
||||
TransactionValidity::Ok(ValidTransaction {
|
||||
priority: TransactionPriority::max_value(),
|
||||
requires: vec![],
|
||||
provides: vec![("BabeEquivocation", tx_tag).encode()],
|
||||
longevity: TransactionLongevity::max_value(),
|
||||
propagate: false,
|
||||
})
|
||||
);
|
||||
|
||||
// the pre dispatch checks should also pass
|
||||
assert_ok!(<Babe as sp_runtime::traits::ValidateUnsigned>::pre_dispatch(&inner));
|
||||
|
||||
// we submit the report
|
||||
Babe::report_equivocation_unsigned(Origin::none(), equivocation_proof, key_owner_proof)
|
||||
.unwrap();
|
||||
|
||||
// the report should now be considered stale and the transaction is invalid
|
||||
assert_err!(
|
||||
<Babe as sp_runtime::traits::ValidateUnsigned>::pre_dispatch(&inner),
|
||||
InvalidTransaction::Stale,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ use sp_runtime::{
|
||||
},
|
||||
DispatchResult, Perbill,
|
||||
};
|
||||
use sp_session::GetSessionNumber;
|
||||
use sp_staking::{
|
||||
offence::{Kind, Offence, OffenceError, ReportOffence},
|
||||
SessionIndex,
|
||||
@@ -376,38 +377,3 @@ impl<FullIdentification: Clone> Offence<FullIdentification>
|
||||
x.square()
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait to get a session number the `MembershipProof` belongs to.
|
||||
pub trait GetSessionNumber {
|
||||
fn session(&self) -> SessionIndex;
|
||||
}
|
||||
|
||||
/// A trait to get the validator count at the session the `MembershipProof`
|
||||
/// belongs to.
|
||||
pub trait GetValidatorCount {
|
||||
fn validator_count(&self) -> sp_session::ValidatorCount;
|
||||
}
|
||||
|
||||
impl GetSessionNumber for frame_support::Void {
|
||||
fn session(&self) -> SessionIndex {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl GetValidatorCount for frame_support::Void {
|
||||
fn validator_count(&self) -> sp_session::ValidatorCount {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl GetSessionNumber for sp_session::MembershipProof {
|
||||
fn session(&self) -> SessionIndex {
|
||||
self.session
|
||||
}
|
||||
}
|
||||
|
||||
impl GetValidatorCount for sp_session::MembershipProof {
|
||||
fn validator_count(&self) -> sp_session::ValidatorCount {
|
||||
self.validator_count
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,15 +49,18 @@ use sp_runtime::{
|
||||
traits::Zero,
|
||||
DispatchResult, KeyTypeId,
|
||||
};
|
||||
use sp_session::{GetSessionNumber, GetValidatorCount};
|
||||
use sp_staking::SessionIndex;
|
||||
|
||||
mod equivocation;
|
||||
#[cfg(all(feature = "std", test))]
|
||||
mod mock;
|
||||
#[cfg(all(feature = "std", test))]
|
||||
mod tests;
|
||||
|
||||
pub use equivocation::{
|
||||
EquivocationHandler, GetSessionNumber, GetValidatorCount, GrandpaEquivocationOffence,
|
||||
GrandpaOffence, GrandpaTimeSlot, HandleEquivocation, ValidateEquivocationReport,
|
||||
EquivocationHandler, GrandpaEquivocationOffence, GrandpaOffence, GrandpaTimeSlot,
|
||||
HandleEquivocation, ValidateEquivocationReport,
|
||||
};
|
||||
|
||||
pub trait Trait: frame_system::Trait {
|
||||
|
||||
@@ -86,10 +86,16 @@ impl ReportOffence<u64, IdentificationTuple, Offence> for OffenceHandler {
|
||||
OFFENCES.with(|l| l.borrow_mut().push((reporters, offence)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_known_offence(_offenders: &[IdentificationTuple], _time_slot: &SessionIndex) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_test_ext() -> sp_io::TestExternalities {
|
||||
let t = frame_system::GenesisConfig::default().build_storage::<Runtime>().unwrap();
|
||||
let t = frame_system::GenesisConfig::default()
|
||||
.build_storage::<Runtime>()
|
||||
.unwrap();
|
||||
t.into()
|
||||
}
|
||||
|
||||
|
||||
@@ -323,21 +323,16 @@ benchmarks! {
|
||||
}
|
||||
|
||||
report_offence_babe {
|
||||
let r in 1 .. MAX_REPORTERS;
|
||||
let n in 0 .. MAX_NOMINATORS.min(MAX_NOMINATIONS as u32);
|
||||
let o = 1;
|
||||
|
||||
// Make r reporters
|
||||
let mut reporters = vec![];
|
||||
for i in 0 .. r {
|
||||
let reporter = account("reporter", i, SEED);
|
||||
reporters.push(reporter);
|
||||
}
|
||||
// for babe equivocation reports the number of reporters
|
||||
// and offenders is always 1
|
||||
let reporters = vec![account("reporter", 1, SEED)];
|
||||
|
||||
// make sure reporters actually get rewarded
|
||||
Staking::<T>::set_slash_reward_fraction(Perbill::one());
|
||||
|
||||
let (mut offenders, raw_offenders) = make_offenders::<T>(o, n)?;
|
||||
let (mut offenders, raw_offenders) = make_offenders::<T>(1, n)?;
|
||||
let keys = ImOnline::<T>::keys();
|
||||
|
||||
let offence = BabeEquivocationOffence {
|
||||
@@ -357,9 +352,9 @@ benchmarks! {
|
||||
assert_eq!(
|
||||
System::<T>::event_count(), 0
|
||||
+ 1 // offence
|
||||
+ 2 * r // reporter (reward + endowment)
|
||||
+ o // offenders slashed
|
||||
+ o * n // nominators slashed
|
||||
+ 2 // reporter (reward + endowment)
|
||||
+ 1 // offenders slashed
|
||||
+ n // nominators slashed
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +185,15 @@ where
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_known_offence(offenders: &[T::IdentificationTuple], time_slot: &O::TimeSlot) -> bool {
|
||||
let any_unknown = offenders.iter().any(|offender| {
|
||||
let report_id = Self::report_id::<O>(time_slot, offender);
|
||||
!<Reports<T>>::contains_key(&report_id)
|
||||
});
|
||||
|
||||
!any_unknown
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait> Module<T> {
|
||||
|
||||
@@ -174,6 +174,77 @@ fn doesnt_deposit_event_for_dups() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_if_an_offence_is_dup() {
|
||||
type TestOffence = Offence<u64>;
|
||||
|
||||
new_test_ext().execute_with(|| {
|
||||
let time_slot = 42;
|
||||
assert_eq!(offence_reports(KIND, time_slot), vec![]);
|
||||
|
||||
let offence = |time_slot, offenders| TestOffence {
|
||||
validator_set_count: 5,
|
||||
time_slot,
|
||||
offenders,
|
||||
};
|
||||
|
||||
let mut test_offence = offence(time_slot, vec![0]);
|
||||
|
||||
// the report for authority 0 at time slot 42 should not be a known
|
||||
// offence
|
||||
assert!(
|
||||
!<Offences as ReportOffence<_, _, TestOffence>>::is_known_offence(
|
||||
&test_offence.offenders,
|
||||
&test_offence.time_slot
|
||||
)
|
||||
);
|
||||
|
||||
// we report an offence for authority 0 at time slot 42
|
||||
Offences::report_offence(vec![], test_offence.clone()).unwrap();
|
||||
|
||||
// the same report should be a known offence now
|
||||
assert!(
|
||||
<Offences as ReportOffence<_, _, TestOffence>>::is_known_offence(
|
||||
&test_offence.offenders,
|
||||
&test_offence.time_slot
|
||||
)
|
||||
);
|
||||
|
||||
// and reporting it again should yield a duplicate report error
|
||||
assert_eq!(
|
||||
Offences::report_offence(vec![], test_offence.clone()),
|
||||
Err(OffenceError::DuplicateReport)
|
||||
);
|
||||
|
||||
// after adding a new offender to the offence report
|
||||
test_offence.offenders.push(1);
|
||||
|
||||
// it should not be a known offence anymore
|
||||
assert!(
|
||||
!<Offences as ReportOffence<_, _, TestOffence>>::is_known_offence(
|
||||
&test_offence.offenders,
|
||||
&test_offence.time_slot
|
||||
)
|
||||
);
|
||||
|
||||
// and reporting it again should work without any error
|
||||
assert_eq!(
|
||||
Offences::report_offence(vec![], test_offence.clone()),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
// creating a new offence for the same authorities on the next slot
|
||||
// should be considered a new offence and thefore not known
|
||||
let test_offence_next_slot = offence(time_slot + 1, vec![0, 1]);
|
||||
assert!(
|
||||
!<Offences as ReportOffence<_, _, TestOffence>>::is_known_offence(
|
||||
&test_offence_next_slot.offenders,
|
||||
&test_offence_next_slot.time_slot
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_properly_count_offences() {
|
||||
// We report two different authorities for the same issue. Ultimately, the 1st authority
|
||||
|
||||
@@ -13,12 +13,14 @@ targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
sp-std = { version = "2.0.0-rc4", default-features = false, path = "../../../primitives/std" }
|
||||
sp-session = { version = "2.0.0-rc4", default-features = false, path = "../../../primitives/session" }
|
||||
sp-runtime = { version = "2.0.0-rc4", default-features = false, path = "../../../primitives/runtime" }
|
||||
frame-system = { version = "2.0.0-rc4", default-features = false, path = "../../system" }
|
||||
frame-benchmarking = { version = "2.0.0-rc4", default-features = false, path = "../../benchmarking" }
|
||||
frame-support = { version = "2.0.0-rc4", default-features = false, path = "../../support" }
|
||||
pallet-staking = { version = "2.0.0-rc4", default-features = false, features = ["runtime-benchmarks"], path = "../../staking" }
|
||||
pallet-session = { version = "2.0.0-rc4", default-features = false, path = "../../session" }
|
||||
rand = { version = "0.7.2", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
serde = { version = "1.0.101" }
|
||||
@@ -33,6 +35,7 @@ pallet-balances = { version = "2.0.0-rc4", path = "../../balances" }
|
||||
default = ["std"]
|
||||
std = [
|
||||
"sp-std/std",
|
||||
"sp-session/std",
|
||||
"sp-runtime/std",
|
||||
"frame-system/std",
|
||||
"frame-benchmarking/std",
|
||||
|
||||
@@ -25,20 +25,30 @@ mod mock;
|
||||
use sp_std::prelude::*;
|
||||
use sp_std::vec;
|
||||
|
||||
use frame_system::RawOrigin;
|
||||
use frame_benchmarking::benchmarks;
|
||||
|
||||
use pallet_session::*;
|
||||
use pallet_session::Module as Session;
|
||||
|
||||
use pallet_staking::{
|
||||
MAX_NOMINATIONS,
|
||||
benchmarking::create_validator_with_nominators,
|
||||
use frame_support::{
|
||||
codec::Decode,
|
||||
storage::StorageValue,
|
||||
traits::{KeyOwnerProofSystem, OnInitialize},
|
||||
};
|
||||
use frame_system::RawOrigin;
|
||||
use pallet_session::{historical::Module as Historical, Module as Session, *};
|
||||
use pallet_staking::{
|
||||
benchmarking::create_validator_with_nominators, testing_utils::create_validators,
|
||||
MAX_NOMINATIONS,
|
||||
};
|
||||
use sp_runtime::traits::{One, StaticLookup};
|
||||
|
||||
const MAX_VALIDATORS: u32 = 1000;
|
||||
|
||||
pub struct Module<T: Trait>(pallet_session::Module<T>);
|
||||
pub trait Trait: pallet_session::Trait + pallet_session::historical::Trait + pallet_staking::Trait {}
|
||||
|
||||
pub trait Trait: pallet_session::Trait + pallet_staking::Trait {}
|
||||
impl<T: Trait> OnInitialize<T::BlockNumber> for Module<T> {
|
||||
fn on_initialize(n: T::BlockNumber) -> frame_support::weights::Weight {
|
||||
pallet_session::Module::<T>::on_initialize(n)
|
||||
}
|
||||
}
|
||||
|
||||
benchmarks! {
|
||||
_ { }
|
||||
@@ -59,6 +69,88 @@ benchmarks! {
|
||||
let proof: Vec<u8> = vec![0,1,2,3];
|
||||
Session::<T>::set_keys(RawOrigin::Signed(v_controller.clone()).into(), keys, proof)?;
|
||||
}: _(RawOrigin::Signed(v_controller))
|
||||
|
||||
check_membership_proof_current_session {
|
||||
let n in 2 .. MAX_VALIDATORS as u32;
|
||||
|
||||
let (key, key_owner_proof1) = check_membership_proof_setup::<T>(n);
|
||||
let key_owner_proof2 = key_owner_proof1.clone();
|
||||
}: {
|
||||
Historical::<T>::check_proof(key, key_owner_proof1);
|
||||
}
|
||||
verify {
|
||||
assert!(Historical::<T>::check_proof(key, key_owner_proof2).is_some());
|
||||
}
|
||||
|
||||
check_membership_proof_historical_session {
|
||||
let n in 2 .. MAX_VALIDATORS as u32;
|
||||
|
||||
let (key, key_owner_proof1) = check_membership_proof_setup::<T>(n);
|
||||
|
||||
// skip to the next session so that the session is historical
|
||||
// and the membership merkle proof must be checked.
|
||||
Session::<T>::rotate_session();
|
||||
|
||||
let key_owner_proof2 = key_owner_proof1.clone();
|
||||
}: {
|
||||
Historical::<T>::check_proof(key, key_owner_proof1);
|
||||
}
|
||||
verify {
|
||||
assert!(Historical::<T>::check_proof(key, key_owner_proof2).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up the benchmark for checking a membership proof. It creates the given
|
||||
/// number of validators, sets random session keys and then creates a membership
|
||||
/// proof for the first authority and returns its key and the proof.
|
||||
fn check_membership_proof_setup<T: Trait>(
|
||||
n: u32,
|
||||
) -> (
|
||||
(sp_runtime::KeyTypeId, &'static [u8; 32]),
|
||||
sp_session::MembershipProof,
|
||||
) {
|
||||
pallet_staking::ValidatorCount::put(n);
|
||||
|
||||
// create validators and set random session keys
|
||||
for (n, who) in create_validators::<T>(n, 1000)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
use rand::RngCore;
|
||||
use rand::SeedableRng;
|
||||
|
||||
let validator = T::Lookup::lookup(who).unwrap();
|
||||
let controller = pallet_staking::Module::<T>::bonded(validator).unwrap();
|
||||
|
||||
let keys = {
|
||||
let mut keys = [0u8; 128];
|
||||
|
||||
// we keep the keys for the first validator as 0x00000...
|
||||
if n > 0 {
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(n as u64);
|
||||
rng.fill_bytes(&mut keys);
|
||||
}
|
||||
|
||||
keys
|
||||
};
|
||||
|
||||
let keys: T::Keys = Decode::decode(&mut &keys[..]).unwrap();
|
||||
let proof: Vec<u8> = vec![];
|
||||
|
||||
Session::<T>::set_keys(RawOrigin::Signed(controller).into(), keys, proof).unwrap();
|
||||
}
|
||||
|
||||
Module::<T>::on_initialize(T::BlockNumber::one());
|
||||
|
||||
// skip sessions until the new validator set is enacted
|
||||
while Session::<T>::validators().len() < n as usize {
|
||||
Session::<T>::rotate_session();
|
||||
}
|
||||
|
||||
let key = (sp_runtime::KeyTypeId(*b"babe"), &[0u8; 32]);
|
||||
|
||||
(key, Historical::<T>::prove(key).unwrap())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -3355,6 +3355,10 @@ impl<T, Reporter, Offender, R, O> ReportOffence<Reporter, Offender, O>
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn is_known_offence(offenders: &[Offender], time_slot: &O::TimeSlot) -> bool {
|
||||
R::is_known_offence(offenders, time_slot)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
|
||||
@@ -30,11 +30,11 @@ pub use sp_tracing;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub use serde;
|
||||
pub use sp_core::Void;
|
||||
#[doc(hidden)]
|
||||
pub use sp_std;
|
||||
#[doc(hidden)]
|
||||
pub use codec;
|
||||
use codec::{Decode, Encode};
|
||||
#[cfg(feature = "std")]
|
||||
#[doc(hidden)]
|
||||
pub use once_cell;
|
||||
@@ -364,11 +364,6 @@ macro_rules! assert_ok {
|
||||
}
|
||||
}
|
||||
|
||||
/// The void type - it cannot exist.
|
||||
// Oh rust, you crack me up...
|
||||
#[derive(Clone, Decode, Encode, Eq, PartialEq, RuntimeDebug)]
|
||||
pub enum Void {}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[doc(hidden)]
|
||||
pub use serde::{Serialize, Deserialize};
|
||||
|
||||
@@ -17,9 +17,10 @@ codec = { package = "parity-scale-codec", version = "1.3.1", default-features =
|
||||
merlin = { version = "2.0", default-features = false }
|
||||
sp-std = { version = "2.0.0-rc4", default-features = false, path = "../../std" }
|
||||
sp-api = { version = "2.0.0-rc4", default-features = false, path = "../../api" }
|
||||
sp-core = { version = "2.0.0-rc4", default-features = false, path = "../../core" }
|
||||
sp-consensus = { version = "0.8.0-rc4", optional = true, path = "../common" }
|
||||
sp-consensus-slots = { version = "0.8.0-rc4", default-features = false, path = "../slots" }
|
||||
sp-consensus-vrf = { version = "0.8.0-rc4", path = "../vrf", default-features = false }
|
||||
sp-core = { version = "2.0.0-rc4", default-features = false, path = "../../core" }
|
||||
sp-inherents = { version = "2.0.0-rc4", default-features = false, path = "../../inherents" }
|
||||
sp-runtime = { version = "2.0.0-rc4", default-features = false, path = "../../runtime" }
|
||||
sp-timestamp = { version = "2.0.0-rc4", default-features = false, path = "../../timestamp" }
|
||||
@@ -27,14 +28,15 @@ sp-timestamp = { version = "2.0.0-rc4", default-features = false, path = "../../
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"sp-core/std",
|
||||
"sp-application-crypto/std",
|
||||
"codec/std",
|
||||
"merlin/std",
|
||||
"sp-std/std",
|
||||
"sp-api/std",
|
||||
"sp-consensus",
|
||||
"sp-consensus-slots/std",
|
||||
"sp-consensus-vrf/std",
|
||||
"sp-core/std",
|
||||
"sp-inherents/std",
|
||||
"sp-runtime/std",
|
||||
"sp-timestamp/std",
|
||||
|
||||
@@ -17,18 +17,14 @@
|
||||
|
||||
//! Private implementation details of BABE digests.
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use super::{BABE_ENGINE_ID, AuthoritySignature};
|
||||
use super::{AuthorityId, AuthorityIndex, SlotNumber, BabeAuthorityWeight, BabeEpochConfiguration, AllowedSlots};
|
||||
#[cfg(feature = "std")]
|
||||
use sp_runtime::{DigestItem, generic::OpaqueDigestItemId};
|
||||
#[cfg(feature = "std")]
|
||||
use std::fmt::Debug;
|
||||
use codec::{Decode, Encode};
|
||||
#[cfg(feature = "std")]
|
||||
use codec::Codec;
|
||||
use super::{
|
||||
AllowedSlots, AuthorityId, AuthorityIndex, AuthoritySignature, BabeAuthorityWeight,
|
||||
BabeEpochConfiguration, SlotNumber, BABE_ENGINE_ID,
|
||||
};
|
||||
use codec::{Codec, Decode, Encode};
|
||||
use sp_std::vec::Vec;
|
||||
use sp_runtime::RuntimeDebug;
|
||||
use sp_runtime::{generic::OpaqueDigestItemId, DigestItem, RuntimeDebug};
|
||||
|
||||
use sp_consensus_vrf::schnorrkel::{Randomness, VRFOutput, VRFProof};
|
||||
|
||||
/// Raw BABE primary slot assignment pre-digest.
|
||||
@@ -151,7 +147,6 @@ impl From<NextConfigDescriptor> for BabeEpochConfiguration {
|
||||
}
|
||||
|
||||
/// A digest item which is usable with BABE consensus.
|
||||
#[cfg(feature = "std")]
|
||||
pub trait CompatibleDigestItem: Sized {
|
||||
/// Construct a digest item which contains a BABE pre-digest.
|
||||
fn babe_pre_digest(seal: PreDigest) -> Self;
|
||||
@@ -172,9 +167,8 @@ pub trait CompatibleDigestItem: Sized {
|
||||
fn as_next_config_descriptor(&self) -> Option<NextConfigDescriptor>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<Hash> CompatibleDigestItem for DigestItem<Hash> where
|
||||
Hash: Debug + Send + Sync + Eq + Clone + Codec + 'static
|
||||
Hash: Send + Sync + Eq + Clone + Codec + 'static
|
||||
{
|
||||
fn babe_pre_digest(digest: PreDigest) -> Self {
|
||||
DigestItem::PreRuntime(BABE_ENGINE_ID, digest.encode())
|
||||
|
||||
@@ -23,17 +23,21 @@
|
||||
pub mod digests;
|
||||
pub mod inherents;
|
||||
|
||||
pub use sp_consensus_vrf::schnorrkel::{
|
||||
Randomness, VRF_PROOF_LENGTH, VRF_OUTPUT_LENGTH, RANDOMNESS_LENGTH
|
||||
};
|
||||
pub use merlin::Transcript;
|
||||
pub use sp_consensus_vrf::schnorrkel::{
|
||||
Randomness, RANDOMNESS_LENGTH, VRF_OUTPUT_LENGTH, VRF_PROOF_LENGTH,
|
||||
};
|
||||
|
||||
use codec::{Encode, Decode};
|
||||
use sp_std::vec::Vec;
|
||||
use sp_runtime::{ConsensusEngineId, RuntimeDebug};
|
||||
use codec::{Decode, Encode};
|
||||
#[cfg(feature = "std")]
|
||||
use sp_core::vrf::{VRFTranscriptData, VRFTranscriptValue};
|
||||
use crate::digests::{NextEpochDescriptor, NextConfigDescriptor};
|
||||
use sp_runtime::{traits::Header, ConsensusEngineId, RuntimeDebug};
|
||||
use sp_std::vec::Vec;
|
||||
|
||||
use crate::digests::{NextConfigDescriptor, NextEpochDescriptor};
|
||||
|
||||
/// Key type for BABE module.
|
||||
pub const KEY_TYPE: sp_core::crypto::KeyTypeId = sp_application_crypto::key_types::BABE;
|
||||
|
||||
mod app {
|
||||
use sp_application_crypto::{app_crypto, key_types::BABE, sr25519};
|
||||
@@ -73,7 +77,10 @@ pub const MEDIAN_ALGORITHM_CARDINALITY: usize = 1200; // arbitrary suggestion by
|
||||
pub type AuthorityIndex = u32;
|
||||
|
||||
/// A slot number.
|
||||
pub type SlotNumber = u64;
|
||||
pub use sp_consensus_slots::SlotNumber;
|
||||
|
||||
/// An equivocation proof for multiple block authorships on the same slot (i.e. double vote).
|
||||
pub type EquivocationProof<H> = sp_consensus_slots::EquivocationProof<H, AuthorityId>;
|
||||
|
||||
/// The weight of an authority.
|
||||
// NOTE: we use a unique name for the weight to avoid conflicts with other
|
||||
@@ -256,6 +263,93 @@ pub struct BabeEpochConfiguration {
|
||||
pub allowed_slots: AllowedSlots,
|
||||
}
|
||||
|
||||
/// Verifies the equivocation proof by making sure that: both headers have
|
||||
/// different hashes, are targetting the same slot, and have valid signatures by
|
||||
/// the same authority.
|
||||
pub fn check_equivocation_proof<H>(proof: EquivocationProof<H>) -> bool
|
||||
where
|
||||
H: Header,
|
||||
{
|
||||
use digests::*;
|
||||
use sp_application_crypto::RuntimeAppPublic;
|
||||
|
||||
let find_pre_digest = |header: &H| {
|
||||
header
|
||||
.digest()
|
||||
.logs()
|
||||
.iter()
|
||||
.find_map(|log| log.as_babe_pre_digest())
|
||||
};
|
||||
|
||||
let verify_seal_signature = |mut header: H, offender: &AuthorityId| {
|
||||
let seal = header.digest_mut().pop()?.as_babe_seal()?;
|
||||
let pre_hash = header.hash();
|
||||
|
||||
if !offender.verify(&pre_hash.as_ref(), &seal) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(())
|
||||
};
|
||||
|
||||
let verify_proof = || {
|
||||
// we must have different headers for the equivocation to be valid
|
||||
if proof.first_header.hash() == proof.second_header.hash() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let first_pre_digest = find_pre_digest(&proof.first_header)?;
|
||||
let second_pre_digest = find_pre_digest(&proof.second_header)?;
|
||||
|
||||
// both headers must be targetting the same slot and it must
|
||||
// be the same as the one in the proof.
|
||||
if proof.slot_number != first_pre_digest.slot_number() ||
|
||||
first_pre_digest.slot_number() != second_pre_digest.slot_number()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
// both headers must have been authored by the same authority
|
||||
if first_pre_digest.authority_index() != second_pre_digest.authority_index() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// we finally verify that the expected authority has signed both headers and
|
||||
// that the signature is valid.
|
||||
verify_seal_signature(proof.first_header, &proof.offender)?;
|
||||
verify_seal_signature(proof.second_header, &proof.offender)?;
|
||||
|
||||
Some(())
|
||||
};
|
||||
|
||||
// NOTE: we isolate the verification code into an helper function that
|
||||
// returns `Option<()>` so that we can use `?` to deal with any intermediate
|
||||
// errors and discard the proof as invalid.
|
||||
verify_proof().is_some()
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
Decode::decode(&mut &self.0[..]).ok()
|
||||
}
|
||||
}
|
||||
|
||||
sp_api::decl_runtime_apis! {
|
||||
/// API necessary for block authorship with BABE.
|
||||
#[api_version(2)]
|
||||
@@ -269,5 +363,34 @@ sp_api::decl_runtime_apis! {
|
||||
|
||||
/// Returns the slot number that started the current epoch.
|
||||
fn current_epoch_start() -> SlotNumber;
|
||||
|
||||
/// Generates a proof of key ownership for the given authority in the
|
||||
/// current epoch. 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 `slot_number` 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 epoch for the given slot 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(
|
||||
slot_number: SlotNumber,
|
||||
authority_id: AuthorityId,
|
||||
) -> Option<OpaqueKeyOwnershipProof>;
|
||||
|
||||
/// 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<Block::Header>,
|
||||
key_owner_proof: OpaqueKeyOwnershipProof,
|
||||
) -> Option<()>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "sp-consensus-slots"
|
||||
version = "0.8.0-rc4"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
description = "Primitives for slots-based consensus"
|
||||
edition = "2018"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://substrate.dev"
|
||||
repository = "https://github.com/paritytech/substrate/"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] }
|
||||
sp-runtime = { version = "2.0.0-rc2", default-features = false, path = "../../runtime" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"sp-runtime/std",
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2020 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.
|
||||
|
||||
//! Primitives for slots-based consensus engines.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
|
||||
/// A slot number.
|
||||
pub type SlotNumber = u64;
|
||||
|
||||
/// Represents an equivocation proof. An equivocation happens when a validator
|
||||
/// produces more than one block on the same slot. The proof of equivocation
|
||||
/// are the given distinct headers that were signed by the validator and which
|
||||
/// include the slot number.
|
||||
#[derive(Clone, Debug, Decode, Encode, PartialEq)]
|
||||
pub struct EquivocationProof<Header, Id> {
|
||||
/// Returns the authority id of the equivocator.
|
||||
pub offender: Id,
|
||||
/// The slot number at which the equivocation happened.
|
||||
pub slot_number: SlotNumber,
|
||||
/// The first header involved in the equivocation.
|
||||
pub first_header: Header,
|
||||
/// The second header involved in the equivocation.
|
||||
pub second_header: Header,
|
||||
}
|
||||
@@ -333,6 +333,11 @@ pub fn to_substrate_wasm_fn_return_value(value: &impl Encode) -> u64 {
|
||||
res
|
||||
}
|
||||
|
||||
/// The void type - it cannot exist.
|
||||
// Oh rust, you crack me up...
|
||||
#[derive(Clone, Decode, Encode, Eq, PartialEq, RuntimeDebug)]
|
||||
pub enum Void {}
|
||||
|
||||
/// Macro for creating `Maybe*` marker traits.
|
||||
///
|
||||
/// Such a maybe-marker trait requires the given bound when `feature = std` and doesn't require
|
||||
|
||||
@@ -64,6 +64,48 @@ pub struct MembershipProof {
|
||||
pub validator_count: ValidatorCount,
|
||||
}
|
||||
|
||||
/// A utility trait to get a session number. This is implemented for
|
||||
/// `MembershipProof` below to fetch the session number the given session
|
||||
/// membership proof is for. It is useful when we need to deal with key owner
|
||||
/// proofs generically (i.e. just typing against the `KeyOwnerProofSystem`
|
||||
/// trait) but still restrict their capabilities.
|
||||
pub trait GetSessionNumber {
|
||||
fn session(&self) -> SessionIndex;
|
||||
}
|
||||
|
||||
/// A utility trait to get the validator count of a given session. This is
|
||||
/// implemented for `MembershipProof` below and fetches the number of validators
|
||||
/// in the session the membership proof is for. It is useful when we need to
|
||||
/// deal with key owner proofs generically (i.e. just typing against the
|
||||
/// `KeyOwnerProofSystem` trait) but still restrict their capabilities.
|
||||
pub trait GetValidatorCount {
|
||||
fn validator_count(&self) -> ValidatorCount;
|
||||
}
|
||||
|
||||
impl GetSessionNumber for sp_core::Void {
|
||||
fn session(&self) -> SessionIndex {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl GetValidatorCount for sp_core::Void {
|
||||
fn validator_count(&self) -> ValidatorCount {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl GetSessionNumber for MembershipProof {
|
||||
fn session(&self) -> SessionIndex {
|
||||
self.session
|
||||
}
|
||||
}
|
||||
|
||||
impl GetValidatorCount for MembershipProof {
|
||||
fn validator_count(&self) -> ValidatorCount {
|
||||
self.validator_count
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the initial session keys with the given seeds, at the given block and store them in
|
||||
/// the client's keystore.
|
||||
#[cfg(feature = "std")]
|
||||
|
||||
@@ -117,10 +117,21 @@ impl sp_runtime::traits::Printable for OffenceError {
|
||||
pub trait ReportOffence<Reporter, Offender, O: Offence<Offender>> {
|
||||
/// Report an `offence` and reward given `reporters`.
|
||||
fn report_offence(reporters: Vec<Reporter>, offence: O) -> Result<(), OffenceError>;
|
||||
|
||||
/// Returns true iff all of the given offenders have been previously reported
|
||||
/// at the given time slot. This function is useful to prevent the sending of
|
||||
/// duplicate offence reports.
|
||||
fn is_known_offence(offenders: &[Offender], time_slot: &O::TimeSlot) -> bool;
|
||||
}
|
||||
|
||||
impl<Reporter, Offender, O: Offence<Offender>> ReportOffence<Reporter, Offender, O> for () {
|
||||
fn report_offence(_reporters: Vec<Reporter>, _offence: O) -> Result<(), OffenceError> { Ok(()) }
|
||||
fn report_offence(_reporters: Vec<Reporter>, _offence: O) -> Result<(), OffenceError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_known_offence(_offenders: &[Offender], _time_slot: &O::TimeSlot) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait to take action on an offence.
|
||||
|
||||
@@ -26,7 +26,7 @@ pub mod system;
|
||||
use sp_std::{prelude::*, marker::PhantomData};
|
||||
use codec::{Encode, Decode, Input, Error};
|
||||
|
||||
use sp_core::{OpaqueMetadata, RuntimeDebug, ChangesTrieConfiguration};
|
||||
use sp_core::{offchain::KeyTypeId, ChangesTrieConfiguration, OpaqueMetadata, RuntimeDebug};
|
||||
use sp_application_crypto::{ed25519, sr25519, ecdsa, RuntimeAppPublic};
|
||||
use trie_db::{TrieMut, Trie};
|
||||
use sp_trie::PrefixedMemoryDB;
|
||||
@@ -49,7 +49,11 @@ use sp_version::RuntimeVersion;
|
||||
pub use sp_core::hash::H256;
|
||||
#[cfg(any(feature = "std", test))]
|
||||
use sp_version::NativeVersion;
|
||||
use frame_support::{impl_outer_origin, parameter_types, weights::{Weight, RuntimeDbWeight}};
|
||||
use frame_support::{
|
||||
impl_outer_origin, parameter_types,
|
||||
traits::KeyOwnerProofSystem,
|
||||
weights::{RuntimeDbWeight, Weight},
|
||||
};
|
||||
use sp_inherents::{CheckInherentsResult, InherentData};
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
@@ -462,6 +466,18 @@ impl pallet_babe::Trait for Runtime {
|
||||
// are manually adding the digests. normally in this situation you'd use
|
||||
// pallet_babe::SameAuthoritiesForever.
|
||||
type EpochChangeTrigger = pallet_babe::ExternalTrigger;
|
||||
|
||||
type KeyOwnerProofSystem = ();
|
||||
|
||||
type KeyOwnerProof =
|
||||
<Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(KeyTypeId, AuthorityId)>>::Proof;
|
||||
|
||||
type KeyOwnerIdentification = <Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(
|
||||
KeyTypeId,
|
||||
AuthorityId,
|
||||
)>>::IdentificationTuple;
|
||||
|
||||
type HandleEquivocation = ();
|
||||
}
|
||||
|
||||
/// Adds one to the given input and returns the final result.
|
||||
@@ -690,6 +706,22 @@ cfg_if! {
|
||||
fn current_epoch_start() -> SlotNumber {
|
||||
<pallet_babe::Module<Runtime>>::current_epoch_start()
|
||||
}
|
||||
|
||||
fn submit_report_equivocation_unsigned_extrinsic(
|
||||
_equivocation_proof: sp_consensus_babe::EquivocationProof<
|
||||
<Block as BlockT>::Header,
|
||||
>,
|
||||
_key_owner_proof: sp_consensus_babe::OpaqueKeyOwnershipProof,
|
||||
) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
fn generate_key_ownership_proof(
|
||||
_slot_number: sp_consensus_babe::SlotNumber,
|
||||
_authority_id: sp_consensus_babe::AuthorityId,
|
||||
) -> Option<sp_consensus_babe::OpaqueKeyOwnershipProof> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl sp_offchain::OffchainWorkerApi<Block> for Runtime {
|
||||
@@ -916,6 +948,22 @@ cfg_if! {
|
||||
fn current_epoch_start() -> SlotNumber {
|
||||
<pallet_babe::Module<Runtime>>::current_epoch_start()
|
||||
}
|
||||
|
||||
fn submit_report_equivocation_unsigned_extrinsic(
|
||||
_equivocation_proof: sp_consensus_babe::EquivocationProof<
|
||||
<Block as BlockT>::Header,
|
||||
>,
|
||||
_key_owner_proof: sp_consensus_babe::OpaqueKeyOwnershipProof,
|
||||
) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
fn generate_key_ownership_proof(
|
||||
_slot_number: sp_consensus_babe::SlotNumber,
|
||||
_authority_id: sp_consensus_babe::AuthorityId,
|
||||
) -> Option<sp_consensus_babe::OpaqueKeyOwnershipProof> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl sp_offchain::OffchainWorkerApi<Block> for Runtime {
|
||||
|
||||
Reference in New Issue
Block a user