mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 03:31:05 +00:00
Rename Palette to FRAME (#4182)
* palette -> frame * PALETTE, Palette -> FRAME * Move folder pallete -> frame * Update docs/Structure.adoc Co-Authored-By: Benjamin Kampmann <ben.kampmann@googlemail.com> * Update docs/README.adoc Co-Authored-By: Benjamin Kampmann <ben.kampmann@googlemail.com> * Update README.adoc
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "pallet-im-online"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
app-crypto = { package = "substrate-application-crypto", path = "../../primitives/application-crypto", default-features = false }
|
||||
authorship = { package = "pallet-authorship", path = "../authorship", default-features = false }
|
||||
codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] }
|
||||
primitives = { package="substrate-primitives", path = "../../primitives/core", default-features = false }
|
||||
rstd = { package = "sr-std", path = "../../primitives/sr-std", default-features = false }
|
||||
serde = { version = "1.0.101", optional = true }
|
||||
session = { package = "pallet-session", path = "../session", default-features = false }
|
||||
runtime-io = { package = "sr-io", path = "../../primitives/sr-io", default-features = false }
|
||||
sr-primitives = { path = "../../primitives/sr-primitives", default-features = false }
|
||||
sr-staking-primitives = { path = "../../primitives/sr-staking-primitives", default-features = false }
|
||||
support = { package = "frame-support", path = "../support", default-features = false }
|
||||
system = { package = "frame-system", path = "../system", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["std", "session/historical"]
|
||||
std = [
|
||||
"app-crypto/std",
|
||||
"authorship/std",
|
||||
"codec/std",
|
||||
"primitives/std",
|
||||
"rstd/std",
|
||||
"serde",
|
||||
"session/std",
|
||||
"runtime-io/std",
|
||||
"sr-primitives/std",
|
||||
"sr-staking-primitives/std",
|
||||
"support/std",
|
||||
"system/std",
|
||||
]
|
||||
@@ -0,0 +1,660 @@
|
||||
// Copyright 2019 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! # I'm online Module
|
||||
//!
|
||||
//! If the local node is a validator (i.e. contains an authority key), this module
|
||||
//! gossips a heartbeat transaction with each new session. The heartbeat functions
|
||||
//! as a simple mechanism to signal that the node is online in the current era.
|
||||
//!
|
||||
//! Received heartbeats are tracked for one era and reset with each new era. The
|
||||
//! module exposes two public functions to query if a heartbeat has been received
|
||||
//! in the current era or session.
|
||||
//!
|
||||
//! The heartbeat is a signed transaction, which was signed using the session key
|
||||
//! and includes the recent best block number of the local validators chain as well
|
||||
//! as the [NetworkState](../../client/offchain/struct.NetworkState.html).
|
||||
//! It is submitted as an Unsigned Transaction via off-chain workers.
|
||||
//!
|
||||
//! - [`im_online::Trait`](./trait.Trait.html)
|
||||
//! - [`Call`](./enum.Call.html)
|
||||
//! - [`Module`](./struct.Module.html)
|
||||
//!
|
||||
//! ## Interface
|
||||
//!
|
||||
//! ### Public Functions
|
||||
//!
|
||||
//! - `is_online` - True if the validator sent a heartbeat in the current session.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```
|
||||
//! use support::{decl_module, dispatch::Result};
|
||||
//! use system::ensure_signed;
|
||||
//! use pallet_im_online::{self as im_online};
|
||||
//!
|
||||
//! pub trait Trait: im_online::Trait {}
|
||||
//!
|
||||
//! decl_module! {
|
||||
//! pub struct Module<T: Trait> for enum Call where origin: T::Origin {
|
||||
//! pub fn is_online(origin, authority_index: u32) -> Result {
|
||||
//! let _sender = ensure_signed(origin)?;
|
||||
//! let _is_online = <im_online::Module<T>>::is_online(authority_index);
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! # fn main() { }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Dependencies
|
||||
//!
|
||||
//! This module depends on the [Session module](../pallet_session/index.html).
|
||||
|
||||
// Ensure we're `no_std` when compiling for Wasm.
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
mod mock;
|
||||
mod tests;
|
||||
|
||||
use app_crypto::RuntimeAppPublic;
|
||||
use codec::{Encode, Decode};
|
||||
use primitives::offchain::{OpaqueNetworkState, StorageKind};
|
||||
use rstd::prelude::*;
|
||||
use rstd::convert::TryInto;
|
||||
use session::historical::IdentificationTuple;
|
||||
use sr_primitives::{
|
||||
RuntimeDebug,
|
||||
traits::{Convert, Member, Printable, Saturating}, Perbill,
|
||||
transaction_validity::{
|
||||
TransactionValidity, ValidTransaction, InvalidTransaction,
|
||||
TransactionPriority,
|
||||
},
|
||||
};
|
||||
use sr_staking_primitives::{
|
||||
SessionIndex,
|
||||
offence::{ReportOffence, Offence, Kind},
|
||||
};
|
||||
use support::{
|
||||
decl_module, decl_event, decl_storage, print, Parameter, debug,
|
||||
traits::Get,
|
||||
};
|
||||
use system::ensure_none;
|
||||
use system::offchain::SubmitUnsignedTransaction;
|
||||
|
||||
pub mod sr25519 {
|
||||
mod app_sr25519 {
|
||||
use app_crypto::{app_crypto, key_types::IM_ONLINE, sr25519};
|
||||
app_crypto!(sr25519, IM_ONLINE);
|
||||
}
|
||||
|
||||
/// An i'm online keypair using sr25519 as its crypto.
|
||||
#[cfg(feature = "std")]
|
||||
pub type AuthorityPair = app_sr25519::Pair;
|
||||
|
||||
/// An i'm online signature using sr25519 as its crypto.
|
||||
pub type AuthoritySignature = app_sr25519::Signature;
|
||||
|
||||
/// An i'm online identifier using sr25519 as its crypto.
|
||||
pub type AuthorityId = app_sr25519::Public;
|
||||
}
|
||||
|
||||
pub mod ed25519 {
|
||||
mod app_ed25519 {
|
||||
use app_crypto::{app_crypto, key_types::IM_ONLINE, ed25519};
|
||||
app_crypto!(ed25519, IM_ONLINE);
|
||||
}
|
||||
|
||||
/// An i'm online keypair using ed25519 as its crypto.
|
||||
#[cfg(feature = "std")]
|
||||
pub type AuthorityPair = app_ed25519::Pair;
|
||||
|
||||
/// An i'm online signature using ed25519 as its crypto.
|
||||
pub type AuthoritySignature = app_ed25519::Signature;
|
||||
|
||||
/// An i'm online identifier using ed25519 as its crypto.
|
||||
pub type AuthorityId = app_ed25519::Public;
|
||||
}
|
||||
|
||||
/// The local storage database key under which the worker progress status
|
||||
/// is tracked.
|
||||
const DB_KEY: &[u8] = b"frame/im-online-worker-status";
|
||||
|
||||
/// It's important to persist the worker state, since e.g. the
|
||||
/// server could be restarted while starting the gossip process, but before
|
||||
/// finishing it. With every execution of the off-chain worker we check
|
||||
/// if we need to recover and resume gossipping or if there is already
|
||||
/// another off-chain worker in the process of gossipping.
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)]
|
||||
struct WorkerStatus<BlockNumber> {
|
||||
done: bool,
|
||||
gossipping_at: BlockNumber,
|
||||
}
|
||||
|
||||
/// Error which may occur while executing the off-chain code.
|
||||
#[derive(RuntimeDebug)]
|
||||
enum OffchainErr {
|
||||
DecodeWorkerStatus,
|
||||
FailedSigning,
|
||||
NetworkState,
|
||||
SubmitTransaction,
|
||||
}
|
||||
|
||||
impl Printable for OffchainErr {
|
||||
fn print(&self) {
|
||||
match self {
|
||||
OffchainErr::DecodeWorkerStatus => print("Offchain error: decoding WorkerStatus failed!"),
|
||||
OffchainErr::FailedSigning => print("Offchain error: signing failed!"),
|
||||
OffchainErr::NetworkState => print("Offchain error: fetching network state failed!"),
|
||||
OffchainErr::SubmitTransaction => print("Offchain error: submitting transaction failed!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type AuthIndex = u32;
|
||||
|
||||
/// Heartbeat which is sent/received.
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)]
|
||||
pub struct Heartbeat<BlockNumber>
|
||||
where BlockNumber: PartialEq + Eq + Decode + Encode,
|
||||
{
|
||||
block_number: BlockNumber,
|
||||
network_state: OpaqueNetworkState,
|
||||
session_index: SessionIndex,
|
||||
authority_index: AuthIndex,
|
||||
}
|
||||
|
||||
pub trait Trait: system::Trait + session::historical::Trait {
|
||||
/// The identifier type for an authority.
|
||||
type AuthorityId: Member + Parameter + RuntimeAppPublic + Default + Ord;
|
||||
|
||||
/// The overarching event type.
|
||||
type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
|
||||
|
||||
/// A dispatchable call type.
|
||||
type Call: From<Call<Self>>;
|
||||
|
||||
/// A transaction submitter.
|
||||
type SubmitTransaction: SubmitUnsignedTransaction<Self, <Self as Trait>::Call>;
|
||||
|
||||
/// An expected duration of the session.
|
||||
///
|
||||
/// This parameter is used to determine the longevity of `heartbeat` transaction
|
||||
/// and a rough time when the heartbeat should be sent.
|
||||
type SessionDuration: Get<Self::BlockNumber>;
|
||||
|
||||
/// A type that gives us the ability to submit unresponsiveness offence reports.
|
||||
type ReportUnresponsiveness:
|
||||
ReportOffence<
|
||||
Self::AccountId,
|
||||
IdentificationTuple<Self>,
|
||||
UnresponsivenessOffence<IdentificationTuple<Self>>,
|
||||
>;
|
||||
}
|
||||
|
||||
decl_event!(
|
||||
pub enum Event<T> where
|
||||
<T as Trait>::AuthorityId,
|
||||
IdentificationTuple = IdentificationTuple<T>,
|
||||
{
|
||||
/// A new heartbeat was received from `AuthorityId`
|
||||
HeartbeatReceived(AuthorityId),
|
||||
/// At the end of the session, no offence was committed.
|
||||
AllGood,
|
||||
/// At the end of the session, at least once validator was found to be offline.
|
||||
SomeOffline(Vec<IdentificationTuple>),
|
||||
}
|
||||
);
|
||||
|
||||
decl_storage! {
|
||||
trait Store for Module<T: Trait> as ImOnline {
|
||||
/// The block number when we should gossip.
|
||||
GossipAt get(fn gossip_at): T::BlockNumber;
|
||||
|
||||
/// The current set of keys that may issue a heartbeat.
|
||||
Keys get(fn keys): Vec<T::AuthorityId>;
|
||||
|
||||
/// For each session index, we keep a mapping of `AuthIndex`
|
||||
/// to `offchain::OpaqueNetworkState`.
|
||||
ReceivedHeartbeats get(fn received_heartbeats): double_map SessionIndex,
|
||||
blake2_256(AuthIndex) => Option<Vec<u8>>;
|
||||
|
||||
/// For each session index, we keep a mapping of `T::ValidatorId` to the
|
||||
/// number of blocks authored by the given authority.
|
||||
AuthoredBlocks get(fn authored_blocks): double_map SessionIndex,
|
||||
blake2_256(T::ValidatorId) => u32;
|
||||
}
|
||||
add_extra_genesis {
|
||||
config(keys): Vec<T::AuthorityId>;
|
||||
build(|config| Module::<T>::initialize_keys(&config.keys))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
decl_module! {
|
||||
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
|
||||
fn deposit_event() = default;
|
||||
|
||||
fn heartbeat(
|
||||
origin,
|
||||
heartbeat: Heartbeat<T::BlockNumber>,
|
||||
// since signature verification is done in `validate_unsigned`
|
||||
// we can skip doing it here again.
|
||||
_signature: <T::AuthorityId as RuntimeAppPublic>::Signature
|
||||
) {
|
||||
ensure_none(origin)?;
|
||||
|
||||
let current_session = <session::Module<T>>::current_index();
|
||||
let exists = <ReceivedHeartbeats>::exists(
|
||||
¤t_session,
|
||||
&heartbeat.authority_index
|
||||
);
|
||||
let keys = Keys::<T>::get();
|
||||
let public = keys.get(heartbeat.authority_index as usize);
|
||||
if let (false, Some(public)) = (exists, public) {
|
||||
Self::deposit_event(Event::<T>::HeartbeatReceived(public.clone()));
|
||||
|
||||
let network_state = heartbeat.network_state.encode();
|
||||
<ReceivedHeartbeats>::insert(
|
||||
¤t_session,
|
||||
&heartbeat.authority_index,
|
||||
&network_state
|
||||
);
|
||||
} else if exists {
|
||||
Err("Duplicated heartbeat.")?
|
||||
} else {
|
||||
Err("Non existent public key.")?
|
||||
}
|
||||
}
|
||||
|
||||
// Runs after every block.
|
||||
fn offchain_worker(now: T::BlockNumber) {
|
||||
debug::RuntimeLogger::init();
|
||||
|
||||
// Only send messages if we are a potential validator.
|
||||
if runtime_io::offchain::is_validator() {
|
||||
Self::offchain(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep track of number of authored blocks per authority, uncles are counted as
|
||||
/// well since they're a valid proof of onlineness.
|
||||
impl<T: Trait + authorship::Trait> authorship::EventHandler<T::ValidatorId, T::BlockNumber> for Module<T> {
|
||||
fn note_author(author: T::ValidatorId) {
|
||||
Self::note_authorship(author);
|
||||
}
|
||||
|
||||
fn note_uncle(author: T::ValidatorId, _age: T::BlockNumber) {
|
||||
Self::note_authorship(author);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait> Module<T> {
|
||||
/// Returns `true` if a heartbeat has been received for the authority at
|
||||
/// `authority_index` in the authorities series or if the authority has
|
||||
/// authored at least one block, during the current session. Otherwise
|
||||
/// `false`.
|
||||
pub fn is_online(authority_index: AuthIndex) -> bool {
|
||||
let current_validators = <session::Module<T>>::validators();
|
||||
|
||||
if authority_index >= current_validators.len() as u32 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let authority = ¤t_validators[authority_index as usize];
|
||||
|
||||
Self::is_online_aux(authority_index, authority)
|
||||
}
|
||||
|
||||
fn is_online_aux(authority_index: AuthIndex, authority: &T::ValidatorId) -> bool {
|
||||
let current_session = <session::Module<T>>::current_index();
|
||||
|
||||
<ReceivedHeartbeats>::exists(¤t_session, &authority_index) ||
|
||||
<AuthoredBlocks<T>>::get(
|
||||
¤t_session,
|
||||
authority,
|
||||
) != 0
|
||||
}
|
||||
|
||||
/// Returns `true` if a heartbeat has been received for the authority at `authority_index` in
|
||||
/// the authorities series, during the current session. Otherwise `false`.
|
||||
pub fn received_heartbeat_in_current_session(authority_index: AuthIndex) -> bool {
|
||||
let current_session = <session::Module<T>>::current_index();
|
||||
<ReceivedHeartbeats>::exists(¤t_session, &authority_index)
|
||||
}
|
||||
|
||||
/// Note that the given authority has authored a block in the current session.
|
||||
fn note_authorship(author: T::ValidatorId) {
|
||||
let current_session = <session::Module<T>>::current_index();
|
||||
|
||||
<AuthoredBlocks<T>>::mutate(
|
||||
¤t_session,
|
||||
author,
|
||||
|authored| *authored += 1,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn offchain(now: T::BlockNumber) {
|
||||
let next_gossip = <GossipAt<T>>::get();
|
||||
let check = Self::check_not_yet_gossipped(now, next_gossip);
|
||||
let (curr_worker_status, not_yet_gossipped) = match check {
|
||||
Ok((s, v)) => (s, v),
|
||||
Err(err) => {
|
||||
print(err);
|
||||
return;
|
||||
},
|
||||
};
|
||||
if next_gossip < now && not_yet_gossipped {
|
||||
let value_set = Self::compare_and_set_worker_status(now, false, curr_worker_status);
|
||||
if !value_set {
|
||||
// value could not be set in local storage, since the value was
|
||||
// different from `curr_worker_status`. this indicates that
|
||||
// another worker was running in parallel.
|
||||
return;
|
||||
}
|
||||
|
||||
match Self::do_gossip_at(now) {
|
||||
Ok(_) => {},
|
||||
Err(err) => print(err),
|
||||
}
|
||||
} else {
|
||||
debug::native::debug!(
|
||||
target: "imonline",
|
||||
"Skipping gossip at: {:?} >= {:?} || {:?}",
|
||||
next_gossip,
|
||||
now,
|
||||
if not_yet_gossipped { "not gossipped" } else { "gossipped" }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn do_gossip_at(block_number: T::BlockNumber) -> Result<(), OffchainErr> {
|
||||
// we run only when a local authority key is configured
|
||||
let authorities = Keys::<T>::get();
|
||||
let mut results = Vec::new();
|
||||
let mut local_keys = T::AuthorityId::all();
|
||||
local_keys.sort();
|
||||
|
||||
for (authority_index, key) in authorities.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, authority)| {
|
||||
local_keys.binary_search(&authority)
|
||||
.ok()
|
||||
.map(|location| (index as u32, &local_keys[location]))
|
||||
})
|
||||
{
|
||||
if Self::is_online(authority_index) {
|
||||
debug::native::info!(
|
||||
target: "imonline",
|
||||
"[index: {:?}] Skipping sending heartbeat at block: {:?}. Already online.",
|
||||
authority_index,
|
||||
block_number
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let network_state = runtime_io::offchain::network_state()
|
||||
.map_err(|_| OffchainErr::NetworkState)?;
|
||||
let heartbeat_data = Heartbeat {
|
||||
block_number,
|
||||
network_state,
|
||||
session_index: <session::Module<T>>::current_index(),
|
||||
authority_index,
|
||||
};
|
||||
|
||||
let signature = key.sign(&heartbeat_data.encode()).ok_or(OffchainErr::FailedSigning)?;
|
||||
let call = Call::heartbeat(heartbeat_data, signature);
|
||||
|
||||
debug::info!(
|
||||
target: "imonline",
|
||||
"[index: {:?}] Reporting im-online at block: {:?}",
|
||||
authority_index,
|
||||
block_number
|
||||
);
|
||||
|
||||
results.push(
|
||||
T::SubmitTransaction::submit_unsigned(call)
|
||||
.map_err(|_| OffchainErr::SubmitTransaction)
|
||||
);
|
||||
}
|
||||
|
||||
// fail only after trying all keys.
|
||||
results.into_iter().collect::<Result<Vec<_>, OffchainErr>>()?;
|
||||
|
||||
// once finished we set the worker status without comparing
|
||||
// if the existing value changed in the meantime. this is
|
||||
// because at this point the heartbeat was definitely submitted.
|
||||
Self::set_worker_status(block_number, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compare_and_set_worker_status(
|
||||
gossipping_at: T::BlockNumber,
|
||||
done: bool,
|
||||
curr_worker_status: Option<Vec<u8>>,
|
||||
) -> bool {
|
||||
let enc = WorkerStatus {
|
||||
done,
|
||||
gossipping_at,
|
||||
};
|
||||
runtime_io::offchain::local_storage_compare_and_set(
|
||||
StorageKind::PERSISTENT,
|
||||
DB_KEY,
|
||||
curr_worker_status,
|
||||
&enc.encode()
|
||||
)
|
||||
}
|
||||
|
||||
fn set_worker_status(
|
||||
gossipping_at: T::BlockNumber,
|
||||
done: bool,
|
||||
) {
|
||||
let enc = WorkerStatus {
|
||||
done,
|
||||
gossipping_at,
|
||||
};
|
||||
runtime_io::offchain::local_storage_set(StorageKind::PERSISTENT, DB_KEY, &enc.encode());
|
||||
}
|
||||
|
||||
// Checks if a heartbeat gossip already occurred at this block number.
|
||||
// Returns a tuple of `(current worker status, bool)`, whereby the bool
|
||||
// is true if not yet gossipped.
|
||||
fn check_not_yet_gossipped(
|
||||
now: T::BlockNumber,
|
||||
next_gossip: T::BlockNumber,
|
||||
) -> Result<(Option<Vec<u8>>, bool), OffchainErr> {
|
||||
let last_gossip = runtime_io::offchain::local_storage_get(StorageKind::PERSISTENT, DB_KEY);
|
||||
match last_gossip {
|
||||
Some(last) => {
|
||||
let worker_status: WorkerStatus<T::BlockNumber> = Decode::decode(&mut &last[..])
|
||||
.map_err(|_| OffchainErr::DecodeWorkerStatus)?;
|
||||
|
||||
let was_aborted = !worker_status.done && worker_status.gossipping_at < now;
|
||||
|
||||
// another off-chain worker is currently in the process of submitting
|
||||
let already_submitting =
|
||||
!worker_status.done && worker_status.gossipping_at == now;
|
||||
|
||||
let not_yet_gossipped =
|
||||
worker_status.done && worker_status.gossipping_at < next_gossip;
|
||||
|
||||
let ret = (was_aborted && !already_submitting) || not_yet_gossipped;
|
||||
Ok((Some(last), ret))
|
||||
},
|
||||
None => Ok((None, true)),
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_keys(keys: &[T::AuthorityId]) {
|
||||
if !keys.is_empty() {
|
||||
assert!(Keys::<T>::get().is_empty(), "Keys are already initialized!");
|
||||
Keys::<T>::put(keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait> sr_primitives::BoundToRuntimeAppPublic for Module<T> {
|
||||
type Public = T::AuthorityId;
|
||||
}
|
||||
|
||||
impl<T: Trait> session::OneSessionHandler<T::AccountId> for Module<T> {
|
||||
type Key = T::AuthorityId;
|
||||
|
||||
fn on_genesis_session<'a, I: 'a>(validators: I)
|
||||
where I: Iterator<Item=(&'a T::AccountId, T::AuthorityId)>
|
||||
{
|
||||
let keys = validators.map(|x| x.1).collect::<Vec<_>>();
|
||||
Self::initialize_keys(&keys);
|
||||
}
|
||||
|
||||
fn on_new_session<'a, I: 'a>(_changed: bool, validators: I, _queued_validators: I)
|
||||
where I: Iterator<Item=(&'a T::AccountId, T::AuthorityId)>
|
||||
{
|
||||
// Tell the offchain worker to start making the next session's heartbeats.
|
||||
// Since we consider producing blocks as being online,
|
||||
// the hearbeat is defered a bit to prevent spaming.
|
||||
let block_number = <system::Module<T>>::block_number();
|
||||
let half_session = T::SessionDuration::get() / 2.into();
|
||||
<GossipAt<T>>::put(block_number + half_session);
|
||||
|
||||
// Remember who the authorities are for the new session.
|
||||
Keys::<T>::put(validators.map(|x| x.1).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
fn on_before_session_ending() {
|
||||
let session_index = <session::Module<T>>::current_index();
|
||||
let keys = Keys::<T>::get();
|
||||
let current_validators = <session::Module<T>>::validators();
|
||||
|
||||
let offenders = current_validators.into_iter().enumerate()
|
||||
.filter(|(index, id)|
|
||||
!Self::is_online_aux(*index as u32, id)
|
||||
).filter_map(|(_, id)|
|
||||
T::FullIdentificationOf::convert(id.clone()).map(|full_id| (id, full_id))
|
||||
).collect::<Vec<IdentificationTuple<T>>>();
|
||||
|
||||
// Remove all received heartbeats and number of authored blocks from the
|
||||
// current session, they have already been processed and won't be needed
|
||||
// anymore.
|
||||
<ReceivedHeartbeats>::remove_prefix(&<session::Module<T>>::current_index());
|
||||
<AuthoredBlocks<T>>::remove_prefix(&<session::Module<T>>::current_index());
|
||||
|
||||
if offenders.is_empty() {
|
||||
Self::deposit_event(RawEvent::AllGood);
|
||||
} else {
|
||||
Self::deposit_event(RawEvent::SomeOffline(offenders.clone()));
|
||||
|
||||
let validator_set_count = keys.len() as u32;
|
||||
let offence = UnresponsivenessOffence { session_index, validator_set_count, offenders };
|
||||
T::ReportUnresponsiveness::report_offence(vec![], offence);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_disabled(_i: usize) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T: Trait> support::unsigned::ValidateUnsigned for Module<T> {
|
||||
type Call = Call<T>;
|
||||
|
||||
fn validate_unsigned(call: &Self::Call) -> TransactionValidity {
|
||||
if let Call::heartbeat(heartbeat, signature) = call {
|
||||
if <Module<T>>::is_online(heartbeat.authority_index) {
|
||||
// we already received a heartbeat for this authority
|
||||
return InvalidTransaction::Stale.into();
|
||||
}
|
||||
|
||||
// check if session index from heartbeat is recent
|
||||
let current_session = <session::Module<T>>::current_index();
|
||||
if heartbeat.session_index != current_session {
|
||||
return InvalidTransaction::Stale.into();
|
||||
}
|
||||
|
||||
// verify that the incoming (unverified) pubkey is actually an authority id
|
||||
let keys = Keys::<T>::get();
|
||||
let authority_id = match keys.get(heartbeat.authority_index as usize) {
|
||||
Some(id) => id,
|
||||
None => return InvalidTransaction::BadProof.into(),
|
||||
};
|
||||
|
||||
// check signature (this is expensive so we do it last).
|
||||
let signature_valid = heartbeat.using_encoded(|encoded_heartbeat| {
|
||||
authority_id.verify(&encoded_heartbeat, &signature)
|
||||
});
|
||||
|
||||
if !signature_valid {
|
||||
return InvalidTransaction::BadProof.into();
|
||||
}
|
||||
|
||||
Ok(ValidTransaction {
|
||||
priority: TransactionPriority::max_value(),
|
||||
requires: vec![],
|
||||
provides: vec![(current_session, authority_id).encode()],
|
||||
longevity: TryInto::<u64>::try_into(T::SessionDuration::get() / 2.into()).unwrap_or(64_u64),
|
||||
propagate: true,
|
||||
})
|
||||
} else {
|
||||
InvalidTransaction::Call.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An offence that is filed if a validator didn't send a heartbeat message.
|
||||
#[derive(RuntimeDebug)]
|
||||
#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))]
|
||||
pub struct UnresponsivenessOffence<Offender> {
|
||||
/// The current session index in which we report the unresponsive validators.
|
||||
///
|
||||
/// It acts as a time measure for unresponsiveness reports and effectively will always point
|
||||
/// at the end of the session.
|
||||
session_index: SessionIndex,
|
||||
/// The size of the validator set in current session/era.
|
||||
validator_set_count: u32,
|
||||
/// Authorities that were unresponsive during the current era.
|
||||
offenders: Vec<Offender>,
|
||||
}
|
||||
|
||||
impl<Offender: Clone> Offence<Offender> for UnresponsivenessOffence<Offender> {
|
||||
const ID: Kind = *b"im-online:offlin";
|
||||
type TimeSlot = SessionIndex;
|
||||
|
||||
fn offenders(&self) -> Vec<Offender> {
|
||||
self.offenders.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.session_index
|
||||
}
|
||||
|
||||
fn slash_fraction(offenders: u32, validator_set_count: u32) -> Perbill {
|
||||
// the formula is min((3 * (k - 1)) / n, 1) * 0.05
|
||||
let x = Perbill::from_rational_approximation(3 * (offenders - 1), validator_set_count);
|
||||
x.saturating_mul(Perbill::from_percent(5))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// Copyright 2019 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Test utilities
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use crate::{Module, Trait};
|
||||
use sr_primitives::Perbill;
|
||||
use sr_staking_primitives::{SessionIndex, offence::ReportOffence};
|
||||
use sr_primitives::testing::{Header, UintAuthorityId, TestXt};
|
||||
use sr_primitives::traits::{IdentityLookup, BlakeTwo256, ConvertInto};
|
||||
use primitives::H256;
|
||||
use support::{impl_outer_origin, impl_outer_dispatch, parameter_types};
|
||||
use {runtime_io, system};
|
||||
|
||||
impl_outer_origin!{
|
||||
pub enum Origin for Runtime {}
|
||||
}
|
||||
|
||||
impl_outer_dispatch! {
|
||||
pub enum Call for Runtime where origin: Origin {
|
||||
imonline::ImOnline,
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub static VALIDATORS: RefCell<Option<Vec<u64>>> = RefCell::new(Some(vec![1, 2, 3]));
|
||||
}
|
||||
|
||||
pub struct TestOnSessionEnding;
|
||||
impl session::OnSessionEnding<u64> for TestOnSessionEnding {
|
||||
fn on_session_ending(_ending_index: SessionIndex, _will_apply_at: SessionIndex)
|
||||
-> Option<Vec<u64>>
|
||||
{
|
||||
VALIDATORS.with(|l| l.borrow_mut().take())
|
||||
}
|
||||
}
|
||||
|
||||
impl session::historical::OnSessionEnding<u64, u64> for TestOnSessionEnding {
|
||||
fn on_session_ending(_ending_index: SessionIndex, _will_apply_at: SessionIndex)
|
||||
-> Option<(Vec<u64>, Vec<(u64, u64)>)>
|
||||
{
|
||||
VALIDATORS.with(|l| l
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.map(|validators| {
|
||||
let full_identification = validators.iter().map(|v| (*v, *v)).collect();
|
||||
(validators, full_identification)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// An extrinsic type used for tests.
|
||||
pub type Extrinsic = TestXt<Call, ()>;
|
||||
type SubmitTransaction = system::offchain::TransactionSubmitter<(), Call, Extrinsic>;
|
||||
type IdentificationTuple = (u64, u64);
|
||||
type Offence = crate::UnresponsivenessOffence<IdentificationTuple>;
|
||||
|
||||
thread_local! {
|
||||
pub static OFFENCES: RefCell<Vec<(Vec<u64>, Offence)>> = RefCell::new(vec![]);
|
||||
}
|
||||
|
||||
/// A mock offence report handler.
|
||||
pub struct OffenceHandler;
|
||||
impl ReportOffence<u64, IdentificationTuple, Offence> for OffenceHandler {
|
||||
fn report_offence(reporters: Vec<u64>, offence: Offence) {
|
||||
OFFENCES.with(|l| l.borrow_mut().push((reporters, offence)));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_test_ext() -> runtime_io::TestExternalities {
|
||||
let t = system::GenesisConfig::default().build_storage::<Runtime>().unwrap();
|
||||
t.into()
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Runtime;
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
pub const MaximumBlockWeight: u32 = 1024;
|
||||
pub const MaximumBlockLength: u32 = 2 * 1024;
|
||||
pub const AvailableBlockRatio: Perbill = Perbill::one();
|
||||
}
|
||||
|
||||
impl system::Trait for Runtime {
|
||||
type Origin = Origin;
|
||||
type Index = u64;
|
||||
type BlockNumber = u64;
|
||||
type Call = Call;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = u64;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type Event = ();
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type MaximumBlockWeight = MaximumBlockWeight;
|
||||
type MaximumBlockLength = MaximumBlockLength;
|
||||
type AvailableBlockRatio = AvailableBlockRatio;
|
||||
type Version = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const Period: u64 = 1;
|
||||
pub const Offset: u64 = 0;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(33);
|
||||
}
|
||||
|
||||
impl session::Trait for Runtime {
|
||||
type ShouldEndSession = session::PeriodicSessions<Period, Offset>;
|
||||
type OnSessionEnding = session::historical::NoteHistoricalRoot<Runtime, TestOnSessionEnding>;
|
||||
type SessionHandler = (ImOnline, );
|
||||
type ValidatorId = u64;
|
||||
type ValidatorIdOf = ConvertInto;
|
||||
type Keys = UintAuthorityId;
|
||||
type Event = ();
|
||||
type SelectInitialValidators = ();
|
||||
type DisabledValidatorsThreshold = DisabledValidatorsThreshold;
|
||||
}
|
||||
|
||||
impl session::historical::Trait for Runtime {
|
||||
type FullIdentification = u64;
|
||||
type FullIdentificationOf = ConvertInto;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const UncleGenerations: u32 = 5;
|
||||
}
|
||||
|
||||
impl authorship::Trait for Runtime {
|
||||
type FindAuthor = ();
|
||||
type UncleGenerations = UncleGenerations;
|
||||
type FilterUncle = ();
|
||||
type EventHandler = ImOnline;
|
||||
}
|
||||
|
||||
impl Trait for Runtime {
|
||||
type AuthorityId = UintAuthorityId;
|
||||
type Event = ();
|
||||
type Call = Call;
|
||||
type SubmitTransaction = SubmitTransaction;
|
||||
type ReportUnresponsiveness = OffenceHandler;
|
||||
type SessionDuration = Period;
|
||||
}
|
||||
|
||||
/// Im Online module.
|
||||
pub type ImOnline = Module<Runtime>;
|
||||
pub type System = system::Module<Runtime>;
|
||||
pub type Session = session::Module<Runtime>;
|
||||
|
||||
pub fn advance_session() {
|
||||
let now = System::block_number();
|
||||
System::set_block_number(now + 1);
|
||||
Session::rotate_session();
|
||||
assert_eq!(Session::current_index(), (now / Period::get()) as u32);
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// Copyright 2019 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Tests for the im-online module.
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use super::*;
|
||||
use crate::mock::*;
|
||||
use primitives::offchain::{
|
||||
OpaquePeerId,
|
||||
OffchainExt,
|
||||
TransactionPoolExt,
|
||||
testing::{TestOffchainExt, TestTransactionPoolExt},
|
||||
};
|
||||
use support::{dispatch, assert_noop};
|
||||
use sr_primitives::testing::UintAuthorityId;
|
||||
|
||||
#[test]
|
||||
fn test_unresponsiveness_slash_fraction() {
|
||||
// A single case of unresponsiveness is not slashed.
|
||||
assert_eq!(
|
||||
UnresponsivenessOffence::<()>::slash_fraction(1, 50),
|
||||
Perbill::zero(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
UnresponsivenessOffence::<()>::slash_fraction(3, 50),
|
||||
Perbill::from_parts(6000000), // 0.6%
|
||||
);
|
||||
|
||||
// One third offline should be punished around 5%.
|
||||
assert_eq!(
|
||||
UnresponsivenessOffence::<()>::slash_fraction(17, 50),
|
||||
Perbill::from_parts(48000000), // 4.8%
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_report_offline_validators() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// given
|
||||
let block = 1;
|
||||
System::set_block_number(block);
|
||||
// buffer new validators
|
||||
Session::rotate_session();
|
||||
// enact the change and buffer another one
|
||||
let validators = vec![1, 2, 3, 4, 5, 6];
|
||||
VALIDATORS.with(|l| *l.borrow_mut() = Some(validators.clone()));
|
||||
Session::rotate_session();
|
||||
|
||||
// when
|
||||
// we end current session and start the next one
|
||||
Session::rotate_session();
|
||||
|
||||
// then
|
||||
let offences = OFFENCES.with(|l| l.replace(vec![]));
|
||||
assert_eq!(offences, vec![
|
||||
(vec![], UnresponsivenessOffence {
|
||||
session_index: 2,
|
||||
validator_set_count: 3,
|
||||
offenders: vec![
|
||||
(1, 1),
|
||||
(2, 2),
|
||||
(3, 3),
|
||||
],
|
||||
})
|
||||
]);
|
||||
|
||||
// should not report when heartbeat is sent
|
||||
for (idx, v) in validators.into_iter().take(4).enumerate() {
|
||||
let _ = heartbeat(block, 3, idx as u32, v.into()).unwrap();
|
||||
}
|
||||
Session::rotate_session();
|
||||
|
||||
// then
|
||||
let offences = OFFENCES.with(|l| l.replace(vec![]));
|
||||
assert_eq!(offences, vec![
|
||||
(vec![], UnresponsivenessOffence {
|
||||
session_index: 3,
|
||||
validator_set_count: 6,
|
||||
offenders: vec![
|
||||
(5, 5),
|
||||
(6, 6),
|
||||
],
|
||||
})
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
fn heartbeat(
|
||||
block_number: u64,
|
||||
session_index: u32,
|
||||
authority_index: u32,
|
||||
id: UintAuthorityId,
|
||||
) -> dispatch::Result {
|
||||
#[allow(deprecated)]
|
||||
use support::unsigned::ValidateUnsigned;
|
||||
|
||||
let heartbeat = Heartbeat {
|
||||
block_number,
|
||||
network_state: OpaqueNetworkState {
|
||||
peer_id: OpaquePeerId(vec![1]),
|
||||
external_addresses: vec![],
|
||||
},
|
||||
session_index,
|
||||
authority_index,
|
||||
};
|
||||
let signature = id.sign(&heartbeat.encode()).unwrap();
|
||||
|
||||
#[allow(deprecated)] // Allow ValidateUnsigned
|
||||
ImOnline::pre_dispatch(&crate::Call::heartbeat(heartbeat.clone(), signature.clone()))?;
|
||||
ImOnline::heartbeat(
|
||||
Origin::system(system::RawOrigin::None),
|
||||
heartbeat,
|
||||
signature
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_mark_online_validator_when_heartbeat_is_received() {
|
||||
new_test_ext().execute_with(|| {
|
||||
advance_session();
|
||||
// given
|
||||
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3, 4, 5, 6]));
|
||||
assert_eq!(Session::validators(), Vec::<u64>::new());
|
||||
// enact the change and buffer another one
|
||||
advance_session();
|
||||
|
||||
assert_eq!(Session::current_index(), 2);
|
||||
assert_eq!(Session::validators(), vec![1, 2, 3]);
|
||||
|
||||
assert!(!ImOnline::is_online(0));
|
||||
assert!(!ImOnline::is_online(1));
|
||||
assert!(!ImOnline::is_online(2));
|
||||
|
||||
// when
|
||||
let _ = heartbeat(1, 2, 0, 1.into()).unwrap();
|
||||
|
||||
// then
|
||||
assert!(ImOnline::is_online(0));
|
||||
assert!(!ImOnline::is_online(1));
|
||||
assert!(!ImOnline::is_online(2));
|
||||
|
||||
// and when
|
||||
let _ = heartbeat(1, 2, 2, 3.into()).unwrap();
|
||||
|
||||
// then
|
||||
assert!(ImOnline::is_online(0));
|
||||
assert!(!ImOnline::is_online(1));
|
||||
assert!(ImOnline::is_online(2));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_heartbeat_should_fail() {
|
||||
new_test_ext().execute_with(|| {
|
||||
advance_session();
|
||||
// given
|
||||
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 4, 4, 5, 6]));
|
||||
assert_eq!(Session::validators(), Vec::<u64>::new());
|
||||
// enact the change and buffer another one
|
||||
advance_session();
|
||||
|
||||
assert_eq!(Session::current_index(), 2);
|
||||
assert_eq!(Session::validators(), vec![1, 2, 3]);
|
||||
|
||||
// when
|
||||
assert_noop!(heartbeat(1, 3, 0, 1.into()), "Transaction is outdated");
|
||||
assert_noop!(heartbeat(1, 1, 0, 1.into()), "Transaction is outdated");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_generate_heartbeats() {
|
||||
let mut ext = new_test_ext();
|
||||
let (offchain, _state) = TestOffchainExt::new();
|
||||
let (pool, state) = TestTransactionPoolExt::new();
|
||||
ext.register_extension(OffchainExt::new(offchain));
|
||||
ext.register_extension(TransactionPoolExt::new(pool));
|
||||
|
||||
ext.execute_with(|| {
|
||||
// given
|
||||
let block = 1;
|
||||
System::set_block_number(block);
|
||||
// buffer new validators
|
||||
Session::rotate_session();
|
||||
// enact the change and buffer another one
|
||||
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3, 4, 5, 6]));
|
||||
Session::rotate_session();
|
||||
|
||||
// when
|
||||
UintAuthorityId::set_all_keys(vec![0, 1, 2]);
|
||||
ImOnline::offchain(2);
|
||||
|
||||
// then
|
||||
let transaction = state.write().transactions.pop().unwrap();
|
||||
// All validators have `0` as their session key, so we generate 3 transactions.
|
||||
assert_eq!(state.read().transactions.len(), 2);
|
||||
// check stuff about the transaction.
|
||||
let ex: Extrinsic = Decode::decode(&mut &*transaction).unwrap();
|
||||
let heartbeat = match ex.1 {
|
||||
crate::mock::Call::ImOnline(crate::Call::heartbeat(h, _)) => h,
|
||||
e => panic!("Unexpected call: {:?}", e),
|
||||
};
|
||||
|
||||
assert_eq!(heartbeat, Heartbeat {
|
||||
block_number: 2,
|
||||
network_state: runtime_io::offchain::network_state().unwrap(),
|
||||
session_index: 2,
|
||||
authority_index: 2,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_cleanup_received_heartbeats_on_session_end() {
|
||||
new_test_ext().execute_with(|| {
|
||||
advance_session();
|
||||
|
||||
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3]));
|
||||
assert_eq!(Session::validators(), Vec::<u64>::new());
|
||||
|
||||
// enact the change and buffer another one
|
||||
advance_session();
|
||||
|
||||
assert_eq!(Session::current_index(), 2);
|
||||
assert_eq!(Session::validators(), vec![1, 2, 3]);
|
||||
|
||||
// send an heartbeat from authority id 0 at session 2
|
||||
let _ = heartbeat(1, 2, 0, 1.into()).unwrap();
|
||||
|
||||
// the heartbeat is stored
|
||||
assert!(!ImOnline::received_heartbeats(&2, &0).is_none());
|
||||
|
||||
advance_session();
|
||||
|
||||
// after the session has ended we have already processed the heartbeat
|
||||
// message, so any messages received on the previous session should have
|
||||
// been pruned.
|
||||
assert!(ImOnline::received_heartbeats(&2, &0).is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_mark_online_validator_when_block_is_authored() {
|
||||
use authorship::EventHandler;
|
||||
|
||||
new_test_ext().execute_with(|| {
|
||||
advance_session();
|
||||
// given
|
||||
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3, 4, 5, 6]));
|
||||
assert_eq!(Session::validators(), Vec::<u64>::new());
|
||||
// enact the change and buffer another one
|
||||
advance_session();
|
||||
|
||||
assert_eq!(Session::current_index(), 2);
|
||||
assert_eq!(Session::validators(), vec![1, 2, 3]);
|
||||
|
||||
for i in 0..3 {
|
||||
assert!(!ImOnline::is_online(i));
|
||||
}
|
||||
|
||||
// when
|
||||
ImOnline::note_author(1);
|
||||
ImOnline::note_uncle(2, 0);
|
||||
|
||||
// then
|
||||
assert!(ImOnline::is_online(0));
|
||||
assert!(ImOnline::is_online(1));
|
||||
assert!(!ImOnline::is_online(2));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_send_a_report_if_already_online() {
|
||||
use authorship::EventHandler;
|
||||
|
||||
let mut ext = new_test_ext();
|
||||
let (offchain, _state) = TestOffchainExt::new();
|
||||
let (pool, pool_state) = TestTransactionPoolExt::new();
|
||||
ext.register_extension(OffchainExt::new(offchain));
|
||||
ext.register_extension(TransactionPoolExt::new(pool));
|
||||
|
||||
ext.execute_with(|| {
|
||||
advance_session();
|
||||
// given
|
||||
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3, 4, 5, 6]));
|
||||
assert_eq!(Session::validators(), Vec::<u64>::new());
|
||||
// enact the change and buffer another one
|
||||
advance_session();
|
||||
assert_eq!(Session::current_index(), 2);
|
||||
assert_eq!(Session::validators(), vec![1, 2, 3]);
|
||||
ImOnline::note_author(2);
|
||||
ImOnline::note_uncle(3, 0);
|
||||
|
||||
// when
|
||||
UintAuthorityId::set_all_keys(vec![0]); // all authorities use session key 0
|
||||
ImOnline::offchain(4);
|
||||
|
||||
// then
|
||||
let transaction = pool_state.write().transactions.pop().unwrap();
|
||||
// All validators have `0` as their session key, but we should only produce 1 hearbeat.
|
||||
assert_eq!(pool_state.read().transactions.len(), 0);
|
||||
// check stuff about the transaction.
|
||||
let ex: Extrinsic = Decode::decode(&mut &*transaction).unwrap();
|
||||
let heartbeat = match ex.1 {
|
||||
crate::mock::Call::ImOnline(crate::Call::heartbeat(h, _)) => h,
|
||||
e => panic!("Unexpected call: {:?}", e),
|
||||
};
|
||||
|
||||
assert_eq!(heartbeat, Heartbeat {
|
||||
block_number: 4,
|
||||
network_state: runtime_io::offchain::network_state().unwrap(),
|
||||
session_index: 2,
|
||||
authority_index: 0,
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user