1#![cfg_attr(not(feature = "std"), no_std)]
78
79mod benchmarking;
80pub mod migration;
81mod mock;
82mod tests;
83pub mod weights;
84
85extern crate alloc;
86
87use alloc::{vec, vec::Vec};
88use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
89use frame_support::{
90 pallet_prelude::*,
91 traits::{
92 EstimateNextSessionRotation, Get, OneSessionHandler, ValidatorSet,
93 ValidatorSetWithIdentification,
94 },
95 BoundedSlice, WeakBoundedVec,
96};
97use frame_system::{
98 offchain::{CreateBare, SubmitTransaction},
99 pallet_prelude::*,
100};
101pub use pallet::*;
102use scale_info::TypeInfo;
103use sp_application_crypto::RuntimeAppPublic;
104use sp_runtime::{
105 offchain::storage::{MutateStorageError, StorageRetrievalError, StorageValueRef},
106 traits::{AtLeast32BitUnsigned, Convert, Saturating, TrailingZeroInput},
107 PerThing, Perbill, Permill, RuntimeDebug, SaturatedConversion,
108};
109use sp_staking::{
110 offence::{Kind, Offence, ReportOffence},
111 SessionIndex,
112};
113pub use weights::WeightInfo;
114
115pub mod sr25519 {
116 mod app_sr25519 {
117 use sp_application_crypto::{app_crypto, key_types::IM_ONLINE, sr25519};
118 app_crypto!(sr25519, IM_ONLINE);
119 }
120
121 sp_application_crypto::with_pair! {
122 pub type AuthorityPair = app_sr25519::Pair;
124 }
125
126 pub type AuthoritySignature = app_sr25519::Signature;
128
129 pub type AuthorityId = app_sr25519::Public;
131}
132
133pub mod ed25519 {
134 mod app_ed25519 {
135 use sp_application_crypto::{app_crypto, ed25519, key_types::IM_ONLINE};
136 app_crypto!(ed25519, IM_ONLINE);
137 }
138
139 sp_application_crypto::with_pair! {
140 pub type AuthorityPair = app_ed25519::Pair;
142 }
143
144 pub type AuthoritySignature = app_ed25519::Signature;
146
147 pub type AuthorityId = app_ed25519::Public;
149}
150
151const DB_PREFIX: &[u8] = b"parity/im-online-heartbeat/";
152const INCLUDE_THRESHOLD: u32 = 3;
155
156#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
162struct HeartbeatStatus<BlockNumber> {
163 pub session_index: SessionIndex,
165 pub sent_at: BlockNumber,
170}
171
172impl<BlockNumber: PartialEq + AtLeast32BitUnsigned + Copy> HeartbeatStatus<BlockNumber> {
173 fn is_recent(&self, session_index: SessionIndex, now: BlockNumber) -> bool {
186 self.session_index == session_index && self.sent_at + INCLUDE_THRESHOLD.into() > now
187 }
188}
189
190#[cfg_attr(test, derive(PartialEq))]
192enum OffchainErr<BlockNumber> {
193 TooEarly,
194 WaitingForInclusion(BlockNumber),
195 AlreadyOnline(u32),
196 FailedSigning,
197 FailedToAcquireLock,
198 SubmitTransaction,
199}
200
201impl<BlockNumber: core::fmt::Debug> core::fmt::Debug for OffchainErr<BlockNumber> {
202 fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
203 match *self {
204 OffchainErr::TooEarly => write!(fmt, "Too early to send heartbeat."),
205 OffchainErr::WaitingForInclusion(ref block) => {
206 write!(fmt, "Heartbeat already sent at {:?}. Waiting for inclusion.", block)
207 },
208 OffchainErr::AlreadyOnline(auth_idx) => {
209 write!(fmt, "Authority {} is already online", auth_idx)
210 },
211 OffchainErr::FailedSigning => write!(fmt, "Failed to sign heartbeat"),
212 OffchainErr::FailedToAcquireLock => write!(fmt, "Failed to acquire lock"),
213 OffchainErr::SubmitTransaction => write!(fmt, "Failed to submit transaction"),
214 }
215 }
216}
217
218pub type AuthIndex = u32;
219
220#[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
222pub struct Heartbeat<BlockNumber>
223where
224 BlockNumber: PartialEq + Eq + Decode + Encode,
225{
226 pub block_number: BlockNumber,
228 pub session_index: SessionIndex,
230 pub authority_index: AuthIndex,
232 pub validators_len: u32,
234}
235
236pub type ValidatorId<T> = <<T as Config>::ValidatorSet as ValidatorSet<
238 <T as frame_system::Config>::AccountId,
239>>::ValidatorId;
240
241pub type IdentificationTuple<T> = (
244 ValidatorId<T>,
245 <<T as Config>::ValidatorSet as ValidatorSetWithIdentification<
246 <T as frame_system::Config>::AccountId,
247 >>::Identification,
248);
249
250type OffchainResult<T, A> = Result<A, OffchainErr<BlockNumberFor<T>>>;
251
252#[frame_support::pallet]
253pub mod pallet {
254 use super::*;
255
256 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
258
259 #[pallet::pallet]
260 #[pallet::storage_version(STORAGE_VERSION)]
261 pub struct Pallet<T>(_);
262
263 #[pallet::config]
264 pub trait Config: CreateBare<Call<Self>> + frame_system::Config {
265 type AuthorityId: Member
267 + Parameter
268 + RuntimeAppPublic
269 + Ord
270 + MaybeSerializeDeserialize
271 + MaxEncodedLen;
272
273 type MaxKeys: Get<u32>;
275
276 type MaxPeerInHeartbeats: Get<u32>;
278
279 #[allow(deprecated)]
281 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
282
283 type ValidatorSet: ValidatorSetWithIdentification<Self::AccountId>;
285
286 type NextSessionRotation: EstimateNextSessionRotation<BlockNumberFor<Self>>;
294
295 type ReportUnresponsiveness: ReportOffence<
297 Self::AccountId,
298 IdentificationTuple<Self>,
299 UnresponsivenessOffence<IdentificationTuple<Self>>,
300 >;
301
302 #[pallet::constant]
307 type UnsignedPriority: Get<TransactionPriority>;
308
309 type WeightInfo: WeightInfo;
311 }
312
313 #[pallet::event]
314 #[pallet::generate_deposit(pub(super) fn deposit_event)]
315 pub enum Event<T: Config> {
316 HeartbeatReceived { authority_id: T::AuthorityId },
318 AllGood,
320 SomeOffline { offline: Vec<IdentificationTuple<T>> },
322 }
323
324 #[pallet::error]
325 pub enum Error<T> {
326 InvalidKey,
328 DuplicatedHeartbeat,
330 }
331
332 #[pallet::storage]
344 pub type HeartbeatAfter<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
345
346 #[pallet::storage]
348 pub type Keys<T: Config> =
349 StorageValue<_, WeakBoundedVec<T::AuthorityId, T::MaxKeys>, ValueQuery>;
350
351 #[pallet::storage]
353 pub type ReceivedHeartbeats<T: Config> =
354 StorageDoubleMap<_, Twox64Concat, SessionIndex, Twox64Concat, AuthIndex, bool>;
355
356 #[pallet::storage]
359 pub type AuthoredBlocks<T: Config> = StorageDoubleMap<
360 _,
361 Twox64Concat,
362 SessionIndex,
363 Twox64Concat,
364 ValidatorId<T>,
365 u32,
366 ValueQuery,
367 >;
368
369 #[pallet::genesis_config]
370 #[derive(frame_support::DefaultNoBound)]
371 pub struct GenesisConfig<T: Config> {
372 pub keys: Vec<T::AuthorityId>,
373 }
374
375 #[pallet::genesis_build]
376 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
377 fn build(&self) {
378 Pallet::<T>::initialize_keys(&self.keys);
379 }
380 }
381
382 #[pallet::call]
383 impl<T: Config> Pallet<T> {
384 #[pallet::call_index(0)]
390 #[pallet::weight(<T as Config>::WeightInfo::validate_unsigned_and_then_heartbeat(
391 heartbeat.validators_len,
392 ))]
393 pub fn heartbeat(
394 origin: OriginFor<T>,
395 heartbeat: Heartbeat<BlockNumberFor<T>>,
396 _signature: <T::AuthorityId as RuntimeAppPublic>::Signature,
399 ) -> DispatchResult {
400 ensure_none(origin)?;
401
402 let current_session = T::ValidatorSet::session_index();
403 let exists =
404 ReceivedHeartbeats::<T>::contains_key(current_session, heartbeat.authority_index);
405 let keys = Keys::<T>::get();
406 let public = keys.get(heartbeat.authority_index as usize);
407 if let (false, Some(public)) = (exists, public) {
408 Self::deposit_event(Event::<T>::HeartbeatReceived { authority_id: public.clone() });
409
410 ReceivedHeartbeats::<T>::insert(current_session, heartbeat.authority_index, true);
411
412 Ok(())
413 } else if exists {
414 Err(Error::<T>::DuplicatedHeartbeat.into())
415 } else {
416 Err(Error::<T>::InvalidKey.into())
417 }
418 }
419 }
420
421 #[pallet::hooks]
422 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
423 fn offchain_worker(now: BlockNumberFor<T>) {
424 if sp_io::offchain::is_validator() {
426 for res in Self::send_heartbeats(now).into_iter().flatten() {
427 if let Err(e) = res {
428 log::debug!(
429 target: "runtime::im-online",
430 "Skipping heartbeat at {:?}: {:?}",
431 now,
432 e,
433 )
434 }
435 }
436 } else {
437 log::trace!(
438 target: "runtime::im-online",
439 "Skipping heartbeat at {:?}. Not a validator.",
440 now,
441 )
442 }
443 }
444 }
445
446 pub(crate) const INVALID_VALIDATORS_LEN: u8 = 10;
449
450 #[pallet::validate_unsigned]
451 impl<T: Config> ValidateUnsigned for Pallet<T> {
452 type Call = Call<T>;
453
454 fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
455 if let Call::heartbeat { heartbeat, signature } = call {
456 if <Pallet<T>>::is_online(heartbeat.authority_index) {
457 return InvalidTransaction::Stale.into()
459 }
460
461 let current_session = T::ValidatorSet::session_index();
463 if heartbeat.session_index != current_session {
464 return InvalidTransaction::Stale.into()
465 }
466
467 let keys = Keys::<T>::get();
469 if keys.len() as u32 != heartbeat.validators_len {
470 return InvalidTransaction::Custom(INVALID_VALIDATORS_LEN).into()
471 }
472 let authority_id = match keys.get(heartbeat.authority_index as usize) {
473 Some(id) => id,
474 None => return InvalidTransaction::BadProof.into(),
475 };
476
477 let signature_valid = heartbeat.using_encoded(|encoded_heartbeat| {
479 authority_id.verify(&encoded_heartbeat, signature)
480 });
481
482 if !signature_valid {
483 return InvalidTransaction::BadProof.into()
484 }
485
486 ValidTransaction::with_tag_prefix("ImOnline")
487 .priority(T::UnsignedPriority::get())
488 .and_provides((current_session, authority_id))
489 .longevity(
490 TryInto::<u64>::try_into(
491 T::NextSessionRotation::average_session_length() / 2u32.into(),
492 )
493 .unwrap_or(64_u64),
494 )
495 .propagate(true)
496 .build()
497 } else {
498 InvalidTransaction::Call.into()
499 }
500 }
501 }
502}
503
504impl<T: Config + pallet_authorship::Config>
507 pallet_authorship::EventHandler<ValidatorId<T>, BlockNumberFor<T>> for Pallet<T>
508{
509 fn note_author(author: ValidatorId<T>) {
510 Self::note_authorship(author);
511 }
512}
513
514impl<T: Config> Pallet<T> {
515 pub fn is_online(authority_index: AuthIndex) -> bool {
520 let current_validators = T::ValidatorSet::validators();
521
522 if authority_index >= current_validators.len() as u32 {
523 return false
524 }
525
526 let authority = ¤t_validators[authority_index as usize];
527
528 Self::is_online_aux(authority_index, authority)
529 }
530
531 fn is_online_aux(authority_index: AuthIndex, authority: &ValidatorId<T>) -> bool {
532 let current_session = T::ValidatorSet::session_index();
533
534 ReceivedHeartbeats::<T>::contains_key(current_session, authority_index) ||
535 AuthoredBlocks::<T>::get(current_session, authority) != 0
536 }
537
538 pub fn received_heartbeat_in_current_session(authority_index: AuthIndex) -> bool {
541 let current_session = T::ValidatorSet::session_index();
542 ReceivedHeartbeats::<T>::contains_key(current_session, authority_index)
543 }
544
545 fn note_authorship(author: ValidatorId<T>) {
547 let current_session = T::ValidatorSet::session_index();
548
549 AuthoredBlocks::<T>::mutate(current_session, author, |authored| *authored += 1);
550 }
551
552 pub(crate) fn send_heartbeats(
553 block_number: BlockNumberFor<T>,
554 ) -> OffchainResult<T, impl Iterator<Item = OffchainResult<T, ()>>> {
555 const START_HEARTBEAT_RANDOM_PERIOD: Permill = Permill::from_percent(10);
556 const START_HEARTBEAT_FINAL_PERIOD: Permill = Permill::from_percent(80);
557
558 let random_choice = |progress: Permill| {
562 let session_length = T::NextSessionRotation::average_session_length();
565 let residual = Permill::from_rational(1u32, session_length.saturated_into());
566 let threshold: Permill = progress.saturating_pow(6).saturating_add(residual);
567
568 let seed = sp_io::offchain::random_seed();
569 let random = <u32>::decode(&mut TrailingZeroInput::new(seed.as_ref()))
570 .expect("input is padded with zeroes; qed");
571 let random = Permill::from_parts(random % Permill::ACCURACY);
572
573 random <= threshold
574 };
575
576 let should_heartbeat = if let (Some(progress), _) =
577 T::NextSessionRotation::estimate_current_session_progress(block_number)
578 {
579 progress >= START_HEARTBEAT_FINAL_PERIOD ||
586 progress >= START_HEARTBEAT_RANDOM_PERIOD && random_choice(progress)
587 } else {
588 let heartbeat_after = <HeartbeatAfter<T>>::get();
591 block_number >= heartbeat_after
592 };
593
594 if !should_heartbeat {
595 return Err(OffchainErr::TooEarly)
596 }
597
598 let session_index = T::ValidatorSet::session_index();
599 let validators_len = Keys::<T>::decode_len().unwrap_or_default() as u32;
600
601 Ok(Self::local_authority_keys().map(move |(authority_index, key)| {
602 Self::send_single_heartbeat(
603 authority_index,
604 key,
605 session_index,
606 block_number,
607 validators_len,
608 )
609 }))
610 }
611
612 fn send_single_heartbeat(
613 authority_index: u32,
614 key: T::AuthorityId,
615 session_index: SessionIndex,
616 block_number: BlockNumberFor<T>,
617 validators_len: u32,
618 ) -> OffchainResult<T, ()> {
619 let prepare_heartbeat = || -> OffchainResult<T, Call<T>> {
621 let heartbeat =
622 Heartbeat { block_number, session_index, authority_index, validators_len };
623
624 let signature = key.sign(&heartbeat.encode()).ok_or(OffchainErr::FailedSigning)?;
625
626 Ok(Call::heartbeat { heartbeat, signature })
627 };
628
629 if Self::is_online(authority_index) {
630 return Err(OffchainErr::AlreadyOnline(authority_index))
631 }
632
633 Self::with_heartbeat_lock(authority_index, session_index, block_number, || {
636 let call = prepare_heartbeat()?;
637 log::info!(
638 target: "runtime::im-online",
639 "[index: {:?}] Reporting im-online at block: {:?} (session: {:?}): {:?}",
640 authority_index,
641 block_number,
642 session_index,
643 call,
644 );
645
646 let xt = T::create_bare(call.into());
647 SubmitTransaction::<T, Call<T>>::submit_transaction(xt)
648 .map_err(|_| OffchainErr::SubmitTransaction)?;
649
650 Ok(())
651 })
652 }
653
654 fn local_authority_keys() -> impl Iterator<Item = (u32, T::AuthorityId)> {
655 let authorities = Keys::<T>::get();
661
662 let mut local_keys = T::AuthorityId::all();
666
667 local_keys.sort();
668
669 authorities.into_iter().enumerate().filter_map(move |(index, authority)| {
670 local_keys
671 .binary_search(&authority)
672 .ok()
673 .map(|location| (index as u32, local_keys[location].clone()))
674 })
675 }
676
677 fn with_heartbeat_lock<R>(
678 authority_index: u32,
679 session_index: SessionIndex,
680 now: BlockNumberFor<T>,
681 f: impl FnOnce() -> OffchainResult<T, R>,
682 ) -> OffchainResult<T, R> {
683 let key = {
684 let mut key = DB_PREFIX.to_vec();
685 key.extend(authority_index.encode());
686 key
687 };
688 let storage = StorageValueRef::persistent(&key);
689 let res = storage.mutate(
690 |status: Result<Option<HeartbeatStatus<BlockNumberFor<T>>>, StorageRetrievalError>| {
691 match status {
696 Ok(Some(status)) if status.is_recent(session_index, now) =>
698 Err(OffchainErr::WaitingForInclusion(status.sent_at)),
699 _ => Ok(HeartbeatStatus { session_index, sent_at: now }),
701 }
702 },
703 );
704 if let Err(MutateStorageError::ValueFunctionFailed(err)) = res {
705 return Err(err)
706 }
707
708 let mut new_status = res.map_err(|_| OffchainErr::FailedToAcquireLock)?;
709
710 let res = f();
712
713 if res.is_err() {
715 new_status.sent_at = 0u32.into();
716 storage.set(&new_status);
717 }
718
719 res
720 }
721
722 fn initialize_keys(keys: &[T::AuthorityId]) {
723 if !keys.is_empty() {
724 assert!(Keys::<T>::get().is_empty(), "Keys are already initialized!");
725 let bounded_keys = <BoundedSlice<'_, _, T::MaxKeys>>::try_from(keys)
726 .expect("More than the maximum number of keys provided");
727 Keys::<T>::put(bounded_keys);
728 }
729 }
730
731 #[cfg(test)]
732 fn set_keys(keys: Vec<T::AuthorityId>) {
733 let bounded_keys = WeakBoundedVec::<_, T::MaxKeys>::try_from(keys)
734 .expect("More than the maximum number of keys provided");
735 Keys::<T>::put(bounded_keys);
736 }
737}
738
739impl<T: Config> sp_runtime::BoundToRuntimeAppPublic for Pallet<T> {
740 type Public = T::AuthorityId;
741}
742
743impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
744 type Key = T::AuthorityId;
745
746 fn on_genesis_session<'a, I: 'a>(validators: I)
747 where
748 I: Iterator<Item = (&'a T::AccountId, T::AuthorityId)>,
749 {
750 let keys = validators.map(|x| x.1).collect::<Vec<_>>();
751 Self::initialize_keys(&keys);
752 }
753
754 fn on_new_session<'a, I: 'a>(_changed: bool, validators: I, _queued_validators: I)
755 where
756 I: Iterator<Item = (&'a T::AccountId, T::AuthorityId)>,
757 {
758 let block_number = <frame_system::Pallet<T>>::block_number();
762 let half_session = T::NextSessionRotation::average_session_length() / 2u32.into();
763 <HeartbeatAfter<T>>::put(block_number + half_session);
764
765 let keys = validators.map(|x| x.1).collect::<Vec<_>>();
767 let bounded_keys = WeakBoundedVec::<_, T::MaxKeys>::force_from(
768 keys,
769 Some(
770 "Warning: The session has more keys than expected. \
771 A runtime configuration adjustment may be needed.",
772 ),
773 );
774 Keys::<T>::put(bounded_keys);
775 }
776
777 fn on_before_session_ending() {
778 let session_index = T::ValidatorSet::session_index();
779 let keys = Keys::<T>::get();
780 let current_validators = T::ValidatorSet::validators();
781
782 let offenders = current_validators
783 .into_iter()
784 .enumerate()
785 .filter(|(index, id)| !Self::is_online_aux(*index as u32, id))
786 .filter_map(|(_, id)| {
787 <T::ValidatorSet as ValidatorSetWithIdentification<T::AccountId>>::IdentificationOf::convert(
788 id.clone()
789 ).map(|full_id| (id, full_id))
790 })
791 .collect::<Vec<IdentificationTuple<T>>>();
792
793 #[allow(deprecated)]
797 ReceivedHeartbeats::<T>::remove_prefix(T::ValidatorSet::session_index(), None);
798 #[allow(deprecated)]
799 AuthoredBlocks::<T>::remove_prefix(T::ValidatorSet::session_index(), None);
800
801 if offenders.is_empty() {
802 Self::deposit_event(Event::<T>::AllGood);
803 } else {
804 Self::deposit_event(Event::<T>::SomeOffline { offline: offenders.clone() });
805
806 let validator_set_count = keys.len() as u32;
807 let offence = UnresponsivenessOffence { session_index, validator_set_count, offenders };
808 if let Err(e) = T::ReportUnresponsiveness::report_offence(vec![], offence) {
809 sp_runtime::print(e);
810 }
811 }
812 }
813
814 fn on_disabled(_i: u32) {
815 }
817}
818
819#[derive(RuntimeDebug, TypeInfo)]
821#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))]
822pub struct UnresponsivenessOffence<Offender> {
823 pub session_index: SessionIndex,
828 pub validator_set_count: u32,
830 pub offenders: Vec<Offender>,
832}
833
834impl<Offender: Clone> Offence<Offender> for UnresponsivenessOffence<Offender> {
835 const ID: Kind = *b"im-online:offlin";
836 type TimeSlot = SessionIndex;
837
838 fn offenders(&self) -> Vec<Offender> {
839 self.offenders.clone()
840 }
841
842 fn session_index(&self) -> SessionIndex {
843 self.session_index
844 }
845
846 fn validator_set_count(&self) -> u32 {
847 self.validator_set_count
848 }
849
850 fn time_slot(&self) -> Self::TimeSlot {
851 self.session_index
852 }
853
854 fn slash_fraction(&self, offenders: u32) -> Perbill {
855 if let Some(threshold) = offenders.checked_sub(self.validator_set_count / 10 + 1) {
859 let x = Perbill::from_rational(3 * threshold, self.validator_set_count);
860 x.saturating_mul(Perbill::from_percent(7))
861 } else {
862 Perbill::default()
863 }
864 }
865}