From 18dab6f5e6f4ee51584be98c5497f568fff98a33 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 19 Aug 2020 14:01:07 +1200 Subject: [PATCH 01/13] =?UTF-8?q?Offchain=20Phragm=C3=A9n=20BREAKING.=20(#?= =?UTF-8?q?4517)=20commit:=20d1238423a3093eb64df90c0eebdbf504c8df56dd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 4 -- bin/node/runtime/src/lib.rs | 1 + frame/authority-discovery/src/lib.rs | 1 + frame/babe/Cargo.toml | 4 -- frame/babe/src/lib.rs | 43 ++++++++++++++++---- frame/babe/src/mock.rs | 61 ++++++++++++++++++++++++---- frame/babe/src/tests.rs | 44 ++++++++++---------- frame/im-online/src/mock.rs | 1 + frame/session/src/lib.rs | 44 ++++++++++++++++++-- frame/session/src/mock.rs | 1 + frame/staking/src/mock.rs | 1 + frame/support/src/traits.rs | 31 +++++++++++++- prml/validator-manager/src/mock.rs | 1 + 13 files changed, 188 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38bc7ca846..3ec2362259 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4069,11 +4069,9 @@ version = "2.0.0-alpha.5" dependencies = [ "frame-support", "frame-system", - "lazy_static", "pallet-session", "pallet-timestamp", "parity-scale-codec", - "parking_lot 0.10.0", "serde", "sp-consensus-babe", "sp-consensus-vrf", @@ -4084,8 +4082,6 @@ dependencies = [ "sp-staking", "sp-std", "sp-timestamp", - "sp-version", - "substrate-test-runtime", ] [[package]] diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index ee295d6cd0..238ecb2c2d 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -262,6 +262,7 @@ impl pallet_session::Trait for Runtime { type ValidatorId = ::AccountId; type ValidatorIdOf = pallet_staking::StashOf; type DisabledValidatorsThreshold = DisabledValidatorsThreshold; + type NextSessionRotation = Babe; } impl pallet_session::historical::Trait for Runtime { diff --git a/frame/authority-discovery/src/lib.rs b/frame/authority-discovery/src/lib.rs index 50128d816f..b1364e86c0 100644 --- a/frame/authority-discovery/src/lib.rs +++ b/frame/authority-discovery/src/lib.rs @@ -121,6 +121,7 @@ mod tests { type ValidatorId = AuthorityId; type ValidatorIdOf = ConvertInto; type DisabledValidatorsThreshold = DisabledValidatorsThreshold; + type NextSessionRotation = pallet_session::PeriodicSessions; } impl pallet_session::historical::Trait for Test { diff --git a/frame/babe/Cargo.toml b/frame/babe/Cargo.toml index 116a7e7883..5b7096b342 100644 --- a/frame/babe/Cargo.toml +++ b/frame/babe/Cargo.toml @@ -25,11 +25,7 @@ sp-consensus-vrf = { version = "0.8.0-alpha.5", default-features = false, path = sp-io = { path = "../../primitives/io", default-features = false , version = "2.0.0-alpha.5"} [dev-dependencies] -lazy_static = "1.4.0" -parking_lot = "0.10.0" -sp-version = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/version" } sp-core = { version = "2.0.0-alpha.5", path = "../../primitives/core" } -substrate-test-runtime = { version = "2.0.0-dev", path = "../../test-utils/runtime" } [features] default = ["std"] diff --git a/frame/babe/src/lib.rs b/frame/babe/src/lib.rs index 9ab7cf39bc..7315d17618 100644 --- a/frame/babe/src/lib.rs +++ b/frame/babe/src/lib.rs @@ -19,8 +19,8 @@ #![cfg_attr(not(feature = "std"), no_std)] #![forbid(unused_must_use, unsafe_code, unused_variables, unused_must_use)] -#![deny(unused_imports)] -pub use pallet_timestamp; + +use pallet_timestamp; use sp_std::{result, prelude::*}; use frame_support::{ @@ -28,8 +28,8 @@ use frame_support::{ weights::{Weight, SimpleDispatchInfo, WeighData}, }; use sp_timestamp::OnTimestampSet; -use sp_runtime::{generic::DigestItem, ConsensusEngineId, Perbill}; -use sp_runtime::traits::{IsMember, SaturatedConversion, Saturating, Hash}; +use sp_runtime::{generic::DigestItem, ConsensusEngineId, Perbill, PerThing}; +use sp_runtime::traits::{IsMember, SaturatedConversion, Saturating, Hash, One}; use sp_staking::{ SessionIndex, offence::{Offence, Kind}, @@ -307,12 +307,34 @@ impl Module { // epoch 0 as having started at the slot of block 1. We want to use // the same randomness and validator set as signalled in the genesis, // so we don't rotate the epoch. - now != sp_runtime::traits::One::one() && { + now != One::one() && { let diff = CurrentSlot::get().saturating_sub(Self::current_epoch_start()); diff >= T::EpochDuration::get() } } + /// Return the _best guess_ block number, at which the next epoch change is predicted to happen. + /// + /// Returns None if the prediction is in the past; This implies an error internally in the Babe + /// and should not happen under normal circumstances. + /// + /// In other word, this is only accurate if no slots are missed. Given missed slots, the slot + /// number will grow while the block number will not. Hence, the result can be interpreted as an + /// upper bound. + // -------------- IMPORTANT NOTE -------------- + // This implementation is linked to how [`should_epoch_change`] is working. This might need to + // be updated accordingly, if the underlying mechanics of slot and epochs change. + pub fn next_expected_epoch_change(now: T::BlockNumber) -> Option { + let next_slot = Self::current_epoch_start().saturating_add(T::EpochDuration::get()); + next_slot + .checked_sub(CurrentSlot::get()) + .map(|slots_remaining| { + // This is a best effort guess. Drifts in the slot/block ratio will cause errors here. + let blocks_remaining: T::BlockNumber = slots_remaining.saturated_into(); + now.saturating_add(blocks_remaining) + }) + } + /// DANGEROUS: Enact an epoch change. Should be done on every block where `should_epoch_change` has returned `true`, /// and the caller is the only caller of this function. /// @@ -324,10 +346,7 @@ impl Module { ) { // PRECONDITION: caller has done initialization and is guaranteed // by the session module to be called before this. - #[cfg(debug_assertions)] - { - assert!(Self::initialized().is_some()) - } + debug_assert!(Self::initialized().is_some()); // Update epoch index let epoch_index = EpochIndex::get() @@ -473,6 +492,12 @@ impl OnTimestampSet for Module { fn on_timestamp_set(_moment: T::Moment) { } } +impl frame_support::traits::EstimateNextSessionRotation for Module { + fn estimate_next_session_rotation(now: T::BlockNumber) -> Option { + Self::next_expected_epoch_change(now) + } +} + impl sp_runtime::BoundToRuntimeAppPublic for Module { type Public = AuthorityId; } diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs index 281af45521..fd2fbebaf8 100644 --- a/frame/babe/src/mock.rs +++ b/frame/babe/src/mock.rs @@ -16,14 +16,22 @@ //! Test utilities -use super::{Trait, Module, GenesisConfig}; +use codec::Encode; +use super::{Trait, Module, GenesisConfig, CurrentSlot}; use sp_runtime::{ - traits::IdentityLookup, Perbill, testing::{Header, UintAuthorityId}, impl_opaque_keys, + Perbill, impl_opaque_keys, + testing::{Header, UintAuthorityId, Digest, DigestItem}, + traits::IdentityLookup, +}; +use frame_system::InitKind; +use frame_support::{ + impl_outer_origin, parameter_types, StorageValue, + traits::OnInitialize, + weights::Weight, }; -use sp_version::RuntimeVersion; -use frame_support::{impl_outer_origin, parameter_types, weights::Weight}; use sp_io; use sp_core::H256; +use sp_consensus_vrf::schnorrkel::{RawVRFOutput, RawVRFProof}; impl_outer_origin!{ pub enum Origin for Test where system = frame_system {} @@ -43,7 +51,6 @@ parameter_types! { pub const MinimumPeriod: u64 = 1; pub const EpochDuration: u64 = 3; pub const ExpectedBlockTime: u64 = 1; - pub const Version: RuntimeVersion = substrate_test_runtime::VERSION; pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(16); } @@ -53,7 +60,7 @@ impl frame_system::Trait for Test { type BlockNumber = u64; type Call = (); type Hash = H256; - type Version = Version; + type Version = (); type Hashing = sp_runtime::traits::BlakeTwo256; type AccountId = DummyValidatorId; type Lookup = IdentityLookup; @@ -78,11 +85,12 @@ impl pallet_session::Trait for Test { type Event = (); type ValidatorId = ::AccountId; type ShouldEndSession = Babe; - type SessionHandler = (Babe,Babe,); + type SessionHandler = (Babe,); type SessionManager = (); type ValidatorIdOf = (); type Keys = MockSessionKeys; type DisabledValidatorsThreshold = DisabledValidatorsThreshold; + type NextSessionRotation = Babe; } impl pallet_timestamp::Trait for Test { @@ -105,5 +113,44 @@ pub fn new_test_ext(authorities: Vec) -> sp_io::TestExternalit t.into() } +pub fn go_to_block(n: u64, s: u64) { + let pre_digest = make_pre_digest(0, s, RawVRFOutput([1; 32]), RawVRFProof([0xff; 64])); + System::initialize(&n, &Default::default(), &Default::default(), &pre_digest, InitKind::Full); + System::set_block_number(n); + if s > 1 { + CurrentSlot::put(s); + } + // includes a call into `Babe::do_initialize`. + Session::on_initialize(n); +} + +/// Slots will grow accordingly to blocks +pub fn progress_to_block(n: u64) { + let mut slot = Babe::current_slot() + 1; + for i in System::block_number()+1..=n { + go_to_block(i, slot); + slot += 1; + } +} + +pub fn make_pre_digest( + authority_index: sp_consensus_babe::AuthorityIndex, + slot_number: sp_consensus_babe::SlotNumber, + vrf_output: RawVRFOutput, + vrf_proof: RawVRFProof, +) -> Digest { + let digest_data = sp_consensus_babe::digests::RawPreDigest::Primary( + sp_consensus_babe::digests::RawPrimaryPreDigest { + authority_index, + slot_number, + vrf_output, + vrf_proof, + } + ); + let log = DigestItem::PreRuntime(sp_consensus_babe::BABE_ENGINE_ID, digest_data.encode()); + Digest { logs: vec![log] } +} + pub type System = frame_system::Module; pub type Babe = Module; +pub type Session = pallet_session::Module; diff --git a/frame/babe/src/tests.rs b/frame/babe/src/tests.rs index 5769b1235c..24aba10017 100644 --- a/frame/babe/src/tests.rs +++ b/frame/babe/src/tests.rs @@ -17,11 +17,10 @@ //! Consensus extension module tests for BABE consensus. use super::*; +use mock::*; use frame_support::traits::OnFinalize; -use mock::{new_test_ext, Babe, System}; -use sp_runtime::testing::{Digest, DigestItem}; -use sp_consensus_vrf::schnorrkel::{RawVRFOutput, RawVRFProof}; use pallet_session::ShouldEndSession; +use sp_consensus_vrf::schnorrkel::{RawVRFOutput, RawVRFProof}; const EMPTY_RANDOMNESS: [u8; 32] = [ 74, 25, 49, 128, 53, 97, 244, 49, @@ -30,24 +29,6 @@ const EMPTY_RANDOMNESS: [u8; 32] = [ 217, 153, 138, 37, 48, 192, 248, 0, ]; -fn make_pre_digest( - authority_index: sp_consensus_babe::AuthorityIndex, - slot_number: sp_consensus_babe::SlotNumber, - vrf_output: RawVRFOutput, - vrf_proof: RawVRFProof, -) -> Digest { - let digest_data = sp_consensus_babe::digests::RawPreDigest::Primary( - sp_consensus_babe::digests::RawPrimaryPreDigest { - authority_index, - slot_number, - vrf_output, - vrf_proof, - } - ); - let log = DigestItem::PreRuntime(sp_consensus_babe::BABE_ENGINE_ID, digest_data.encode()); - Digest { logs: vec![log] } -} - #[test] fn empty_randomness_is_correct() { let s = compute_randomness([0; RANDOMNESS_LENGTH], 0, std::iter::empty(), None); @@ -132,3 +113,24 @@ fn authority_index() { "Trivially invalid authorities are ignored") }) } + +#[test] +fn can_predict_next_epoch_change() { + new_test_ext(vec![]).execute_with(|| { + assert_eq!(::EpochDuration::get(), 3); + // this sets the genesis slot to 6; + go_to_block(1, 6); + assert_eq!(Babe::genesis_slot(), 6); + assert_eq!(Babe::current_slot(), 6); + assert_eq!(Babe::epoch_index(), 0); + + progress_to_block(5); + + assert_eq!(Babe::epoch_index(), 5 / 3); + assert_eq!(Babe::current_slot(), 10); + + // next epoch change will be at + assert_eq!(Babe::current_epoch_start(), 9); // next change will be 12, 2 slots from now + assert_eq!(Babe::next_expected_epoch_change(System::block_number()), Some(5 + 2)); + }) +} diff --git a/frame/im-online/src/mock.rs b/frame/im-online/src/mock.rs index c4a5fe1f7a..bcd628e2d7 100644 --- a/frame/im-online/src/mock.rs +++ b/frame/im-online/src/mock.rs @@ -140,6 +140,7 @@ impl pallet_session::Trait for Runtime { type Keys = UintAuthorityId; type Event = (); type DisabledValidatorsThreshold = DisabledValidatorsThreshold; + type NextSessionRotation = pallet_session::PeriodicSessions; } impl pallet_session::historical::Trait for Runtime { diff --git a/frame/session/src/lib.rs b/frame/session/src/lib.rs index 93cee18b95..c1707158c9 100644 --- a/frame/session/src/lib.rs +++ b/frame/session/src/lib.rs @@ -102,13 +102,15 @@ use sp_std::{prelude::*, marker::PhantomData, ops::{Sub, Rem}}; use codec::Decode; use sp_runtime::{KeyTypeId, Perbill, RuntimeAppPublic, BoundToRuntimeAppPublic}; -use sp_runtime::traits::{Convert, Zero, Member, OpaqueKeys}; +use sp_runtime::traits::{Convert, Zero, Member, OpaqueKeys, Saturating}; use sp_staking::SessionIndex; use frame_support::{ ensure, decl_module, decl_event, decl_storage, decl_error, ConsensusEngineId, Parameter, - weights::{Weight, SimpleDispatchInfo, WeighData}, - traits::{Get, FindAuthor, ValidatorRegistration}, + traits::{ + Get, FindAuthor, ValidatorRegistration, EstimateNextSessionRotation, EstimateNextNewSession, + }, dispatch::{self, DispatchResult, DispatchError}, + weights::{Weight, SimpleDispatchInfo, WeighData}, }; use frame_system::{self as system, ensure_signed}; @@ -147,6 +149,29 @@ impl< } } +impl< + BlockNumber: Rem + Sub + Zero + PartialOrd + Saturating + Clone, + Period: Get, + Offset: Get, +> EstimateNextSessionRotation for PeriodicSessions { + fn estimate_next_session_rotation(now: BlockNumber) -> Option { + let offset = Offset::get(); + let period = Period::get(); + Some(if now > offset { + let block_after_last_session = (now.clone() - offset) % period.clone(); + if block_after_last_session > Zero::zero() { + now.saturating_add( + period.saturating_sub(block_after_last_session) + ) + } else { + Zero::zero() + } + } else { + offset + }) + } +} + /// A trait for managing creation of new validator set. pub trait SessionManager { /// Plan a new session, and optionally provide the new validator set. @@ -330,6 +355,11 @@ pub trait Trait: frame_system::Trait { /// Indicator for when to end the session. type ShouldEndSession: ShouldEndSession; + /// Something that can predict the next session rotation. This should typically come from the + /// same logical unit that provides [`ShouldEndSession`], yet, it gives a best effort estimate. + /// It is helpful to implement [`EstimateNextNewSession`]. + type NextSessionRotation: EstimateNextSessionRotation; + /// Handler for managing new session. type SessionManager: SessionManager; @@ -734,3 +764,11 @@ impl> FindAuthor validators.get(i as usize).map(|k| k.clone()) } } + +impl EstimateNextNewSession for Module { + /// This session module always calls new_session and next_session at the same time, hence we + /// do a simple proxy and pass the function to next rotation. + fn estimate_next_new_session(now: T::BlockNumber) -> Option { + T::NextSessionRotation::estimate_next_session_rotation(now) + } +} diff --git a/frame/session/src/mock.rs b/frame/session/src/mock.rs index da61862489..fe6b209a23 100644 --- a/frame/session/src/mock.rs +++ b/frame/session/src/mock.rs @@ -198,6 +198,7 @@ impl Trait for Test { type Keys = MockSessionKeys; type Event = (); type DisabledValidatorsThreshold = DisabledValidatorsThreshold; + type NextSessionRotation = (); } #[cfg(feature = "historical")] diff --git a/frame/staking/src/mock.rs b/frame/staking/src/mock.rs index 95e14a0e2f..92670f7efe 100644 --- a/frame/staking/src/mock.rs +++ b/frame/staking/src/mock.rs @@ -171,6 +171,7 @@ impl pallet_session::Trait for Test { type SessionHandler = TestSessionHandler; type Keys = UintAuthorityId; type DisabledValidatorsThreshold = DisabledValidatorsThreshold; + type NextSessionRotation = pallet_session::PeriodicSessions; } impl pallet_session::historical::Trait for Test { diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index aed520f326..da7a5cc494 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -24,10 +24,39 @@ use sp_core::u32_trait::Value as U32; use sp_runtime::{ RuntimeDebug, ConsensusEngineId, DispatchResult, DispatchError, - traits::{MaybeSerializeDeserialize, AtLeast32Bit, Saturating, TrailingZeroInput}, + traits::{MaybeSerializeDeserialize, AtLeast32Bit, Saturating, TrailingZeroInput, Bounded}, }; use crate::dispatch::Parameter; +/// Something that can estimate at which block the next session rotation will happen. This should +/// be the same logical unit that dictates `ShouldEndSession` to the session module. No Assumptions +/// are made about the scheduling of the sessions. +pub trait EstimateNextSessionRotation { + /// Return the block number at which the next session rotation is estimated to happen. + /// + /// None should be returned if the estimation fails to come to an answer + fn estimate_next_session_rotation(now: BlockNumber) -> Option; +} + +impl EstimateNextSessionRotation for () { + fn estimate_next_session_rotation(_: BlockNumber) -> Option { + Default::default() + } +} + +/// Something that can estimate at which block the next `new_session` will be triggered. This must +/// always be implemented by the session module. +pub trait EstimateNextNewSession { + /// Return the block number at which the next new session is estimated to happen. + fn estimate_next_new_session(now: BlockNumber) -> Option; +} + +impl EstimateNextNewSession for () { + fn estimate_next_new_session(_: BlockNumber) -> Option { + Default::default() + } +} + /// Anything that can have a `::len()` method. pub trait Len { /// Return the length of data type. diff --git a/prml/validator-manager/src/mock.rs b/prml/validator-manager/src/mock.rs index 5007b96e44..cbb6e16332 100644 --- a/prml/validator-manager/src/mock.rs +++ b/prml/validator-manager/src/mock.rs @@ -76,6 +76,7 @@ impl pallet_session::Trait for Test { type ValidatorId = ::AccountId; type ValidatorIdOf = ConvertInto; type DisabledValidatorsThreshold = DisabledValidatorsThreshold; + type NextSessionRotation = (); } impl frame_system::Trait for Test { From f125d3c884c4ba3d0ab80d0797ef454c5aa33fb6 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 19 Aug 2020 15:18:04 +1200 Subject: [PATCH 02/13] Add cherry-picks Make all `PerThing` types implement all trait methods on the type (#5422) 871e128a8ea131421577aeb83bb2278c77597877 Build for only one target for docs.rs (#5427) 31352af110e834d86ea301f6df49eb18dac3ee95 --- frame/babe/Cargo.toml | 3 +++ frame/babe/src/lib.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frame/babe/Cargo.toml b/frame/babe/Cargo.toml index 5b7096b342..8f88550022 100644 --- a/frame/babe/Cargo.toml +++ b/frame/babe/Cargo.toml @@ -45,3 +45,6 @@ std = [ "pallet-session/std", "sp-io/std", ] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/frame/babe/src/lib.rs b/frame/babe/src/lib.rs index 7315d17618..32d5d3c7a5 100644 --- a/frame/babe/src/lib.rs +++ b/frame/babe/src/lib.rs @@ -28,7 +28,7 @@ use frame_support::{ weights::{Weight, SimpleDispatchInfo, WeighData}, }; use sp_timestamp::OnTimestampSet; -use sp_runtime::{generic::DigestItem, ConsensusEngineId, Perbill, PerThing}; +use sp_runtime::{generic::DigestItem, ConsensusEngineId, Perbill}; use sp_runtime::traits::{IsMember, SaturatedConversion, Saturating, Hash, One}; use sp_staking::{ SessionIndex, From ca7d343384bc03acc1207d93f77644146acb8a67 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 19 Aug 2020 15:28:49 +1200 Subject: [PATCH 03/13] support: add lateness trait (#5519) 0c7f8b22029941d5d7304719de4d308d2fba8025 --- frame/babe/src/lib.rs | 26 +++++++++++++++++++++++++- frame/support/src/traits.rs | 17 ++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/frame/babe/src/lib.rs b/frame/babe/src/lib.rs index 32d5d3c7a5..5f85b91088 100644 --- a/frame/babe/src/lib.rs +++ b/frame/babe/src/lib.rs @@ -152,6 +152,13 @@ decl_storage! { /// Temporary value (cleared at block finalization) which is `Some` /// if per-block initialization has already been called for current block. Initialized get(fn initialized): Option; + + /// How late the current block is compared to its parent. + /// + /// This entry is populated as part of block execution and is cleaned up + /// on block finalization. Querying this storage entry outside of block + /// execution context should always yield zero. + Lateness get(fn lateness): T::BlockNumber; } add_extra_genesis { config(authorities): Vec<(AuthorityId, BabeAuthorityWeight)>; @@ -190,6 +197,9 @@ decl_module! { if let Some(Some(vrf_output)) = Initialized::take() { Self::deposit_vrf_output(&vrf_output); } + + // remove temporary "environment" entry from storage + Lateness::::kill(); } } } @@ -443,7 +453,15 @@ impl Module { Self::deposit_consensus(ConsensusLog::NextEpochData(next)) } - CurrentSlot::put(digest.slot_number()); + // the slot number of the current block being initialized + let current_slot = digest.slot_number(); + + // how many slots were skipped between current and last block + let lateness = current_slot.saturating_sub(CurrentSlot::get() + 1); + let lateness = T::BlockNumber::from(lateness as u32); + + Lateness::::put(lateness); + CurrentSlot::put(current_slot); if let RawPreDigest::Primary(primary) = digest { // place the VRF output into the `Initialized` storage item @@ -498,6 +516,12 @@ impl frame_support::traits::EstimateNextSessionRotation frame_support::traits::Lateness for Module { + fn lateness(&self) -> T::BlockNumber { + Self::lateness() + } +} + impl sp_runtime::BoundToRuntimeAppPublic for Module { type Public = AuthorityId; } diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index da7a5cc494..0df5aba923 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -24,7 +24,7 @@ use sp_core::u32_trait::Value as U32; use sp_runtime::{ RuntimeDebug, ConsensusEngineId, DispatchResult, DispatchError, - traits::{MaybeSerializeDeserialize, AtLeast32Bit, Saturating, TrailingZeroInput, Bounded}, + traits::{MaybeSerializeDeserialize, AtLeast32Bit, Saturating, TrailingZeroInput, Bounded, Zero}, }; use crate::dispatch::Parameter; @@ -935,6 +935,21 @@ impl Randomness for () { } } +/// Trait to be used by block producing consensus engine modules to determine +/// how late the current block is (e.g. in a slot-based proposal mechanism how +/// many slots were skipped since the previous block). +pub trait Lateness { + /// Returns a generic measure of how late the current block is compared to + /// its parent. + fn lateness(&self) -> N; +} + +impl Lateness for () { + fn lateness(&self) -> N { + Zero::zero() + } +} + /// Implementors of this trait provide information about whether or not some validator has /// been registered with them. The [Session module](../../pallet_session/index.html) is an implementor. pub trait ValidatorRegistration { From be1bba62bfb7a74511d8046e98ca395f079fd62f Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 19 Aug 2020 15:32:29 +1200 Subject: [PATCH 04/13] Revise docs on randomness (#5497) 3ac409ba427170af77c9d97440b30d98fc0464dd --- frame/babe/src/lib.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/frame/babe/src/lib.rs b/frame/babe/src/lib.rs index 5f85b91088..15edeed5dd 100644 --- a/frame/babe/src/lib.rs +++ b/frame/babe/src/lib.rs @@ -205,6 +205,21 @@ decl_module! { } impl RandomnessT<::Hash> for Module { + /// Some BABE blocks have VRF outputs where the block producer has exactly one bit of influence, + /// either they make the block or they do not make the block and thus someone else makes the + /// next block. Yet, this randomness is not fresh in all BABE blocks. + /// + /// If that is an insufficient security guarantee then two things can be used to improve this + /// randomness: + /// + /// - Name, in advance, the block number whose random value will be used; ensure your module + /// retains a buffer of previous random values for its subject and then index into these in + /// order to obviate the ability of your user to look up the parent hash and choose when to + /// transact based upon it. + /// - Require your user to first commit to an additional value by first posting its hash. + /// Require them to reveal the value to determine the final result, hashing it with the + /// output of this random function. This reduces the ability of a cabal of block producers + /// from conspiring against individuals. fn random(subject: &[u8]) -> T::Hash { let mut subject = subject.to_vec(); subject.reserve(VRF_OUTPUT_LENGTH); From 194020ea724ca38b8b647a159197d3ee8366af67 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 20 Aug 2020 10:15:34 +1200 Subject: [PATCH 05/13] Relax substrate licensing scheme (#5947) commit: dbf2163250833e6ac898e7f6c3c8f89f08a7c19d --- frame/babe/Cargo.toml | 2 +- frame/babe/src/lib.rs | 25 +++++++++++++------------ frame/babe/src/mock.rs | 27 ++++++++++++++------------- frame/babe/src/tests.rs | 27 ++++++++++++++------------- 4 files changed, 42 insertions(+), 39 deletions(-) diff --git a/frame/babe/Cargo.toml b/frame/babe/Cargo.toml index 8f88550022..ffdbab74d5 100644 --- a/frame/babe/Cargo.toml +++ b/frame/babe/Cargo.toml @@ -3,7 +3,7 @@ name = "pallet-babe" version = "2.0.0-alpha.5" authors = ["Parity Technologies "] edition = "2018" -license = "GPL-3.0" +license = "Apache-2.0" homepage = "https://substrate.dev" repository = "https://github.com/paritytech/substrate/" description = "Consensus extension module for BABE consensus. Collects on-chain randomness from VRF outputs and manages epoch transitions." diff --git a/frame/babe/src/lib.rs b/frame/babe/src/lib.rs index 15edeed5dd..91d5c6dc2c 100644 --- a/frame/babe/src/lib.rs +++ b/frame/babe/src/lib.rs @@ -1,18 +1,19 @@ -// Copyright 2019-2020 Parity Technologies (UK) Ltd. // This file is part of Substrate. -// Substrate is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. +// Copyright (C) 2019-2020 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 -// Substrate is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Substrate. If not, see . +// 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. //! Consensus extension module for BABE consensus. Collects on-chain randomness //! from VRF outputs and manages epoch transitions. diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs index fd2fbebaf8..a2261c71c0 100644 --- a/frame/babe/src/mock.rs +++ b/frame/babe/src/mock.rs @@ -1,18 +1,19 @@ -// Copyright 2019-2020 Parity Technologies (UK) Ltd. // This file is part of Substrate. -// Substrate is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Substrate is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Substrate. If not, see . +// Copyright (C) 2019-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. //! Test utilities diff --git a/frame/babe/src/tests.rs b/frame/babe/src/tests.rs index 24aba10017..a1eb928374 100644 --- a/frame/babe/src/tests.rs +++ b/frame/babe/src/tests.rs @@ -1,18 +1,19 @@ -// Copyright 2019-2020 Parity Technologies (UK) Ltd. // This file is part of Substrate. -// Substrate is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Substrate is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Substrate. If not, see . +// Copyright (C) 2019-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. //! Consensus extension module tests for BABE consensus. From 14a023284a159835e7075e23b7c6f7277780dbeb Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 20 Aug 2020 10:49:02 +1200 Subject: [PATCH 06/13] Add notes about safe uses of twox (#6082) commit: 73ff9385303dc06665c9753ec3e7303647e3b8bc --- frame/assets/src/lib.rs | 2 ++ frame/babe/src/lib.rs | 2 ++ frame/contracts/src/lib.rs | 2 ++ frame/democracy/src/lib.rs | 9 ++++++++- frame/elections-phragmen/src/lib.rs | 2 ++ frame/elections/src/lib.rs | 9 +++++++++ frame/generic-asset/src/lib.rs | 8 ++++++++ frame/grandpa/src/lib.rs | 2 ++ frame/identity/src/lib.rs | 4 ++++ 9 files changed, 39 insertions(+), 1 deletion(-) diff --git a/frame/assets/src/lib.rs b/frame/assets/src/lib.rs index 0e516b83d4..ed9f08baa0 100644 --- a/frame/assets/src/lib.rs +++ b/frame/assets/src/lib.rs @@ -232,6 +232,8 @@ decl_storage! { /// The next asset identifier up for grabs. NextAssetId get(fn next_asset_id): T::AssetId; /// The total unit supply of an asset. + /// + /// TWOX-NOTE: `AssetId` is trusted, so this is safe. TotalSupply: map hasher(twox_64_concat) T::AssetId => T::Balance; } } diff --git a/frame/babe/src/lib.rs b/frame/babe/src/lib.rs index 91d5c6dc2c..75b7a6bcf7 100644 --- a/frame/babe/src/lib.rs +++ b/frame/babe/src/lib.rs @@ -148,6 +148,8 @@ decl_storage! { /// We reset all segments and return to `0` at the beginning of every /// epoch. SegmentIndex build(|_| 0): u32; + + /// TWOX-NOTE: `SegmentIndex` is an increasing integer, so this is okay. UnderConstruction: map hasher(twox_64_concat) u32 => Vec; /// Temporary value (cleared at block finalization) which is `Some` diff --git a/frame/contracts/src/lib.rs b/frame/contracts/src/lib.rs index fba081fe5b..8e3f5fb529 100644 --- a/frame/contracts/src/lib.rs +++ b/frame/contracts/src/lib.rs @@ -967,6 +967,8 @@ decl_storage! { /// The subtrie counter. pub AccountCounter: u64 = 0; /// The code associated with a given account. + /// + /// TWOX-NOTE: SAFE since `AccountId` is a secure hash. pub ContractInfoOf: map hasher(twox_64_concat) T::AccountId => Option>; /// The price of one unit of gas. GasPrice get(fn gas_price) config(): BalanceOf = 1.into(); diff --git a/frame/democracy/src/lib.rs b/frame/democracy/src/lib.rs index d682107743..75d05a5e80 100644 --- a/frame/democracy/src/lib.rs +++ b/frame/democracy/src/lib.rs @@ -433,6 +433,8 @@ decl_storage! { /// `ReferendumCount` if there isn't a unbaked referendum. pub LowestUnbaked get(fn lowest_unbaked) build(|_| 0 as ReferendumIndex): ReferendumIndex; /// Information concerning any given referendum. + /// + /// TWOX-NOTE: SAFE as indexes are not under an attacker’s control. pub ReferendumInfoOf get(fn referendum_info): map hasher(twox_64_concat) ReferendumIndex => Option>; @@ -451,6 +453,9 @@ decl_storage! { /// Who is able to vote for whom. Value is the fund-holding account, key is the /// vote-transaction-sending account. + /// TWOX-NOTE: OK ― `AccountId` is a secure hash. + // TODO: Refactor proxy into its own pallet. + // https://github.com/paritytech/substrate/issues/5322 pub Proxy get(fn proxy): map hasher(twox_64_concat) T::AccountId => Option>; /// Get the account (and lock periods) to which another account is delegating vote. @@ -459,7 +464,9 @@ decl_storage! { /// Accounts for which there are locks in action which may be removed at some point in the /// future. The value is the block number at which the lock expires and may be removed. - pub Locks get(locks): map hasher(twox_64_concat) T::AccountId => Option; + /// + /// TWOX-NOTE: OK ― `AccountId` is a secure hash. + pub Locks get(fn locks): map hasher(twox_64_concat) T::AccountId => Option; /// True if the last referendum tabled was submitted externally. False if it was a public /// proposal. diff --git a/frame/elections-phragmen/src/lib.rs b/frame/elections-phragmen/src/lib.rs index 42f74e2374..c7d0699721 100644 --- a/frame/elections-phragmen/src/lib.rs +++ b/frame/elections-phragmen/src/lib.rs @@ -160,6 +160,8 @@ decl_storage! { pub ElectionRounds get(fn election_rounds): u32 = Zero::zero(); /// Votes and locked stake of a particular voter. + /// + /// TWOX-NOTE: SAFE as `AccountId` is a crypto hash pub Voting: map hasher(twox_64_concat) T::AccountId => (BalanceOf, Vec); /// The present candidate list. Sorted based on account-id. A current member or runner-up diff --git a/frame/elections/src/lib.rs b/frame/elections/src/lib.rs index 3b88b878d4..cda269bc10 100644 --- a/frame/elections/src/lib.rs +++ b/frame/elections/src/lib.rs @@ -235,16 +235,25 @@ decl_storage! { // bit-wise manner. In order to get a human-readable representation (`Vec`), use // [`all_approvals_of`]. Furthermore, each vector of scalars is chunked with the cap of // `APPROVAL_SET_SIZE`. + /// + /// TWOX-NOTE: SAFE as `AccountId` is a crypto hash and `SetIndex` is not + /// attacker-controlled. pub ApprovalsOf get(fn approvals_of): map hasher(twox_64_concat) (T::AccountId, SetIndex) => Vec; /// The vote index and list slot that the candidate `who` was registered or `None` if they /// are not currently registered. + /// + /// TWOX-NOTE: SAFE as `AccountId` is a crypto hash. pub RegisterInfoOf get(fn candidate_reg_info): map hasher(twox_64_concat) T::AccountId => Option<(VoteIndex, u32)>; /// Basic information about a voter. + /// + /// TWOX-NOTE: SAFE as `AccountId` is a crypto hash. pub VoterInfoOf get(fn voter_info): map hasher(twox_64_concat) T::AccountId => Option>>; /// The present voter list (chunked and capped at [`VOTER_SET_SIZE`]). + /// + /// TWOX-NOTE: OKAY ― `SetIndex` is not user-controlled data. pub Voters get(fn voters): map hasher(twox_64_concat) SetIndex => Vec>; /// the next free set to store a voter in. This will keep growing. pub NextVoterSet get(fn next_nonfull_voter_set): SetIndex = 0; diff --git a/frame/generic-asset/src/lib.rs b/frame/generic-asset/src/lib.rs index 1fc8932d80..3d89a9efee 100644 --- a/frame/generic-asset/src/lib.rs +++ b/frame/generic-asset/src/lib.rs @@ -550,16 +550,22 @@ pub struct BalanceLock { decl_storage! { trait Store for Module as GenericAsset { /// Total issuance of a given asset. + /// + /// TWOX-NOTE: `AssetId` is trusted. pub TotalIssuance get(fn total_issuance) build(|config: &GenesisConfig| { let issuance = config.initial_balance * (config.endowed_accounts.len() as u32).into(); config.assets.iter().map(|id| (id.clone(), issuance)).collect::>() }): map hasher(twox_64_concat) T::AssetId => T::Balance; /// The free balance of a given asset under an account. + /// + /// TWOX-NOTE: `AssetId` is trusted. pub FreeBalance: double_map hasher(twox_64_concat) T::AssetId, hasher(blake2_128_concat) T::AccountId => T::Balance; /// The reserved balance of a given asset under an account. + /// + /// TWOX-NOTE: `AssetId` is trusted. pub ReservedBalance: double_map hasher(twox_64_concat) T::AssetId, hasher(blake2_128_concat) T::AccountId => T::Balance; @@ -567,6 +573,8 @@ decl_storage! { pub NextAssetId get(fn next_asset_id) config(): T::AssetId; /// Permission options for a given asset. + /// + /// TWOX-NOTE: `AssetId` is trusted. pub Permissions get(fn get_permission) build(|config: &GenesisConfig| { config.permissions .iter() diff --git a/frame/grandpa/src/lib.rs b/frame/grandpa/src/lib.rs index 306545149c..370a6efb08 100644 --- a/frame/grandpa/src/lib.rs +++ b/frame/grandpa/src/lib.rs @@ -169,6 +169,8 @@ decl_storage! { /// A mapping from grandpa set ID to the index of the *most recent* session for which its /// members were responsible. + /// + /// TWOX-NOTE: `SetId` is not under user control. SetIdSession get(fn session_for_set): map hasher(twox_64_concat) SetId => Option; } add_extra_genesis { diff --git a/frame/identity/src/lib.rs b/frame/identity/src/lib.rs index a4e2fe67c8..8d9be65e3b 100644 --- a/frame/identity/src/lib.rs +++ b/frame/identity/src/lib.rs @@ -381,6 +381,8 @@ pub struct RegistrarInfo< decl_storage! { trait Store for Module as Identity { /// Information that is pertinent to identify the entity behind an account. + /// + /// TWOX-NOTE: OK ― `AccountId` is a secure hash. pub IdentityOf get(fn identity): map hasher(twox_64_concat) T::AccountId => Option>>; @@ -392,6 +394,8 @@ decl_storage! { /// Alternative "sub" identities of this account. /// /// The first item is the deposit, the second is a vector of the accounts. + /// + /// TWOX-NOTE: OK ― `AccountId` is a secure hash. pub SubsOf get(fn subs_of): map hasher(twox_64_concat) T::AccountId => (BalanceOf, Vec); From 89ef7d7ab072be3e455d81a796e38e4147c204b6 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 20 Aug 2020 13:46:09 +1200 Subject: [PATCH 07/13] Correct BABE randomness by calculating InOut bytes directly in pallet (#5876) commit: 2b67057a020c4973b3af68a6868b1aa2b982e03d [ci skip] --- Cargo.lock | 2 + client/consensus/babe/src/authorship.rs | 21 +--- client/consensus/babe/src/verification.rs | 4 +- frame/babe/Cargo.toml | 4 +- frame/babe/src/lib.rs | 56 +++++++---- frame/babe/src/mock.rs | 41 ++++++-- frame/babe/src/tests.rs | 37 +++++-- primitives/consensus/babe/Cargo.toml | 2 + primitives/consensus/babe/src/digests.rs | 80 ++++++--------- primitives/consensus/babe/src/lib.rs | 17 ++++ primitives/consensus/vrf/Cargo.toml | 4 +- primitives/consensus/vrf/src/schnorrkel.rs | 111 +-------------------- 12 files changed, 162 insertions(+), 217 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ec2362259..dab16ccff3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4073,6 +4073,7 @@ dependencies = [ "pallet-timestamp", "parity-scale-codec", "serde", + "sp-application-crypto", "sp-consensus-babe", "sp-consensus-vrf", "sp-core", @@ -7344,6 +7345,7 @@ dependencies = [ name = "sp-consensus-babe" version = "0.8.0-alpha.5" dependencies = [ + "merlin", "parity-scale-codec", "sp-api", "sp-application-crypto", diff --git a/client/consensus/babe/src/authorship.rs b/client/consensus/babe/src/authorship.rs index 074e582bff..ed31916560 100644 --- a/client/consensus/babe/src/authorship.rs +++ b/client/consensus/babe/src/authorship.rs @@ -16,10 +16,12 @@ //! BABE authority selection and slot claiming. -use merlin::Transcript; use sp_consensus_babe::{ - AuthorityId, BabeAuthorityWeight, BABE_ENGINE_ID, BABE_VRF_PREFIX, - SlotNumber, AuthorityPair, BabeConfiguration + make_transcript, AuthorityId, BabeAuthorityWeight, BABE_VRF_PREFIX, + SlotNumber, AuthorityPair, +}; +use sp_consensus_babe::digests::{ + PreDigest, PrimaryPreDigest, SecondaryPlainPreDigest, SecondaryVRFPreDigest, }; use sp_consensus_babe::digests::{PreDigest, PrimaryPreDigest, SecondaryPreDigest}; use sp_consensus_vrf::schnorrkel::{VRFOutput, VRFProof}; @@ -87,19 +89,6 @@ pub(super) fn secondary_slot_author( Some(&expected_author.0) } -pub(super) fn make_transcript( - randomness: &[u8], - slot_number: u64, - epoch: u64, -) -> Transcript { - let mut transcript = Transcript::new(&BABE_ENGINE_ID); - transcript.append_u64(b"slot number", slot_number); - transcript.append_u64(b"current epoch", epoch); - transcript.append_message(b"chain randomness", randomness); - transcript -} - - /// Claim a secondary slot if it is our turn to propose, returning the /// pre-digest to use when authoring the block, or `None` if it is not our turn /// to propose. diff --git a/client/consensus/babe/src/verification.rs b/client/consensus/babe/src/verification.rs index 2fd37280b3..264fda4900 100644 --- a/client/consensus/babe/src/verification.rs +++ b/client/consensus/babe/src/verification.rs @@ -17,14 +17,14 @@ //! Verification for BABE headers. use sp_runtime::{traits::Header, traits::DigestItemFor}; use sp_core::{Pair, Public}; -use sp_consensus_babe::{AuthoritySignature, SlotNumber, AuthorityPair, AuthorityId}; +use sp_consensus_babe::{make_transcript, AuthoritySignature, SlotNumber, AuthorityPair, AuthorityId}; use sp_consensus_babe::digests::{ PreDigest, PrimaryPreDigest, SecondaryPreDigest, CompatibleDigestItem }; use sc_consensus_slots::CheckedHeader; use log::{debug, trace}; use super::{find_pre_digest, babe_err, Epoch, BlockT, Error}; -use super::authorship::{make_transcript, calculate_primary_threshold, check_primary_threshold, secondary_slot_author}; +use super::authorship::{calculate_primary_threshold, check_primary_threshold, secondary_slot_author}; /// BABE verification parameters pub(super) struct VerificationParams<'a, B: 'a + BlockT> { diff --git a/frame/babe/Cargo.toml b/frame/babe/Cargo.toml index ffdbab74d5..6d13d0ceda 100644 --- a/frame/babe/Cargo.toml +++ b/frame/babe/Cargo.toml @@ -12,6 +12,7 @@ description = "Consensus extension module for BABE consensus. Collects on-chain codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] } serde = { version = "1.0.101", optional = true } sp-inherents = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/inherents" } +sp-application-crypto = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/application-crypto" } sp-std = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/std" } sp-runtime = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/runtime" } sp-staking = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/staking" } @@ -22,7 +23,7 @@ sp-timestamp = { version = "2.0.0-alpha.5", default-features = false, path = ".. pallet-session = { version = "2.0.0-alpha.5", default-features = false, path = "../session" } sp-consensus-babe = { version = "0.8.0-alpha.5", default-features = false, path = "../../primitives/consensus/babe" } sp-consensus-vrf = { version = "0.8.0-alpha.5", default-features = false, path = "../../primitives/consensus/vrf" } -sp-io = { path = "../../primitives/io", default-features = false , version = "2.0.0-alpha.5"} +sp-io = { path = "../../primitives/io", default-features = false, version = "2.0.0-alpha.5"} [dev-dependencies] sp-core = { version = "2.0.0-alpha.5", path = "../../primitives/core" } @@ -33,6 +34,7 @@ std = [ "serde", "codec/std", "sp-std/std", + "sp-application-crypto/std", "frame-support/std", "sp-runtime/std", "sp-staking/std", diff --git a/frame/babe/src/lib.rs b/frame/babe/src/lib.rs index 75b7a6bcf7..dabe07b45f 100644 --- a/frame/babe/src/lib.rs +++ b/frame/babe/src/lib.rs @@ -35,13 +35,14 @@ use sp_staking::{ SessionIndex, offence::{Offence, Kind}, }; +use sp_application_crypto::Public; 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, RawPreDigest}, + digests::{NextEpochDescriptor, PreDigest}, }; use sp_consensus_vrf::schnorrkel; pub use sp_consensus_babe::{AuthorityId, VRF_OUTPUT_LENGTH, RANDOMNESS_LENGTH, PUBLIC_KEY_LENGTH}; @@ -103,7 +104,7 @@ impl EpochChangeTrigger for SameAuthoritiesForever { const UNDER_CONSTRUCTION_SEGMENT_LENGTH: usize = 256; -type MaybeVrf = Option; +type MaybeRandomness = Option; decl_storage! { trait Store for Module as Babe { @@ -150,11 +151,11 @@ decl_storage! { SegmentIndex build(|_| 0): u32; /// TWOX-NOTE: `SegmentIndex` is an increasing integer, so this is okay. - UnderConstruction: map hasher(twox_64_concat) u32 => Vec; + UnderConstruction: map hasher(twox_64_concat) u32 => Vec; /// Temporary value (cleared at block finalization) which is `Some` /// if per-block initialization has already been called for current block. - Initialized get(fn initialized): Option; + Initialized get(fn initialized): Option; /// How late the current block is compared to its parent. /// @@ -197,8 +198,8 @@ decl_module! { // that this block was the first in a new epoch, the changeover logic has // already occurred at this point, so the under-construction randomness // will only contain outputs from the right epoch. - if let Some(Some(vrf_output)) = Initialized::take() { - Self::deposit_vrf_output(&vrf_output); + if let Some(Some(randomness)) = Initialized::take() { + Self::deposit_randomness(&randomness); } // remove temporary "environment" entry from storage @@ -241,7 +242,7 @@ impl FindAuthor for Module { { for (id, mut data) in digests.into_iter() { if id == BABE_ENGINE_ID { - let pre_digest: RawPreDigest = RawPreDigest::decode(&mut data).ok()?; + let pre_digest: PreDigest = PreDigest::decode(&mut data).ok()?; return Some(pre_digest.authority_index()) } } @@ -418,17 +419,17 @@ impl Module { >::deposit_log(log.into()) } - fn deposit_vrf_output(vrf_output: &schnorrkel::RawVRFOutput) { + fn deposit_randomness(randomness: &schnorrkel::Randomness) { let segment_idx = ::get(); let mut segment = ::get(&segment_idx); if segment.len() < UNDER_CONSTRUCTION_SEGMENT_LENGTH { // push onto current segment: not full. - segment.push(*vrf_output); + segment.push(*randomness); ::insert(&segment_idx, &segment); } else { // move onto the next segment and update the index. let segment_idx = segment_idx + 1; - ::insert(&segment_idx, &vec![vrf_output.clone()]); + ::insert(&segment_idx, &vec![randomness.clone()]); ::put(&segment_idx); } } @@ -441,18 +442,18 @@ impl Module { return; } - let maybe_pre_digest: Option = >::digest() + let maybe_pre_digest: Option = >::digest() .logs .iter() .filter_map(|s| s.as_pre_runtime()) .filter_map(|(id, mut data)| if id == BABE_ENGINE_ID { - RawPreDigest::decode(&mut data).ok() + PreDigest::decode(&mut data).ok() } else { None }) .next(); - let maybe_vrf = maybe_pre_digest.and_then(|digest| { + let maybe_randomness: Option = maybe_pre_digest.and_then(|digest| { // on the first non-zero block (i.e. block #1) // this is where the first epoch (epoch #0) actually starts. // we need to adjust internal storage accordingly. @@ -481,17 +482,38 @@ impl Module { Lateness::::put(lateness); CurrentSlot::put(current_slot); - if let RawPreDigest::Primary(primary) = digest { + if let PreDigest::Primary(primary) = digest { // place the VRF output into the `Initialized` storage item // and it'll be put onto the under-construction randomness // later, once we've decided which epoch this block is in. - Some(primary.vrf_output) + // + // Reconstruct the bytes of VRFInOut using the authority id. + Authorities::get() + .get(primary.authority_index as usize) + .and_then(|author| { + schnorrkel::PublicKey::from_bytes(author.0.as_slice()).ok() + }) + .and_then(|pubkey| { + let transcript = sp_consensus_babe::make_transcript( + &Self::randomness(), + current_slot, + EpochIndex::get(), + ); + + primary.vrf_output.0.attach_input_hash( + &pubkey, + transcript + ).ok() + }) + .map(|inout| { + inout.make_bytes(&sp_consensus_babe::BABE_VRF_INOUT_CONTEXT) + }) } else { None } }); - Initialized::put(maybe_vrf); + Initialized::put(maybe_randomness); // enact epoch change, if necessary. T::EpochChangeTrigger::trigger::(now) @@ -580,7 +602,7 @@ impl pallet_session::OneSessionHandler for Module { fn compute_randomness( last_epoch_randomness: schnorrkel::Randomness, epoch_index: u64, - rho: impl Iterator, + rho: impl Iterator, rho_size_hint: Option, ) -> schnorrkel::Randomness { let mut s = Vec::with_capacity(40 + rho_size_hint.unwrap_or(0) * VRF_OUTPUT_LENGTH); diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs index a2261c71c0..9ff576d7b7 100644 --- a/frame/babe/src/mock.rs +++ b/frame/babe/src/mock.rs @@ -31,11 +31,12 @@ use frame_support::{ weights::Weight, }; use sp_io; -use sp_core::H256; -use sp_consensus_vrf::schnorrkel::{RawVRFOutput, RawVRFProof}; +use sp_core::{H256, U256, crypto::Pair}; +use sp_consensus_babe::AuthorityPair; +use sp_consensus_vrf::schnorrkel::{VRFOutput, VRFProof}; impl_outer_origin!{ - pub enum Origin for Test where system = frame_system {} + pub enum Origin for Test where system = frame_system {} } type DummyValidatorId = u64; @@ -106,16 +107,20 @@ impl Trait for Test { type EpochChangeTrigger = crate::ExternalTrigger; } -pub fn new_test_ext(authorities: Vec) -> sp_io::TestExternalities { +pub fn new_test_ext(authorities_len: usize) -> (Vec, sp_io::TestExternalities) { + let pairs = (0..authorities_len).map(|i| { + AuthorityPair::from_seed(&U256::from(i).into()) + }).collect::>(); + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); GenesisConfig { - authorities: authorities.into_iter().map(|a| (UintAuthorityId(a).to_public_key(), 1)).collect(), + authorities: pairs.iter().map(|a| (a.public(), 1)).collect(), }.assimilate_storage::(&mut t).unwrap(); - t.into() + (pairs, t.into()) } pub fn go_to_block(n: u64, s: u64) { - let pre_digest = make_pre_digest(0, s, RawVRFOutput([1; 32]), RawVRFProof([0xff; 64])); + let pre_digest = make_secondary_plain_pre_digest(0, s); System::initialize(&n, &Default::default(), &Default::default(), &pre_digest, InitKind::Full); System::set_block_number(n); if s > 1 { @@ -137,11 +142,11 @@ pub fn progress_to_block(n: u64) { pub fn make_pre_digest( authority_index: sp_consensus_babe::AuthorityIndex, slot_number: sp_consensus_babe::SlotNumber, - vrf_output: RawVRFOutput, - vrf_proof: RawVRFProof, + vrf_output: VRFOutput, + vrf_proof: VRFProof, ) -> Digest { - let digest_data = sp_consensus_babe::digests::RawPreDigest::Primary( - sp_consensus_babe::digests::RawPrimaryPreDigest { + let digest_data = sp_consensus_babe::digests::PreDigest::Primary( + sp_consensus_babe::digests::PrimaryPreDigest { authority_index, slot_number, vrf_output, @@ -152,6 +157,20 @@ pub fn make_pre_digest( Digest { logs: vec![log] } } +pub fn make_secondary_plain_pre_digest( + authority_index: sp_consensus_babe::AuthorityIndex, + slot_number: sp_consensus_babe::SlotNumber, +) -> Digest { + let digest_data = sp_consensus_babe::digests::PreDigest::SecondaryPlain( + sp_consensus_babe::digests::SecondaryPlainPreDigest { + authority_index, + slot_number, + } + ); + let log = DigestItem::PreRuntime(sp_consensus_babe::BABE_ENGINE_ID, digest_data.encode()); + Digest { logs: vec![log] } +} + pub type System = frame_system::Module; pub type Babe = Module; pub type Session = pallet_session::Module; diff --git a/frame/babe/src/tests.rs b/frame/babe/src/tests.rs index a1eb928374..be2d3ed036 100644 --- a/frame/babe/src/tests.rs +++ b/frame/babe/src/tests.rs @@ -21,7 +21,8 @@ use super::*; use mock::*; use frame_support::traits::OnFinalize; use pallet_session::ShouldEndSession; -use sp_consensus_vrf::schnorrkel::{RawVRFOutput, RawVRFProof}; +use sp_core::crypto::IsWrappedBy; +use sp_consensus_vrf::schnorrkel::{VRFOutput, VRFProof}; const EMPTY_RANDOMNESS: [u8; 32] = [ 74, 25, 49, 128, 53, 97, 244, 49, @@ -38,14 +39,14 @@ fn empty_randomness_is_correct() { #[test] fn initial_values() { - new_test_ext(vec![0, 1, 2, 3]).execute_with(|| { + new_test_ext(4).1.execute_with(|| { assert_eq!(Babe::authorities().len(), 4) }) } #[test] fn check_module() { - new_test_ext(vec![0, 1, 2, 3]).execute_with(|| { + new_test_ext(4).1.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"); @@ -54,14 +55,29 @@ fn check_module() { #[test] fn first_block_epoch_zero_start() { - new_test_ext(vec![0, 1, 2, 3]).execute_with(|| { + let (pairs, mut ext) = new_test_ext(4); + + ext.execute_with(|| { let genesis_slot = 100; - let first_vrf = RawVRFOutput([1; 32]); + + let pair = sp_core::sr25519::Pair::from_ref(&pairs[0]).as_ref(); + let transcript = sp_consensus_babe::make_transcript( + &Babe::randomness(), + genesis_slot, + 0, + ); + let vrf_inout = pair.vrf_sign(transcript); + let vrf_randomness: sp_consensus_vrf::schnorrkel::Randomness = vrf_inout.0 + .make_bytes::<[u8; 32]>(&sp_consensus_babe::BABE_VRF_INOUT_CONTEXT); + let vrf_output = VRFOutput(vrf_inout.0.to_output()); + let vrf_proof = VRFProof(vrf_inout.1); + + let first_vrf = vrf_output; let pre_digest = make_pre_digest( 0, genesis_slot, first_vrf.clone(), - RawVRFProof([0xff; 64]), + vrf_proof, ); assert_eq!(Babe::genesis_slot(), 0); @@ -84,7 +100,7 @@ fn first_block_epoch_zero_start() { let header = System::finalize(); assert_eq!(SegmentIndex::get(), 0); - assert_eq!(UnderConstruction::get(0), vec![first_vrf]); + assert_eq!(UnderConstruction::get(0), vec![vrf_randomness]); assert_eq!(Babe::randomness(), [0; 32]); assert_eq!(NextRandomness::get(), [0; 32]); @@ -92,10 +108,9 @@ fn first_block_epoch_zero_start() { assert_eq!(pre_digest.logs.len(), 1); assert_eq!(header.digest.logs[0], pre_digest.logs[0]); - let authorities = Babe::authorities(); let consensus_log = sp_consensus_babe::ConsensusLog::NextEpochData( sp_consensus_babe::digests::NextEpochDescriptor { - authorities, + authorities: Babe::authorities(), randomness: Babe::randomness(), } ); @@ -108,7 +123,7 @@ fn first_block_epoch_zero_start() { #[test] fn authority_index() { - new_test_ext(vec![0, 1, 2, 3]).execute_with(|| { + new_test_ext(4).1.execute_with(|| { assert_eq!( Babe::find_author((&[(BABE_ENGINE_ID, &[][..])]).into_iter().cloned()), None, "Trivially invalid authorities are ignored") @@ -117,7 +132,7 @@ fn authority_index() { #[test] fn can_predict_next_epoch_change() { - new_test_ext(vec![]).execute_with(|| { + new_test_ext(0).1.execute_with(|| { assert_eq!(::EpochDuration::get(), 3); // this sets the genesis slot to 6; go_to_block(1, 6); diff --git a/primitives/consensus/babe/Cargo.toml b/primitives/consensus/babe/Cargo.toml index cb64925ef5..cd41aa1251 100644 --- a/primitives/consensus/babe/Cargo.toml +++ b/primitives/consensus/babe/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/paritytech/substrate/" [dependencies] sp-application-crypto = { version = "2.0.0-alpha.5", default-features = false, path = "../../application-crypto" } codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false } +merlin = { version = "2.0", default-features = false } sp-std = { version = "2.0.0-alpha.5", default-features = false, path = "../../std" } sp-api = { version = "2.0.0-alpha.5", default-features = false, path = "../../api" } sp-consensus = { version = "0.8.0-alpha.5", optional = true, path = "../common" } @@ -24,6 +25,7 @@ default = ["std"] std = [ "sp-application-crypto/std", "codec/std", + "merlin/std", "sp-std/std", "sp-api/std", "sp-consensus", diff --git a/primitives/consensus/babe/src/digests.rs b/primitives/consensus/babe/src/digests.rs index 6079aa88c8..f8ae672f4b 100644 --- a/primitives/consensus/babe/src/digests.rs +++ b/primitives/consensus/babe/src/digests.rs @@ -22,19 +22,17 @@ use super::{AuthorityId, AuthorityIndex, SlotNumber, BabeAuthorityWeight}; #[cfg(feature = "std")] use sp_runtime::{DigestItem, generic::OpaqueDigestItemId}; #[cfg(feature = "std")] -use std::{fmt::Debug, convert::{TryFrom, TryInto}}; +use std::fmt::Debug; use codec::{Decode, Encode}; #[cfg(feature = "std")] use codec::Codec; use sp_std::vec::Vec; use sp_runtime::RuntimeDebug; -use sp_consensus_vrf::schnorrkel::{self, Randomness}; -#[cfg(feature = "std")] -use sp_consensus_vrf::schnorrkel::SignatureError; +use sp_consensus_vrf::schnorrkel::{Randomness, VRFOutput, VRFProof}; /// Raw BABE primary slot assignment pre-digest. #[derive(Clone, RuntimeDebug, Encode, Decode)] -pub struct RawPrimaryPreDigest { +pub struct PrimaryPreDigest { /// Authority index pub authority_index: super::AuthorityIndex, /// Slot number @@ -45,24 +43,6 @@ pub struct RawPrimaryPreDigest; - -#[cfg(feature = "std")] -impl TryFrom for PrimaryPreDigest { - type Error = SignatureError; - - fn try_from(raw: RawPrimaryPreDigest) -> Result { - Ok(PrimaryPreDigest { - authority_index: raw.authority_index, - slot_number: raw.slot_number, - vrf_output: raw.vrf_output.try_into()?, - vrf_proof: raw.vrf_proof.try_into()?, - }) - } -} - /// BABE secondary slot assignment pre-digest. #[derive(Clone, RuntimeDebug, Encode, Decode)] pub struct SecondaryPreDigest { @@ -77,37 +57,51 @@ pub struct SecondaryPreDigest { pub slot_number: SlotNumber, } +/// BABE secondary deterministic slot assignment with VRF outputs. +#[derive(Clone, RuntimeDebug, Encode, Decode)] +pub struct SecondaryVRFPreDigest { + /// Authority index + pub authority_index: super::AuthorityIndex, + /// Slot number + pub slot_number: SlotNumber, + /// VRF output + pub vrf_output: VRFOutput, + /// VRF proof + pub vrf_proof: VRFProof, +} + /// A BABE pre-runtime digest. This contains all data required to validate a /// block and for the BABE runtime module. Slots can be assigned to a primary /// (VRF based) and to a secondary (slot number based). #[derive(Clone, RuntimeDebug, Encode, Decode)] -pub enum RawPreDigest { +pub enum PreDigest { /// A primary VRF-based slot assignment. #[codec(index = "1")] - Primary(RawPrimaryPreDigest), + Primary(PrimaryPreDigest), /// A secondary deterministic slot assignment. #[codec(index = "2")] - Secondary(SecondaryPreDigest), + SecondaryPlain(SecondaryPlainPreDigest), + /// A secondary deterministic slot assignment with VRF outputs. + #[codec(index = "3")] + SecondaryVRF(SecondaryVRFPreDigest), } -#[cfg(feature = "std")] -/// A BABE pre-runtime digest for std. -pub type PreDigest = RawPreDigest; - -impl RawPreDigest { +impl PreDigest { /// Returns the slot number of the pre digest. pub fn authority_index(&self) -> AuthorityIndex { match self { - RawPreDigest::Primary(primary) => primary.authority_index, - RawPreDigest::Secondary(secondary) => secondary.authority_index, + PreDigest::Primary(primary) => primary.authority_index, + PreDigest::SecondaryPlain(secondary) => secondary.authority_index, + PreDigest::SecondaryVRF(secondary) => secondary.authority_index, } } /// Returns the slot number of the pre digest. pub fn slot_number(&self) -> SlotNumber { match self { - RawPreDigest::Primary(primary) => primary.slot_number, - RawPreDigest::Secondary(secondary) => secondary.slot_number, + PreDigest::Primary(primary) => primary.slot_number, + PreDigest::SecondaryPlain(secondary) => secondary.slot_number, + PreDigest::SecondaryVRF(secondary) => secondary.slot_number, } } @@ -115,24 +109,12 @@ impl RawPreDigest { /// of the chain. pub fn added_weight(&self) -> crate::BabeBlockWeight { match self { - RawPreDigest::Primary(_) => 1, - RawPreDigest::Secondary(_) => 0, + PreDigest::Primary(_) => 1, + PreDigest::SecondaryPlain(_) | PreDigest::SecondaryVRF(_) => 0, } } } -#[cfg(feature = "std")] -impl TryFrom for PreDigest { - type Error = SignatureError; - - fn try_from(raw: RawPreDigest) -> Result { - Ok(match raw { - RawPreDigest::Primary(primary) => PreDigest::Primary(primary.try_into()?), - RawPreDigest::Secondary(secondary) => PreDigest::Secondary(secondary), - }) - } -} - /// Information about the next epoch. This is broadcast in the first block /// of the epoch. #[derive(Decode, Encode, Default, PartialEq, Eq, Clone, RuntimeDebug)] diff --git a/primitives/consensus/babe/src/lib.rs b/primitives/consensus/babe/src/lib.rs index 33701860d1..a5c7e0de3b 100644 --- a/primitives/consensus/babe/src/lib.rs +++ b/primitives/consensus/babe/src/lib.rs @@ -25,6 +25,7 @@ pub mod inherents; pub use sp_consensus_vrf::schnorrkel::{ Randomness, VRF_PROOF_LENGTH, VRF_OUTPUT_LENGTH, RANDOMNESS_LENGTH }; +pub use merlin::Transcript; use codec::{Encode, Decode}; use sp_std::vec::Vec; @@ -39,6 +40,9 @@ mod app { /// The prefix used by BABE for its VRF keys. pub const BABE_VRF_PREFIX: &[u8] = b"substrate-babe-vrf"; +/// BABE VRFInOut context. +pub static BABE_VRF_INOUT_CONTEXT: &[u8] = b"BabeVRFInOutContext"; + /// A Babe authority keypair. Necessarily equivalent to the schnorrkel public key used in /// the main Babe module. If that ever changes, then this must, too. #[cfg(feature = "std")] @@ -76,6 +80,19 @@ pub type BabeAuthorityWeight = u64; /// The weight of a BABE block. pub type BabeBlockWeight = u32; +/// Make a VRF transcript from given randomness, slot number and epoch. +pub fn make_transcript( + randomness: &Randomness, + slot_number: u64, + epoch: u64, +) -> Transcript { + let mut transcript = Transcript::new(&BABE_ENGINE_ID); + transcript.append_u64(b"slot number", slot_number); + transcript.append_u64(b"current epoch", epoch); + transcript.append_message(b"chain randomness", &randomness[..]); + transcript +} + /// An consensus log item for BABE. #[derive(Decode, Encode, Clone, PartialEq, Eq)] pub enum ConsensusLog { diff --git a/primitives/consensus/vrf/Cargo.toml b/primitives/consensus/vrf/Cargo.toml index 8fedd3f0e1..3a2150d83e 100644 --- a/primitives/consensus/vrf/Cargo.toml +++ b/primitives/consensus/vrf/Cargo.toml @@ -10,7 +10,7 @@ homepage = "https://substrate.dev" [dependencies] codec = { version = "1.0.0", package = "parity-scale-codec", default-features = false } -schnorrkel = { version = "0.9.1", features = ["preaudit_deprecated"], optional = true } +schnorrkel = { version = "0.9.1", features = ["preaudit_deprecated", "u64_backend"], default-features = false } sp-std = { version = "2.0.0-alpha.5", path = "../../std", default-features = false } sp-core = { version = "2.0.0-alpha.5", path = "../../core", default-features = false } sp-runtime = { version = "2.0.0-alpha.5", default-features = false, path = "../../runtime" } @@ -19,7 +19,7 @@ sp-runtime = { version = "2.0.0-alpha.5", default-features = false, path = "../. default = ["std"] std = [ "codec/std", - "schnorrkel", + "schnorrkel/std", "sp-std/std", "sp-core/std", "sp-runtime/std", diff --git a/primitives/consensus/vrf/src/schnorrkel.rs b/primitives/consensus/vrf/src/schnorrkel.rs index 635160aa00..c1c2a7c21a 100644 --- a/primitives/consensus/vrf/src/schnorrkel.rs +++ b/primitives/consensus/vrf/src/schnorrkel.rs @@ -16,72 +16,38 @@ //! Schnorrkel-based VRF. -use codec::{Encode, Decode}; -#[cfg(feature = "std")] +use codec::{Encode, Decode, EncodeLike}; +use sp_std::{convert::TryFrom, prelude::*}; use sp_core::U512; -use sp_runtime::RuntimeDebug; use sp_std::ops::{Deref, DerefMut}; -#[cfg(feature = "std")] -use std::convert::TryFrom; -#[cfg(feature = "std")] -use codec::EncodeLike; -#[cfg(feature = "std")] use schnorrkel::errors::MultiSignatureStage; -#[cfg(feature = "std")] -pub use schnorrkel::{SignatureError, vrf::{VRF_PROOF_LENGTH, VRF_OUTPUT_LENGTH}}; - -/// The length of the VRF proof. -#[cfg(not(feature = "std"))] -pub const VRF_PROOF_LENGTH: usize = 64; - -/// The length of the VRF output. -#[cfg(not(feature = "std"))] -pub const VRF_OUTPUT_LENGTH: usize = 32; +pub use schnorrkel::{SignatureError, PublicKey, vrf::{VRF_PROOF_LENGTH, VRF_OUTPUT_LENGTH}}; /// The length of the Randomness. pub const RANDOMNESS_LENGTH: usize = VRF_OUTPUT_LENGTH; -/// Raw VRF output. -#[derive(Clone, Copy, Eq, PartialEq, RuntimeDebug, Encode, Decode)] -pub struct RawVRFOutput(pub [u8; VRF_OUTPUT_LENGTH]); - -impl Deref for RawVRFOutput { - type Target = [u8; VRF_OUTPUT_LENGTH]; - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DerefMut for RawVRFOutput { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } -} - /// VRF output type available for `std` environment, suitable for schnorrkel operations. -#[cfg(feature = "std")] #[derive(Clone, Debug, PartialEq, Eq)] pub struct VRFOutput(pub schnorrkel::vrf::VRFOutput); -#[cfg(feature = "std")] impl Deref for VRFOutput { type Target = schnorrkel::vrf::VRFOutput; fn deref(&self) -> &Self::Target { &self.0 } } -#[cfg(feature = "std")] impl DerefMut for VRFOutput { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -#[cfg(feature = "std")] impl Encode for VRFOutput { fn encode(&self) -> Vec { self.0.as_bytes().encode() } } -#[cfg(feature = "std")] impl EncodeLike for VRFOutput { } -#[cfg(feature = "std")] impl Decode for VRFOutput { fn decode(i: &mut R) -> Result { let decoded = <[u8; VRF_OUTPUT_LENGTH]>::decode(i)?; @@ -89,7 +55,6 @@ impl Decode for VRFOutput { } } -#[cfg(feature = "std")] impl TryFrom<[u8; VRF_OUTPUT_LENGTH]> for VRFOutput { type Error = SignatureError; @@ -98,91 +63,39 @@ impl TryFrom<[u8; VRF_OUTPUT_LENGTH]> for VRFOutput { } } -#[cfg(feature = "std")] -impl TryFrom for VRFOutput { - type Error = SignatureError; - - fn try_from(raw: RawVRFOutput) -> Result { - schnorrkel::vrf::VRFOutput::from_bytes(&raw.0).map(VRFOutput) - } -} - -#[cfg(feature = "std")] -impl From for RawVRFOutput { - fn from(output: VRFOutput) -> RawVRFOutput { - RawVRFOutput(output.to_bytes()) - } -} - -/// Raw VRF proof. -#[derive(Clone, Copy, Encode, Decode)] -pub struct RawVRFProof(pub [u8; VRF_PROOF_LENGTH]); - -impl Deref for RawVRFProof { - type Target = [u8; VRF_PROOF_LENGTH]; - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DerefMut for RawVRFProof { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } -} - -#[cfg(feature = "std")] -impl std::fmt::Debug for RawVRFProof { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", &self) - } -} - -impl core::cmp::PartialEq for RawVRFProof { - fn eq(&self, other: &Self) -> bool { - self == other - } -} - -impl core::cmp::Eq for RawVRFProof { } - /// VRF proof type available for `std` environment, suitable for schnorrkel operations. -#[cfg(feature = "std")] #[derive(Clone, Debug, PartialEq, Eq)] pub struct VRFProof(pub schnorrkel::vrf::VRFProof); -#[cfg(feature = "std")] impl PartialOrd for VRFProof { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -#[cfg(feature = "std")] impl Ord for VRFProof { fn cmp(&self, other: &Self) -> core::cmp::Ordering { U512::from(self.0.to_bytes()).cmp(&U512::from(other.0.to_bytes())) } } -#[cfg(feature = "std")] impl Deref for VRFProof { type Target = schnorrkel::vrf::VRFProof; fn deref(&self) -> &Self::Target { &self.0 } } -#[cfg(feature = "std")] impl DerefMut for VRFProof { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -#[cfg(feature = "std")] impl Encode for VRFProof { fn encode(&self) -> Vec { self.0.to_bytes().encode() } } -#[cfg(feature = "std")] impl EncodeLike for VRFProof { } -#[cfg(feature = "std")] impl Decode for VRFProof { fn decode(i: &mut R) -> Result { let decoded = <[u8; VRF_PROOF_LENGTH]>::decode(i)?; @@ -190,7 +103,6 @@ impl Decode for VRFProof { } } -#[cfg(feature = "std")] impl TryFrom<[u8; VRF_PROOF_LENGTH]> for VRFProof { type Error = SignatureError; @@ -199,23 +111,6 @@ impl TryFrom<[u8; VRF_PROOF_LENGTH]> for VRFProof { } } -#[cfg(feature = "std")] -impl TryFrom for VRFProof { - type Error = SignatureError; - - fn try_from(raw: RawVRFProof) -> Result { - schnorrkel::vrf::VRFProof::from_bytes(&raw.0).map(VRFProof) - } -} - -#[cfg(feature = "std")] -impl From for RawVRFProof { - fn from(output: VRFProof) -> RawVRFProof { - RawVRFProof(output.to_bytes()) - } -} - -#[cfg(feature = "std")] fn convert_error(e: SignatureError) -> codec::Error { use SignatureError::*; use MultiSignatureStage::*; From 7c85c027c3803d8542699cf26cd31909b8335a56 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 20 Aug 2020 15:13:19 +1200 Subject: [PATCH 08/13] Add changes for #5876 to compile & run tests --- bin/node/runtime/src/lib.rs | 8 +- client/consensus/babe/rpc/src/lib.rs | 13 +- client/consensus/babe/src/authorship.rs | 89 ++++++-- client/consensus/babe/src/aux_schema.rs | 37 +++- client/consensus/babe/src/lib.rs | 242 ++++++++++++++-------- client/consensus/babe/src/migration.rs | 64 ++++++ client/consensus/babe/src/tests.rs | 30 +-- client/consensus/babe/src/verification.rs | 76 +++++-- client/consensus/epochs/src/lib.rs | 49 +++++ client/consensus/slots/src/lib.rs | 119 ++++++++++- client/rpc/src/state/tests.rs | 2 +- primitives/consensus/babe/src/digests.rs | 42 +++- primitives/consensus/babe/src/lib.rs | 108 +++++++++- primitives/core/src/traits.rs | 8 + test-utils/runtime/src/lib.rs | 14 +- 15 files changed, 728 insertions(+), 173 deletions(-) create mode 100644 client/consensus/babe/src/migration.rs diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 238ecb2c2d..54504d1731 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -12,7 +12,7 @@ // GNU General Public License for more details. // You should have received a copy of the GNU General Public License -// along with Substrate. If not, see . +// along with Substrate. If not, see . //! The Substrate runtime. This can be compiled with ``#[no_std]`, ready for Wasm. @@ -768,19 +768,19 @@ impl_runtime_apis! { } impl sp_consensus_babe::BabeApi for Runtime { - fn configuration() -> sp_consensus_babe::BabeConfiguration { + fn configuration() -> sp_consensus_babe::BabeGenesisConfiguration { // The choice of `c` parameter (where `1 - c` represents the // probability of a slot being empty), is done in accordance to the // slot duration and expected target block time, for safely // resisting network delays of maximum two seconds. // - sp_consensus_babe::BabeConfiguration { + sp_consensus_babe::BabeGenesisConfiguration { slot_duration: Babe::slot_duration(), epoch_length: EpochDuration::get(), c: PRIMARY_PROBABILITY, genesis_authorities: Babe::authorities(), randomness: Babe::randomness(), - secondary_slots: true, + allowed_slots: sp_consensus_babe::AllowedSlots::PrimaryAndSecondaryPlainSlots, } } diff --git a/client/consensus/babe/rpc/src/lib.rs b/client/consensus/babe/rpc/src/lib.rs index cb78504b1f..fa5421761c 100644 --- a/client/consensus/babe/rpc/src/lib.rs +++ b/client/consensus/babe/rpc/src/lib.rs @@ -118,14 +118,17 @@ impl BabeApi for BabeRPCHandler for slot_number in epoch_start..epoch_end { let epoch = epoch_data(&shared_epoch, &client, &babe_config, slot_number, &select_chain)?; - if let Some((claim, key)) = authorship::claim_slot(slot_number, &epoch, &babe_config, &keystore) { + if let Some((claim, key)) = authorship::claim_slot(slot_number, &epoch, &keystore) { match claim { PreDigest::Primary { .. } => { claims.entry(key.public()).or_default().primary.push(slot_number); } - PreDigest::Secondary { .. } => { + PreDigest::SecondaryPlain { .. } => { claims.entry(key.public()).or_default().secondary.push(slot_number); } + PreDigest::SecondaryVRF { .. } => { + claims.entry(key.public()).or_default().secondary_vrf.push(slot_number); + }, }; } } @@ -144,6 +147,8 @@ pub struct EpochAuthorship { primary: Vec, /// the array of secondary slots that can be claimed secondary: Vec, + /// The array of secondary VRF slots that can be claimed. + secondary_vrf: Vec, } /// Errors encountered by the RPC @@ -184,7 +189,7 @@ fn epoch_data( &parent.hash(), parent.number().clone(), slot_number, - |slot| babe_config.genesis_epoch(slot), + |slot| Epoch::genesis(&babe_config, slot), ) .map_err(|e| Error::Consensus(ConsensusError::ChainLookup(format!("{:?}", e))))? .ok_or(Error::Consensus(ConsensusError::InvalidAuthoritiesSet)) @@ -236,7 +241,7 @@ mod tests { io.extend_with(BabeApi::to_delegate(handler)); let request = r#"{"jsonrpc":"2.0","method":"babe_epochAuthorship","params": [],"id":1}"#; - let response = r#"{"jsonrpc":"2.0","result":{"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY":{"primary":[0],"secondary":[1,2,4]}},"id":1}"#; + let response = r#"{"jsonrpc":"2.0","result":{"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY":{"primary":[0],"secondary":[1,2,4],"secondary_vrf":[]}},"id":1}"#; assert_eq!(Some(response.into()), io.handle_request_sync(request)); } diff --git a/client/consensus/babe/src/authorship.rs b/client/consensus/babe/src/authorship.rs index ed31916560..1810f9f5be 100644 --- a/client/consensus/babe/src/authorship.rs +++ b/client/consensus/babe/src/authorship.rs @@ -23,7 +23,6 @@ use sp_consensus_babe::{ use sp_consensus_babe::digests::{ PreDigest, PrimaryPreDigest, SecondaryPlainPreDigest, SecondaryVRFPreDigest, }; -use sp_consensus_babe::digests::{PreDigest, PrimaryPreDigest, SecondaryPreDigest}; use sp_consensus_vrf::schnorrkel::{VRFOutput, VRFProof}; use sp_core::{U256, blake2_256}; use codec::Encode; @@ -49,14 +48,44 @@ pub(super) fn calculate_primary_threshold( authorities[authority_index].1 as f64 / authorities.iter().map(|(_, weight)| weight).sum::() as f64; - let calc = || { - let p = BigRational::from_float(1f64 - (1f64 - c).powf(theta))?; - let numer = p.numer().to_biguint()?; - let denom = p.denom().to_biguint()?; - ((BigUint::one() << 128) * numer / denom).to_u128() - }; + assert!(theta > 0.0, "authority with weight 0."); - calc().unwrap_or(u128::max_value()) + // NOTE: in the equation `p = 1 - (1 - c)^theta` the value of `p` is always + // capped by `c`. For all pratical purposes `c` should always be set to a + // value < 0.5, as such in the computations below we should never be near + // edge cases like `0.999999`. + + let p = BigRational::from_float(1f64 - (1f64 - c).powf(theta)).expect( + "returns None when the given value is not finite; \ + c is a configuration parameter defined in (0, 1]; \ + theta must be > 0 if the given authority's weight is > 0; \ + theta represents the validator's relative weight defined in (0, 1]; \ + powf will always return values in (0, 1] given both the \ + base and exponent are in that domain; \ + qed.", + ); + + let numer = p.numer().to_biguint().expect( + "returns None when the given value is negative; \ + p is defined as `1 - n` where n is defined in (0, 1]; \ + p must be a value in [0, 1); \ + qed." + ); + + let denom = p.denom().to_biguint().expect( + "returns None when the given value is negative; \ + p is defined as `1 - n` where n is defined in (0, 1]; \ + p must be a value in [0, 1); \ + qed." + ); + + ((BigUint::one() << 128) * numer / denom).to_u128().expect( + "returns None if the underlying value cannot be represented with 128 bits; \ + we start with 2^128 which is one more than can be represented with 128 bits; \ + we multiple by p which is defined in [0, 1); \ + the result must be lower than 2^128 by at least one and thus representable with 128 bits; \ + qed.", + ) } /// Returns true if the given VRF output is lower than the given threshold, @@ -94,10 +123,12 @@ pub(super) fn secondary_slot_author( /// to propose. fn claim_secondary_slot( slot_number: SlotNumber, - authorities: &[(AuthorityId, BabeAuthorityWeight)], + epoch: &Epoch, keystore: &KeyStorePtr, - randomness: [u8; 32], + author_secondary_vrf: bool, ) -> Option<(PreDigest, AuthorityPair)> { + let Epoch { authorities, randomness, epoch_index, .. } = epoch; + if authorities.is_empty() { return None; } @@ -105,7 +136,7 @@ fn claim_secondary_slot( let expected_author = super::authorship::secondary_slot_author( slot_number, authorities, - randomness, + *randomness, )?; let keystore = keystore.read(); @@ -117,10 +148,27 @@ fn claim_secondary_slot( }) { if pair.public() == *expected_author { - let pre_digest = PreDigest::Secondary(SecondaryPreDigest { - slot_number, - authority_index: authority_index as u32, - }); + let pre_digest = if author_secondary_vrf { + let transcript = super::authorship::make_transcript( + randomness, + slot_number, + *epoch_index, + ); + + let s = get_keypair(&pair).vrf_sign(transcript); + + PreDigest::SecondaryVRF(SecondaryVRFPreDigest { + slot_number, + vrf_output: VRFOutput(s.0.to_output()), + vrf_proof: VRFProof(s.1), + authority_index: authority_index as u32, + }) + } else { + PreDigest::SecondaryPlain(SecondaryPlainPreDigest { + slot_number, + authority_index: authority_index as u32, + }) + }; return Some((pre_digest, pair)); } @@ -136,17 +184,18 @@ fn claim_secondary_slot( pub fn claim_slot( slot_number: SlotNumber, epoch: &Epoch, - config: &BabeConfiguration, keystore: &KeyStorePtr, ) -> Option<(PreDigest, AuthorityPair)> { - claim_primary_slot(slot_number, epoch, config.c, keystore) + claim_primary_slot(slot_number, epoch, epoch.config.c, keystore) .or_else(|| { - if config.secondary_slots { + if epoch.config.allowed_slots.is_secondary_plain_slots_allowed() || + epoch.config.allowed_slots.is_secondary_vrf_slots_allowed() + { claim_secondary_slot( slot_number, - &epoch.authorities, + &epoch, keystore, - epoch.randomness, + epoch.config.allowed_slots.is_secondary_vrf_slots_allowed(), ) } else { None diff --git a/client/consensus/babe/src/aux_schema.rs b/client/consensus/babe/src/aux_schema.rs index 6f69e65940..2a3f23981d 100644 --- a/client/consensus/babe/src/aux_schema.rs +++ b/client/consensus/babe/src/aux_schema.rs @@ -24,13 +24,13 @@ use codec::{Decode, Encode}; use sc_client_api::backend::AuxStore; use sp_blockchain::{Result as ClientResult, Error as ClientError}; use sp_runtime::traits::Block as BlockT; -use sp_consensus_babe::BabeBlockWeight; +use sp_consensus_babe::{BabeBlockWeight, BabeGenesisConfiguration}; use sc_consensus_epochs::{EpochChangesFor, SharedEpochChanges, migration::EpochChangesForV0}; -use crate::Epoch; +use crate::{Epoch, migration::EpochV0}; const BABE_EPOCH_CHANGES_VERSION: &[u8] = b"babe_epoch_changes_version"; const BABE_EPOCH_CHANGES_KEY: &[u8] = b"babe_epoch_changes"; -const BABE_EPOCH_CHANGES_CURRENT_VERSION: u32 = 1; +const BABE_EPOCH_CHANGES_CURRENT_VERSION: u32 = 2; fn block_weight_key(block_hash: H) -> Vec { (b"block_weight", block_hash).encode() @@ -53,14 +53,19 @@ fn load_decode(backend: &B, key: &[u8]) -> ClientResult> /// Load or initialize persistent epoch change data from backend. pub(crate) fn load_epoch_changes( backend: &B, + config: &BabeGenesisConfiguration, ) -> ClientResult> { let version = load_decode::<_, u32>(backend, BABE_EPOCH_CHANGES_VERSION)?; let maybe_epoch_changes = match version { - None => load_decode::<_, EpochChangesForV0>( + None => load_decode::<_, EpochChangesForV0>( backend, BABE_EPOCH_CHANGES_KEY, - )?.map(|v0| v0.migrate()), + )?.map(|v0| v0.migrate().map(|_, _, epoch| epoch.migrate(config))), + Some(1) => load_decode::<_, EpochChangesFor>( + backend, + BABE_EPOCH_CHANGES_KEY, + )?.map(|v1| v1.map(|_, _, epoch| epoch.migrate(config))), Some(BABE_EPOCH_CHANGES_CURRENT_VERSION) => load_decode::<_, EpochChangesFor>( backend, BABE_EPOCH_CHANGES_KEY, @@ -74,7 +79,7 @@ pub(crate) fn load_epoch_changes( let epoch_changes = Arc::new(Mutex::new(maybe_epoch_changes.unwrap_or_else(|| { info!(target: "babe", - "Creating empty BABE epoch changes on what appears to be first startup." + "👶 Creating empty BABE epoch changes on what appears to be first startup." ); EpochChangesFor::::default() }))); @@ -131,18 +136,19 @@ pub(crate) fn load_block_weight( #[cfg(test)] mod test { use super::*; - use crate::Epoch; + use crate::migration::EpochV0; use fork_tree::ForkTree; use substrate_test_runtime_client; use sp_core::H256; use sp_runtime::traits::NumberFor; + use sp_consensus_babe::{AllowedSlots, BabeGenesisConfiguration}; use sc_consensus_epochs::{PersistedEpoch, PersistedEpochHeader, EpochHeader}; use sp_consensus::Error as ConsensusError; use sc_network_test::Block as TestBlock; #[test] fn load_decode_from_v0_epoch_changes() { - let epoch = Epoch { + let epoch = EpochV0 { start_slot: 0, authorities: vec![], randomness: [0; 32], @@ -160,7 +166,7 @@ mod test { client.insert_aux( &[(BABE_EPOCH_CHANGES_KEY, - &EpochChangesForV0::::from_raw(v0_tree).encode()[..])], + &EpochChangesForV0::::from_raw(v0_tree).encode()[..])], &[], ).unwrap(); @@ -169,7 +175,16 @@ mod test { None, ); - let epoch_changes = load_epoch_changes::(&client).unwrap(); + let epoch_changes = load_epoch_changes::( + &client, &BabeGenesisConfiguration { + slot_duration: 10, + epoch_length: 4, + c: (3, 10), + genesis_authorities: Vec::new(), + randomness: Default::default(), + allowed_slots: AllowedSlots::PrimaryAndSecondaryPlainSlots, + }, + ).unwrap(); assert!( epoch_changes.lock() @@ -192,7 +207,7 @@ mod test { assert_eq!( load_decode::<_, u32>(&client, BABE_EPOCH_CHANGES_VERSION).unwrap(), - Some(1), + Some(2), ); } } diff --git a/client/consensus/babe/src/lib.rs b/client/consensus/babe/src/lib.rs index 5365aae2aa..028fe8e5e3 100644 --- a/client/consensus/babe/src/lib.rs +++ b/client/consensus/babe/src/lib.rs @@ -49,6 +49,11 @@ //! //! `blake2_256(epoch_randomness ++ slot_number) % authorities_len`. //! +//! The secondary slots supports either a `SecondaryPlain` or `SecondaryVRF` +//! variant. Comparing with `SecondaryPlain` variant, the `SecondaryVRF` variant +//! generates an additional VRF output. The output is not included in beacon +//! randomness, but can be consumed by parachains. +//! //! The fork choice rule is weight-based, where weight equals the number of //! primary blocks in the chain. We will pick the heaviest chain (more primary //! blocks) and will go with the longest one in case of a tie. @@ -59,11 +64,13 @@ #![forbid(unsafe_code)] #![warn(missing_docs)] pub use sp_consensus_babe::{ - BabeApi, ConsensusLog, BABE_ENGINE_ID, SlotNumber, BabeConfiguration, + BabeApi, ConsensusLog, BABE_ENGINE_ID, SlotNumber, + BabeEpochConfiguration, BabeGenesisConfiguration, AuthorityId, AuthorityPair, AuthoritySignature, BabeAuthorityWeight, VRF_OUTPUT_LENGTH, digests::{ - CompatibleDigestItem, NextEpochDescriptor, PreDigest, PrimaryPreDigest, SecondaryPreDigest, + CompatibleDigestItem, NextEpochDescriptor, NextConfigDescriptor, PreDigest, + PrimaryPreDigest, SecondaryPlainPreDigest, }, }; pub use sp_consensus::SyncOracle; @@ -101,7 +108,7 @@ use sc_client_api::{ use sp_block_builder::BlockBuilder as BlockBuilderApi; use futures::prelude::*; -use log::{warn, debug, info, trace}; +use log::{debug, info, log, trace, warn}; use sc_consensus_slots::{ SlotWorker, SlotInfo, SlotCompatible, StorageChanges, CheckedHeader, check_equivocation, }; @@ -118,36 +125,43 @@ use sp_api::ApiExt; mod aux_schema; mod verification; +mod migration; pub mod authorship; #[cfg(test)] mod tests; /// BABE epoch information -#[derive(Decode, Encode, Default, PartialEq, Eq, Clone, Debug)] +#[derive(Decode, Encode, PartialEq, Eq, Clone, Debug)] pub struct Epoch { - /// The epoch index + /// The epoch index. pub epoch_index: u64, - /// The starting slot of the epoch, + /// The starting slot of the epoch. pub start_slot: SlotNumber, - /// The duration of this epoch + /// The duration of this epoch. pub duration: SlotNumber, - /// The authorities and their weights + /// The authorities and their weights. pub authorities: Vec<(AuthorityId, BabeAuthorityWeight)>, - /// Randomness for this epoch + /// Randomness for this epoch. pub randomness: [u8; VRF_OUTPUT_LENGTH], + /// Configuration of the epoch. + pub config: BabeEpochConfiguration, } impl EpochT for Epoch { - type NextEpochDescriptor = NextEpochDescriptor; + type NextEpochDescriptor = (NextEpochDescriptor, BabeEpochConfiguration); type SlotNumber = SlotNumber; - fn increment(&self, descriptor: NextEpochDescriptor) -> Epoch { + fn increment( + &self, + (descriptor, config): (NextEpochDescriptor, BabeEpochConfiguration) + ) -> Epoch { Epoch { epoch_index: self.epoch_index + 1, start_slot: self.start_slot + self.duration, duration: self.duration, authorities: descriptor.authorities, randomness: descriptor.randomness, + config, } } @@ -160,6 +174,27 @@ impl EpochT for Epoch { } } +impl Epoch { + /// Create the genesis epoch (epoch #0). This is defined to start at the slot of + /// the first block, so that has to be provided. + pub fn genesis( + genesis_config: &BabeGenesisConfiguration, + slot_number: SlotNumber + ) -> Epoch { + Epoch { + epoch_index: 0, + start_slot: slot_number, + duration: genesis_config.epoch_length, + authorities: genesis_config.genesis_authorities.clone(), + randomness: genesis_config.randomness.clone(), + config: BabeEpochConfiguration { + c: genesis_config.c, + allowed_slots: genesis_config.allowed_slots, + }, + } + } +} + #[derive(derive_more::Display, Debug)] enum Error { #[display(fmt = "Multiple BABE pre-runtime digests, rejecting!")] @@ -168,6 +203,8 @@ enum Error { NoPreRuntimeDigest, #[display(fmt = "Multiple BABE epoch change digests, rejecting!")] MultipleEpochChangeDigests, + #[display(fmt = "Multiple BABE config change digests, rejecting!")] + MultipleConfigChangeDigests, #[display(fmt = "Could not extract timestamp and slot: {:?}", _0)] Extraction(sp_consensus::Error), #[display(fmt = "Could not fetch epoch at {:?}", _0)] @@ -200,6 +237,8 @@ enum Error { FetchParentHeader(sp_blockchain::Error), #[display(fmt = "Expected epoch change to happen at {:?}, s{}", _0, _1)] ExpectedEpochChange(B::Hash, u64), + #[display(fmt = "Unexpected config change")] + UnexpectedConfigChange, #[display(fmt = "Unexpected epoch change")] UnexpectedEpochChange, #[display(fmt = "Parent block of {} has no associated weight", _0)] @@ -222,16 +261,6 @@ fn babe_err(error: Error) -> Error { error } -macro_rules! babe_info { - ($($i: expr),+) => { - { - info!(target: "babe", $($i),+); - format!($($i),+) - } - }; -} - - /// Intermediate value passed to block importer. pub struct BabeIntermediate { /// The epoch descriptor. @@ -246,7 +275,7 @@ pub static INTERMEDIATE_KEY: &[u8] = b"babe1"; // and `super::babe::Config` can be eliminated. // https://github.com/paritytech/substrate/issues/2434 #[derive(Clone)] -pub struct Config(sc_consensus_slots::SlotDuration); +pub struct Config(sc_consensus_slots::SlotDuration); impl Config { /// Either fetch the slot duration from disk or compute it from the genesis @@ -255,7 +284,26 @@ impl Config { C: AuxStore + ProvideRuntimeApi, C::Api: BabeApi, { trace!(target: "babe", "Getting slot duration"); - match sc_consensus_slots::SlotDuration::get_or_compute(client, |a, b| a.configuration(b)).map(Self) { + match sc_consensus_slots::SlotDuration::get_or_compute(client, |a, b| { + let has_api_v1 = a.has_api_with::, _>( + &b, |v| v == 1, + )?; + let has_api_v2 = a.has_api_with::, _>( + &b, |v| v == 2, + )?; + + if has_api_v1 { + #[allow(deprecated)] { + Ok(a.configuration_before_version_2(b)?.into()) + } + } else if has_api_v2 { + a.configuration(b) + } else { + Err(sp_blockchain::Error::VersionInvalid( + "Unsupported or invalid BabeApi version".to_string() + )) + } + }).map(Self) { Ok(s) => Ok(s), Err(s) => { warn!(target: "babe", "Failed to get slot duration"); @@ -263,24 +311,12 @@ impl Config { } } } - - /// Create the genesis epoch (epoch #0). This is defined to start at the slot of - /// the first block, so that has to be provided. - pub fn genesis_epoch(&self, slot_number: SlotNumber) -> Epoch { - Epoch { - epoch_index: 0, - start_slot: slot_number, - duration: self.epoch_length, - authorities: self.genesis_authorities.clone(), - randomness: self.randomness.clone(), - } - } } impl std::ops::Deref for Config { - type Target = BabeConfiguration; + type Target = BabeGenesisConfiguration; - fn deref(&self) -> &BabeConfiguration { + fn deref(&self) -> &BabeGenesisConfiguration { &*self.0 } } @@ -368,7 +404,7 @@ pub fn start_babe(BabeParams { &inherent_data_providers, )?; - babe_info!("👶 Starting BABE Authorship worker"); + info!(target: "babe", "👶 Starting BABE Authorship worker"); Ok(sc_consensus_slots::start_slot_worker( config.0, select_chain, @@ -438,7 +474,7 @@ impl sc_consensus_slots::SimpleSlotWorker for BabeWork fn authorities_len(&self, epoch_descriptor: &Self::EpochData) -> Option { self.epoch_changes.lock() - .viable_epoch(&epoch_descriptor, |slot| self.config.genesis_epoch(slot)) + .viable_epoch(&epoch_descriptor, |slot| Epoch::genesis(&self.config, slot)) .map(|epoch| epoch.as_ref().authorities.len()) } @@ -453,9 +489,8 @@ impl sc_consensus_slots::SimpleSlotWorker for BabeWork slot_number, self.epoch_changes.lock().viable_epoch( &epoch_descriptor, - |slot| self.config.genesis_epoch(slot) + |slot| Epoch::genesis(&self.config, slot) )?.as_ref(), - &*self.config, &self.keystore, ); @@ -520,38 +555,28 @@ impl sc_consensus_slots::SimpleSlotWorker for BabeWork fn proposing_remaining_duration( &self, head: &B::Header, - slot_info: &SlotInfo + slot_info: &SlotInfo, ) -> Option { - // never give more than 2^this times the lenience. - const BACKOFF_CAP: u64 = 8; - - // how many slots it takes before we double the lenience. - const BACKOFF_STEP: u64 = 2; - let slot_remaining = self.slot_remaining_duration(slot_info); + let parent_slot = match find_pre_digest::(head) { Err(_) => return Some(slot_remaining), Ok(d) => d.slot_number(), }; - // we allow a lenience of the number of slots since the head of the - // chain was produced, minus 1 (since there is always a difference of at least 1) - // - // exponential back-off. - // in normal cases we only attempt to issue blocks up to the end of the slot. - // when the chain has been stalled for a few slots, we give more lenience. - let slot_lenience = slot_info.number.saturating_sub(parent_slot + 1); - - let slot_lenience = std::cmp::min(slot_lenience, BACKOFF_CAP); - let slot_duration = slot_info.duration << (slot_lenience / BACKOFF_STEP); + if let Some(slot_lenience) = + sc_consensus_slots::slot_lenience_exponential(parent_slot, slot_info) + { + debug!(target: "babe", + "No block for {} slots. Applying exponential lenience of {}s", + slot_info.number.saturating_sub(parent_slot + 1), + slot_lenience.as_secs(), + ); - if slot_lenience >= 1 { - debug!(target: "babe", "No block for {} slots. Applying 2^({}/{}) lenience", - slot_lenience, slot_lenience, BACKOFF_STEP); + Some(slot_remaining + slot_lenience) + } else { + Some(slot_remaining) } - - let slot_lenience = Duration::from_secs(slot_duration); - Some(slot_lenience + slot_remaining) } } @@ -582,7 +607,7 @@ fn find_pre_digest(header: &B::Header) -> Result> // genesis block doesn't contain a pre digest so let's generate a // dummy one to not break any invariants in the rest of the code if header.number().is_zero() { - return Ok(PreDigest::Secondary(SecondaryPreDigest { + return Ok(PreDigest::SecondaryPlain(SecondaryPlainPreDigest { slot_number: 0, authority_index: 0, })); @@ -619,6 +644,24 @@ fn find_next_epoch_digest(header: &B::Header) Ok(epoch_digest) } +/// Extract the BABE config change digest from the given header, if it exists. +fn find_next_config_digest(header: &B::Header) + -> Result, Error> + where DigestItemFor: CompatibleDigestItem, +{ + let mut config_digest: Option<_> = None; + for log in header.digest().logs() { + trace!(target: "babe", "Checking log {:?}, looking for epoch change digest.", log); + let log = log.try_to::(OpaqueDigestItemId::Consensus(&BABE_ENGINE_ID)); + match (log, config_digest.is_some()) { + (Some(ConsensusLog::NextConfigData(_)), true) => return Err(babe_err(Error::MultipleConfigChangeDigests)), + (Some(ConsensusLog::NextConfigData(config)), false) => config_digest = Some(config), + _ => trace!(target: "babe", "Ignoring digest not meant for us"), + } + } + + Ok(config_digest) +} #[derive(Default, Clone)] struct TimeSource(Arc, Vec<(Instant, u64)>)>>); @@ -746,7 +789,7 @@ impl Verifier for BabeVerifier where .ok_or_else(|| Error::::FetchEpoch(parent_hash))?; let viable_epoch = epoch_changes.viable_epoch( &epoch_descriptor, - |slot| self.config.genesis_epoch(slot) + |slot| Epoch::genesis(&self.config, slot) ).ok_or_else(|| Error::::FetchEpoch(parent_hash))?; // We add one to the current slot to allow for some small drift. @@ -756,7 +799,6 @@ impl Verifier for BabeVerifier where pre_digest: Some(pre_digest.clone()), slot_now: slot_now + 1, epoch: viable_epoch.as_ref(), - config: &self.config, }; match verification::check_header::(v_params)? { @@ -978,19 +1020,32 @@ impl BlockImport for BabeBlockImport(&block.header) .map_err(|e| ConsensusError::ClientImport(e.to_string()))?; + let next_config_digest = find_next_config_digest::(&block.header) + .map_err(|e| ConsensusError::ClientImport(e.to_string()))?; - match (first_in_epoch, next_epoch_digest.is_some()) { - (true, true) => {}, - (false, false) => {}, - (true, false) => { + match (first_in_epoch, next_epoch_digest.is_some(), next_config_digest.is_some()) { + (true, true, _) => {}, + (false, false, false) => {}, + (false, false, true) => { + return Err( + ConsensusError::ClientImport( + babe_err(Error::::UnexpectedConfigChange).into(), + ) + ) + }, + (true, false, _) => { return Err( ConsensusError::ClientImport( babe_err(Error::::ExpectedEpochChange(hash, slot_number)).into(), ) - ); + ) }, - (false, true) => { - return Err(ConsensusError::ClientImport(Error::::UnexpectedEpochChange.into())); + (false, true, _) => { + return Err( + ConsensusError::ClientImport( + babe_err(Error::::UnexpectedEpochChange).into(), + ) + ) }, } @@ -1005,20 +1060,38 @@ impl BlockImport for BabeBlockImport::FetchEpoch(parent_hash).into()) })?; - babe_info!("👶 New epoch {} launching at block {} (block slot {} >= start slot {}).", - viable_epoch.as_ref().epoch_index, - hash, - slot_number, - viable_epoch.as_ref().start_slot); + let epoch_config = next_config_digest.map(Into::into).unwrap_or_else( + || viable_epoch.as_ref().config.clone() + ); + + // restrict info logging during initial sync to avoid spam + let log_level = if block.origin == BlockOrigin::NetworkInitialSync { + log::Level::Debug + } else { + log::Level::Info + }; + + log!(target: "babe", + log_level, + "👶 New epoch {} launching at block {} (block slot {} >= start slot {}).", + viable_epoch.as_ref().epoch_index, + hash, + slot_number, + viable_epoch.as_ref().start_slot, + ); - let next_epoch = viable_epoch.increment(next_epoch_descriptor); + let next_epoch = viable_epoch.increment((next_epoch_descriptor, epoch_config)); - babe_info!("👶 Next epoch starts at slot {}", next_epoch.as_ref().start_slot); + log!(target: "babe", + log_level, + "👶 Next epoch starts at slot {}", + next_epoch.as_ref().start_slot, + ); // prune the tree of epochs not part of the finalized chain or // that are not live anymore, and then track the given epoch change @@ -1158,7 +1231,7 @@ pub fn block_import( ) -> ClientResult<(BabeBlockImport, BabeLink)> where Client: AuxStore + HeaderBackend + HeaderMetadata, { - let epoch_changes = aux_schema::load_epoch_changes::(&*client)?; + let epoch_changes = aux_schema::load_epoch_changes::(&*client, &config)?; let link = BabeLink { epoch_changes: epoch_changes.clone(), time_source: Default::default(), @@ -1251,13 +1324,12 @@ pub mod test_helpers { &parent.hash(), parent.number().clone(), slot_number, - |slot| link.config.genesis_epoch(slot), + |slot| Epoch::genesis(&link.config, slot), ).unwrap().unwrap(); authorship::claim_slot( slot_number, &epoch, - &link.config, keystore, ).map(|(digest, _)| digest) } diff --git a/client/consensus/babe/src/migration.rs b/client/consensus/babe/src/migration.rs new file mode 100644 index 0000000000..2a5a8749cc --- /dev/null +++ b/client/consensus/babe/src/migration.rs @@ -0,0 +1,64 @@ +use codec::{Encode, Decode}; +use sc_consensus_epochs::Epoch as EpochT; +use crate::{ + Epoch, SlotNumber, AuthorityId, BabeAuthorityWeight, BabeGenesisConfiguration, + BabeEpochConfiguration, VRF_OUTPUT_LENGTH, NextEpochDescriptor, +}; + +/// BABE epoch information, version 0. +#[derive(Decode, Encode, PartialEq, Eq, Clone, Debug)] +pub struct EpochV0 { + /// The epoch index. + pub epoch_index: u64, + /// The starting slot of the epoch. + pub start_slot: SlotNumber, + /// The duration of this epoch. + pub duration: SlotNumber, + /// The authorities and their weights. + pub authorities: Vec<(AuthorityId, BabeAuthorityWeight)>, + /// Randomness for this epoch. + pub randomness: [u8; VRF_OUTPUT_LENGTH], +} + +impl EpochT for EpochV0 { + type NextEpochDescriptor = NextEpochDescriptor; + type SlotNumber = SlotNumber; + + fn increment( + &self, + descriptor: NextEpochDescriptor + ) -> EpochV0 { + EpochV0 { + epoch_index: self.epoch_index + 1, + start_slot: self.start_slot + self.duration, + duration: self.duration, + authorities: descriptor.authorities, + randomness: descriptor.randomness, + } + } + + fn start_slot(&self) -> SlotNumber { + self.start_slot + } + + fn end_slot(&self) -> SlotNumber { + self.start_slot + self.duration + } +} + +impl EpochV0 { + /// Migrate the sturct to current epoch version. + pub fn migrate(self, config: &BabeGenesisConfiguration) -> Epoch { + Epoch { + epoch_index: self.epoch_index, + start_slot: self.start_slot, + duration: self.duration, + authorities: self.authorities, + randomness: self.randomness, + config: BabeEpochConfiguration { + c: config.c, + allowed_slots: config.allowed_slots, + }, + } + } +} diff --git a/client/consensus/babe/src/tests.rs b/client/consensus/babe/src/tests.rs index 20b924669d..71d2e724b1 100644 --- a/client/consensus/babe/src/tests.rs +++ b/client/consensus/babe/src/tests.rs @@ -22,7 +22,7 @@ use super::*; use authorship::claim_slot; -use sp_consensus_babe::{AuthorityPair, SlotNumber}; +use sp_consensus_babe::{AuthorityPair, SlotNumber, AllowedSlots}; use sc_block_builder::{BlockBuilder, BlockBuilderProvider}; use sp_consensus::{ NoNetwork as DummyOracle, Proposal, RecordProof, @@ -127,7 +127,7 @@ impl DummyProposer { &self.parent_hash, self.parent_number, this_slot, - |slot| self.factory.config.genesis_epoch(slot), + |slot| Epoch::genesis(&self.factory.config, slot), ) .expect("client has data to find epoch") .expect("can compute epoch for baked block"); @@ -436,7 +436,7 @@ fn authoring_blocks() { #[should_panic] fn rejects_missing_inherent_digest() { run_one_test(|header: &mut TestHeader, stage| { - let v = std::mem::replace(&mut header.digest_mut().logs, vec![]); + let v = std::mem::take(&mut header.digest_mut().logs); header.digest_mut().logs = v.into_iter() .filter(|v| stage == Stage::PostSeal || v.as_babe_pre_digest().is_none()) .collect() @@ -447,7 +447,7 @@ fn rejects_missing_inherent_digest() { #[should_panic] fn rejects_missing_seals() { run_one_test(|header: &mut TestHeader, stage| { - let v = std::mem::replace(&mut header.digest_mut().logs, vec![]); + let v = std::mem::take(&mut header.digest_mut().logs); header.digest_mut().logs = v.into_iter() .filter(|v| stage == Stage::PreSeal || v.as_babe_seal().is_none()) .collect() @@ -458,7 +458,7 @@ fn rejects_missing_seals() { #[should_panic] fn rejects_missing_consensus_digests() { run_one_test(|header: &mut TestHeader, stage| { - let v = std::mem::replace(&mut header.digest_mut().logs, vec![]); + let v = std::mem::take(&mut header.digest_mut().logs); header.digest_mut().logs = v.into_iter() .filter(|v| stage == Stage::PostSeal || v.as_next_epoch_descriptor().is_none()) .collect() @@ -505,28 +505,32 @@ fn can_author_block() { randomness: [0; 32], epoch_index: 1, duration: 100, + config: BabeEpochConfiguration { + c: (3, 10), + allowed_slots: AllowedSlots::PrimaryAndSecondaryPlainSlots, + }, }; - let mut config = crate::BabeConfiguration { + let mut config = crate::BabeGenesisConfiguration { slot_duration: 1000, epoch_length: 100, c: (3, 10), genesis_authorities: Vec::new(), randomness: [0; 32], - secondary_slots: true, + allowed_slots: AllowedSlots::PrimaryAndSecondaryPlainSlots, }; // with secondary slots enabled it should never be empty - match claim_slot(i, &epoch, &config, &keystore) { + match claim_slot(i, &epoch, &keystore) { None => i += 1, Some(s) => debug!(target: "babe", "Authored block {:?}", s.0), } // otherwise with only vrf-based primary slots we might need to try a couple // of times. - config.secondary_slots = false; + config.allowed_slots = AllowedSlots::PrimarySlots; loop { - match claim_slot(i, &epoch, &config, &keystore) { + match claim_slot(i, &epoch, &keystore) { None => i += 1, Some(s) => { debug!(target: "babe", "Authored block {:?}", s.0); @@ -553,7 +557,7 @@ fn propose_and_import_block( let pre_digest = sp_runtime::generic::Digest { logs: vec![ Item::babe_pre_digest( - PreDigest::Secondary(SecondaryPreDigest { + PreDigest::SecondaryPlain(SecondaryPlainPreDigest { authority_index: 0, slot_number, }), @@ -632,7 +636,7 @@ fn importing_block_one_sets_genesis_epoch() { &mut block_import, ); - let genesis_epoch = data.link.config.genesis_epoch(999); + let genesis_epoch = Epoch::genesis(&data.link.config, 999); let epoch_changes = data.link.epoch_changes.lock(); let epoch_for_second_block = epoch_changes.epoch_data_for_child_of( @@ -640,7 +644,7 @@ fn importing_block_one_sets_genesis_epoch() { &block_hash, 1, 1000, - |slot| data.link.config.genesis_epoch(slot), + |slot| Epoch::genesis(&data.link.config, slot), ).unwrap().unwrap(); assert_eq!(epoch_for_second_block, genesis_epoch); diff --git a/client/consensus/babe/src/verification.rs b/client/consensus/babe/src/verification.rs index 264fda4900..fd3c27be4f 100644 --- a/client/consensus/babe/src/verification.rs +++ b/client/consensus/babe/src/verification.rs @@ -19,7 +19,8 @@ use sp_runtime::{traits::Header, traits::DigestItemFor}; use sp_core::{Pair, Public}; use sp_consensus_babe::{make_transcript, AuthoritySignature, SlotNumber, AuthorityPair, AuthorityId}; use sp_consensus_babe::digests::{ - PreDigest, PrimaryPreDigest, SecondaryPreDigest, CompatibleDigestItem + PreDigest, PrimaryPreDigest, SecondaryPlainPreDigest, SecondaryVRFPreDigest, + CompatibleDigestItem }; use sc_consensus_slots::CheckedHeader; use log::{debug, trace}; @@ -28,18 +29,16 @@ use super::authorship::{calculate_primary_threshold, check_primary_threshold, se /// BABE verification parameters pub(super) struct VerificationParams<'a, B: 'a + BlockT> { - /// the header being verified. + /// The header being verified. pub(super) header: B::Header, - /// the pre-digest of the header being verified. this is optional - if prior + /// The pre-digest of the header being verified. this is optional - if prior /// verification code had to read it, it can be included here to avoid duplicate /// work. pub(super) pre_digest: Option, - /// the slot number of the current time. + /// The slot number of the current time. pub(super) slot_now: SlotNumber, - /// epoch descriptor of the epoch this block _should_ be under, if it's valid. + /// Epoch descriptor of the epoch this block _should_ be under, if it's valid. pub(super) epoch: &'a Epoch, - /// genesis config of this BABE chain. - pub(super) config: &'a super::Config, } /// Check a header has been signed by the right key. If the slot is too far in @@ -63,7 +62,6 @@ pub(super) fn check_header( pre_digest, slot_now, epoch, - config, } = params; let authorities = &epoch.authorities; @@ -102,13 +100,21 @@ pub(super) fn check_header( primary, sig, &epoch, - config.c, + epoch.config.c, )?; }, - PreDigest::Secondary(secondary) if config.secondary_slots => { - debug!(target: "babe", "Verifying Secondary block"); - - check_secondary_header::( + PreDigest::SecondaryPlain(secondary) if epoch.config.allowed_slots.is_secondary_plain_slots_allowed() => { + debug!(target: "babe", "Verifying Secondary plain block"); + check_secondary_plain_header::( + pre_hash, + secondary, + sig, + &epoch, + )?; + }, + PreDigest::SecondaryVRF(secondary) if epoch.config.allowed_slots.is_secondary_vrf_slots_allowed() => { + debug!(target: "babe", "Verifying Secondary VRF block"); + check_secondary_vrf_header::( pre_hash, secondary, sig, @@ -182,9 +188,9 @@ fn check_primary_header( /// properly signed by the expected authority, which we have a deterministic way /// of computing. Additionally, the weight of this block must stay the same /// compared to its parent since it is a secondary block. -fn check_secondary_header( +fn check_secondary_plain_header( pre_hash: B::Hash, - pre_digest: &SecondaryPreDigest, + pre_digest: &SecondaryPlainPreDigest, signature: AuthoritySignature, epoch: &Epoch, ) -> Result<(), Error> { @@ -208,3 +214,43 @@ fn check_secondary_header( Err(Error::BadSignature(pre_hash)) } } + +/// Check a secondary VRF slot proposal header. +fn check_secondary_vrf_header( + pre_hash: B::Hash, + pre_digest: &SecondaryVRFPreDigest, + signature: AuthoritySignature, + epoch: &Epoch, +) -> Result<(), Error> { + // check the signature is valid under the expected authority and + // chain state. + let expected_author = secondary_slot_author( + pre_digest.slot_number, + &epoch.authorities, + epoch.randomness, + ).ok_or_else(|| Error::NoSecondaryAuthorExpected)?; + + let author = &epoch.authorities[pre_digest.authority_index as usize].0; + + if expected_author != author { + return Err(Error::InvalidAuthor(expected_author.clone(), author.clone())); + } + + if AuthorityPair::verify(&signature, pre_hash.as_ref(), author) { + let transcript = make_transcript( + &epoch.randomness, + pre_digest.slot_number, + epoch.epoch_index, + ); + + schnorrkel::PublicKey::from_bytes(author.as_slice()).and_then(|p| { + p.vrf_verify(transcript, &pre_digest.vrf_output, &pre_digest.vrf_proof) + }).map_err(|s| { + babe_err(Error::VRFVerificationFailed(s)) + })?; + + Ok(()) + } else { + Err(Error::BadSignature(pre_hash)) + } +} diff --git a/client/consensus/epochs/src/lib.rs b/client/consensus/epochs/src/lib.rs index 001c172b34..4ca8502e66 100644 --- a/client/consensus/epochs/src/lib.rs +++ b/client/consensus/epochs/src/lib.rs @@ -335,6 +335,55 @@ impl EpochChanges where self.inner.rebalance() } + /// Map the epoch changes from one storing data to a different one. + pub fn map(self, mut f: F) -> EpochChanges where + B: Epoch, + F: FnMut(&Hash, &Number, E) -> B, + { + EpochChanges { + inner: self.inner.map(&mut |_, _, header| { + match header { + PersistedEpochHeader::Genesis(epoch_0, epoch_1) => { + PersistedEpochHeader::Genesis( + EpochHeader { + start_slot: epoch_0.start_slot, + end_slot: epoch_0.end_slot, + }, + EpochHeader { + start_slot: epoch_1.start_slot, + end_slot: epoch_1.end_slot, + }, + ) + }, + PersistedEpochHeader::Regular(epoch_n) => { + PersistedEpochHeader::Regular( + EpochHeader { + start_slot: epoch_n.start_slot, + end_slot: epoch_n.end_slot, + }, + ) + }, + } + }), + epochs: self.epochs.into_iter().map(|((hash, number), epoch)| { + let bepoch = match epoch { + PersistedEpoch::Genesis(epoch_0, epoch_1) => { + PersistedEpoch::Genesis( + f(&hash, &number, epoch_0), + f(&hash, &number, epoch_1), + ) + }, + PersistedEpoch::Regular(epoch_n) => { + PersistedEpoch::Regular( + f(&hash, &number, epoch_n) + ) + }, + }; + ((hash, number), bepoch) + }).collect(), + } + } + /// Prune out finalized epochs, except for the ancestor of the finalized /// block. The given slot should be the slot number at which the finalized /// block was authored. diff --git a/client/consensus/slots/src/lib.rs b/client/consensus/slots/src/lib.rs index d0f1f6ec4b..1228acc39a 100644 --- a/client/consensus/slots/src/lib.rs +++ b/client/consensus/slots/src/lib.rs @@ -466,7 +466,7 @@ impl SlotDuration { cb(client.runtime_api(), &BlockId::number(Zero::zero()))?; info!( - "Loaded block-time = {:?} milliseconds from genesis on first-launch", + "⏱ Loaded block-time = {:?} milliseconds from genesis on first-launch", genesis_slot_duration ); @@ -483,3 +483,120 @@ impl SlotDuration { self.0.clone() } } + +// Calculate a slot duration lenience based on the number of missed slots from current +/// to parent. If the number of skipped slots is greated than 0 this method will apply +/// an exponential backoff of at most `2^7 * slot_duration`, if no slots were skipped +/// this method will return `None.` +pub fn slot_lenience_exponential(parent_slot: u64, slot_info: &SlotInfo) -> Option { + // never give more than 2^this times the lenience. + const BACKOFF_CAP: u64 = 7; + + // how many slots it takes before we double the lenience. + const BACKOFF_STEP: u64 = 2; + + // we allow a lenience of the number of slots since the head of the + // chain was produced, minus 1 (since there is always a difference of at least 1) + // + // exponential back-off. + // in normal cases we only attempt to issue blocks up to the end of the slot. + // when the chain has been stalled for a few slots, we give more lenience. + let skipped_slots = slot_info.number.saturating_sub(parent_slot + 1); + + if skipped_slots == 0 { + None + } else { + let slot_lenience = skipped_slots / BACKOFF_STEP; + let slot_lenience = std::cmp::min(slot_lenience, BACKOFF_CAP); + let slot_lenience = 1 << slot_lenience; + Some(Duration::from_millis(slot_lenience * slot_info.duration)) + } +} + +/// Calculate a slot duration lenience based on the number of missed slots from current +/// to parent. If the number of skipped slots is greated than 0 this method will apply +/// a linear backoff of at most `20 * slot_duration`, if no slots were skipped +/// this method will return `None.` +pub fn slot_lenience_linear(parent_slot: u64, slot_info: &SlotInfo) -> Option { + // never give more than 20 times more lenience. + const BACKOFF_CAP: u64 = 20; + + // we allow a lenience of the number of slots since the head of the + // chain was produced, minus 1 (since there is always a difference of at least 1) + // + // linear back-off. + // in normal cases we only attempt to issue blocks up to the end of the slot. + // when the chain has been stalled for a few slots, we give more lenience. + let skipped_slots = slot_info.number.saturating_sub(parent_slot + 1); + + if skipped_slots == 0 { + None + } else { + let slot_lenience = std::cmp::min(skipped_slots, BACKOFF_CAP); + Some(Duration::from_millis(slot_lenience * slot_info.duration)) + } +} + +#[cfg(test)] +mod test { + use std::time::{Duration, Instant}; + + const SLOT_DURATION: Duration = Duration::from_millis(6000); + + fn slot(n: u64) -> super::slots::SlotInfo { + super::slots::SlotInfo { + number: n, + last_number: n - 1, + duration: SLOT_DURATION.as_millis() as u64, + timestamp: Default::default(), + inherent_data: Default::default(), + ends_at: Instant::now(), + } + } + + #[test] + fn linear_slot_lenience() { + // if no slots are skipped there should be no lenience + assert_eq!(super::slot_lenience_linear(1, &slot(2)), None); + + // otherwise the lenience is incremented linearly with + // the number of skipped slots. + for n in 3..=22 { + assert_eq!( + super::slot_lenience_linear(1, &slot(n)), + Some(SLOT_DURATION * (n - 2) as u32), + ); + } + + // but we cap it to a maximum of 20 slots + assert_eq!( + super::slot_lenience_linear(1, &slot(23)), + Some(SLOT_DURATION * 20), + ); + } + + #[test] + fn exponential_slot_lenience() { + // if no slots are skipped there should be no lenience + assert_eq!(super::slot_lenience_exponential(1, &slot(2)), None); + + // otherwise the lenience is incremented exponentially every two slots + for n in 3..=17 { + assert_eq!( + super::slot_lenience_exponential(1, &slot(n)), + Some(SLOT_DURATION * 2u32.pow((n / 2 - 1) as u32)), + ); + } + + // but we cap it to a maximum of 14 slots + assert_eq!( + super::slot_lenience_exponential(1, &slot(18)), + Some(SLOT_DURATION * 2u32.pow(7)), + ); + + assert_eq!( + super::slot_lenience_exponential(1, &slot(19)), + Some(SLOT_DURATION * 2u32.pow(7)), + ); + } +} diff --git a/client/rpc/src/state/tests.rs b/client/rpc/src/state/tests.rs index ef3e3824aa..978003d6f9 100644 --- a/client/rpc/src/state/tests.rs +++ b/client/rpc/src/state/tests.rs @@ -439,7 +439,7 @@ fn should_return_runtime_version() { let result = "{\"specName\":\"test\",\"implName\":\"parity-test\",\"authoringVersion\":1,\ \"specVersion\":2,\"implVersion\":2,\"apis\":[[\"0xdf6acb689907609b\",2],\ [\"0x37e397fc7c91f5e4\",1],[\"0xd2bc9897eed08f15\",2],[\"0x40fe3ad401f8959a\",5],\ - [\"0xc6e9a76309f39b09\",1],[\"0xdd718d5cc53262d4\",1],[\"0xcbca25e39f142387\",1],\ + [\"0xc6e9a76309f39b09\",1],[\"0xdd718d5cc53262d4\",1],[\"0xcbca25e39f142387\",2],\ [\"0xf78b278be53f454c\",2],[\"0xab3c0572291feb8b\",1],[\"0xbc9d89904f5b923f\",1]]}"; let runtime_version = api.runtime_version(None.into()).wait().unwrap(); diff --git a/primitives/consensus/babe/src/digests.rs b/primitives/consensus/babe/src/digests.rs index f8ae672f4b..24be9b1b14 100644 --- a/primitives/consensus/babe/src/digests.rs +++ b/primitives/consensus/babe/src/digests.rs @@ -18,7 +18,7 @@ #[cfg(feature = "std")] use super::{BABE_ENGINE_ID, AuthoritySignature}; -use super::{AuthorityId, AuthorityIndex, SlotNumber, BabeAuthorityWeight}; +use super::{AuthorityId, AuthorityIndex, SlotNumber, BabeAuthorityWeight, BabeEpochConfiguration, AllowedSlots}; #[cfg(feature = "std")] use sp_runtime::{DigestItem, generic::OpaqueDigestItemId}; #[cfg(feature = "std")] @@ -45,7 +45,7 @@ pub struct PrimaryPreDigest { /// BABE secondary slot assignment pre-digest. #[derive(Clone, RuntimeDebug, Encode, Decode)] -pub struct SecondaryPreDigest { +pub struct SecondaryPlainPreDigest { /// Authority index /// /// This is not strictly-speaking necessary, since the secondary slots @@ -117,7 +117,7 @@ impl PreDigest { /// Information about the next epoch. This is broadcast in the first block /// of the epoch. -#[derive(Decode, Encode, Default, PartialEq, Eq, Clone, RuntimeDebug)] +#[derive(Decode, Encode, PartialEq, Eq, Clone, RuntimeDebug)] pub struct NextEpochDescriptor { /// The authorities. pub authorities: Vec<(AuthorityId, BabeAuthorityWeight)>, @@ -126,6 +126,29 @@ pub struct NextEpochDescriptor { pub randomness: Randomness, } +/// Information about the next epoch config, if changed. This is broadcast in the first +/// block of the epoch, and applies using the same rules as `NextEpochDescriptor`. +#[derive(Decode, Encode, PartialEq, Eq, Clone, RuntimeDebug)] +pub enum NextConfigDescriptor { + /// Version 1. + #[codec(index = "1")] + V1 { + /// Value of `c` in `BabeEpochConfiguration`. + c: (u64, u64), + /// Value of `allowed_slots` in `BabeEpochConfiguration`. + allowed_slots: AllowedSlots, + } +} + +impl From for BabeEpochConfiguration { + fn from(desc: NextConfigDescriptor) -> Self { + match desc { + NextConfigDescriptor::V1 { c, allowed_slots } => + Self { c, allowed_slots }, + } + } +} + /// A digest item which is usable with BABE consensus. #[cfg(feature = "std")] pub trait CompatibleDigestItem: Sized { @@ -141,8 +164,11 @@ pub trait CompatibleDigestItem: Sized { /// If this item is a BABE signature, return the signature. fn as_babe_seal(&self) -> Option; - /// If this item is a BABE epoch, return it. + /// If this item is a BABE epoch descriptor, return it. fn as_next_epoch_descriptor(&self) -> Option; + + /// If this item is a BABE config descriptor, return it. + fn as_next_config_descriptor(&self) -> Option; } #[cfg(feature = "std")] @@ -172,4 +198,12 @@ impl CompatibleDigestItem for DigestItem where _ => None, }) } + + fn as_next_config_descriptor(&self) -> Option { + self.try_to(OpaqueDigestItemId::Consensus(&BABE_ENGINE_ID)) + .and_then(|x: super::ConsensusLog| match x { + super::ConsensusLog::NextConfigData(n) => Some(n), + _ => None, + }) + } } diff --git a/primitives/consensus/babe/src/lib.rs b/primitives/consensus/babe/src/lib.rs index a5c7e0de3b..5f26349ef9 100644 --- a/primitives/consensus/babe/src/lib.rs +++ b/primitives/consensus/babe/src/lib.rs @@ -30,7 +30,7 @@ pub use merlin::Transcript; use codec::{Encode, Decode}; use sp_std::vec::Vec; use sp_runtime::{ConsensusEngineId, RuntimeDebug}; -use crate::digests::NextEpochDescriptor; +use crate::digests::{NextEpochDescriptor, NextConfigDescriptor}; mod app { use sp_application_crypto::{app_crypto, key_types::BABE, sr25519}; @@ -104,11 +104,15 @@ pub enum ConsensusLog { /// Disable the authority with given index. #[codec(index = "2")] OnDisabled(AuthorityIndex), + /// The epoch has changed, and the epoch after the current one will + /// enact different epoch configurations. + #[codec(index = "3")] + NextConfigData(NextConfigDescriptor), } /// Configuration data used by the BABE consensus engine. #[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)] -pub struct BabeConfiguration { +pub struct BabeGenesisConfigurationV1 { /// The slot duration in milliseconds for BABE. Currently, only /// the value provided by this type at genesis will be used. /// @@ -137,8 +141,78 @@ pub struct BabeConfiguration { pub secondary_slots: bool, } +impl From for BabeGenesisConfiguration { + fn from(v1: BabeGenesisConfigurationV1) -> Self { + Self { + slot_duration: v1.slot_duration, + epoch_length: v1.epoch_length, + c: v1.c, + genesis_authorities: v1.genesis_authorities, + randomness: v1.randomness, + allowed_slots: if v1.secondary_slots { + AllowedSlots::PrimaryAndSecondaryPlainSlots + } else { + AllowedSlots::PrimarySlots + }, + } + } +} + +/// Configuration data used by the BABE consensus engine. +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)] +pub struct BabeGenesisConfiguration { + /// The slot duration in milliseconds for BABE. Currently, only + /// the value provided by this type at genesis will be used. + /// + /// Dynamic slot duration may be supported in the future. + pub slot_duration: u64, + + /// The duration of epochs in slots. + pub epoch_length: SlotNumber, + + /// A constant value that is used in the threshold calculation formula. + /// Expressed as a rational where the first member of the tuple is the + /// numerator and the second is the denominator. The rational should + /// represent a value between 0 and 1. + /// In the threshold formula calculation, `1 - c` represents the probability + /// of a slot being empty. + pub c: (u64, u64), + + /// The authorities for the genesis epoch. + pub genesis_authorities: Vec<(AuthorityId, BabeAuthorityWeight)>, + + /// The randomness for the genesis epoch. + pub randomness: Randomness, + + /// Type of allowed slots. + pub allowed_slots: AllowedSlots, +} + +/// Types of allowed slots. +#[derive(Clone, Copy, PartialEq, Eq, Encode, Decode, RuntimeDebug)] +pub enum AllowedSlots { + /// Only allow primary slots. + PrimarySlots, + /// Allow primary and secondary plain slots. + PrimaryAndSecondaryPlainSlots, + /// Allow primary and secondary VRF slots. + PrimaryAndSecondaryVRFSlots, +} + +impl AllowedSlots { + /// Whether plain secondary slots are allowed. + pub fn is_secondary_plain_slots_allowed(&self) -> bool { + *self == Self::PrimaryAndSecondaryPlainSlots + } + + /// Whether VRF secondary slots are allowed. + pub fn is_secondary_vrf_slots_allowed(&self) -> bool { + *self == Self::PrimaryAndSecondaryVRFSlots + } +} + #[cfg(feature = "std")] -impl sp_consensus::SlotData for BabeConfiguration { +impl sp_consensus::SlotData for BabeGenesisConfiguration { fn slot_duration(&self) -> u64 { self.slot_duration } @@ -146,14 +220,32 @@ impl sp_consensus::SlotData for BabeConfiguration { const SLOT_KEY: &'static [u8] = b"babe_configuration"; } +/// Configuration data used by the BABE consensus engine. +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)] +pub struct BabeEpochConfiguration { + /// A constant value that is used in the threshold calculation formula. + /// Expressed as a rational where the first member of the tuple is the + /// numerator and the second is the denominator. The rational should + /// represent a value between 0 and 1. + /// In the threshold formula calculation, `1 - c` represents the probability + /// of a slot being empty. + pub c: (u64, u64), + + /// Whether this chain should run with secondary slots, which are assigned + /// in round-robin manner. + pub allowed_slots: AllowedSlots, +} + sp_api::decl_runtime_apis! { /// API necessary for block authorship with BABE. + #[api_version(2)] pub trait BabeApi { - /// Return the configuration for BABE. Currently, - /// only the value provided by this type at genesis will be used. - /// - /// Dynamic configuration may be supported in the future. - fn configuration() -> BabeConfiguration; + /// Return the genesis configuration for BABE. The configuration is only read on genesis. + fn configuration() -> BabeGenesisConfiguration; + + /// Return the configuration for BABE. Version 1. + #[changed_in(2)] + fn configuration() -> BabeGenesisConfigurationV1; /// Returns the slot number that started the current epoch. fn current_epoch_start() -> SlotNumber; diff --git a/primitives/core/src/traits.rs b/primitives/core/src/traits.rs index 21a0b77ca7..12a470417a 100644 --- a/primitives/core/src/traits.rs +++ b/primitives/core/src/traits.rs @@ -230,3 +230,11 @@ impl TaskExecutorExt { Self(spawn_handle) } } + +/// Something that can spawn a blocking future. +pub trait SpawnBlocking { + /// Spawn the given blocking future. + /// + /// The given `name` is used to identify the future in tracing. + fn spawn_blocking(&self, name: &'static str, future: futures::future::BoxFuture<'static, ()>); +} diff --git a/test-utils/runtime/src/lib.rs b/test-utils/runtime/src/lib.rs index 3f029bcf29..ef2766c7b8 100644 --- a/test-utils/runtime/src/lib.rs +++ b/test-utils/runtime/src/lib.rs @@ -53,7 +53,7 @@ use cfg_if::cfg_if; use sp_core::storage::ChildType; // Ensure Babe and Aura use the same crypto to simplify things a bit. -pub use sp_consensus_babe::{AuthorityId, SlotNumber}; +pub use sp_consensus_babe::{AuthorityId, SlotNumber, AllowedSlots}; pub type AuraId = sp_consensus_aura::sr25519::AuthorityId; // Include the WASM binary @@ -629,15 +629,15 @@ cfg_if! { } impl sp_consensus_babe::BabeApi for Runtime { - fn configuration() -> sp_consensus_babe::BabeConfiguration { - sp_consensus_babe::BabeConfiguration { + fn configuration() -> sp_consensus_babe::BabeGenesisConfiguration { + sp_consensus_babe::BabeGenesisConfiguration { slot_duration: 1000, epoch_length: EpochDuration::get(), c: (3, 10), genesis_authorities: system::authorities() .into_iter().map(|x|(x, 1)).collect(), randomness: >::randomness(), - secondary_slots: true, + allowed_slots: AllowedSlots::PrimaryAndSecondaryPlainSlots, } } @@ -823,15 +823,15 @@ cfg_if! { } impl sp_consensus_babe::BabeApi for Runtime { - fn configuration() -> sp_consensus_babe::BabeConfiguration { - sp_consensus_babe::BabeConfiguration { + fn configuration() -> sp_consensus_babe::BabeGenesisConfiguration { + sp_consensus_babe::BabeGenesisConfiguration { slot_duration: 1000, epoch_length: EpochDuration::get(), c: (3, 10), genesis_authorities: system::authorities() .into_iter().map(|x|(x, 1)).collect(), randomness: >::randomness(), - secondary_slots: true, + allowed_slots: AllowedSlots::PrimaryAndSecondaryPlainSlots, } } From adf0e3b7ed5bf6bbe721fefdac62f62e25c806e1 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 21 Aug 2020 08:14:25 +1200 Subject: [PATCH 09/13] babe: report equivocations (#6362) commit: 18334ee173f0ec4b62d2ca05c5b0c1f0b05b1b17 [ci skip] --- Cargo.lock | 12 + bin/node/cli/src/service.rs | 1 + bin/node/runtime/src/lib.rs | 25 +++ client/consensus/babe/Cargo.toml | 1 + client/consensus/babe/src/lib.rs | 190 ++++++++++++---- client/consensus/slots/Cargo.toml | 1 + client/consensus/slots/src/aux_schema.rs | 35 +-- frame/babe/src/benchmarking.rs | 108 +++++++++ frame/babe/src/equivocation.rs | 271 +++++++++++++++++++++++ frame/babe/src/lib.rs | 233 +++++++++++++------ primitives/consensus/babe/Cargo.toml | 3 + primitives/consensus/babe/src/digests.rs | 22 +- primitives/consensus/babe/src/lib.rs | 154 ++++++++++++- primitives/consensus/slots/Cargo.toml | 23 ++ primitives/consensus/slots/src/lib.rs | 41 ++++ primitives/core/Cargo.toml | 2 + primitives/core/src/lib.rs | 2 + primitives/core/src/vrf.rs | 99 +++++++++ test-utils/runtime/src/lib.rs | 32 +++ 19 files changed, 1093 insertions(+), 162 deletions(-) create mode 100644 frame/babe/src/benchmarking.rs create mode 100644 frame/babe/src/equivocation.rs create mode 100644 primitives/consensus/slots/Cargo.toml create mode 100644 primitives/consensus/slots/src/lib.rs create mode 100644 primitives/core/src/vrf.rs diff --git a/Cargo.lock b/Cargo.lock index dab16ccff3..bc36dcecc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6234,6 +6234,7 @@ dependencies = [ "sp-api", "sp-blockchain", "sp-consensus", + "sp-consensus-slots", "sp-core", "sp-inherents", "sp-runtime", @@ -7350,7 +7351,9 @@ dependencies = [ "sp-api", "sp-application-crypto", "sp-consensus", + "sp-consensus-slots", "sp-consensus-vrf", + "sp-core", "sp-inherents", "sp-runtime", "sp-std", @@ -7368,6 +7371,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-alpha.5" @@ -7397,6 +7408,7 @@ dependencies = [ "lazy_static", "libsecp256k1", "log 0.4.8", + "merlin", "num-traits", "parity-scale-codec", "parity-util-mem", diff --git a/bin/node/cli/src/service.rs b/bin/node/cli/src/service.rs index 95f028a7e2..8c4e818dd2 100644 --- a/bin/node/cli/src/service.rs +++ b/bin/node/cli/src/service.rs @@ -87,6 +87,7 @@ macro_rules! new_full_start { block_import.clone(), Some(Box::new(justification_import)), None, + select_chain, client, inherent_data_providers.clone(), )?; diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 54504d1731..f61a5d4481 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -52,6 +52,7 @@ use pallet_im_online::sr25519::{AuthorityId as ImOnlineId}; use sp_authority_discovery::AuthorityId as AuthorityDiscoveryId; use pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo; use pallet_contracts_rpc_runtime_api::ContractExecResult; +use pallet_session::{historical as pallet_session_historical}; use frame_system::offchain::TransactionSubmitter; use sp_inherents::{InherentData, CheckInherentsResult}; @@ -664,6 +665,7 @@ construct_runtime!( ImOnline: pallet_im_online::{Module, Call, Storage, Event, ValidateUnsigned, Config}, AuthorityDiscovery: pallet_authority_discovery::{Module, Call, Config}, Offences: pallet_offences::{Module, Call, Storage, Event}, + Historical: pallet_session_historical::{Module}, RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Module, Call, Storage}, Identity: pallet_identity::{Module, Call, Storage, Event}, Society: pallet_society::{Module, Call, Storage, Event, Config}, @@ -787,6 +789,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 { + 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<::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 for Runtime { diff --git a/client/consensus/babe/Cargo.toml b/client/consensus/babe/Cargo.toml index e2c7c6efc7..ddc3f53134 100644 --- a/client/consensus/babe/Cargo.toml +++ b/client/consensus/babe/Cargo.toml @@ -36,6 +36,7 @@ sc-consensus-uncles = { version = "0.8.0-alpha.5", path = "../uncles" } sc-consensus-slots = { version = "0.8.0-alpha.5", path = "../slots" } sp-runtime = { version = "2.0.0-alpha.5", path = "../../../primitives/runtime" } fork-tree = { version = "2.0.0-alpha.5", path = "../../../utils/fork-tree" } +prometheus-endpoint = { package = "substrate-prometheus-endpoint", path = "../../../utils/prometheus" , version = "0.8.0-alpha.5"} futures = "0.3.4" futures-timer = "3.0.1" parking_lot = "0.10.0" diff --git a/client/consensus/babe/src/lib.rs b/client/consensus/babe/src/lib.rs index 028fe8e5e3..69be45423c 100644 --- a/client/consensus/babe/src/lib.rs +++ b/client/consensus/babe/src/lib.rs @@ -76,13 +76,14 @@ pub use sp_consensus_babe::{ pub use sp_consensus::SyncOracle; use std::{ collections::HashMap, sync::Arc, u64, pin::Pin, time::{Instant, Duration}, - any::Any, borrow::Cow + any::Any, borrow::Cow, convert::TryInto, }; -use sp_consensus_babe; use sp_consensus::{ImportResult, CanAuthorWith}; use sp_consensus::import_queue::{ BoxJustificationImport, BoxFinalityProofImport, }; +use sp_core::{crypto::Public, traits::BareCryptoStore}; +use sp_application_crypto::AppKey; use sp_runtime::{ generic::{BlockId, OpaqueDigestItemId}, Justification, traits::{Block as BlockT, Header, DigestItemFor, Zero}, @@ -90,7 +91,6 @@ use sp_runtime::{ use sp_api::{ProvideRuntimeApi, NumberFor}; use sc_keystore::KeyStorePtr; use parking_lot::Mutex; -use sp_core::Pair; use sp_inherents::{InherentDataProviders, InherentData}; use sc_telemetry::{telemetry, CONSENSUS_TRACE, CONSENSUS_DEBUG}; use sp_consensus::{ @@ -109,6 +109,7 @@ use sp_block_builder::BlockBuilder as BlockBuilderApi; use futures::prelude::*; use log::{debug, info, log, trace, warn}; +use prometheus_endpoint::Registry; use sc_consensus_slots::{ SlotWorker, SlotInfo, SlotCompatible, StorageChanges, CheckedHeader, check_equivocation, }; @@ -186,7 +187,7 @@ impl Epoch { start_slot: slot_number, duration: genesis_config.epoch_length, authorities: genesis_config.genesis_authorities.clone(), - randomness: genesis_config.randomness.clone(), + randomness: genesis_config.randomness, config: BabeEpochConfiguration { c: genesis_config.c, allowed_slots: genesis_config.allowed_slots, @@ -399,7 +400,7 @@ pub fn start_babe(BabeParams { register_babe_inherent_data_provider(&inherent_data_providers, config.slot_duration())?; sc_consensus_uncles::register_uncles_inherent_data_provider( - client.clone(), + client, select_chain.clone(), &inherent_data_providers, )?; @@ -441,7 +442,7 @@ impl sc_consensus_slots::SimpleSlotWorker for BabeWork Error: std::error::Error + Send + From + From + 'static, { type EpochData = ViableEpochDescriptor, Epoch>; - type Claim = (PreDigest, AuthorityPair); + type Claim = (PreDigest, AuthorityId); type SyncOracle = SO; type CreateProposer = Pin> + Send + 'static @@ -494,7 +495,7 @@ impl sc_consensus_slots::SimpleSlotWorker for BabeWork &self.keystore, ); - if let Some(_) = s { + if s.is_some() { debug!(target: "babe", "Claimed slot {}", slot_number); } @@ -518,12 +519,30 @@ impl sc_consensus_slots::SimpleSlotWorker for BabeWork StorageChanges, Self::Claim, Self::EpochData, - ) -> sp_consensus::BlockImportParams + Send> { - Box::new(|header, header_hash, body, storage_changes, (_, pair), epoch_descriptor| { + ) -> Result< + sp_consensus::BlockImportParams, + sp_consensus::Error> + Send + 'static> + { + let keystore = self.keystore.clone(); + Box::new(move |header, header_hash, body, storage_changes, (_, public), epoch_descriptor| { // sign the pre-sealed hash of the block and then // add it to a digest item. - let signature = pair.sign(header_hash.as_ref()); - let digest_item = as CompatibleDigestItem>::babe_seal(signature); + let public_type_pair = public.clone().into(); + let public = public.to_raw_vec(); + let signature = keystore.read() + .sign_with( + ::ID, + &public_type_pair, + header_hash.as_ref() + ) + .map_err(|e| sp_consensus::Error::CannotSign( + public.clone(), e.to_string(), + ))?; + let signature: AuthoritySignature = signature.clone().try_into() + .map_err(|_| sp_consensus::Error::InvalidSignature( + signature, public + ))?; + let digest_item = as CompatibleDigestItem>::babe_seal(signature.into()); let mut import_block = BlockImportParams::new(BlockOrigin::Own, header); import_block.post_digests.push(digest_item); @@ -534,7 +553,7 @@ impl sc_consensus_slots::SimpleSlotWorker for BabeWork Box::new(BabeIntermediate:: { epoch_descriptor }) as Box, ); - import_block + Ok(import_block) }) } @@ -701,27 +720,29 @@ impl BabeLink { } /// A verifier for Babe blocks. -pub struct BabeVerifier { +pub struct BabeVerifier { client: Arc, + select_chain: SelectChain, inherent_data_providers: sp_inherents::InherentDataProviders, config: Config, epoch_changes: SharedEpochChanges, time_source: TimeSource, } -impl BabeVerifier - where - Block: BlockT, - Client: HeaderBackend + HeaderMetadata + ProvideRuntimeApi, - Client::Api: BlockBuilderApi, +impl BabeVerifier +where + Block: BlockT, + Client: AuxStore + HeaderBackend + HeaderMetadata + ProvideRuntimeApi, + Client::Api: BlockBuilderApi + + BabeApi, + SelectChain: sp_consensus::SelectChain, { fn check_inherents( &self, block: Block, block_id: BlockId, inherent_data: InherentData, - ) -> Result<(), Error> - { + ) -> Result<(), Error> { let inherent_res = self.client.runtime_api().check_inherents( &block_id, block, @@ -738,13 +759,95 @@ impl BabeVerifier Ok(()) } } + + fn check_and_report_equivocation( + &self, + slot_now: SlotNumber, + slot: SlotNumber, + header: &Block::Header, + author: &AuthorityId, + origin: &BlockOrigin, + ) -> Result<(), Error> { + // 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| { + 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 Verifier for BabeVerifier where +impl Verifier for BabeVerifier +where Block: BlockT, Client: HeaderMetadata + HeaderBackend + ProvideRuntimeApi - + Send + Sync + AuxStore + ProvideCache, + + Send + Sync + AuxStore + ProvideCache, Client::Api: BlockBuilderApi + BabeApi, + SelectChain: sp_consensus::SelectChain, { fn verify( &mut self, @@ -796,7 +899,7 @@ impl Verifier for BabeVerifier where // FIXME #1019 in the future, alter this queue to allow deferring of headers let v_params = verification::VerificationParams { header: header.clone(), - pre_digest: Some(pre_digest.clone()), + pre_digest: Some(pre_digest), slot_now: slot_now + 1, epoch: viable_epoch.as_ref(), }; @@ -805,28 +908,18 @@ impl Verifier for BabeVerifier 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 @@ -952,7 +1045,7 @@ impl BlockImport for BabeBlockImport>, ) -> Result { let hash = block.post_hash(); - let number = block.header.number().clone(); + let number = *block.header.number(); // early exit if block already in chain, otherwise the check for // epoch changes will error when trying to re-import an epoch change @@ -1133,7 +1226,7 @@ impl BlockImport for BabeBlockImport BlockImport for BabeBlockImport BlockImport for BabeBlockImport( /// /// 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( +pub fn import_queue( babe_link: BabeLink, block_import: Inner, justification_import: Option>, finality_proof_import: Option>, client: Arc, + select_chain: SelectChain, inherent_data_providers: InherentDataProviders, + registry: Option<&Registry>, ) -> ClientResult>> where Inner: BlockImport> + Send + Sync + 'static, Client: ProvideRuntimeApi + ProvideCache + Send + Sync + AuxStore + 'static, Client: HeaderBackend + HeaderMetadata, Client::Api: BlockBuilderApi + BabeApi + ApiExt, + SelectChain: sp_consensus::SelectChain + 'static, { register_babe_inherent_data_provider(&inherent_data_providers, babe_link.config.slot_duration)?; let verifier = BabeVerifier { - client: client.clone(), + client, + select_chain, inherent_data_providers, config: babe_link.config, epoch_changes: babe_link.epoch_changes, @@ -1294,6 +1391,7 @@ pub fn import_queue( Box::new(block_import), justification_import, finality_proof_import, + registry, )) } diff --git a/client/consensus/slots/Cargo.toml b/client/consensus/slots/Cargo.toml index 4e388cdec0..bce01e4174 100644 --- a/client/consensus/slots/Cargo.toml +++ b/client/consensus/slots/Cargo.toml @@ -14,6 +14,7 @@ codec = { package = "parity-scale-codec", version = "1.3.0" } sc-client-api = { version = "2.0.0-alpha.5", path = "../../api" } sp-core = { version = "2.0.0-alpha.5", path = "../../../primitives/core" } sp-blockchain = { version = "2.0.0-alpha.5", path = "../../../primitives/blockchain" } +sp-consensus-slots = { version = "0.8.0-alpha.5", path = "../../../primitives/consensus/slots" } sp-runtime = { version = "2.0.0-alpha.5", path = "../../../primitives/runtime" } sp-state-machine = { version = "0.8.0-alpha.5", path = "../../../primitives/state-machine" } sp-api = { version = "2.0.0-alpha.5", path = "../../../primitives/api" } diff --git a/client/consensus/slots/src/aux_schema.rs b/client/consensus/slots/src/aux_schema.rs index d54190ca07..1f1fe37068 100644 --- a/client/consensus/slots/src/aux_schema.rs +++ b/client/consensus/slots/src/aux_schema.rs @@ -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(backend: &C, key: &[u8]) -> ClientResult> } } -/// Represents an equivocation proof. -#[derive(Debug, Clone)] -pub struct EquivocationProof { - slot: u64, - fst_header: H, - snd_header: H, -} - -impl EquivocationProof { - /// 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( slot: u64, header: &H, signer: &P, -) -> ClientResult>> +) -> ClientResult>> where H: Header, C: AuxStore, @@ -114,9 +90,10 @@ pub fn check_equivocation( // 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, diff --git a/frame/babe/src/benchmarking.rs b/frame/babe/src/benchmarking.rs new file mode 100644 index 0000000000..e168c1b93b --- /dev/null +++ b/frame/babe/src/benchmarking.rs @@ -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; + +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
= + Decode::decode(&mut &EQUIVOCATION_PROOF_BLOB[..]).unwrap(); + + let equivocation_proof2 = equivocation_proof1.clone(); + }: { + sp_consensus_babe::check_equivocation_proof::
(equivocation_proof1); + } verify { + assert!(sp_consensus_babe::check_equivocation_proof::
(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] + 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() + ); + }); + } +} diff --git a/frame/babe/src/equivocation.rs b/frame/babe/src/equivocation.rs new file mode 100644 index 0000000000..85ab6e6592 --- /dev/null +++ b/frame/babe/src/equivocation.rs @@ -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 { + /// Report an offence proved by the given reporters. + fn report_offence( + reporters: Vec, + offence: BabeEquivocationOffence, + ) -> 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, + key_owner_proof: T::KeyOwnerProof, + ) -> DispatchResult; + + /// Fetch the current block author id, if defined. + fn block_author() -> Option; +} + +impl HandleEquivocation for () { + fn report_offence( + _reporters: Vec, + _offence: BabeEquivocationOffence, + ) -> Result<(), OffenceError> { + Ok(()) + } + + fn is_known_offence(_offenders: &[T::KeyOwnerIdentification], _time_slot: &SlotNumber) -> bool { + true + } + + fn submit_unsigned_equivocation_report( + _equivocation_proof: EquivocationProof, + _key_owner_proof: T::KeyOwnerProof, + ) -> DispatchResult { + Ok(()) + } + + fn block_author() -> Option { + 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 { + _phantom: sp_std::marker::PhantomData<(I, R)>, +} + +impl Default for EquivocationHandler { + fn default() -> Self { + Self { + _phantom: Default::default(), + } + } +} + +impl HandleEquivocation for EquivocationHandler +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>, + // A system for reporting offences after valid equivocation reports are + // processed. + R: ReportOffence< + T::AccountId, + T::KeyOwnerIdentification, + BabeEquivocationOffence, + >, +{ + fn report_offence( + reporters: Vec, + offence: BabeEquivocationOffence, + ) -> 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, + key_owner_proof: T::KeyOwnerProof, + ) -> DispatchResult { + use frame_system::offchain::SubmitTransaction;; + + let call = Call::report_equivocation_unsigned(equivocation_proof, key_owner_proof); + + match SubmitTransaction::>::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 { + Some(>::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 frame_support::unsigned::ValidateUnsigned for Module { + type Call = Call; + 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 { + /// 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 Offence + for BabeEquivocationOffence +{ + const ID: Kind = *b"babe:equivocatio"; + type TimeSlot = SlotNumber; + + fn offenders(&self) -> Vec { + 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() + } +} diff --git a/frame/babe/src/lib.rs b/frame/babe/src/lib.rs index dabe07b45f..bdb1d03426 100644 --- a/frame/babe/src/lib.rs +++ b/frame/babe/src/lib.rs @@ -19,39 +19,46 @@ //! from VRF outputs and manages epoch transitions. #![cfg_attr(not(feature = "std"), no_std)] -#![forbid(unused_must_use, unsafe_code, unused_variables, unused_must_use)] +#![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}, - weights::{Weight, SimpleDispatchInfo, WeighData}, -}; -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}, + decl_error, decl_module, decl_storage, + traits::{FindAuthor, Get, KeyOwnerProofSystem, Randomness as RandomnessT}, + weights::Weight, + Parameter, }; +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, 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}; -#[cfg(all(feature = "std", test))] -mod tests; +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; + +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; } /// Trigger an epoch change, if any should take place. @@ -106,6 +137,17 @@ const UNDER_CONSTRUCTION_SEGMENT_LENGTH: usize = 256; type MaybeRandomness = Option; +decl_error! { + pub enum Error for Module { + /// 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 as Babe { /// Current epoch index. @@ -205,6 +247,46 @@ decl_module! { // remove temporary "environment" entry from storage Lateness::::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. + fn report_equivocation( + origin, + equivocation_proof: EquivocationProof, + 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. + fn report_equivocation_unsigned( + origin, + equivocation_proof: EquivocationProof, + key_owner_proof: T::KeyOwnerProof, + ) { + ensure_none(origin)?; + + Self::do_report_equivocation( + T::HandleEquivocation::block_author(), + equivocation_proof, + key_owner_proof, + )?; + } } } @@ -271,52 +353,6 @@ impl pallet_session::ShouldEndSession for Module { } } -// TODO [slashing]: @marcio use this, remove the dead_code annotation. -/// A BABE equivocation offence report. -/// -/// When a validator released two or more blocks at the same slot. -struct BabeEquivocationOffence { - /// A babe slot number in which this incident happened. - slot: u64, - /// The session index in which the incident happened. - session_index: SessionIndex, - /// The size of the validator set at the time of the offence. - validator_set_count: u32, - /// The authority that produced the equivocation. - offender: FullIdentification, -} - -impl Offence for BabeEquivocationOffence { - const ID: Kind = *b"babe:equivocatio"; - type TimeSlot = u64; - - fn offenders(&self) -> Vec { - 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 Module { /// Determine the BABE slot duration based on the Timestamp module configuration. pub fn slot_duration() -> T::Moment { @@ -544,6 +580,69 @@ impl Module { Authorities::put(authorities); } } + + fn do_report_equivocation( + reporter: Option, + equivocation_proof: EquivocationProof, + key_owner_proof: T::KeyOwnerProof, + ) -> Result<(), Error> { + 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::(); + + // 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, + key_owner_proof: T::KeyOwnerProof, + ) -> Option<()> { + T::HandleEquivocation::submit_unsigned_equivocation_report( + equivocation_proof, + key_owner_proof, + ) + .ok() + } } impl OnTimestampSet for Module { diff --git a/primitives/consensus/babe/Cargo.toml b/primitives/consensus/babe/Cargo.toml index cd41aa1251..b64000de6d 100644 --- a/primitives/consensus/babe/Cargo.toml +++ b/primitives/consensus/babe/Cargo.toml @@ -15,7 +15,9 @@ merlin = { version = "2.0", default-features = false } sp-std = { version = "2.0.0-alpha.5", default-features = false, path = "../../std" } sp-api = { version = "2.0.0-alpha.5", default-features = false, path = "../../api" } sp-consensus = { version = "0.8.0-alpha.5", optional = true, path = "../common" } +sp-consensus-slots = { version = "0.8.0-rc4", path = "../slots", default-features = false } sp-consensus-vrf = { version = "0.8.0-alpha.5", path = "../vrf", default-features = false } +sp-core = { version = "2.0.0-alpha.5", default-features = false, path = "../../core" } sp-inherents = { version = "2.0.0-alpha.5", default-features = false, path = "../../inherents" } sp-runtime = { version = "2.0.0-alpha.5", default-features = false, path = "../../runtime" } sp-timestamp = { version = "2.0.0-alpha.5", default-features = false, path = "../../timestamp" } @@ -29,6 +31,7 @@ std = [ "sp-std/std", "sp-api/std", "sp-consensus", + "sp-consensus-slots/std", "sp-consensus-vrf/std", "sp-inherents/std", "sp-runtime/std", diff --git a/primitives/consensus/babe/src/digests.rs b/primitives/consensus/babe/src/digests.rs index 24be9b1b14..3177d0412a 100644 --- a/primitives/consensus/babe/src/digests.rs +++ b/primitives/consensus/babe/src/digests.rs @@ -16,18 +16,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. @@ -150,7 +146,6 @@ impl From 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; @@ -171,9 +166,8 @@ pub trait CompatibleDigestItem: Sized { fn as_next_config_descriptor(&self) -> Option; } -#[cfg(feature = "std")] impl CompatibleDigestItem for DigestItem 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()) diff --git a/primitives/consensus/babe/src/lib.rs b/primitives/consensus/babe/src/lib.rs index 5f26349ef9..c31ad1a958 100644 --- a/primitives/consensus/babe/src/lib.rs +++ b/primitives/consensus/babe/src/lib.rs @@ -22,15 +22,21 @@ pub mod digests; pub mod inherents; +pub use merlin::Transcript; pub use sp_consensus_vrf::schnorrkel::{ - Randomness, VRF_PROOF_LENGTH, VRF_OUTPUT_LENGTH, RANDOMNESS_LENGTH + Randomness, RANDOMNESS_LENGTH, VRF_OUTPUT_LENGTH, VRF_PROOF_LENGTH, }; -pub use merlin::Transcript; -use codec::{Encode, Decode}; +use codec::{Decode, Encode}; +#[cfg(feature = "std")] +use sp_core::vrf::{VRFTranscriptData, VRFTranscriptValue}; +use sp_runtime::{traits::Header, ConsensusEngineId, RuntimeDebug}; use sp_std::vec::Vec; -use sp_runtime::{ConsensusEngineId, RuntimeDebug}; -use crate::digests::{NextEpochDescriptor, NextConfigDescriptor}; + +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}; @@ -70,7 +76,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 = sp_consensus_slots::EquivocationProof; /// The weight of an authority. // NOTE: we use a unique name for the weight to avoid conflicts with other @@ -93,6 +102,23 @@ pub fn make_transcript( transcript } +/// Make a VRF transcript data container +#[cfg(feature = "std")] +pub fn make_transcript_data( + randomness: &Randomness, + slot_number: u64, + epoch: u64, +) -> VRFTranscriptData { + VRFTranscriptData { + label: &BABE_ENGINE_ID, + items: vec![ + ("slot number", VRFTranscriptValue::U64(slot_number)), + ("current epoch", VRFTranscriptValue::U64(epoch)), + ("chain randomness", VRFTranscriptValue::Bytes(&randomness[..])), + ] + } +} + /// An consensus log item for BABE. #[derive(Decode, Encode, Clone, PartialEq, Eq)] pub enum ConsensusLog { @@ -236,6 +262,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(proof: EquivocationProof) -> 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); +impl OpaqueKeyOwnershipProof { + /// Create a new `OpaqueKeyOwnershipProof` using the given encoded + /// representation. + pub fn new(inner: Vec) -> OpaqueKeyOwnershipProof { + OpaqueKeyOwnershipProof(inner) + } + + /// Try to decode this `OpaqueKeyOwnershipProof` into the given concrete key + /// ownership proof type. + pub fn decode(self) -> Option { + Decode::decode(&mut &self.0[..]).ok() + } +} + sp_api::decl_runtime_apis! { /// API necessary for block authorship with BABE. #[api_version(2)] @@ -249,5 +362,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; + + /// 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, + key_owner_proof: OpaqueKeyOwnershipProof, + ) -> Option<()>; } } diff --git a/primitives/consensus/slots/Cargo.toml b/primitives/consensus/slots/Cargo.toml new file mode 100644 index 0000000000..bb1e76baa7 --- /dev/null +++ b/primitives/consensus/slots/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "sp-consensus-slots" +version = "0.8.0-rc4" +authors = ["Parity Technologies "] +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-alpha.5", default-features = false, path = "../../runtime" } + +[features] +default = ["std"] +std = [ + "codec/std", + "sp-runtime/std", +] diff --git a/primitives/consensus/slots/src/lib.rs b/primitives/consensus/slots/src/lib.rs new file mode 100644 index 0000000000..f898cf9da6 --- /dev/null +++ b/primitives/consensus/slots/src/lib.rs @@ -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 { + /// 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, +} diff --git a/primitives/core/Cargo.toml b/primitives/core/Cargo.toml index 4dbbf85e17..1d34e1427f 100644 --- a/primitives/core/Cargo.toml +++ b/primitives/core/Cargo.toml @@ -44,6 +44,7 @@ sha2 = { version = "0.8.0", default-features = false, optional = true } hex = { version = "0.4", default-features = false, optional = true } twox-hash = { version = "1.5.0", default-features = false, optional = true } libsecp256k1 = { version = "0.3.2", default-features = false, features = ["hmac"], optional = true } +merlin = { version = "2.0", default-features = false, optional = true } sp-runtime-interface = { version = "2.0.0-alpha.5", default-features = false, path = "../runtime-interface" } @@ -118,4 +119,5 @@ full_crypto = [ "twox-hash", "libsecp256k1", "sp-runtime-interface/disable_target_static_assertions", + "merlin", ] diff --git a/primitives/core/src/lib.rs b/primitives/core/src/lib.rs index 8d5ad7daae..e1060ebee7 100644 --- a/primitives/core/src/lib.rs +++ b/primitives/core/src/lib.rs @@ -72,6 +72,8 @@ pub mod traits; pub mod testing; #[cfg(feature = "std")] pub mod tasks; +#[cfg(feature = "std")] +pub mod vrf; pub use self::hash::{H160, H256, H512, convert_hash}; pub use self::uint::{U256, U512}; diff --git a/primitives/core/src/vrf.rs b/primitives/core/src/vrf.rs new file mode 100644 index 0000000000..d392587cb7 --- /dev/null +++ b/primitives/core/src/vrf.rs @@ -0,0 +1,99 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-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. + +//! VRF-specifc data types and helpers + +use codec::Encode; +use merlin::Transcript; +use schnorrkel::vrf::{VRFOutput, VRFProof}; +/// An enum whose variants represent possible +/// accepted values to construct the VRF transcript +#[derive(Clone, Encode)] +pub enum VRFTranscriptValue<'a> { + /// Value is an array of bytes + Bytes(&'a [u8]), + /// Value is a u64 integer + U64(u64), +} +/// VRF Transcript data +#[derive(Clone, Encode)] +pub struct VRFTranscriptData<'a> { + /// The transcript's label + pub label: &'static [u8], + /// Additional data to be registered into the transcript + pub items: Vec<(&'static str, VRFTranscriptValue<'a>)>, +} +/// VRF signature data +pub struct VRFSignature { + /// The VRFOutput serialized + pub output: VRFOutput, + /// The calculated VRFProof + pub proof: VRFProof, +} + +/// Construct a `Transcript` object from data. +/// +/// Returns `merlin::Transcript` +pub fn make_transcript(data: VRFTranscriptData) -> Transcript { + let mut transcript = Transcript::new(data.label); + for (label, value) in data.items.into_iter() { + match value { + VRFTranscriptValue::Bytes(bytes) => { + transcript.append_message(label.as_bytes(), &bytes); + }, + VRFTranscriptValue::U64(val) => { + transcript.append_u64(label.as_bytes(), val); + } + } + } + transcript +} + + +#[cfg(test)] +mod tests { + use super::*; + use crate::vrf::VRFTranscriptValue; + use rand::RngCore; + use rand_chacha::{ + rand_core::SeedableRng, + ChaChaRng, + }; + + #[test] + fn transcript_creation_matches() { + let mut orig_transcript = Transcript::new(b"My label"); + orig_transcript.append_u64(b"one", 1); + orig_transcript.append_message(b"two", "test".as_bytes()); + + let new_transcript = make_transcript(VRFTranscriptData { + label: b"My label", + items: vec![ + ("one", VRFTranscriptValue::U64(1)), + ("two", VRFTranscriptValue::Bytes("test".as_bytes())), + ], + }); + let test = |t: Transcript| -> [u8; 16] { + let mut b = [0u8; 16]; + t.build_rng() + .finalize(&mut ChaChaRng::from_seed([0u8;32])) + .fill_bytes(&mut b); + b + }; + debug_assert!(test(orig_transcript) == test(new_transcript)); + } +} diff --git a/test-utils/runtime/src/lib.rs b/test-utils/runtime/src/lib.rs index ef2766c7b8..6009448a1f 100644 --- a/test-utils/runtime/src/lib.rs +++ b/test-utils/runtime/src/lib.rs @@ -644,6 +644,22 @@ cfg_if! { fn current_epoch_start() -> SlotNumber { >::current_epoch_start() } + + fn submit_report_equivocation_unsigned_extrinsic( + _equivocation_proof: sp_consensus_babe::EquivocationProof< + ::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 { + None + } } impl sp_offchain::OffchainWorkerApi for Runtime { @@ -838,6 +854,22 @@ cfg_if! { fn current_epoch_start() -> SlotNumber { >::current_epoch_start() } + + fn submit_report_equivocation_unsigned_extrinsic( + _equivocation_proof: sp_consensus_babe::EquivocationProof< + ::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 { + None + } } impl sp_offchain::OffchainWorkerApi for Runtime { From a9ad1f39ca4bb35d037771bb071ceb21b99caf31 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 21 Aug 2020 20:53:43 +1200 Subject: [PATCH 10/13] Add changes for #6362 to compile & run tests --- Cargo.lock | 16 + bin/node-template/node/src/service.rs | 22 +- bin/node/cli/src/service.rs | 33 +- bin/node/runtime/src/lib.rs | 26 +- client/consensus/aura/Cargo.toml | 1 + client/consensus/aura/src/lib.rs | 7 +- client/consensus/babe/Cargo.toml | 2 +- client/consensus/babe/src/lib.rs | 37 +- client/consensus/babe/src/tests.rs | 12 +- client/consensus/manual-seal/src/lib.rs | 1 + client/consensus/pow/src/lib.rs | 3 +- client/consensus/slots/src/lib.rs | 15 +- client/keystore/Cargo.toml | 3 +- client/network/test/src/block_import.rs | 2 +- client/network/test/src/lib.rs | 2 + client/service/src/builder.rs | 16 +- frame/babe/Cargo.toml | 52 ++- frame/babe/src/equivocation.rs | 14 +- frame/babe/src/lib.rs | 8 +- frame/babe/src/mock.rs | 299 ++++++++++++- frame/babe/src/tests.rs | 416 +++++++++++++++++- frame/im-online/src/mock.rs | 4 + frame/offences/src/lib.rs | 9 + frame/session/Cargo.toml | 2 + frame/session/src/historical.rs | 70 +-- frame/staking/src/lib.rs | 4 + frame/support/src/lib.rs | 6 +- frame/support/src/traits.rs | 15 + frame/system/src/offchain.rs | 10 + primitives/consensus/common/Cargo.toml | 2 + .../consensus/common/src/import_queue.rs | 42 +- .../common/src/import_queue/basic_queue.rs | 45 +- primitives/consensus/common/src/lib.rs | 1 + primitives/consensus/common/src/metrics.rs | 80 ++++ primitives/core/Cargo.toml | 1 + primitives/core/src/lib.rs | 5 + .../runtime/src/transaction_validity.rs | 131 ++++++ primitives/session/Cargo.toml | 13 +- primitives/session/src/lib.rs | 61 ++- primitives/staking/src/offence.rs | 13 +- test-utils/runtime/src/lib.rs | 20 +- utils/prometheus/src/lib.rs | 3 + 42 files changed, 1342 insertions(+), 182 deletions(-) create mode 100644 primitives/consensus/common/src/metrics.rs diff --git a/Cargo.lock b/Cargo.lock index bc36dcecc0..b40169a25c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4067,9 +4067,15 @@ dependencies = [ name = "pallet-babe" version = "2.0.0-alpha.5" 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", @@ -4080,6 +4086,7 @@ dependencies = [ "sp-inherents", "sp-io", "sp-runtime", + "sp-session", "sp-staking", "sp-std", "sp-timestamp", @@ -4517,6 +4524,7 @@ dependencies = [ "sp-core", "sp-io", "sp-runtime", + "sp-session", "sp-staking", "sp-std", "sp-trie", @@ -6080,6 +6088,7 @@ dependencies = [ "sp-runtime", "sp-timestamp", "sp-version", + "substrate-prometheus-endpoint", "substrate-test-runtime-client", "tempfile", ] @@ -6130,6 +6139,7 @@ dependencies = [ "sp-runtime", "sp-timestamp", "sp-version", + "substrate-prometheus-endpoint", "substrate-test-runtime-client", "tempfile", ] @@ -6401,6 +6411,7 @@ version = "2.0.0-alpha.5" dependencies = [ "derive_more", "hex", + "merlin", "parking_lot 0.10.0", "rand 0.7.3", "serde_json", @@ -7327,6 +7338,8 @@ dependencies = [ "sp-std", "sp-test-primitives", "sp-version", + "substrate-prometheus-endpoint", + "wasm-timer", ] [[package]] @@ -7416,6 +7429,7 @@ dependencies = [ "pretty_assertions", "primitive-types", "rand 0.7.3", + "rand_chacha 0.2.2", "regex", "schnorrkel", "serde", @@ -7663,9 +7677,11 @@ dependencies = [ name = "sp-session" version = "2.0.0-alpha.5" dependencies = [ + "parity-scale-codec", "sp-api", "sp-core", "sp-runtime", + "sp-staking", "sp-std", ] diff --git a/bin/node-template/node/src/service.rs b/bin/node-template/node/src/service.rs index 22102c61d8..4393fc0b23 100644 --- a/bin/node-template/node/src/service.rs +++ b/bin/node-template/node/src/service.rs @@ -37,7 +37,7 @@ macro_rules! new_full_start { })? .with_transaction_pool(|builder| { let pool_api = sc_transaction_pool::FullChainApi::new( - builder.client().clone() + builder.client().clone(), ); Ok(sc_transaction_pool::BasicPool::new( builder.config().transaction_pool.clone(), @@ -45,7 +45,13 @@ macro_rules! new_full_start { builder.prometheus_registry(), )) })? - .with_import_queue(|_config, client, mut select_chain, _transaction_pool| { + .with_import_queue(| + _config, + client, + mut select_chain, + _transaction_pool, + registry, + | { let select_chain = select_chain.take() .ok_or_else(|| sc_service::Error::SelectChainRequired)?; @@ -63,6 +69,7 @@ macro_rules! new_full_start { None, client, inherent_data_providers.clone(), + registry, )?; import_setup = Some((grandpa_block_import, grandpa_link)); @@ -209,7 +216,15 @@ pub fn new_light(config: Configuration) ); Ok(pool) })? - .with_import_queue_and_fprb(|_config, client, backend, fetcher, _select_chain, _tx_pool| { + .with_import_queue_and_fprb(| + _config, + client, + backend, + fetcher, + _select_chain, + _tx_pool, + prometheus_registry, + | { let fetch_checker = fetcher .map(|fetcher| fetcher.checker().clone()) .ok_or_else(|| "Trying to start light import queue without active fetch checker")?; @@ -230,6 +245,7 @@ pub fn new_light(config: Configuration) Some(Box::new(finality_proof_import)), client, inherent_data_providers.clone(), + prometheus_registry, )?; Ok((import_queue, finality_proof_request_builder)) diff --git a/bin/node/cli/src/service.rs b/bin/node/cli/src/service.rs index 8c4e818dd2..3987645b9c 100644 --- a/bin/node/cli/src/service.rs +++ b/bin/node/cli/src/service.rs @@ -58,7 +58,7 @@ macro_rules! new_full_start { })? .with_transaction_pool(|builder| { let pool_api = sc_transaction_pool::FullChainApi::new( - builder.client().clone() + builder.client().clone(), ); Ok(sc_transaction_pool::BasicPool::new( builder.config().transaction_pool.clone(), @@ -66,13 +66,19 @@ macro_rules! new_full_start { builder.prometheus_registry(), )) })? - .with_import_queue(|_config, client, mut select_chain, _transaction_pool| { + .with_import_queue(| + _config, + client, + mut select_chain, + _transaction_pool, + prometheus_registry, + | { let select_chain = select_chain.take() .ok_or_else(|| sc_service::Error::SelectChainRequired)?; 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(); @@ -87,9 +93,10 @@ macro_rules! new_full_start { block_import.clone(), Some(Box::new(justification_import)), None, - select_chain, client, + select_chain, inherent_data_providers.clone(), + prometheus_registry, )?; import_setup = Some((block_import, grandpa_link, babe_link)); @@ -313,7 +320,7 @@ pub fn new_light(config: Configuration) .ok_or_else(|| "Trying to start light transaction pool without active fetcher")?; let pool_api = sc_transaction_pool::LightChainApi::new( builder.client().clone(), - fetcher.clone() + fetcher, ); let pool = sc_transaction_pool::BasicPool::with_revalidation_type( builder.config().transaction_pool.clone(), @@ -323,10 +330,22 @@ pub fn new_light(config: Configuration) ); Ok(pool) })? - .with_import_queue_and_fprb(|_config, client, backend, fetcher, _select_chain, _tx_pool| { + .with_import_queue_and_fprb(| + _config, + client, + backend, + fetcher, + mut select_chain, + _tx_pool, + 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, @@ -350,7 +369,9 @@ pub fn new_light(config: Configuration) None, Some(Box::new(finality_proof_import)), client.clone(), + select_chain, inherent_data_providers.clone(), + registry, )?; Ok((import_queue, finality_proof_request_builder)) diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index f61a5d4481..046c8b62c9 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -23,10 +23,14 @@ use sp_std::prelude::*; use frame_support::{ construct_runtime, debug, parameter_types, - traits::{Currency, Imbalance, OnUnbalanced, Randomness}, + traits::{Currency, Imbalance, KeyOwnerProofSystem, OnUnbalanced, Randomness}, weights::Weight, }; -use sp_core::u32_trait::{_1, _2, _3, _4}; +use sp_core::{ + crypto::KeyTypeId, + u32_trait::{_1, _2, _3, _4}, + OpaqueMetadata, +}; pub use node_primitives::{AccountId, AssetId, Signature}; use node_primitives::{Balance, BlockNumber, Hash, Index, Moment}; pub use pallet_generic_asset::AssetInfo; @@ -45,7 +49,6 @@ use sp_runtime::traits::{ use sp_version::RuntimeVersion; #[cfg(any(feature = "std", test))] use sp_version::NativeVersion; -use sp_core::OpaqueMetadata; use pallet_grandpa::AuthorityList as GrandpaAuthorityList; use pallet_grandpa::fg_primitives; use pallet_im_online::sr25519::{AuthorityId as ImOnlineId}; @@ -185,6 +188,21 @@ impl pallet_babe::Trait for Runtime { type EpochDuration = EpochDuration; type ExpectedBlockTime = ExpectedBlockTime; type EpochChangeTrigger = pallet_babe::ExternalTrigger; + + type KeyOwnerProofSystem = Historical; + + type KeyOwnerProof = >::Proof; + + type KeyOwnerIdentification = >::IdentificationTuple; + + type HandleEquivocation = + pallet_babe::EquivocationHandler; } parameter_types! { @@ -884,7 +902,7 @@ impl_runtime_apis! { fn decode_session_keys( encoded: Vec, - ) -> Option, sp_core::crypto::KeyTypeId)>> { + ) -> Option, KeyTypeId)>> { SessionKeys::decode_into_raw_public_keys(&encoded) } } diff --git a/client/consensus/aura/Cargo.toml b/client/consensus/aura/Cargo.toml index 04ed44026f..12513fbbd5 100644 --- a/client/consensus/aura/Cargo.toml +++ b/client/consensus/aura/Cargo.toml @@ -33,6 +33,7 @@ sp-api = { version = "2.0.0-alpha.5", path = "../../../primitives/api" } sp-runtime = { version = "2.0.0-alpha.5", path = "../../../primitives/runtime" } sp-timestamp = { version = "2.0.0-alpha.5", path = "../../../primitives/timestamp" } sc-telemetry = { version = "2.0.0-alpha.5", path = "../../telemetry" } +prometheus-endpoint = { package = "substrate-prometheus-endpoint", path = "../../../utils/prometheus", version = "0.8.0-alpha.5" } [dev-dependencies] sp-keyring = { version = "2.0.0-alpha.5", path = "../../../primitives/keyring" } diff --git a/client/consensus/aura/src/lib.rs b/client/consensus/aura/src/lib.rs index 56674546d3..0c7681a333 100644 --- a/client/consensus/aura/src/lib.rs +++ b/client/consensus/aura/src/lib.rs @@ -36,6 +36,7 @@ use std::{ use futures::prelude::*; use parking_lot::Mutex; use log::{debug, info, trace}; +use prometheus_endpoint::Registry; use codec::{Encode, Decode, Codec}; @@ -452,8 +453,8 @@ fn check_header( 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(), ); } @@ -795,6 +796,7 @@ pub fn import_queue( finality_proof_import: Option>, client: Arc, inherent_data_providers: InherentDataProviders, + registry: Option<&Registry>, ) -> Result>, sp_consensus::Error> where B: BlockT, C::Api: BlockBuilderApi + AuraApi> + ApiExt, @@ -818,6 +820,7 @@ pub fn import_queue( Box::new(block_import), justification_import, finality_proof_import, + registry, )) } diff --git a/client/consensus/babe/Cargo.toml b/client/consensus/babe/Cargo.toml index ddc3f53134..6437ddf304 100644 --- a/client/consensus/babe/Cargo.toml +++ b/client/consensus/babe/Cargo.toml @@ -36,7 +36,7 @@ sc-consensus-uncles = { version = "0.8.0-alpha.5", path = "../uncles" } sc-consensus-slots = { version = "0.8.0-alpha.5", path = "../slots" } sp-runtime = { version = "2.0.0-alpha.5", path = "../../../primitives/runtime" } fork-tree = { version = "2.0.0-alpha.5", path = "../../../utils/fork-tree" } -prometheus-endpoint = { package = "substrate-prometheus-endpoint", path = "../../../utils/prometheus" , version = "0.8.0-alpha.5"} +prometheus-endpoint = { package = "substrate-prometheus-endpoint", path = "../../../utils/prometheus", version = "0.8.0-alpha.5" } futures = "0.3.4" futures-timer = "3.0.1" parking_lot = "0.10.0" diff --git a/client/consensus/babe/src/lib.rs b/client/consensus/babe/src/lib.rs index 69be45423c..274ee74a8c 100644 --- a/client/consensus/babe/src/lib.rs +++ b/client/consensus/babe/src/lib.rs @@ -76,14 +76,12 @@ pub use sp_consensus_babe::{ pub use sp_consensus::SyncOracle; use std::{ collections::HashMap, sync::Arc, u64, pin::Pin, time::{Instant, Duration}, - any::Any, borrow::Cow, convert::TryInto, + any::Any, borrow::Cow }; use sp_consensus::{ImportResult, CanAuthorWith}; use sp_consensus::import_queue::{ BoxJustificationImport, BoxFinalityProofImport, }; -use sp_core::{crypto::Public, traits::BareCryptoStore}; -use sp_application_crypto::AppKey; use sp_runtime::{ generic::{BlockId, OpaqueDigestItemId}, Justification, traits::{Block as BlockT, Header, DigestItemFor, Zero}, @@ -91,6 +89,7 @@ use sp_runtime::{ use sp_api::{ProvideRuntimeApi, NumberFor}; use sc_keystore::KeyStorePtr; use parking_lot::Mutex; +use sp_core::Pair; use sp_inherents::{InherentDataProviders, InherentData}; use sc_telemetry::{telemetry, CONSENSUS_TRACE, CONSENSUS_DEBUG}; use sp_consensus::{ @@ -442,7 +441,7 @@ impl sc_consensus_slots::SimpleSlotWorker for BabeWork Error: std::error::Error + Send + From + From + 'static, { type EpochData = ViableEpochDescriptor, Epoch>; - type Claim = (PreDigest, AuthorityId); + type Claim = (PreDigest, AuthorityPair); type SyncOracle = SO; type CreateProposer = Pin> + Send + 'static @@ -519,30 +518,12 @@ impl sc_consensus_slots::SimpleSlotWorker for BabeWork StorageChanges, Self::Claim, Self::EpochData, - ) -> Result< - sp_consensus::BlockImportParams, - sp_consensus::Error> + Send + 'static> - { - let keystore = self.keystore.clone(); - Box::new(move |header, header_hash, body, storage_changes, (_, public), epoch_descriptor| { + ) -> sp_consensus::BlockImportParams + Send> { + Box::new(|header, header_hash, body, storage_changes, (_, pair), epoch_descriptor| { // sign the pre-sealed hash of the block and then // add it to a digest item. - let public_type_pair = public.clone().into(); - let public = public.to_raw_vec(); - let signature = keystore.read() - .sign_with( - ::ID, - &public_type_pair, - header_hash.as_ref() - ) - .map_err(|e| sp_consensus::Error::CannotSign( - public.clone(), e.to_string(), - ))?; - let signature: AuthoritySignature = signature.clone().try_into() - .map_err(|_| sp_consensus::Error::InvalidSignature( - signature, public - ))?; - let digest_item = as CompatibleDigestItem>::babe_seal(signature.into()); + let signature = pair.sign(header_hash.as_ref()); + let digest_item = as CompatibleDigestItem>::babe_seal(signature); let mut import_block = BlockImportParams::new(BlockOrigin::Own, header); import_block.post_digests.push(digest_item); @@ -553,7 +534,7 @@ impl sc_consensus_slots::SimpleSlotWorker for BabeWork Box::new(BabeIntermediate:: { epoch_descriptor }) as Box, ); - Ok(import_block) + import_block }) } @@ -1226,7 +1207,7 @@ impl BlockImport for BabeBlockImport::Header; type TestExtrinsic = ::Extrinsic; +type TestSelectChain = sc_client::LongestChain< + substrate_test_runtime_client::Backend, + TestBlock, +>; + pub struct TestVerifier { - inner: BabeVerifier, + inner: BabeVerifier, mutator: Mutator, } @@ -286,15 +291,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(), diff --git a/client/consensus/manual-seal/src/lib.rs b/client/consensus/manual-seal/src/lib.rs index cf797f982c..6dcb42320e 100644 --- a/client/consensus/manual-seal/src/lib.rs +++ b/client/consensus/manual-seal/src/lib.rs @@ -107,6 +107,7 @@ pub fn import_queue(block_import: BoxBlockImport) -> BasicQueu block_import, None, None, + None, ) } diff --git a/client/consensus/pow/src/lib.rs b/client/consensus/pow/src/lib.rs index 49d2e64f60..cce2fc6f4a 100644 --- a/client/consensus/pow/src/lib.rs +++ b/client/consensus/pow/src/lib.rs @@ -475,7 +475,8 @@ pub fn import_queue( verifier, block_import, None, - None + None, + None, )) } diff --git a/client/consensus/slots/src/lib.rs b/client/consensus/slots/src/lib.rs index 1228acc39a..7a630210fa 100644 --- a/client/consensus/slots/src/lib.rs +++ b/client/consensus/slots/src/lib.rs @@ -20,7 +20,6 @@ //! time during which certain events can and/or must occur. This crate //! provides generic functionality for slots. -#![deny(warnings)] #![forbid(unsafe_code, missing_docs)] mod slots; @@ -451,7 +450,7 @@ impl SlotDuration { CB: FnOnce(ApiRef, &BlockId) -> sp_blockchain::Result, T: SlotData + Encode + Decode + Debug, { - match client.get_aux(T::SLOT_KEY)? { + let slot_duration = match client.get_aux(T::SLOT_KEY)? { Some(v) => ::decode(&mut &v[..]) .map(SlotDuration) .map_err(|_| { @@ -467,7 +466,7 @@ impl SlotDuration { info!( "⏱ Loaded block-time = {:?} milliseconds from genesis on first-launch", - genesis_slot_duration + genesis_slot_duration.slot_duration() ); genesis_slot_duration @@ -475,7 +474,15 @@ impl SlotDuration { Ok(SlotDuration(genesis_slot_duration)) } + }?; + + if slot_duration.slot_duration() == 0 { + return Err(sp_blockchain::Error::Msg( + "Invalid value for slot_duration: the value must be greater than 0.".into(), + )) } + + Ok(slot_duration) } /// Returns slot data value. @@ -484,7 +491,7 @@ impl SlotDuration { } } -// Calculate a slot duration lenience based on the number of missed slots from current +/// Calculate a slot duration lenience based on the number of missed slots from current /// to parent. If the number of skipped slots is greated than 0 this method will apply /// an exponential backoff of at most `2^7 * slot_duration`, if no slots were skipped /// this method will return `None.` diff --git a/client/keystore/Cargo.toml b/client/keystore/Cargo.toml index 5b709630ef..da4e85fe82 100644 --- a/client/keystore/Cargo.toml +++ b/client/keystore/Cargo.toml @@ -15,10 +15,11 @@ derive_more = "0.99.2" sp-core = { version = "2.0.0-alpha.5", path = "../../primitives/core" } sp-application-crypto = { version = "2.0.0-alpha.5", path = "../../primitives/application-crypto" } hex = "0.4.0" +merlin = { version = "2.0", default-features = false } +parking_lot = "0.10.0" rand = "0.7.2" serde_json = "1.0.41" subtle = "2.1.1" -parking_lot = "0.10.0" [dev-dependencies] tempfile = "3.1.0" diff --git a/client/network/test/src/block_import.rs b/client/network/test/src/block_import.rs index aa6d275141..1a67512303 100644 --- a/client/network/test/src/block_import.rs +++ b/client/network/test/src/block_import.rs @@ -84,7 +84,7 @@ fn async_import_queue_drops() { // Perform this test multiple times since it exhibits non-deterministic behavior. for _ in 0..100 { let verifier = PassThroughVerifier(true); - let queue = BasicQueue::new(verifier, Box::new(substrate_test_runtime_client::new()), None, None); + let queue = BasicQueue::new(verifier, Box::new(substrate_test_runtime_client::new()), None, None, None); drop(queue); } } diff --git a/client/network/test/src/lib.rs b/client/network/test/src/lib.rs index 8ff06fc5ac..136bbf58fe 100644 --- a/client/network/test/src/lib.rs +++ b/client/network/test/src/lib.rs @@ -595,6 +595,7 @@ pub trait TestNetFactory: Sized { Box::new(block_import.clone()), justification_import, finality_proof_import, + None, )); let listen_addr = build_multiaddr![Memory(rand::random::())]; @@ -669,6 +670,7 @@ pub trait TestNetFactory: Sized { Box::new(block_import.clone()), justification_import, finality_proof_import, + None, )); let listen_addr = build_multiaddr![Memory(rand::random::())]; diff --git a/client/service/src/builder.rs b/client/service/src/builder.rs index a1cf1b55b4..f676b36b58 100644 --- a/client/service/src/builder.rs +++ b/client/service/src/builder.rs @@ -495,7 +495,7 @@ impl /// Defines which import queue to use. pub fn with_import_queue( self, - builder: impl FnOnce(&Configuration, Arc, Option, Arc) + builder: impl FnOnce(&Configuration, Arc, Option, Arc, Option<&Registry>) -> Result ) -> Result, Error> @@ -504,7 +504,8 @@ impl &self.config, self.client.clone(), self.select_chain.clone(), - self.transaction_pool.clone() + self.transaction_pool.clone(), + self.config.prometheus_config.as_ref().map(|config| &config.registry), )?; Ok(ServiceBuilder { @@ -594,6 +595,7 @@ impl Option, Option, Arc, + Option<&Registry>, ) -> Result<(UImpQu, Option), Error> ) -> Result, Error> @@ -604,7 +606,8 @@ impl self.backend.clone(), self.fetcher.clone(), self.select_chain.clone(), - self.transaction_pool.clone() + self.transaction_pool.clone(), + self.config.prometheus_config.as_ref().map(|config| &config.registry), )?; Ok(ServiceBuilder { @@ -636,12 +639,13 @@ impl Option, Option, Arc, + Option<&Registry>, ) -> Result<(UImpQu, UFprb), Error> ) -> Result, Error> where TSc: Clone, TFchr: Clone { - self.with_import_queue_and_opt_fprb(|cfg, cl, b, f, sc, tx| - builder(cfg, cl, b, f, sc, tx) + self.with_import_queue_and_opt_fprb(|cfg, cl, b, f, sc, tx, pr| + builder(cfg, cl, b, f, sc, tx, pr) .map(|(q, f)| (q, Some(f))) ) } @@ -1276,4 +1280,4 @@ async fn extrinsic_notifications( ready(()) }) .await; -} \ No newline at end of file +} diff --git a/frame/babe/Cargo.toml b/frame/babe/Cargo.toml index 6d13d0ceda..7a24988075 100644 --- a/frame/babe/Cargo.toml +++ b/frame/babe/Cargo.toml @@ -8,45 +8,57 @@ homepage = "https://substrate.dev" repository = "https://github.com/paritytech/substrate/" description = "Consensus extension module for BABE consensus. Collects on-chain randomness from VRF outputs and manages epoch transitions." +[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"] } -serde = { version = "1.0.101", optional = true } -sp-inherents = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/inherents" } -sp-application-crypto = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/application-crypto" } -sp-std = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/std" } -sp-runtime = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/runtime" } -sp-staking = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/staking" } +frame-benchmarking = { version = "2.0.0-alpha.5", default-features = false, path = "../benchmarking", optional = true } frame-support = { version = "2.0.0-alpha.5", default-features = false, path = "../support" } frame-system = { version = "2.0.0-alpha.5", default-features = false, path = "../system" } -pallet-timestamp = { version = "2.0.0-alpha.5", default-features = false, path = "../timestamp" } -sp-timestamp = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/timestamp" } +pallet-authorship = { version = "2.0.0-alpha.5", default-features = false, path = "../authorship" } pallet-session = { version = "2.0.0-alpha.5", default-features = false, path = "../session" } +pallet-timestamp = { version = "2.0.0-alpha.5", default-features = false, path = "../timestamp" } +serde = { version = "1.0.101", optional = true } +sp-application-crypto = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/application-crypto" } sp-consensus-babe = { version = "0.8.0-alpha.5", default-features = false, path = "../../primitives/consensus/babe" } sp-consensus-vrf = { version = "0.8.0-alpha.5", default-features = false, path = "../../primitives/consensus/vrf" } -sp-io = { path = "../../primitives/io", default-features = false, version = "2.0.0-alpha.5"} +sp-inherents = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/inherents" } +sp-io = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/runtime" } +sp-session = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/session" } +sp-staking = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/staking" } +sp-std = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/std" } +sp-timestamp = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/timestamp" } [dev-dependencies] +frame-benchmarking = { version = "2.0.0-alpha.5", path = "../benchmarking" } +pallet-balances = { version = "2.0.0-alpha.5", path = "../balances" } +pallet-offences = { version = "2.0.0-alpha.5", path = "../offences" } +pallet-staking = { version = "2.0.0-alpha.5", path = "../staking" } +pallet-staking-reward-curve = { version = "2.0.0-alpha.5", path = "../staking/reward-curve" } sp-core = { version = "2.0.0-alpha.5", path = "../../primitives/core" } [features] default = ["std"] std = [ - "serde", "codec/std", - "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", ] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] +runtime-benchmarks = ["frame-benchmarking"] diff --git a/frame/babe/src/equivocation.rs b/frame/babe/src/equivocation.rs index 85ab6e6592..6179921a29 100644 --- a/frame/babe/src/equivocation.rs +++ b/frame/babe/src/equivocation.rs @@ -120,7 +120,7 @@ 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>, + T: Trait + pallet_authorship::Trait, // + frame_system::offchain::SubmitUnsignedTransaction>, // A system for reporting offences after valid equivocation reports are // processed. R: ReportOffence< @@ -144,14 +144,14 @@ where equivocation_proof: EquivocationProof, key_owner_proof: T::KeyOwnerProof, ) -> DispatchResult { - use frame_system::offchain::SubmitTransaction;; + use frame_system::offchain::SubmitUnsignedTransaction; - let call = Call::report_equivocation_unsigned(equivocation_proof, key_owner_proof); + let call = Call::::report_equivocation_unsigned(equivocation_proof, key_owner_proof); - match SubmitTransaction::>::submit_unsigned_transaction(call.into()) { - Ok(()) => debug::info!("Submitted BABE equivocation report."), - Err(e) => debug::error!("Error submitting equivocation report: {:?}", e), - } + // match >>::submit_unsigned(call.into()) { + // Ok(()) => debug::info!("Submitted BABE equivocation report."), + // Err(e) => debug::error!("Error submitting equivocation report: {:?}", e), + // } Ok(()) } diff --git a/frame/babe/src/lib.rs b/frame/babe/src/lib.rs index bdb1d03426..8b14e2813d 100644 --- a/frame/babe/src/lib.rs +++ b/frame/babe/src/lib.rs @@ -25,10 +25,10 @@ use codec::{Decode, Encode}; use frame_support::{ decl_error, decl_module, decl_storage, traits::{FindAuthor, Get, KeyOwnerProofSystem, Randomness as RandomnessT}, - weights::Weight, + weights::{SimpleDispatchInfo, Weight, WeighData}, Parameter, }; -use frame_system::{ensure_none, ensure_signed}; +use frame_system::{self as system, ensure_none, ensure_signed}; use sp_application_crypto::Public; use sp_runtime::{ generic::DigestItem, @@ -51,8 +51,8 @@ pub use sp_consensus_babe::{AuthorityId, PUBLIC_KEY_LENGTH, RANDOMNESS_LENGTH, V mod equivocation; -#[cfg(any(feature = "runtime-benchmarks", test))] -mod benchmarking; +// #[cfg(any(feature = "runtime-benchmarks", test))] +// mod benchmarking; #[cfg(all(feature = "std", test))] mod mock; #[cfg(all(feature = "std", test))] diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs index 9ff576d7b7..158f01accc 100644 --- a/frame/babe/src/mock.rs +++ b/frame/babe/src/mock.rs @@ -21,19 +21,22 @@ use codec::Encode; use super::{Trait, Module, GenesisConfig, CurrentSlot}; use sp_runtime::{ Perbill, impl_opaque_keys, + curve::PiecewiseLinear, testing::{Header, UintAuthorityId, Digest, DigestItem}, - traits::IdentityLookup, + traits::{Convert, Header as _, IdentityLookup, SaturatedConversion}, }; use frame_system::InitKind; use frame_support::{ impl_outer_origin, parameter_types, StorageValue, - traits::OnInitialize, + 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 {} @@ -50,7 +53,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); @@ -79,7 +81,7 @@ impl frame_system::Trait for Test { impl_opaque_keys! { pub struct MockSessionKeys { - pub dummy: UintAuthorityId, + pub babe_authority: super::Module, } } @@ -88,46 +90,168 @@ impl pallet_session::Trait for Test { type ValidatorId = ::AccountId; type ShouldEndSession = Babe; type SessionHandler = (Babe,); - type SessionManager = (); + type SessionManager = pallet_session::historical::NoteHistoricalRoot; type ValidatorIdOf = (); type Keys = MockSessionKeys; type DisabledValidatorsThreshold = DisabledValidatorsThreshold; type NextSessionRotation = Babe; } +impl pallet_session::historical::Trait for Test { + type FullIdentification = pallet_staking::Exposure; + type FullIdentificationOf = pallet_staking::ExposureOf; +} + +parameter_types! { + pub const UncleGenerations: u64 = 0; +} + +impl pallet_authorship::Trait for Test { + type FindAuthor = pallet_session::FindAccountFromAuthorIndex; + type UncleGenerations = UncleGenerations; + type FilterUncle = (); + type EventHandler = (); +} + +parameter_types! { + pub const MinimumPeriod: u64 = 1; +} + impl pallet_timestamp::Trait for Test { type Moment = u64; type OnTimestampSet = Babe; type MinimumPeriod = MinimumPeriod; } +parameter_types! { + pub const ExistentialDeposit: u128 = 1; + pub const CreationFee: u128 = 0; +} + +impl pallet_balances::Trait for Test { + type Balance = u128; + type OnReapAccount = System; + type OnNewAccount = (); + type Event = (); + type DustRemoval = (); + type TransferPayment = (); + type ExistentialDeposit = ExistentialDeposit; + type CreationFee = CreationFee; +} + +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 for CurrencyToVoteHandler { + fn convert(x: u128) -> u128 { + x + } +} + +impl Convert 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; + type SessionInterface = Self; + type UnixTime = pallet_timestamp::Module; + type RewardCurve = RewardCurve; + type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator; +} + +impl pallet_offences::Trait for Test { + type Event = (); + type IdentificationTuple = pallet_session::historical::IdentificationTuple; + type OnOffenceHandler = Staking; +} + impl Trait for Test { type EpochDuration = EpochDuration; type ExpectedBlockTime = ExpectedBlockTime; type EpochChangeTrigger = crate::ExternalTrigger; -} -pub fn new_test_ext(authorities_len: usize) -> (Vec, sp_io::TestExternalities) { - let pairs = (0..authorities_len).map(|i| { - AuthorityPair::from_seed(&U256::from(i).into()) - }).collect::>(); + type KeyOwnerProofSystem = Historical; + + type KeyOwnerProof = + >::Proof; - let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); - GenesisConfig { - authorities: pairs.iter().map(|a| (a.public(), 1)).collect(), - }.assimilate_storage::(&mut t).unwrap(); - (pairs, t.into()) + type KeyOwnerIdentification = >::IdentificationTuple; + + type HandleEquivocation = super::EquivocationHandler; } +pub type Balances = pallet_balances::Module; +pub type Historical = pallet_session::historical::Module; +pub type Offences = pallet_offences::Module; +pub type Session = pallet_session::Module; +pub type Staking = pallet_staking::Module; +pub type System = frame_system::Module; +pub type Timestamp = pallet_timestamp::Module; +pub type Babe = Module; + 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 @@ -139,6 +263,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, @@ -171,6 +308,124 @@ pub fn make_secondary_plain_pre_digest( Digest { logs: vec![log] } } -pub type System = frame_system::Module; -pub type Babe = Module; -pub type Session = pallet_session::Module; +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, sp_io::TestExternalities) { + let pairs = (0..authorities_len).map(|i| { + AuthorityPair::from_seed(&U256::from(i).into()) + }).collect::>(); + + 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) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .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::::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:: { keys: session_keys } + .assimilate_storage(&mut t) + .unwrap(); + + pallet_balances::GenesisConfig:: { balances } + .assimilate_storage(&mut t) + .unwrap(); + + let staking_config = pallet_staking::GenesisConfig:: { + 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
{ + 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 = ::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, + } +} diff --git a/frame/babe/src/tests.rs b/frame/babe/src/tests.rs index be2d3ed036..d285a20ae9 100644 --- a/frame/babe/src/tests.rs +++ b/frame/babe/src/tests.rs @@ -18,11 +18,14 @@ //! Consensus extension module tests for BABE consensus. use super::*; +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_vrf::schnorrkel::{VRFOutput, VRFProof}; +use sp_core::crypto::{IsWrappedBy, Pair}; const EMPTY_RANDOMNESS: [u8; 32] = [ 74, 25, 49, 128, 53, 97, 244, 49, @@ -39,14 +42,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"); @@ -55,7 +58,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; @@ -123,7 +126,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") @@ -132,7 +135,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!(::EpochDuration::get(), 3); // this sets the genesis slot to 6; go_to_block(1, 6); @@ -150,3 +153,402 @@ fn can_predict_next_epoch_change() { assert_eq!(Babe::next_expected_epoch_change(System::block_number()), Some(5 + 2)); }) } + +#[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::::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::::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::::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!( + ::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!( + ::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!(::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!( + ::pre_dispatch(&inner), + InvalidTransaction::Stale, + ); + }); +} diff --git a/frame/im-online/src/mock.rs b/frame/im-online/src/mock.rs index bcd628e2d7..e21a2fbc38 100644 --- a/frame/im-online/src/mock.rs +++ b/frame/im-online/src/mock.rs @@ -83,6 +83,10 @@ impl ReportOffence 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 { diff --git a/frame/offences/src/lib.rs b/frame/offences/src/lib.rs index 3e7f8c9537..2e17413a52 100644 --- a/frame/offences/src/lib.rs +++ b/frame/offences/src/lib.rs @@ -138,6 +138,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::(time_slot, offender); + !>::contains_key(&report_id) + }); + + !any_unknown + } } impl Module { diff --git a/frame/session/Cargo.toml b/frame/session/Cargo.toml index e22708387c..90646041f9 100644 --- a/frame/session/Cargo.toml +++ b/frame/session/Cargo.toml @@ -13,6 +13,7 @@ serde = { version = "1.0.101", optional = true } codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] } sp-std = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/std" } sp-runtime = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/runtime" } +sp-session = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/session" } sp-staking = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/staking" } frame-support = { version = "2.0.0-alpha.5", default-features = false, path = "../support" } frame-system = { version = "2.0.0-alpha.5", default-features = false, path = "../system" } @@ -35,6 +36,7 @@ std = [ "sp-std/std", "frame-support/std", "sp-runtime/std", + "sp-session/std", "sp-staking/std", "pallet-timestamp/std", "sp-trie/std", diff --git a/frame/session/src/historical.rs b/frame/session/src/historical.rs index f9990dd1e8..29cfd2f25c 100644 --- a/frame/session/src/historical.rs +++ b/frame/session/src/historical.rs @@ -29,13 +29,13 @@ use sp_std::prelude::*; use codec::{Encode, Decode}; use sp_runtime::{KeyTypeId, RuntimeDebug}; use sp_runtime::traits::{Convert, OpaqueKeys}; +use sp_session::{MembershipProof, ValidatorCount}; use frame_support::{decl_module, decl_storage}; use frame_support::{Parameter, print}; use sp_trie::{MemoryDB, Trie, TrieMut, Recorder, EMPTY_PREFIX}; use sp_trie::trie_types::{TrieDBMut, TrieDB}; use super::{SessionIndex, Module as SessionModule}; -type ValidatorCount = u32; /// Trait necessary for the historical module. pub trait Trait: super::Trait { @@ -116,6 +116,7 @@ impl crate::SessionManager for NoteHistoricalRoot { fn new_session(new_index: SessionIndex) -> Option> { + StoredRange::mutate(|range| { range.get_or_insert_with(|| (new_index, new_index)).1 = new_index + 1; }); @@ -126,7 +127,7 @@ impl crate::SessionManager for NoteHistoricalRoot::generate_for(new_validators) { Ok(trie) => >::insert(new_index, &(trie.root, count)), Err(reason) => { @@ -143,9 +144,11 @@ impl crate::SessionManager for NoteHistoricalRoot>::start_session(start_index) } + fn end_session(end_index: SessionIndex) { >::end_session(end_index) } @@ -154,7 +157,7 @@ impl crate::SessionManager for NoteHistoricalRoot = (::ValidatorId, ::FullIdentification); -/// a trie instance for checking and generating proofs. +/// A trie instance for checking and generating proofs. pub struct ProvingTrie { db: MemoryDB, root: T::Hash, @@ -250,57 +253,60 @@ impl ProvingTrie { .ok()? .and_then(|raw| >::decode(&mut &*raw).ok()) } - -} - -/// Proof of ownership of a specific key. -#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] -pub struct Proof { - session: SessionIndex, - trie_nodes: Vec>, -} - -impl Proof { - /// Returns a session this proof was generated for. - pub fn session(&self) -> SessionIndex { - self.session - } } impl> frame_support::traits::KeyOwnerProofSystem<(KeyTypeId, D)> for Module { - type Proof = Proof; + type Proof = MembershipProof; type IdentificationTuple = IdentificationTuple; fn prove(key: (KeyTypeId, D)) -> Option { let session = >::current_index(); - let validators = >::validators().into_iter() + let validators = >::validators() + .into_iter() .filter_map(|validator| { T::FullIdentificationOf::convert(validator.clone()) .map(|full_id| (validator, full_id)) - }); + }) + .collect::>(); + + let count = validators.len() as ValidatorCount; + let trie = ProvingTrie::::generate_for(validators).ok()?; let (id, data) = key; - - trie.prove(id, data.as_ref()).map(|trie_nodes| Proof { - session, - trie_nodes, - }) + trie.prove(id, data.as_ref()) + .map(|trie_nodes| MembershipProof { + session, + trie_nodes, + validator_count: count, + }) } - fn check_proof(key: (KeyTypeId, D), proof: Proof) -> Option> { + fn check_proof(key: (KeyTypeId, D), proof: Self::Proof) -> Option> { let (id, data) = key; if proof.session == >::current_index() { - >::key_owner(id, data.as_ref()).and_then(|owner| - T::FullIdentificationOf::convert(owner.clone()).map(move |id| (owner, id)) - ) + >::key_owner(id, data.as_ref()).and_then(|owner| { + T::FullIdentificationOf::convert(owner.clone()).and_then(move |id| { + let count = >::validators().len() as ValidatorCount; + + if count != proof.validator_count { + return None; + } + + Some((owner, id)) + }) + }) } else { - let (root, _) = >::get(&proof.session)?; - let trie = ProvingTrie::::from_nodes(root, &proof.trie_nodes); + let (root, count) = >::get(&proof.session)?; + + if count != proof.validator_count { + return None; + } + let trie = ProvingTrie::::from_nodes(root, &proof.trie_nodes); trie.query(id, data.as_ref()) } } diff --git a/frame/staking/src/lib.rs b/frame/staking/src/lib.rs index 159e262308..bca8ffd7d1 100644 --- a/frame/staking/src/lib.rs +++ b/frame/staking/src/lib.rs @@ -2192,6 +2192,10 @@ impl ReportOffence Ok(()) } } + + fn is_known_offence(offenders: &[Offender], time_slot: &O::TimeSlot) -> bool { + R::is_known_offence(offenders, time_slot) + } } /// Check that list is sorted and has no duplicates. diff --git a/frame/support/src/lib.rs b/frame/support/src/lib.rs index 87d133eebd..58f4e279ed 100644 --- a/frame/support/src/lib.rs +++ b/frame/support/src/lib.rs @@ -28,6 +28,7 @@ pub extern crate tracing; #[cfg(feature = "std")] pub use serde; +pub use sp_core::Void; #[doc(hidden)] pub use sp_std; #[doc(hidden)] @@ -217,11 +218,6 @@ macro_rules! assert_ok { } } -/// The void type - it cannot exist. -// Oh rust, you crack me up... -#[derive(Clone, Eq, PartialEq, RuntimeDebug)] -pub enum Void {} - #[cfg(feature = "std")] #[doc(hidden)] pub use serde::{Serialize, Deserialize}; diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index 0df5aba923..80bb256c57 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -185,6 +185,21 @@ pub trait KeyOwnerProofSystem { fn check_proof(key: Key, proof: Self::Proof) -> Option; } +impl KeyOwnerProofSystem for () { + // The proof and identification tuples is any bottom type to guarantee that the methods of this + // implementation can never be called or return anything other than `None`. + type Proof = crate::Void; + type IdentificationTuple = crate::Void; + + fn prove(_key: Key) -> Option { + None + } + + fn check_proof(_key: Key, _proof: Self::Proof) -> Option { + None + } +} + /// Handler for when some currency "account" decreased in balance for /// some reason. /// diff --git a/frame/system/src/offchain.rs b/frame/system/src/offchain.rs index 0ccd60a320..b444b5d7c1 100644 --- a/frame/system/src/offchain.rs +++ b/frame/system/src/offchain.rs @@ -362,3 +362,13 @@ impl SubmitSignedTransaction for TransactionSubmitter } } } + +/// A definition of types required to submit transactions from within the runtime. +pub trait SendTransactionTypes { + /// The extrinsic type expected by the runtime. + type Extrinsic: ExtrinsicT + codec::Encode; + /// The runtime's call type. + /// + /// This has additional bound to be able to be created from pallet-local `Call` types. + type OverarchingCall: From; +} diff --git a/primitives/consensus/common/Cargo.toml b/primitives/consensus/common/Cargo.toml index 49c1df0bb7..b3604363a6 100644 --- a/primitives/consensus/common/Cargo.toml +++ b/primitives/consensus/common/Cargo.toml @@ -26,6 +26,8 @@ sp-runtime = { version = "2.0.0-alpha.5", path = "../../runtime" } codec = { package = "parity-scale-codec", version = "1.3.0", features = ["derive"] } parking_lot = "0.10.0" serde = { version = "1.0", features = ["derive"] } +prometheus-endpoint = { package = "substrate-prometheus-endpoint", path = "../../../utils/prometheus", version = "0.8.0-alpha.5"} +wasm-timer = "0.2.4" [dev-dependencies] sp-test-primitives = { version = "2.0.0-dev", path = "../../test-primitives" } diff --git a/primitives/consensus/common/src/import_queue.rs b/primitives/consensus/common/src/import_queue.rs index 2da0bcac0c..5b5c4ad3e1 100644 --- a/primitives/consensus/common/src/import_queue.rs +++ b/primitives/consensus/common/src/import_queue.rs @@ -26,13 +26,17 @@ //! queues to be instantiated simply. use std::collections::HashMap; + use sp_runtime::{Justification, traits::{Block as BlockT, Header as _, NumberFor}}; -use crate::error::Error as ConsensusError; -use crate::block_import::{ - BlockImport, BlockOrigin, BlockImportParams, ImportedAux, JustificationImport, ImportResult, - BlockCheckParams, FinalityProofImport, -}; +use crate::{ + error::Error as ConsensusError, + block_import::{ + BlockImport, BlockOrigin, BlockImportParams, ImportedAux, JustificationImport, ImportResult, + BlockCheckParams, FinalityProofImport, + }, + metrics::Metrics, +}; pub use basic_queue::BasicQueue; mod basic_queue; @@ -185,6 +189,17 @@ pub fn import_single_block, Transaction>( block_origin: BlockOrigin, block: IncomingBlock, verifier: &mut V, +) -> Result>, BlockImportError> { + import_single_block_metered(import_handle, block_origin, block, verifier, None) +} + +/// Single block import function with metering. +pub(crate) fn import_single_block_metered, Transaction>( + import_handle: &mut dyn BlockImport, + block_origin: BlockOrigin, + block: IncomingBlock, + verifier: &mut V, + metrics: Option, ) -> Result>, BlockImportError> { let peer = block.origin; @@ -206,8 +221,8 @@ pub fn import_single_block, Transaction>( let hash = header.hash(); let parent_hash = header.parent_hash().clone(); - let import_error = |e| { - match e { + let import_handler = |import| { + match import { Ok(ImportResult::AlreadyInChain) => { trace!(target: "sync", "Block already in chain {}: {:?}", number, hash); Ok(BlockImportResult::ImportedKnown(number)) @@ -231,7 +246,8 @@ pub fn import_single_block, Transaction>( } } }; - match import_error(import_handle.check_block(BlockCheckParams { + + match import_handler(import_handle.check_block(BlockCheckParams { hash, number, parent_hash, @@ -242,6 +258,7 @@ pub fn import_single_block, Transaction>( r => return Ok(r), // Any other successful result means that the block is already imported. } + let started = wasm_timer::Instant::now(); let (mut import_block, maybe_keys) = verifier.verify(block_origin, header, justification, block.body) .map_err(|msg| { if let Some(ref peer) = peer { @@ -249,14 +266,21 @@ pub fn import_single_block, Transaction>( } else { trace!(target: "sync", "Verifying {}({}) failed: {}", number, hash, msg); } + if let Some(metrics) = metrics.as_ref() { + metrics.report_verification(false, started.elapsed()); + } BlockImportError::VerificationFailed(peer.clone(), msg) })?; + if let Some(metrics) = metrics.as_ref() { + metrics.report_verification(true, started.elapsed()); + } + let mut cache = HashMap::new(); if let Some(keys) = maybe_keys { cache.extend(keys.into_iter()); } import_block.allow_missing_state = block.allow_missing_state; - import_error(import_handle.import_block(import_block.convert_transaction(), cache)) + import_handler(import_handle.import_block(import_block.convert_transaction(), cache)) } diff --git a/primitives/consensus/common/src/import_queue/basic_queue.rs b/primitives/consensus/common/src/import_queue/basic_queue.rs index 0d1aed7fb1..8d9b3e10a5 100644 --- a/primitives/consensus/common/src/import_queue/basic_queue.rs +++ b/primitives/consensus/common/src/import_queue/basic_queue.rs @@ -19,13 +19,17 @@ use futures::{prelude::*, channel::mpsc, task::Context, task::Poll}; use futures_timer::Delay; use parking_lot::{Mutex, Condvar}; use sp_runtime::{Justification, traits::{Block as BlockT, Header as HeaderT, NumberFor}}; - -use crate::block_import::BlockOrigin; -use crate::import_queue::{ - BlockImportResult, BlockImportError, Verifier, BoxBlockImport, BoxFinalityProofImport, - BoxJustificationImport, ImportQueue, Link, Origin, - IncomingBlock, import_single_block, - buffered_link::{self, BufferedLinkSender, BufferedLinkReceiver} +use prometheus_endpoint::Registry; + +use crate::{ + block_import::BlockOrigin, + import_queue::{ + BlockImportResult, BlockImportError, Verifier, BoxBlockImport, BoxFinalityProofImport, + BoxJustificationImport, ImportQueue, Link, Origin, + IncomingBlock, import_single_block_metered, + buffered_link::{self, BufferedLinkSender, BufferedLinkReceiver}, + }, + metrics::Metrics, }; /// Interface to a basic block import queue that is importing blocks sequentially in a separate @@ -73,14 +77,21 @@ impl BasicQueue { block_import: BoxBlockImport, justification_import: Option>, finality_proof_import: Option>, + prometheus_registry: Option<&Registry>, ) -> Self { let (result_sender, result_port) = buffered_link::buffered_link(); + let metrics = prometheus_registry.and_then(|r| + Metrics::register(r) + .map_err(|err| { log::warn!("Failed to register Prometheus metrics: {}", err); }) + .ok() + ); let (future, worker_sender) = BlockImportWorker::new( result_sender, verifier, block_import, justification_import, finality_proof_import, + metrics, ); let guard = Arc::new((Mutex::new(0usize), Condvar::new())); @@ -185,6 +196,7 @@ struct BlockImportWorker { justification_import: Option>, finality_proof_import: Option>, delay_between_blocks: Duration, + metrics: Option, _phantom: PhantomData, } @@ -195,6 +207,7 @@ impl BlockImportWorker { block_import: BoxBlockImport, justification_import: Option>, finality_proof_import: Option>, + metrics: Option, ) -> (impl Future + Send, mpsc::UnboundedSender>) { let (sender, mut port) = mpsc::unbounded(); @@ -203,6 +216,7 @@ impl BlockImportWorker { justification_import, finality_proof_import, delay_between_blocks: Duration::new(0, 0), + metrics, _phantom: PhantomData, }; @@ -263,7 +277,7 @@ impl BlockImportWorker { // a `Future` into `importing`. let (bi, verif) = block_import_verifier.take() .expect("block_import_verifier is always Some; qed"); - importing = Some(worker.import_a_batch_of_blocks(bi, verif, origin, blocks)); + importing = Some(worker.import_batch(bi, verif, origin, blocks)); }, ToWorkerMsg::ImportFinalityProof(who, hash, number, proof) => { let (_, verif) = block_import_verifier.as_mut() @@ -285,16 +299,17 @@ impl BlockImportWorker { /// /// For lifetime reasons, the `BlockImport` implementation must be passed by value, and is /// yielded back in the output once the import is finished. - fn import_a_batch_of_blocks>( + fn import_batch>( &mut self, block_import: BoxBlockImport, verifier: V, origin: BlockOrigin, - blocks: Vec> + blocks: Vec>, ) -> impl Future, V)> { let mut result_sender = self.result_sender.clone(); + let metrics = self.metrics.clone(); - import_many_blocks(block_import, origin, blocks, verifier, self.delay_between_blocks) + import_many_blocks(block_import, origin, blocks, verifier, self.delay_between_blocks, metrics) .then(move |(imported, count, results, block_import, verifier)| { result_sender.blocks_processed(imported, count, results); future::ready((block_import, verifier)) @@ -365,6 +380,7 @@ fn import_many_blocks, Transaction>( blocks: Vec>, verifier: V, delay_between_blocks: Duration, + metrics: Option, ) -> impl Future< Output = ( usize, @@ -436,14 +452,19 @@ fn import_many_blocks, Transaction>( Err(BlockImportError::Cancelled) } else { // The actual import. - import_single_block( + import_single_block_metered( &mut **import_handle, blocks_origin.clone(), block, verifier, + metrics.clone(), ) }; + if let Some(metrics) = metrics.as_ref() { + metrics.report_import::(&import_result); + } + if import_result.is_ok() { trace!(target: "sync", "Block imported successfully {:?} ({})", block_number, block_hash); imported += 1; diff --git a/primitives/consensus/common/src/lib.rs b/primitives/consensus/common/src/lib.rs index 09dc031dc9..a9bd5bb1c5 100644 --- a/primitives/consensus/common/src/lib.rs +++ b/primitives/consensus/common/src/lib.rs @@ -44,6 +44,7 @@ pub mod block_import; mod select_chain; pub mod import_queue; pub mod evaluation; +mod metrics; // block size limit. const MAX_BLOCK_SIZE: usize = 4 * 1024 * 1024 + 512; diff --git a/primitives/consensus/common/src/metrics.rs b/primitives/consensus/common/src/metrics.rs new file mode 100644 index 0000000000..90df85a294 --- /dev/null +++ b/primitives/consensus/common/src/metrics.rs @@ -0,0 +1,80 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Metering tools for consensus + +use prometheus_endpoint::{register, U64, Registry, PrometheusError, Opts, CounterVec, HistogramVec, HistogramOpts}; + +use sp_runtime::traits::{Block as BlockT, NumberFor}; + +use crate::import_queue::{BlockImportResult, BlockImportError}; + +/// Generic Prometheus metrics for common consensus functionality. +#[derive(Clone)] +pub(crate) struct Metrics { + pub import_queue_processed: CounterVec, + pub block_verification_time: HistogramVec, +} + +impl Metrics { + pub(crate) fn register(registry: &Registry) -> Result { + Ok(Self { + import_queue_processed: register( + CounterVec::new( + Opts::new("import_queue_processed_total", "Blocks processed by import queue"), + &["result"] // 'success or failure + )?, + registry, + )?, + block_verification_time: register( + HistogramVec::new( + HistogramOpts::new( + "block_verification_time", + "Histogram of time taken to import blocks", + ), + &["result"], + )?, + registry, + )?, + }) + } + + pub fn report_import( + &self, + result: &Result>, BlockImportError>, + ) { + let label = match result { + Ok(_) => "success", + Err(BlockImportError::IncompleteHeader(_)) => "incomplete_header", + Err(BlockImportError::VerificationFailed(_,_)) => "verification_failed", + Err(BlockImportError::BadBlock(_)) => "bad_block", + Err(BlockImportError::MissingState) => "missing_state", + Err(BlockImportError::UnknownParent) => "unknown_parent", + Err(BlockImportError::Cancelled) => "cancelled", + Err(BlockImportError::Other(_)) => "failed", + }; + + self.import_queue_processed.with_label_values( + &[label] + ).inc(); + } + + pub fn report_verification(&self, success: bool, time: std::time::Duration) { + self.block_verification_time.with_label_values( + &[if success { "success" } else { "verification_failed" }] + ).observe(time.as_secs_f64()); + } +} diff --git a/primitives/core/Cargo.toml b/primitives/core/Cargo.toml index 1d34e1427f..e4704f8e24 100644 --- a/primitives/core/Cargo.toml +++ b/primitives/core/Cargo.toml @@ -55,6 +55,7 @@ hex-literal = "0.2.1" rand = "0.7.2" criterion = "0.2.11" serde_json = "1.0" +rand_chacha = "0.2.2" [[bench]] name = "bench" diff --git a/primitives/core/src/lib.rs b/primitives/core/src/lib.rs index e1060ebee7..9741d9c5b6 100644 --- a/primitives/core/src/lib.rs +++ b/primitives/core/src/lib.rs @@ -320,6 +320,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 diff --git a/primitives/runtime/src/transaction_validity.rs b/primitives/runtime/src/transaction_validity.rs index 78f724b4d2..596d121b53 100644 --- a/primitives/runtime/src/transaction_validity.rs +++ b/primitives/runtime/src/transaction_validity.rs @@ -236,6 +236,17 @@ impl Default for ValidTransaction { } impl ValidTransaction { + /// Initiate `ValidTransaction` builder object with a particular prefix for tags. + /// + /// To avoid conflicts between different parts in runtime it's recommended to build `requires` + /// and `provides` tags with a unique prefix. + pub fn with_tag_prefix(prefix: &'static str) -> ValidTransactionBuilder { + ValidTransactionBuilder { + prefix: Some(prefix), + validity: Default::default(), + } + } + /// Combine two instances into one, as a best effort. This will take the superset of each of the /// `provides` and `requires` tags, it will sum the priorities, take the minimum longevity and /// the logic *And* of the propagate flags. @@ -250,6 +261,104 @@ impl ValidTransaction { } } +/// `ValidTransaction` builder. +/// +/// +/// Allows to easily construct `ValidTransaction` and most importantly takes care of +/// prefixing `requires` and `provides` tags to avoid conflicts. +#[derive(Default, Clone, RuntimeDebug)] +pub struct ValidTransactionBuilder { + prefix: Option<&'static str>, + validity: ValidTransaction, +} + +impl ValidTransactionBuilder { + /// Set the priority of a transaction. + /// + /// Note that the final priority for `FRAME` is combined from all `SignedExtension`s. + /// Most likely for unsigned transactions you want the priority to be higher + /// than for regular transactions. We recommend exposing a base priority for unsigned + /// transactions as a runtime module parameter, so that the runtime can tune inter-module + /// priorities. + pub fn priority(mut self, priority: TransactionPriority) -> Self { + self.validity.priority = priority; + self + } + + /// Set the longevity of a transaction. + /// + /// By default the transaction will be considered valid forever and will not be revalidated + /// by the transaction pool. It's recommended though to set the longevity to a finite value + /// though. If unsure, it's also reasonable to expose this parameter via module configuration + /// and let the runtime decide. + pub fn longevity(mut self, longevity: TransactionLongevity) -> Self { + self.validity.longevity = longevity; + self + } + + /// Set the propagate flag. + /// + /// Set to `false` if the transaction is not meant to be gossiped to peers. Combined with + /// `TransactionSource::Local` validation it can be used to have special kind of + /// transactions that are only produced and included by the validator nodes. + pub fn propagate(mut self, propagate: bool) -> Self { + self.validity.propagate = propagate; + self + } + + /// Add a `TransactionTag` to the set of required tags. + /// + /// The tag will be encoded and prefixed with module prefix (if any). + /// If you'd rather add a raw `require` tag, consider using `#combine_with` method. + pub fn and_requires(mut self, tag: impl Encode) -> Self { + self.validity.requires.push(match self.prefix.as_ref() { + Some(prefix) => (prefix, tag).encode(), + None => tag.encode(), + }); + self + } + + /// Add a `TransactionTag` to the set of provided tags. + /// + /// The tag will be encoded and prefixed with module prefix (if any). + /// If you'd rather add a raw `require` tag, consider using `#combine_with` method. + pub fn and_provides(mut self, tag: impl Encode) -> Self { + self.validity.provides.push(match self.prefix.as_ref() { + Some(prefix) => (prefix, tag).encode(), + None => tag.encode(), + }); + self + } + + /// Augment the builder with existing `ValidTransaction`. + /// + /// This method does add the prefix to `require` or `provides` tags. + pub fn combine_with(mut self, validity: ValidTransaction) -> Self { + self.validity = core::mem::take(&mut self.validity).combine_with(validity); + self + } + + /// Finalize the builder and produce `TransactionValidity`. + /// + /// Note the result will always be `Ok`. Use `Into` to produce `ValidTransaction`. + pub fn build(self) -> TransactionValidity { + self.into() + } +} + +impl From for TransactionValidity { + fn from(builder: ValidTransactionBuilder) -> Self { + Ok(builder.into()) + } +} + +impl From for ValidTransaction { + fn from(builder: ValidTransactionBuilder) -> Self { + builder.validity + } +} + + #[cfg(test)] mod tests { use super::*; @@ -273,4 +382,26 @@ mod tests { // decode back assert_eq!(TransactionValidity::decode(&mut &*encoded), Ok(v)); } + + #[test] + fn builder_should_prefix_the_tags() { + const PREFIX: &str = "test"; + let a: ValidTransaction = ValidTransaction::with_tag_prefix(PREFIX) + .and_requires(1) + .and_requires(2) + .and_provides(3) + .and_provides(4) + .propagate(false) + .longevity(5) + .priority(3) + .priority(6) + .into(); + assert_eq!(a, ValidTransaction { + propagate: false, + longevity: 5, + priority: 6, + requires: vec![(PREFIX, 1).encode(), (PREFIX, 2).encode()], + provides: vec![(PREFIX, 3).encode(), (PREFIX, 4).encode()], + }); + } } diff --git a/primitives/session/Cargo.toml b/primitives/session/Cargo.toml index 0a10107f98..2386c5fcf5 100644 --- a/primitives/session/Cargo.toml +++ b/primitives/session/Cargo.toml @@ -9,11 +9,20 @@ repository = "https://github.com/paritytech/substrate/" description = "Primitives for sessions" [dependencies] +codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] } sp-api = { version = "2.0.0-alpha.5", default-features = false, path = "../api" } -sp-std = { version = "2.0.0-alpha.5", default-features = false, path = "../std" } sp-core = { version = "2.0.0-alpha.5", default-features = false, path = "../core" } +sp-std = { version = "2.0.0-alpha.5", default-features = false, path = "../std" } +sp-staking = { version = "2.0.0-alpha.5", default-features = false, path = "../staking" } sp-runtime = { version = "2.0.0-alpha.5", optional = true, path = "../runtime" } [features] default = [ "std" ] -std = [ "sp-api/std", "sp-std/std", "sp-runtime", "sp-core/std" ] +std = [ + "codec/std", + "sp-api/std", + "sp-core/std", + "sp-std/std", + "sp-staking/std", + "sp-runtime/std", +] diff --git a/primitives/session/src/lib.rs b/primitives/session/src/lib.rs index 8e2a68d050..2a4980ebfd 100644 --- a/primitives/session/src/lib.rs +++ b/primitives/session/src/lib.rs @@ -18,14 +18,17 @@ #![cfg_attr(not(feature = "std"), no_std)] -use sp_std::vec::Vec; +use codec::{Encode, Decode}; #[cfg(feature = "std")] use sp_runtime::{generic::BlockId, traits::Block as BlockT}; #[cfg(feature = "std")] use sp_api::ProvideRuntimeApi; +use sp_core::RuntimeDebug; use sp_core::crypto::KeyTypeId; +use sp_staking::SessionIndex; +use sp_std::vec::Vec; sp_api::decl_runtime_apis! { /// Session keys runtime api. @@ -46,6 +49,62 @@ sp_api::decl_runtime_apis! { } } +/// Number of validators in a given session. +pub type ValidatorCount = u32; + +/// Proof of membership of a specific key in a given session. +#[derive(Encode, Decode, Clone, Eq, PartialEq, Default, RuntimeDebug)] +pub struct MembershipProof { + /// The session index on which the specific key is a member. + pub session: SessionIndex, + /// Trie nodes of a merkle proof of session membership. + pub trie_nodes: Vec>, + /// The validator count of the session on which the specific key is a member. + 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")] diff --git a/primitives/staking/src/offence.rs b/primitives/staking/src/offence.rs index 06e73f018b..93aefde6e0 100644 --- a/primitives/staking/src/offence.rs +++ b/primitives/staking/src/offence.rs @@ -118,10 +118,21 @@ impl sp_runtime::traits::Printable for OffenceError { pub trait ReportOffence> { /// Report an `offence` and reward given `reporters`. fn report_offence(reporters: Vec, offence: O) -> Result<(), OffenceError>; + + /// Returns true if 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> ReportOffence for () { - fn report_offence(_reporters: Vec, _offence: O) -> Result<(), OffenceError> { Ok(()) } + fn report_offence(_reporters: Vec, _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. diff --git a/test-utils/runtime/src/lib.rs b/test-utils/runtime/src/lib.rs index 6009448a1f..108180fa80 100644 --- a/test-utils/runtime/src/lib.rs +++ b/test-utils/runtime/src/lib.rs @@ -25,7 +25,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, RuntimeAppPublic}; use trie_db::{TrieMut, Trie}; use sp_trie::PrefixedMemoryDB; @@ -47,7 +47,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}; +use frame_support::{ + impl_outer_origin, parameter_types, + traits::KeyOwnerProofSystem, + weights::Weight, +}; use sp_inherents::{CheckInherentsResult, InherentData}; use cfg_if::cfg_if; use sp_core::storage::ChildType; @@ -426,6 +430,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 = + >::Proof; + + type KeyOwnerIdentification = >::IdentificationTuple; + + type HandleEquivocation = (); } /// Adds one to the given input and returns the final result. diff --git a/utils/prometheus/src/lib.rs b/utils/prometheus/src/lib.rs index 4a8f05d929..9030704cb7 100644 --- a/utils/prometheus/src/lib.rs +++ b/utils/prometheus/src/lib.rs @@ -16,7 +16,10 @@ use futures_util::{FutureExt, future::Future}; pub use prometheus::{ + self, Registry, Error as PrometheusError, Opts, + Histogram, HistogramOpts, HistogramVec, + exponential_buckets, core::{ GenericGauge as Gauge, GenericCounter as Counter, GenericGaugeVec as GaugeVec, GenericCounterVec as CounterVec, From 593b4d69549045dfbbebebc6a02ab10c6083378a Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 24 Aug 2020 16:08:39 +1200 Subject: [PATCH 11/13] Remove warnings, add a TODO --- frame/babe/src/equivocation.rs | 5 +++-- frame/babe/src/lib.rs | 2 +- frame/babe/src/mock.rs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frame/babe/src/equivocation.rs b/frame/babe/src/equivocation.rs index 6179921a29..2bd5bc4365 100644 --- a/frame/babe/src/equivocation.rs +++ b/frame/babe/src/equivocation.rs @@ -144,9 +144,10 @@ where equivocation_proof: EquivocationProof, key_owner_proof: T::KeyOwnerProof, ) -> DispatchResult { - use frame_system::offchain::SubmitUnsignedTransaction; + // TODO: bring in off-chain refactor around SendTransactionType + // use frame_system::offchain::SubmitUnsignedTransaction; - let call = Call::::report_equivocation_unsigned(equivocation_proof, key_owner_proof); + let _call = Call::::report_equivocation_unsigned(equivocation_proof, key_owner_proof); // match >>::submit_unsigned(call.into()) { // Ok(()) => debug::info!("Submitted BABE equivocation report."), diff --git a/frame/babe/src/lib.rs b/frame/babe/src/lib.rs index 8b14e2813d..e3b9126ff4 100644 --- a/frame/babe/src/lib.rs +++ b/frame/babe/src/lib.rs @@ -40,7 +40,7 @@ use sp_std::{prelude::*, result}; use sp_timestamp::OnTimestampSet; use sp_consensus_babe::{ - digests::{NextConfigDescriptor, NextEpochDescriptor, PreDigest}, + digests::{NextEpochDescriptor, PreDigest}, inherents::{BabeInherentData, INHERENT_IDENTIFIER}, BabeAuthorityWeight, ConsensusLog, EquivocationProof, SlotNumber, BABE_ENGINE_ID, }; diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs index 158f01accc..e04a4f06aa 100644 --- a/frame/babe/src/mock.rs +++ b/frame/babe/src/mock.rs @@ -22,7 +22,7 @@ use super::{Trait, Module, GenesisConfig, CurrentSlot}; use sp_runtime::{ Perbill, impl_opaque_keys, curve::PiecewiseLinear, - testing::{Header, UintAuthorityId, Digest, DigestItem}, + testing::{Header, Digest, DigestItem}, traits::{Convert, Header as _, IdentityLookup, SaturatedConversion}, }; use frame_system::InitKind; From de945f22de33bf100305d8d13d92f104961d235e Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 31 Aug 2020 12:29:31 +1200 Subject: [PATCH 12/13] Bump spec version --- bin/node/runtime/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 046c8b62c9..e02359f2bd 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -88,8 +88,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // and set impl_version to 0. If only runtime // implementation changes and behavior does not, then leave spec_version as // is and increment impl_version. - spec_version: 240, - impl_version: 1, + spec_version: 241, + impl_version: 0, apis: RUNTIME_API_VERSIONS, }; From 3f2c94ca33d5bb9397ea297b200271ea26a70175 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 1 Sep 2020 15:44:39 +1200 Subject: [PATCH 13/13] Address comments --- Cargo.lock | 1 + client/consensus/babe/Cargo.toml | 1 + client/consensus/babe/src/tests.rs | 48 ++++++++++++++++++++++++++++-- client/consensus/epochs/src/lib.rs | 2 +- frame/babe/src/mock.rs | 6 ++-- 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b40169a25c..aa6754bcc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6111,6 +6111,7 @@ dependencies = [ "parking_lot 0.10.0", "pdqselect", "rand 0.7.3", + "rand_chacha 0.2.2", "sc-block-builder", "sc-client", "sc-client-api", diff --git a/client/consensus/babe/Cargo.toml b/client/consensus/babe/Cargo.toml index 6437ddf304..01347c2919 100644 --- a/client/consensus/babe/Cargo.toml +++ b/client/consensus/babe/Cargo.toml @@ -56,6 +56,7 @@ sc-service = { version = "0.8.0-alpha.5", path = "../../service" } substrate-test-runtime-client = { version = "2.0.0-dev", path = "../../../test-utils/runtime/client" } sc-block-builder = { version = "0.8.0-alpha.5", path = "../../block-builder" } env_logger = "0.7.0" +rand_chacha = "0.2.2" tempfile = "3.1.0" [features] diff --git a/client/consensus/babe/src/tests.rs b/client/consensus/babe/src/tests.rs index 1e724c6f38..34caa40cdf 100644 --- a/client/consensus/babe/src/tests.rs +++ b/client/consensus/babe/src/tests.rs @@ -21,8 +21,14 @@ #![allow(deprecated)] use super::*; use authorship::claim_slot; - -use sp_consensus_babe::{AuthorityPair, SlotNumber, AllowedSlots}; +use sp_core::{crypto::Pair, vrf::make_transcript as transcript_from_data}; +use sp_consensus_babe::{ + AuthorityPair, + SlotNumber, + AllowedSlots, + make_transcript, + make_transcript_data, +}; use sc_block_builder::{BlockBuilder, BlockBuilderProvider}; use sp_consensus::{ NoNetwork as DummyOracle, Proposal, RecordProof, @@ -35,6 +41,11 @@ use sp_runtime::{generic::DigestItem, traits::{Block as BlockT, DigestFor}}; use sc_client_api::{BlockchainEvents, backend::TransactionFor}; use log::debug; use std::{time::Duration, cell::RefCell, task::Poll}; +use rand::RngCore; +use rand_chacha::{ + rand_core::SeedableRng, + ChaChaRng, +}; type Item = DigestItem; @@ -806,3 +817,36 @@ fn verify_slots_are_strictly_increasing() { &mut block_import, ); } + +#[test] +fn babe_transcript_generation_match() { + let _ = env_logger::try_init(); + let keystore_path = tempfile::tempdir().expect("Creates keystore path"); + let keystore = sc_keystore::Store::open(keystore_path.path(), None).expect("Creates keystore"); + let pair = keystore.write().insert_ephemeral_from_seed::("//Alice") + .expect("Generates authority pair"); + + let epoch = Epoch { + start_slot: 0, + authorities: vec![(pair.public(), 1)], + randomness: [0; 32], + epoch_index: 1, + duration: 100, + config: BabeEpochConfiguration { + c: (3, 10), + allowed_slots: AllowedSlots::PrimaryAndSecondaryPlainSlots, + }, + }; + + let orig_transcript = make_transcript(&epoch.randomness.clone(), 1, epoch.epoch_index); + let new_transcript = make_transcript_data(&epoch.randomness, 1, epoch.epoch_index); + + let test = |t: merlin::Transcript| -> [u8; 16] { + let mut b = [0u8; 16]; + t.build_rng() + .finalize(&mut ChaChaRng::from_seed([0u8;32])) + .fill_bytes(&mut b); + b + }; + debug_assert!(test(orig_transcript) == test(transcript_from_data(new_transcript))); +} diff --git a/client/consensus/epochs/src/lib.rs b/client/consensus/epochs/src/lib.rs index 4ca8502e66..acb07dd668 100644 --- a/client/consensus/epochs/src/lib.rs +++ b/client/consensus/epochs/src/lib.rs @@ -335,7 +335,7 @@ impl EpochChanges where self.inner.rebalance() } - /// Map the epoch changes from one storing data to a different one. + /// Map the epoch changes from one storing data to a different one. pub fn map(self, mut f: F) -> EpochChanges where B: Epoch, F: FnMut(&Hash, &Number, E) -> B, diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs index e04a4f06aa..fd3c2c5c62 100644 --- a/frame/babe/src/mock.rs +++ b/frame/babe/src/mock.rs @@ -23,7 +23,7 @@ use sp_runtime::{ Perbill, impl_opaque_keys, curve::PiecewiseLinear, testing::{Header, Digest, DigestItem}, - traits::{Convert, Header as _, IdentityLookup, SaturatedConversion}, + traits::{Convert, Header as _, IdentityLookup, OpaqueKeys, SaturatedConversion}, }; use frame_system::InitKind; use frame_support::{ @@ -89,9 +89,9 @@ impl pallet_session::Trait for Test { type Event = (); type ValidatorId = ::AccountId; type ShouldEndSession = Babe; - type SessionHandler = (Babe,); + type SessionHandler = ::KeyTypeIdProviders; type SessionManager = pallet_session::historical::NoteHistoricalRoot; - type ValidatorIdOf = (); + type ValidatorIdOf = pallet_staking::StashOf; type Keys = MockSessionKeys; type DisabledValidatorsThreshold = DisabledValidatorsThreshold; type NextSessionRotation = Babe;