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 Debug, PerThing, Perbill, Permill, 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, Debug, 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, Debug, 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 #[allow(deprecated)]
451 #[pallet::validate_unsigned]
452 impl<T: Config> ValidateUnsigned for Pallet<T> {
453 type Call = Call<T>;
454
455 fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
456 if let Call::heartbeat { heartbeat, signature } = call {
457 if <Pallet<T>>::is_online(heartbeat.authority_index) {
458 return InvalidTransaction::Stale.into();
460 }
461
462 let current_session = T::ValidatorSet::session_index();
464 if heartbeat.session_index != current_session {
465 return InvalidTransaction::Stale.into();
466 }
467
468 let keys = Keys::<T>::get();
470 if keys.len() as u32 != heartbeat.validators_len {
471 return InvalidTransaction::Custom(INVALID_VALIDATORS_LEN).into();
472 }
473 let authority_id = match keys.get(heartbeat.authority_index as usize) {
474 Some(id) => id,
475 None => return InvalidTransaction::BadProof.into(),
476 };
477
478 let signature_valid = heartbeat.using_encoded(|encoded_heartbeat| {
480 authority_id.verify(&encoded_heartbeat, signature)
481 });
482
483 if !signature_valid {
484 return InvalidTransaction::BadProof.into();
485 }
486
487 ValidTransaction::with_tag_prefix("ImOnline")
488 .priority(T::UnsignedPriority::get())
489 .and_provides((current_session, authority_id))
490 .longevity(
491 TryInto::<u64>::try_into(
492 T::NextSessionRotation::average_session_length() / 2u32.into(),
493 )
494 .unwrap_or(64_u64),
495 )
496 .propagate(true)
497 .build()
498 } else {
499 InvalidTransaction::Call.into()
500 }
501 }
502 }
503}
504
505impl<T: Config + pallet_authorship::Config>
508 pallet_authorship::EventHandler<ValidatorId<T>, BlockNumberFor<T>> for Pallet<T>
509{
510 fn note_author(author: ValidatorId<T>) {
511 Self::note_authorship(author);
512 }
513}
514
515impl<T: Config> Pallet<T> {
516 pub fn is_online(authority_index: AuthIndex) -> bool {
521 let current_validators = T::ValidatorSet::validators();
522
523 if authority_index >= current_validators.len() as u32 {
524 return false;
525 }
526
527 let authority = ¤t_validators[authority_index as usize];
528
529 Self::is_online_aux(authority_index, authority)
530 }
531
532 fn is_online_aux(authority_index: AuthIndex, authority: &ValidatorId<T>) -> bool {
533 let current_session = T::ValidatorSet::session_index();
534
535 ReceivedHeartbeats::<T>::contains_key(current_session, authority_index) ||
536 AuthoredBlocks::<T>::get(current_session, authority) != 0
537 }
538
539 pub fn received_heartbeat_in_current_session(authority_index: AuthIndex) -> bool {
542 let current_session = T::ValidatorSet::session_index();
543 ReceivedHeartbeats::<T>::contains_key(current_session, authority_index)
544 }
545
546 fn note_authorship(author: ValidatorId<T>) {
548 let current_session = T::ValidatorSet::session_index();
549
550 AuthoredBlocks::<T>::mutate(current_session, author, |authored| *authored += 1);
551 }
552
553 pub(crate) fn send_heartbeats(
554 block_number: BlockNumberFor<T>,
555 ) -> OffchainResult<T, impl Iterator<Item = OffchainResult<T, ()>>> {
556 const START_HEARTBEAT_RANDOM_PERIOD: Permill = Permill::from_percent(10);
557 const START_HEARTBEAT_FINAL_PERIOD: Permill = Permill::from_percent(80);
558
559 let random_choice = |progress: Permill| {
563 let session_length = T::NextSessionRotation::average_session_length();
566 let residual = Permill::from_rational(1u32, session_length.saturated_into());
567 let threshold: Permill = progress.saturating_pow(6).saturating_add(residual);
568
569 let seed = sp_io::offchain::random_seed();
570 let random = <u32>::decode(&mut TrailingZeroInput::new(seed.as_ref()))
571 .expect("input is padded with zeroes; qed");
572 let random = Permill::from_parts(random % Permill::ACCURACY);
573
574 random <= threshold
575 };
576
577 let should_heartbeat = if let (Some(progress), _) =
578 T::NextSessionRotation::estimate_current_session_progress(block_number)
579 {
580 progress >= START_HEARTBEAT_FINAL_PERIOD ||
587 progress >= START_HEARTBEAT_RANDOM_PERIOD && random_choice(progress)
588 } else {
589 let heartbeat_after = <HeartbeatAfter<T>>::get();
592 block_number >= heartbeat_after
593 };
594
595 if !should_heartbeat {
596 return Err(OffchainErr::TooEarly);
597 }
598
599 let session_index = T::ValidatorSet::session_index();
600 let validators_len = Keys::<T>::decode_len().unwrap_or_default() as u32;
601
602 Ok(Self::local_authority_keys().map(move |(authority_index, key)| {
603 Self::send_single_heartbeat(
604 authority_index,
605 key,
606 session_index,
607 block_number,
608 validators_len,
609 )
610 }))
611 }
612
613 fn send_single_heartbeat(
614 authority_index: u32,
615 key: T::AuthorityId,
616 session_index: SessionIndex,
617 block_number: BlockNumberFor<T>,
618 validators_len: u32,
619 ) -> OffchainResult<T, ()> {
620 let prepare_heartbeat = || -> OffchainResult<T, Call<T>> {
622 let heartbeat =
623 Heartbeat { block_number, session_index, authority_index, validators_len };
624
625 let signature = key.sign(&heartbeat.encode()).ok_or(OffchainErr::FailedSigning)?;
626
627 Ok(Call::heartbeat { heartbeat, signature })
628 };
629
630 if Self::is_online(authority_index) {
631 return Err(OffchainErr::AlreadyOnline(authority_index));
632 }
633
634 Self::with_heartbeat_lock(authority_index, session_index, block_number, || {
637 let call = prepare_heartbeat()?;
638 log::info!(
639 target: "runtime::im-online",
640 "[index: {:?}] Reporting im-online at block: {:?} (session: {:?}): {:?}",
641 authority_index,
642 block_number,
643 session_index,
644 call,
645 );
646
647 let xt = T::create_bare(call.into());
648 SubmitTransaction::<T, Call<T>>::submit_transaction(xt)
649 .map_err(|_| OffchainErr::SubmitTransaction)?;
650
651 Ok(())
652 })
653 }
654
655 fn local_authority_keys() -> impl Iterator<Item = (u32, T::AuthorityId)> {
656 let authorities = Keys::<T>::get();
662
663 let mut local_keys = T::AuthorityId::all();
667
668 local_keys.sort();
669
670 authorities.into_iter().enumerate().filter_map(move |(index, authority)| {
671 local_keys
672 .binary_search(&authority)
673 .ok()
674 .map(|location| (index as u32, local_keys[location].clone()))
675 })
676 }
677
678 fn with_heartbeat_lock<R>(
679 authority_index: u32,
680 session_index: SessionIndex,
681 now: BlockNumberFor<T>,
682 f: impl FnOnce() -> OffchainResult<T, R>,
683 ) -> OffchainResult<T, R> {
684 let key = {
685 let mut key = DB_PREFIX.to_vec();
686 key.extend(authority_index.encode());
687 key
688 };
689 let storage = StorageValueRef::persistent(&key);
690 let res = storage.mutate(
691 |status: Result<Option<HeartbeatStatus<BlockNumberFor<T>>>, StorageRetrievalError>| {
692 match status {
697 Ok(Some(status)) if status.is_recent(session_index, now) => {
699 Err(OffchainErr::WaitingForInclusion(status.sent_at))
700 },
701 _ => Ok(HeartbeatStatus { session_index, sent_at: now }),
703 }
704 },
705 );
706 if let Err(MutateStorageError::ValueFunctionFailed(err)) = res {
707 return Err(err);
708 }
709
710 let mut new_status = res.map_err(|_| OffchainErr::FailedToAcquireLock)?;
711
712 let res = f();
714
715 if res.is_err() {
717 new_status.sent_at = 0u32.into();
718 storage.set(&new_status);
719 }
720
721 res
722 }
723
724 fn initialize_keys(keys: &[T::AuthorityId]) {
725 if !keys.is_empty() {
726 assert!(Keys::<T>::get().is_empty(), "Keys are already initialized!");
727 let bounded_keys = <BoundedSlice<'_, _, T::MaxKeys>>::try_from(keys)
728 .expect("More than the maximum number of keys provided");
729 Keys::<T>::put(bounded_keys);
730 }
731 }
732
733 #[cfg(test)]
734 fn set_keys(keys: Vec<T::AuthorityId>) {
735 let bounded_keys = WeakBoundedVec::<_, T::MaxKeys>::try_from(keys)
736 .expect("More than the maximum number of keys provided");
737 Keys::<T>::put(bounded_keys);
738 }
739}
740
741impl<T: Config> sp_runtime::BoundToRuntimeAppPublic for Pallet<T> {
742 type Public = T::AuthorityId;
743}
744
745impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
746 type Key = T::AuthorityId;
747
748 fn on_genesis_session<'a, I: 'a>(validators: I)
749 where
750 I: Iterator<Item = (&'a T::AccountId, T::AuthorityId)>,
751 {
752 let keys = validators.map(|x| x.1).collect::<Vec<_>>();
753 Self::initialize_keys(&keys);
754 }
755
756 fn on_new_session<'a, I: 'a>(_changed: bool, validators: I, _queued_validators: I)
757 where
758 I: Iterator<Item = (&'a T::AccountId, T::AuthorityId)>,
759 {
760 let block_number = <frame_system::Pallet<T>>::block_number();
764 let half_session = T::NextSessionRotation::average_session_length() / 2u32.into();
765 <HeartbeatAfter<T>>::put(block_number + half_session);
766
767 let keys = validators.map(|x| x.1).collect::<Vec<_>>();
769 let bounded_keys = WeakBoundedVec::<_, T::MaxKeys>::force_from(
770 keys,
771 Some(
772 "Warning: The session has more keys than expected. \
773 A runtime configuration adjustment may be needed.",
774 ),
775 );
776 Keys::<T>::put(bounded_keys);
777 }
778
779 fn on_before_session_ending() {
780 let session_index = T::ValidatorSet::session_index();
781 let keys = Keys::<T>::get();
782 let current_validators = T::ValidatorSet::validators();
783
784 let offenders = current_validators
785 .into_iter()
786 .enumerate()
787 .filter(|(index, id)| !Self::is_online_aux(*index as u32, id))
788 .filter_map(|(_, id)| {
789 <T::ValidatorSet as ValidatorSetWithIdentification<T::AccountId>>::IdentificationOf::convert(
790 id.clone()
791 ).map(|full_id| (id, full_id))
792 })
793 .collect::<Vec<IdentificationTuple<T>>>();
794
795 #[allow(deprecated)]
799 ReceivedHeartbeats::<T>::remove_prefix(T::ValidatorSet::session_index(), None);
800 #[allow(deprecated)]
801 AuthoredBlocks::<T>::remove_prefix(T::ValidatorSet::session_index(), None);
802
803 if offenders.is_empty() {
804 Self::deposit_event(Event::<T>::AllGood);
805 } else {
806 Self::deposit_event(Event::<T>::SomeOffline { offline: offenders.clone() });
807
808 let validator_set_count = keys.len() as u32;
809 let offence = UnresponsivenessOffence { session_index, validator_set_count, offenders };
810 if let Err(e) = T::ReportUnresponsiveness::report_offence(vec![], offence) {
811 sp_runtime::print(e);
812 }
813 }
814 }
815
816 fn on_disabled(_i: u32) {
817 }
819}
820
821#[derive(Debug, TypeInfo)]
823#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))]
824pub struct UnresponsivenessOffence<Offender> {
825 pub session_index: SessionIndex,
830 pub validator_set_count: u32,
832 pub offenders: Vec<Offender>,
834}
835
836impl<Offender: Clone> Offence<Offender> for UnresponsivenessOffence<Offender> {
837 const ID: Kind = *b"im-online:offlin";
838 type TimeSlot = SessionIndex;
839
840 fn offenders(&self) -> Vec<Offender> {
841 self.offenders.clone()
842 }
843
844 fn session_index(&self) -> SessionIndex {
845 self.session_index
846 }
847
848 fn validator_set_count(&self) -> u32 {
849 self.validator_set_count
850 }
851
852 fn time_slot(&self) -> Self::TimeSlot {
853 self.session_index
854 }
855
856 fn slash_fraction(&self, offenders: u32) -> Perbill {
857 if let Some(threshold) = offenders.checked_sub(self.validator_set_count / 10 + 1) {
861 let x = Perbill::from_rational(3 * threshold, self.validator_set_count);
862 x.saturating_mul(Perbill::from_percent(7))
863 } else {
864 Perbill::default()
865 }
866 }
867}