#![cfg_attr(not(feature = "std"), no_std)]
mod benchmarking;
pub mod migration;
mod mock;
mod tests;
pub mod weights;
use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::{
pallet_prelude::*,
traits::{
EstimateNextSessionRotation, Get, OneSessionHandler, ValidatorSet,
ValidatorSetWithIdentification,
},
BoundedSlice, WeakBoundedVec,
};
use frame_system::{
offchain::{SendTransactionTypes, SubmitTransaction},
pallet_prelude::*,
};
pub use pallet::*;
use scale_info::TypeInfo;
use sp_application_crypto::RuntimeAppPublic;
use sp_runtime::{
offchain::storage::{MutateStorageError, StorageRetrievalError, StorageValueRef},
traits::{AtLeast32BitUnsigned, Convert, Saturating, TrailingZeroInput},
PerThing, Perbill, Permill, RuntimeDebug, SaturatedConversion,
};
use sp_staking::{
offence::{DisableStrategy, Kind, Offence, ReportOffence},
SessionIndex,
};
use sp_std::prelude::*;
pub use weights::WeightInfo;
pub mod sr25519 {
mod app_sr25519 {
use sp_application_crypto::{app_crypto, key_types::IM_ONLINE, sr25519};
app_crypto!(sr25519, IM_ONLINE);
}
sp_application_crypto::with_pair! {
pub type AuthorityPair = app_sr25519::Pair;
}
pub type AuthoritySignature = app_sr25519::Signature;
pub type AuthorityId = app_sr25519::Public;
}
pub mod ed25519 {
mod app_ed25519 {
use sp_application_crypto::{app_crypto, ed25519, key_types::IM_ONLINE};
app_crypto!(ed25519, IM_ONLINE);
}
sp_application_crypto::with_pair! {
pub type AuthorityPair = app_ed25519::Pair;
}
pub type AuthoritySignature = app_ed25519::Signature;
pub type AuthorityId = app_ed25519::Public;
}
const DB_PREFIX: &[u8] = b"parity/im-online-heartbeat/";
const INCLUDE_THRESHOLD: u32 = 3;
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
struct HeartbeatStatus<BlockNumber> {
pub session_index: SessionIndex,
pub sent_at: BlockNumber,
}
impl<BlockNumber: PartialEq + AtLeast32BitUnsigned + Copy> HeartbeatStatus<BlockNumber> {
fn is_recent(&self, session_index: SessionIndex, now: BlockNumber) -> bool {
self.session_index == session_index && self.sent_at + INCLUDE_THRESHOLD.into() > now
}
}
#[cfg_attr(test, derive(PartialEq))]
enum OffchainErr<BlockNumber> {
TooEarly,
WaitingForInclusion(BlockNumber),
AlreadyOnline(u32),
FailedSigning,
FailedToAcquireLock,
SubmitTransaction,
}
impl<BlockNumber: sp_std::fmt::Debug> sp_std::fmt::Debug for OffchainErr<BlockNumber> {
fn fmt(&self, fmt: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
match *self {
OffchainErr::TooEarly => write!(fmt, "Too early to send heartbeat."),
OffchainErr::WaitingForInclusion(ref block) => {
write!(fmt, "Heartbeat already sent at {:?}. Waiting for inclusion.", block)
},
OffchainErr::AlreadyOnline(auth_idx) => {
write!(fmt, "Authority {} is already online", auth_idx)
},
OffchainErr::FailedSigning => write!(fmt, "Failed to sign heartbeat"),
OffchainErr::FailedToAcquireLock => write!(fmt, "Failed to acquire lock"),
OffchainErr::SubmitTransaction => write!(fmt, "Failed to submit transaction"),
}
}
}
pub type AuthIndex = u32;
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub struct Heartbeat<BlockNumber>
where
BlockNumber: PartialEq + Eq + Decode + Encode,
{
pub block_number: BlockNumber,
pub session_index: SessionIndex,
pub authority_index: AuthIndex,
pub validators_len: u32,
}
pub type ValidatorId<T> = <<T as Config>::ValidatorSet as ValidatorSet<
<T as frame_system::Config>::AccountId,
>>::ValidatorId;
pub type IdentificationTuple<T> = (
ValidatorId<T>,
<<T as Config>::ValidatorSet as ValidatorSetWithIdentification<
<T as frame_system::Config>::AccountId,
>>::Identification,
);
type OffchainResult<T, A> = Result<A, OffchainErr<BlockNumberFor<T>>>;
#[frame_support::pallet]
pub mod pallet {
use super::*;
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: SendTransactionTypes<Call<Self>> + frame_system::Config {
type AuthorityId: Member
+ Parameter
+ RuntimeAppPublic
+ Ord
+ MaybeSerializeDeserialize
+ MaxEncodedLen;
type MaxKeys: Get<u32>;
type MaxPeerInHeartbeats: Get<u32>;
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type ValidatorSet: ValidatorSetWithIdentification<Self::AccountId>;
type NextSessionRotation: EstimateNextSessionRotation<BlockNumberFor<Self>>;
type ReportUnresponsiveness: ReportOffence<
Self::AccountId,
IdentificationTuple<Self>,
UnresponsivenessOffence<IdentificationTuple<Self>>,
>;
#[pallet::constant]
type UnsignedPriority: Get<TransactionPriority>;
type WeightInfo: WeightInfo;
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
HeartbeatReceived { authority_id: T::AuthorityId },
AllGood,
SomeOffline { offline: Vec<IdentificationTuple<T>> },
}
#[pallet::error]
pub enum Error<T> {
InvalidKey,
DuplicatedHeartbeat,
}
#[pallet::storage]
#[pallet::getter(fn heartbeat_after)]
pub(super) type HeartbeatAfter<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn keys)]
pub(super) type Keys<T: Config> =
StorageValue<_, WeakBoundedVec<T::AuthorityId, T::MaxKeys>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn received_heartbeats)]
pub(super) type ReceivedHeartbeats<T: Config> =
StorageDoubleMap<_, Twox64Concat, SessionIndex, Twox64Concat, AuthIndex, bool>;
#[pallet::storage]
#[pallet::getter(fn authored_blocks)]
pub(super) type AuthoredBlocks<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
SessionIndex,
Twox64Concat,
ValidatorId<T>,
u32,
ValueQuery,
>;
#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
pub keys: Vec<T::AuthorityId>,
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
Pallet::<T>::initialize_keys(&self.keys);
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(<T as Config>::WeightInfo::validate_unsigned_and_then_heartbeat(
heartbeat.validators_len,
))]
pub fn heartbeat(
origin: OriginFor<T>,
heartbeat: Heartbeat<BlockNumberFor<T>>,
_signature: <T::AuthorityId as RuntimeAppPublic>::Signature,
) -> DispatchResult {
ensure_none(origin)?;
let current_session = T::ValidatorSet::session_index();
let exists =
ReceivedHeartbeats::<T>::contains_key(current_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 { authority_id: public.clone() });
ReceivedHeartbeats::<T>::insert(current_session, heartbeat.authority_index, true);
Ok(())
} else if exists {
Err(Error::<T>::DuplicatedHeartbeat.into())
} else {
Err(Error::<T>::InvalidKey.into())
}
}
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn offchain_worker(now: BlockNumberFor<T>) {
if sp_io::offchain::is_validator() {
for res in Self::send_heartbeats(now).into_iter().flatten() {
if let Err(e) = res {
log::debug!(
target: "runtime::im-online",
"Skipping heartbeat at {:?}: {:?}",
now,
e,
)
}
}
} else {
log::trace!(
target: "runtime::im-online",
"Skipping heartbeat at {:?}. Not a validator.",
now,
)
}
}
}
pub(crate) const INVALID_VALIDATORS_LEN: u8 = 10;
#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
if let Call::heartbeat { heartbeat, signature } = call {
if <Pallet<T>>::is_online(heartbeat.authority_index) {
return InvalidTransaction::Stale.into()
}
let current_session = T::ValidatorSet::session_index();
if heartbeat.session_index != current_session {
return InvalidTransaction::Stale.into()
}
let keys = Keys::<T>::get();
if keys.len() as u32 != heartbeat.validators_len {
return InvalidTransaction::Custom(INVALID_VALIDATORS_LEN).into()
}
let authority_id = match keys.get(heartbeat.authority_index as usize) {
Some(id) => id,
None => return InvalidTransaction::BadProof.into(),
};
let signature_valid = heartbeat.using_encoded(|encoded_heartbeat| {
authority_id.verify(&encoded_heartbeat, signature)
});
if !signature_valid {
return InvalidTransaction::BadProof.into()
}
ValidTransaction::with_tag_prefix("ImOnline")
.priority(T::UnsignedPriority::get())
.and_provides((current_session, authority_id))
.longevity(
TryInto::<u64>::try_into(
T::NextSessionRotation::average_session_length() / 2u32.into(),
)
.unwrap_or(64_u64),
)
.propagate(true)
.build()
} else {
InvalidTransaction::Call.into()
}
}
}
}
impl<T: Config + pallet_authorship::Config>
pallet_authorship::EventHandler<ValidatorId<T>, BlockNumberFor<T>> for Pallet<T>
{
fn note_author(author: ValidatorId<T>) {
Self::note_authorship(author);
}
}
impl<T: Config> Pallet<T> {
pub fn is_online(authority_index: AuthIndex) -> bool {
let current_validators = T::ValidatorSet::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: &ValidatorId<T>) -> bool {
let current_session = T::ValidatorSet::session_index();
ReceivedHeartbeats::<T>::contains_key(current_session, authority_index) ||
AuthoredBlocks::<T>::get(current_session, authority) != 0
}
pub fn received_heartbeat_in_current_session(authority_index: AuthIndex) -> bool {
let current_session = T::ValidatorSet::session_index();
ReceivedHeartbeats::<T>::contains_key(current_session, authority_index)
}
fn note_authorship(author: ValidatorId<T>) {
let current_session = T::ValidatorSet::session_index();
AuthoredBlocks::<T>::mutate(current_session, author, |authored| *authored += 1);
}
pub(crate) fn send_heartbeats(
block_number: BlockNumberFor<T>,
) -> OffchainResult<T, impl Iterator<Item = OffchainResult<T, ()>>> {
const START_HEARTBEAT_RANDOM_PERIOD: Permill = Permill::from_percent(10);
const START_HEARTBEAT_FINAL_PERIOD: Permill = Permill::from_percent(80);
let random_choice = |progress: Permill| {
let session_length = T::NextSessionRotation::average_session_length();
let residual = Permill::from_rational(1u32, session_length.saturated_into());
let threshold: Permill = progress.saturating_pow(6).saturating_add(residual);
let seed = sp_io::offchain::random_seed();
let random = <u32>::decode(&mut TrailingZeroInput::new(seed.as_ref()))
.expect("input is padded with zeroes; qed");
let random = Permill::from_parts(random % Permill::ACCURACY);
random <= threshold
};
let should_heartbeat = if let (Some(progress), _) =
T::NextSessionRotation::estimate_current_session_progress(block_number)
{
progress >= START_HEARTBEAT_FINAL_PERIOD ||
progress >= START_HEARTBEAT_RANDOM_PERIOD && random_choice(progress)
} else {
let heartbeat_after = <HeartbeatAfter<T>>::get();
block_number >= heartbeat_after
};
if !should_heartbeat {
return Err(OffchainErr::TooEarly)
}
let session_index = T::ValidatorSet::session_index();
let validators_len = Keys::<T>::decode_len().unwrap_or_default() as u32;
Ok(Self::local_authority_keys().map(move |(authority_index, key)| {
Self::send_single_heartbeat(
authority_index,
key,
session_index,
block_number,
validators_len,
)
}))
}
fn send_single_heartbeat(
authority_index: u32,
key: T::AuthorityId,
session_index: SessionIndex,
block_number: BlockNumberFor<T>,
validators_len: u32,
) -> OffchainResult<T, ()> {
let prepare_heartbeat = || -> OffchainResult<T, Call<T>> {
let heartbeat =
Heartbeat { block_number, session_index, authority_index, validators_len };
let signature = key.sign(&heartbeat.encode()).ok_or(OffchainErr::FailedSigning)?;
Ok(Call::heartbeat { heartbeat, signature })
};
if Self::is_online(authority_index) {
return Err(OffchainErr::AlreadyOnline(authority_index))
}
Self::with_heartbeat_lock(authority_index, session_index, block_number, || {
let call = prepare_heartbeat()?;
log::info!(
target: "runtime::im-online",
"[index: {:?}] Reporting im-online at block: {:?} (session: {:?}): {:?}",
authority_index,
block_number,
session_index,
call,
);
SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call.into())
.map_err(|_| OffchainErr::SubmitTransaction)?;
Ok(())
})
}
fn local_authority_keys() -> impl Iterator<Item = (u32, T::AuthorityId)> {
let authorities = Keys::<T>::get();
let mut local_keys = T::AuthorityId::all();
local_keys.sort();
authorities.into_iter().enumerate().filter_map(move |(index, authority)| {
local_keys
.binary_search(&authority)
.ok()
.map(|location| (index as u32, local_keys[location].clone()))
})
}
fn with_heartbeat_lock<R>(
authority_index: u32,
session_index: SessionIndex,
now: BlockNumberFor<T>,
f: impl FnOnce() -> OffchainResult<T, R>,
) -> OffchainResult<T, R> {
let key = {
let mut key = DB_PREFIX.to_vec();
key.extend(authority_index.encode());
key
};
let storage = StorageValueRef::persistent(&key);
let res = storage.mutate(
|status: Result<Option<HeartbeatStatus<BlockNumberFor<T>>>, StorageRetrievalError>| {
match status {
Ok(Some(status)) if status.is_recent(session_index, now) =>
Err(OffchainErr::WaitingForInclusion(status.sent_at)),
_ => Ok(HeartbeatStatus { session_index, sent_at: now }),
}
},
);
if let Err(MutateStorageError::ValueFunctionFailed(err)) = res {
return Err(err)
}
let mut new_status = res.map_err(|_| OffchainErr::FailedToAcquireLock)?;
let res = f();
if res.is_err() {
new_status.sent_at = 0u32.into();
storage.set(&new_status);
}
res
}
fn initialize_keys(keys: &[T::AuthorityId]) {
if !keys.is_empty() {
assert!(Keys::<T>::get().is_empty(), "Keys are already initialized!");
let bounded_keys = <BoundedSlice<'_, _, T::MaxKeys>>::try_from(keys)
.expect("More than the maximum number of keys provided");
Keys::<T>::put(bounded_keys);
}
}
#[cfg(test)]
fn set_keys(keys: Vec<T::AuthorityId>) {
let bounded_keys = WeakBoundedVec::<_, T::MaxKeys>::try_from(keys)
.expect("More than the maximum number of keys provided");
Keys::<T>::put(bounded_keys);
}
}
impl<T: Config> sp_runtime::BoundToRuntimeAppPublic for Pallet<T> {
type Public = T::AuthorityId;
}
impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<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)>,
{
let block_number = <frame_system::Pallet<T>>::block_number();
let half_session = T::NextSessionRotation::average_session_length() / 2u32.into();
<HeartbeatAfter<T>>::put(block_number + half_session);
let keys = validators.map(|x| x.1).collect::<Vec<_>>();
let bounded_keys = WeakBoundedVec::<_, T::MaxKeys>::force_from(
keys,
Some(
"Warning: The session has more keys than expected. \
A runtime configuration adjustment may be needed.",
),
);
Keys::<T>::put(bounded_keys);
}
fn on_before_session_ending() {
let session_index = T::ValidatorSet::session_index();
let keys = Keys::<T>::get();
let current_validators = T::ValidatorSet::validators();
let offenders = current_validators
.into_iter()
.enumerate()
.filter(|(index, id)| !Self::is_online_aux(*index as u32, id))
.filter_map(|(_, id)| {
<T::ValidatorSet as ValidatorSetWithIdentification<T::AccountId>>::IdentificationOf::convert(
id.clone()
).map(|full_id| (id, full_id))
})
.collect::<Vec<IdentificationTuple<T>>>();
#[allow(deprecated)]
ReceivedHeartbeats::<T>::remove_prefix(T::ValidatorSet::session_index(), None);
#[allow(deprecated)]
AuthoredBlocks::<T>::remove_prefix(T::ValidatorSet::session_index(), None);
if offenders.is_empty() {
Self::deposit_event(Event::<T>::AllGood);
} else {
Self::deposit_event(Event::<T>::SomeOffline { offline: offenders.clone() });
let validator_set_count = keys.len() as u32;
let offence = UnresponsivenessOffence { session_index, validator_set_count, offenders };
if let Err(e) = T::ReportUnresponsiveness::report_offence(vec![], offence) {
sp_runtime::print(e);
}
}
}
fn on_disabled(_i: u32) {
}
}
#[derive(RuntimeDebug, TypeInfo)]
#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))]
pub struct UnresponsivenessOffence<Offender> {
pub session_index: SessionIndex,
pub validator_set_count: u32,
pub 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 disable_strategy(&self) -> DisableStrategy {
DisableStrategy::Never
}
fn slash_fraction(&self, offenders: u32) -> Perbill {
if let Some(threshold) = offenders.checked_sub(self.validator_set_count / 10 + 1) {
let x = Perbill::from_rational(3 * threshold, self.validator_set_count);
x.saturating_mul(Perbill::from_percent(7))
} else {
Perbill::default()
}
}
}