diff --git a/Cargo.lock b/Cargo.lock index 8810fc7ebb98f..82952d8a5e803 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3885,6 +3885,7 @@ dependencies = [ "sr-io 2.0.0", "sr-primitives 2.0.0", "sr-std 2.0.0", + "srml-authorship 0.1.0", "srml-session 2.0.0", "srml-support 2.0.0", "srml-system 2.0.0", diff --git a/core/sr-primitives/src/traits.rs b/core/sr-primitives/src/traits.rs index f7d55dda79a12..6e1b8286314c6 100644 --- a/core/sr-primitives/src/traits.rs +++ b/core/sr-primitives/src/traits.rs @@ -602,6 +602,24 @@ pub trait IsMember { fn is_member(member_id: &MemberId) -> bool; } +/// Mean for getting the current session keys. +pub trait CurrentSessionKeys { + /// Get the keys of the current session. + fn current_keys() -> Vec<(AccountId, Key)>; +} + +impl CurrentSessionKeys for () { + fn current_keys() -> Vec<(T, Key)> { + Vec::new() + } +} + +/// Disable a validator referenced by an `AccountId`. +pub trait DisableValidator { + /// Disable a validator referenced by an `AccountId`. + fn disable(account_id: &AccountId) -> Result<(), ()>; +} + /// Something which fulfills the abstract idea of a Substrate header. It has types for a `Number`, /// a `Hash` and a `Digest`. It provides access to an `extrinsics_root`, `state_root` and /// `parent_hash`, as well as a `digest` and a block `number`. diff --git a/core/test-runtime/src/lib.rs b/core/test-runtime/src/lib.rs index 98d2437059659..da6e6032393e4 100644 --- a/core/test-runtime/src/lib.rs +++ b/core/test-runtime/src/lib.rs @@ -368,6 +368,7 @@ parameter_types! { impl srml_babe::Trait for Runtime { type EpochDuration = EpochDuration; + type CurrentSessionKeys = (); type ExpectedBlockTime = ExpectedBlockTime; } diff --git a/node/cli/src/chain_spec.rs b/node/cli/src/chain_spec.rs index 6372146a45ecd..b2e20420ada6b 100644 --- a/node/cli/src/chain_spec.rs +++ b/node/cli/src/chain_spec.rs @@ -21,8 +21,8 @@ use primitives::{ed25519, sr25519, Pair, crypto::UncheckedInto}; use node_primitives::{AccountId, Balance}; use node_runtime::{ BabeConfig, BalancesConfig, ContractsConfig, CouncilConfig, DemocracyConfig, - ElectionsConfig, GrandpaConfig, ImOnlineConfig, IndicesConfig, Perbill, - SessionConfig, SessionKeys, StakerStatus, StakingConfig, SudoConfig, SystemConfig, + ElectionsConfig, GrandpaConfig, IndicesConfig, Perbill, SessionConfig, + SessionKeys, StakerStatus, StakingConfig, SudoConfig, SystemConfig, TechnicalCommitteeConfig, WASM_BINARY, }; use node_runtime::constants::{time::*, currency::*}; @@ -161,10 +161,6 @@ fn staging_testnet_config_genesis() -> GenesisConfig { babe: Some(BabeConfig { authorities: initial_authorities.iter().map(|x| (x.2.clone(), 1)).collect(), }), - im_online: Some(ImOnlineConfig { - gossip_at: 0, - last_new_era_start: 0, - }), grandpa: Some(GrandpaConfig { authorities: initial_authorities.iter().map(|x| (x.3.clone(), 1)).collect(), }), @@ -304,10 +300,6 @@ pub fn testnet_genesis( babe: Some(BabeConfig { authorities: initial_authorities.iter().map(|x| (x.2.clone(), 1)).collect(), }), - im_online: Some(ImOnlineConfig{ - gossip_at: 0, - last_new_era_start: 0, - }), grandpa: Some(GrandpaConfig { authorities: initial_authorities.iter().map(|x| (x.3.clone(), 1)).collect(), }), diff --git a/node/executor/src/lib.rs b/node/executor/src/lib.rs index 572d0ca1499bc..83fd65a0317c2 100644 --- a/node/executor/src/lib.rs +++ b/node/executor/src/lib.rs @@ -388,7 +388,6 @@ mod tests { gas_price: 1 * MILLICENTS, }), sudo: Some(Default::default()), - im_online: Some(Default::default()), grandpa: Some(GrandpaConfig { authorities: vec![], }), diff --git a/node/runtime/src/lib.rs b/node/runtime/src/lib.rs index 06285571ba8b2..f75cde04a6b85 100644 --- a/node/runtime/src/lib.rs +++ b/node/runtime/src/lib.rs @@ -79,8 +79,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // and set impl_version to equal spec_version. If only runtime // implementation changes and behavior does not, then leave spec_version as // is and increment impl_version. - spec_version: 129, - impl_version: 129, + spec_version: 130, + impl_version: 130, apis: RUNTIME_API_VERSIONS, }; @@ -134,6 +134,7 @@ parameter_types! { impl babe::Trait for Runtime { type EpochDuration = EpochDuration; type ExpectedBlockTime = ExpectedBlockTime; + type CurrentSessionKeys = session::CurrentSessionKeys; } impl indices::Trait for Runtime { @@ -184,7 +185,7 @@ impl authorship::Trait for Runtime { type FindAuthor = session::FindAccountFromAuthorIndex; type UncleGenerations = UncleGenerations; type FilterUncle = (); - type EventHandler = Staking; + type EventHandler = (ImOnline, Staking); } type SessionHandlers = (Grandpa, Babe, ImOnline); @@ -212,6 +213,7 @@ impl session::Trait for Runtime { type Keys = SessionKeys; type ValidatorId = AccountId; type ValidatorIdOf = staking::StashOf; + type AccountIdOf = staking::ControllerOf; type SelectInitialValidators = Staking; } @@ -375,6 +377,8 @@ impl im_online::Trait for Runtime { type SessionsPerEra = SessionsPerEra; type UncheckedExtrinsic = UncheckedExtrinsic; type IsValidAuthorityId = Babe; + type AuthorityIdOf = babe::AuthorityIdOf; + type DisableValidator = staking::DisableValidatorInterface; } impl grandpa::Trait for Runtime { @@ -415,7 +419,7 @@ construct_runtime!( Treasury: treasury::{Module, Call, Storage, Event}, Contracts: contracts, Sudo: sudo, - ImOnline: im_online::{default, ValidateUnsigned}, + ImOnline: im_online::{Module, Call, Event, ValidateUnsigned}, } ); diff --git a/srml/babe/src/lib.rs b/srml/babe/src/lib.rs index 8e72fdffd4fbf..3358a69c90914 100644 --- a/srml/babe/src/lib.rs +++ b/srml/babe/src/lib.rs @@ -25,7 +25,10 @@ use rstd::{result, prelude::*}; use srml_support::{decl_storage, decl_module, StorageValue, StorageMap, traits::FindAuthor, traits::Get}; use timestamp::{OnTimestampSet}; use sr_primitives::{generic::DigestItem, ConsensusEngineId}; -use sr_primitives::traits::{IsMember, SaturatedConversion, Saturating, RandomnessBeacon, Convert}; +use sr_primitives::traits::{ + CurrentSessionKeys, IsMember, SaturatedConversion, Saturating, RandomnessBeacon, Convert, + TypedKey, +}; #[cfg(feature = "std")] use timestamp::TimestampInherentData; use parity_codec::{Encode, Decode}; @@ -109,7 +112,11 @@ impl ProvideInherentData for InherentDataProvider { pub trait Trait: timestamp::Trait { type EpochDuration: Get; + type ExpectedBlockTime: Get; + + /// Retrieve the current session keys. + type CurrentSessionKeys: CurrentSessionKeys; } /// The length of the BABE randomness @@ -234,6 +241,27 @@ impl IsMember for Module { } } +/// A `Convert` implementation that finds the babe authority id of the given controller +/// account, if any. +pub struct AuthorityIdOf(rstd::marker::PhantomData, rstd::marker::PhantomData); + +impl Convert> for AuthorityIdOf + where + T: Trait, + AuthorityId: Decode + Default + TypedKey, +{ + fn convert(account_id: T::AccountId) -> Option { + let keys = T::CurrentSessionKeys::current_keys::(); + let maybe_authority_id = keys + .into_iter() + .find(|(id, _)| { + *id == account_id + }) + .map(|(_, a)| a); + maybe_authority_id + } +} + impl session::ShouldEndSession for Module { fn should_end_session(_: T::BlockNumber) -> bool { let diff = CurrentSlot::get().saturating_sub(EpochStartSlot::get()); diff --git a/srml/im-online/Cargo.toml b/srml/im-online/Cargo.toml index c015e5e4c51a8..594be69df2d8a 100644 --- a/srml/im-online/Cargo.toml +++ b/srml/im-online/Cargo.toml @@ -5,6 +5,7 @@ authors = ["Parity Technologies "] edition = "2018" [dependencies] +authorship = { package = "srml-authorship", path = "../authorship", default-features = false } parity-codec = { version = "4.1.1", default-features = false, features = ["derive"] } sr-primitives = { path = "../../core/sr-primitives", default-features = false } primitives = { package = "substrate-primitives", path = "../../core/primitives", default-features = false } diff --git a/srml/im-online/src/lib.rs b/srml/im-online/src/lib.rs index 8254cb60780fd..a2ecd01328015 100644 --- a/srml/im-online/src/lib.rs +++ b/srml/im-online/src/lib.rs @@ -63,7 +63,9 @@ //! //! ## Dependencies //! -//! This module depends on the [Session module](../srml_session/index.html). +//! This module depends on the [Session module](../srml_session/index.html) for +//! generic session functionality and on the [Authorship module](../srml_authorship/index.html) +//! to mark validators automatically as online once they author a block. // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] @@ -76,7 +78,9 @@ use primitives::{ }; use parity_codec::{Encode, Decode}; use sr_primitives::{ - ApplyError, traits::{Member, IsMember, Extrinsic as ExtrinsicT}, + ApplyError, traits::{ + Convert, DisableValidator, Member, IsMember, Extrinsic as ExtrinsicT, Zero, + }, transaction_validity::{TransactionValidity, TransactionLongevity, ValidTransaction}, }; use rstd::prelude::*; @@ -158,6 +162,12 @@ pub trait Trait: system::Trait + session::Trait { /// Determine if an `AuthorityId` is a valid authority. type IsValidAuthorityId: IsMember; + + /// A conversion of `AccountId` to `AuthorityId`. + type AuthorityIdOf: Convert>; + + /// Disable a given validator. + type DisableValidator: DisableValidator; } decl_event!( @@ -173,15 +183,24 @@ decl_event!( decl_storage! { trait Store for Module as ImOnline { // The block number when we should gossip. - GossipAt get(gossip_at) config(): T::BlockNumber; + GossipAt get(gossip_at) build(|_| T::BlockNumber::zero()): T::BlockNumber; // The session index when the last new era started. - LastNewEraStart get(last_new_era_start) config(): Option; + LastNewEraStart get(last_new_era_start): Option; // For each session index we keep a mapping of `AuthorityId` to // `offchain::OpaqueNetworkState`. ReceivedHeartbeats get(received_heartbeats): double_map session::SessionIndex, blake2_256(T::AuthorityId) => Vec; + + // For each session index we track if an `AuthorityId` was noted as + // a block author by the authorship module. + BlockAuthors get(block_authors): double_map session::SessionIndex, + blake2_256(T::AuthorityId) => bool; + + // The validators in the current session. + CurrentSessionValidators get(current_session_validators): + Vec<(T::AccountId, T::AuthorityId)>; } } @@ -336,25 +355,61 @@ impl Module { Some(start) => { // iterate over every session for index in start..curr { - if >::exists(&index, authority_id) { + let got_heartbeat = >::exists(&index, authority_id); + let was_author = >::exists(&index, authority_id); + if got_heartbeat || was_author { return true; } } false }, - None => >::exists(&curr, authority_id), + None => { + let got_heartbeat = >::exists(&curr, authority_id); + let was_author = >::exists(&curr, authority_id); + got_heartbeat || was_author + }, } } /// Returns `true` if a heartbeat has been received for `AuthorityId` /// during the current session. Otherwise `false`. pub fn is_online_in_current_session(authority_id: &T::AuthorityId) -> bool { - let current_session = >::current_index(); - >::exists(¤t_session, authority_id) + let curr = >::current_index(); + let got_heartbeat = >::exists(&curr, authority_id); + let was_author = >::exists(&curr, authority_id); + got_heartbeat || was_author + } + + /// Returns `true` if a heartbeat has been received for `AuthorityId` + /// during the previous session. Otherwise `false`. + pub fn was_online_in_previous_session(authority_id: &T::AuthorityId) -> bool { + let curr = >::current_index(); + if curr == 0 { + return false; + } + + let index = curr - 1; + let got_heartbeat = >::exists(&index, authority_id); + let was_author = >::exists(&index, authority_id); + got_heartbeat || was_author + } + + /// Disables all validators which haven't been online in the previous session. + fn disable_offline_validators() { + >::current_session_validators() + .iter() + .for_each(|(validator_id, authority_id)| { + if !Self::was_online_in_previous_session(authority_id) { + let _ = T::DisableValidator::disable(validator_id); + } + }); } /// Session has just changed. - fn new_session() { + fn new_session(validators: Vec<(T::AccountId, T::AuthorityId)>) { + Self::disable_offline_validators(); + >::put(validators); + let now = >::block_number(); >::put(now); @@ -381,9 +436,13 @@ impl Module { Some(start) => { for index in start..curr { >::remove_prefix(&index); + >::remove_prefix(&index); } }, - None => >::remove_prefix(&curr), + None => { + >::remove_prefix(&curr); + >::remove_prefix(&curr); + }, } } } @@ -391,8 +450,18 @@ impl Module { impl session::OneSessionHandler for Module { type Key = ::AuthorityId; - fn on_new_session<'a, I: 'a>(_changed: bool, _validators: I, _next_validators: I) { - Self::new_session(); + fn on_new_session<'a, I: 'a>( + _changed: bool, + validators: I, + _next_validators: I, + ) + where I: Iterator + { + let validators: Vec<(T::AccountId, T::AuthorityId)> = validators + .map(|(acc_id, auth_id)| (acc_id.clone(), auth_id)) + .collect(); + + Self::new_session(validators); } fn on_disabled(_i: usize) { @@ -400,6 +469,25 @@ impl session::OneSessionHandler for Module { } } +/// Mark nodes which authored automatically as online. +impl authorship::EventHandler for Module { + fn note_author(account_id: T::AccountId) { + let maybe_authority_id = T::AuthorityIdOf::convert(account_id); + if let Some(authority_id) = maybe_authority_id { + let current_session = >::current_index(); + + let exists = >::exists(¤t_session, &authority_id); + if !exists { + >::insert(¤t_session, &authority_id, &true); + } + } + } + + fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) { + // ignore + } +} + impl srml_support::unsigned::ValidateUnsigned for Module { type Call = Call; diff --git a/srml/session/src/lib.rs b/srml/session/src/lib.rs index a189a91da2848..bd584c1648def 100644 --- a/srml/session/src/lib.rs +++ b/srml/session/src/lib.rs @@ -123,7 +123,10 @@ use rstd::{prelude::*, marker::PhantomData, ops::{Sub, Rem}}; use parity_codec::Decode; use sr_primitives::KeyTypeId; use sr_primitives::weights::SimpleDispatchInfo; -use sr_primitives::traits::{Convert, Zero, Member, OpaqueKeys, TypedKey}; +use sr_primitives::traits::{ + Convert, CurrentSessionKeys as CurrentSessionKeysT, Zero, Member, OpaqueKeys, + TypedKey, +}; use srml_support::{ dispatch::Result, ConsensusEngineId, StorageValue, StorageDoubleMap, for_each_tuple, decl_module, decl_event, decl_storage, @@ -266,6 +269,9 @@ pub trait Trait: system::Trait { /// A conversion to validator ID to account ID. type ValidatorIdOf: Convert>; + /// A conversion of validator ID to account ID. + type AccountIdOf: Convert>; + /// Indicator for when to end the session. type ShouldEndSession: ShouldEndSession; @@ -313,6 +319,9 @@ decl_storage! { /// The first key is always `DEDUP_KEY_PREFIX` to have all the data in the same branch of /// the trie. Having all data in the same branch should prevent slowing down other queries. KeyOwner: double_map hasher(twox_64_concat) Vec, blake2_256((KeyTypeId, Vec)) => Option; + + /// Returns the keys of the current session for all validators. + CurrentKeys get(current_keys): Vec<(T::ValidatorId, T::Keys)>; } add_extra_genesis { config(keys): Vec<(T::ValidatorId, T::Keys)>; @@ -332,6 +341,8 @@ decl_storage! { .expect("genesis config must not contain duplicates; qed"); } + >::put(config.keys.clone()); + let initial_validators = T::SelectInitialValidators::select_initial_validators() .unwrap_or_else(|| config.keys.iter().map(|(ref v, _)| v.clone()).collect()); @@ -455,6 +466,18 @@ impl Module { T::SessionHandler::on_new_session::(changed, &session_keys, &queued_amalgamated); } + pub fn get_current_keys() -> Vec<(T::ValidatorId, Key)> { + Self::current_keys() + .into_iter() + .map(|k| { + (k.0, + k.1.get::(::KEY_TYPE) + .unwrap_or_default() + ) + }) + .collect() + } + /// Disable the validator of index `i`. pub fn disable_index(i: usize) { T::SessionHandler::on_disabled(i); @@ -539,6 +562,25 @@ impl OnFreeBalanceZero for Module { } } +/// Returns the current session keys. +pub struct CurrentSessionKeys(rstd::marker::PhantomData); + +impl CurrentSessionKeysT<::AccountId> for CurrentSessionKeys where + T::AccountIdOf: Convert::AccountId>>, +{ + fn current_keys() -> Vec<(::AccountId, Key)> + where Key: Decode + Default + TypedKey + { + >::get_current_keys::() + .into_iter() + .map(|(validator_id, keys)| { + let account_id = T::AccountIdOf::convert(validator_id).unwrap_or_default(); + (account_id, keys) + }) + .collect() + } +} + /// Wraps the author-scraping logic for consensus engines that can recover /// the canonical index of an author. This then transforms it into the /// registering account-ID of that session key index. diff --git a/srml/session/src/mock.rs b/srml/session/src/mock.rs index 734f5bbde4bd6..dd5db689f75a5 100644 --- a/srml/session/src/mock.rs +++ b/srml/session/src/mock.rs @@ -153,6 +153,7 @@ impl Trait for Test { type ValidatorIdOf = ConvertInto; type Keys = UintAuthorityId; type Event = (); + type AccountIdOf = (); type SelectInitialValidators = (); } diff --git a/srml/staking/src/lib.rs b/srml/staking/src/lib.rs index dd540e78b1f36..f0bdd9a3d6916 100644 --- a/srml/staking/src/lib.rs +++ b/srml/staking/src/lib.rs @@ -297,7 +297,7 @@ use sr_primitives::Perbill; use sr_primitives::weights::SimpleDispatchInfo; use sr_primitives::traits::{ Convert, Zero, One, StaticLookup, CheckedSub, CheckedShl, Saturating, Bounded, - SaturatedConversion, SimpleArithmetic + SaturatedConversion, SimpleArithmetic, TypedKey, DisableValidator, }; #[cfg(feature = "std")] use sr_primitives::{Serialize, Deserialize}; @@ -480,6 +480,8 @@ pub trait SessionInterface: system::Trait { fn validators() -> Vec; /// Prune historical session tries up to but not including the given index. fn prune_historical_up_to(up_to: session::SessionIndex); + /// Get the keys of the current session. + fn current_keys() -> Vec<(AccountId, Key)>; } impl SessionInterface<::AccountId> for T where @@ -491,7 +493,8 @@ impl SessionInterface<::AccountId> for T where T::SessionHandler: session::SessionHandler<::AccountId>, T::OnSessionEnding: session::OnSessionEnding<::AccountId>, T::SelectInitialValidators: session::SelectInitialValidators<::AccountId>, - T::ValidatorIdOf: Convert<::AccountId, Option<::AccountId>> + T::ValidatorIdOf: Convert<::AccountId, Option<::AccountId>>, + T::AccountIdOf: Convert<::AccountId, Option<::AccountId>>, { fn disable_validator(validator: &::AccountId) -> Result<(), ()> { >::disable(validator) @@ -501,6 +504,12 @@ impl SessionInterface<::AccountId> for T where >::validators() } + fn current_keys() -> Vec<(::AccountId, Key)> + where Key: Decode + Default + TypedKey + { + >::get_current_keys::() + } + fn prune_historical_up_to(up_to: session::SessionIndex) { >::prune_up_to(up_to); } @@ -1438,6 +1447,16 @@ impl OnFreeBalanceZero for Module { } } +/// A `Convert` implementation that finds the controller account of the given stash account, +/// if any. +pub struct DisableValidatorInterface(rstd::marker::PhantomData); + +impl DisableValidator for DisableValidatorInterface { + fn disable(account_id: &T::AccountId) -> Result<(), ()> { + T::SessionInterface::disable_validator(account_id) + } +} + /// Add reward points to block authors: /// * 20 points to the block producer for producing a (non-uncle) block in the relay chain, /// * 2 points to the block producer for each reference to a previously unreferenced uncle, and @@ -1452,6 +1471,16 @@ impl authorship::EventHandler(rstd::marker::PhantomData); + +impl Convert> for ControllerOf { + fn convert(stash: T::AccountId) -> Option { + >::bonded(&stash) + } +} + // This is guarantee not to overflow on whatever values. // `num` must be inferior to `den` otherwise it will be reduce to `den`. fn multiply_by_rational(value: N, num: u32, den: u32) -> N diff --git a/srml/staking/src/mock.rs b/srml/staking/src/mock.rs index 344ef70e3b6f4..abb7bf99a7d92 100644 --- a/srml/staking/src/mock.rs +++ b/srml/staking/src/mock.rs @@ -157,6 +157,7 @@ impl session::Trait for Test { type ValidatorId = AccountId; type ValidatorIdOf = crate::StashOf; type SelectInitialValidators = Staking; + type AccountIdOf = crate::ControllerOf; } impl session::historical::Trait for Test {