1#![cfg_attr(not(feature = "std"), no_std)]
56
57pub use pallet::*;
58
59#[cfg(test)]
60pub mod mock;
61
62extern crate alloc;
63use alloc::vec::Vec;
64use frame_support::{
65 pallet_prelude::*,
66 traits::{Defensive, DefensiveSaturating, RewardsReporter},
67};
68pub use pallet_staking_async_rc_client::SendToAssetHub;
69use pallet_staking_async_rc_client::{self as rc_client};
70use sp_runtime::SaturatedConversion;
71use sp_staking::offence::OffenceDetails;
72
73pub type BalanceOf<T> = <T as Config>::CurrencyBalance;
75
76pub type OffenceDetailsOf<T> = OffenceDetails<
78 <T as frame_system::Config>::AccountId,
79 (
80 <T as frame_system::Config>::AccountId,
81 sp_staking::Exposure<<T as frame_system::Config>::AccountId, BalanceOf<T>>,
82 ),
83>;
84
85const LOG_TARGET: &str = "runtime::staking-async::ah-client";
86
87#[macro_export]
89macro_rules! log {
90 ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
91 log::$level!(
92 target: $crate::LOG_TARGET,
93 concat!("[{:?}] ⬇️ ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
94 )
95 };
96}
97
98pub use pallet_session::SessionInterface;
103
104#[derive(
106 Default,
107 DecodeWithMemTracking,
108 Encode,
109 Decode,
110 MaxEncodedLen,
111 TypeInfo,
112 Clone,
113 PartialEq,
114 Eq,
115 RuntimeDebug,
116 serde::Serialize,
117 serde::Deserialize,
118)]
119pub enum OperatingMode {
120 #[default]
128 Passive,
129
130 Buffered,
138
139 Active,
146}
147
148impl OperatingMode {
149 fn can_accept_validator_set(&self) -> bool {
150 matches!(self, OperatingMode::Active)
151 }
152}
153
154pub struct DefaultExposureOf<T>(core::marker::PhantomData<T>);
157
158impl<T: Config>
159 sp_runtime::traits::Convert<
160 T::AccountId,
161 Option<sp_staking::Exposure<T::AccountId, BalanceOf<T>>>,
162 > for DefaultExposureOf<T>
163{
164 fn convert(
165 validator: T::AccountId,
166 ) -> Option<sp_staking::Exposure<T::AccountId, BalanceOf<T>>> {
167 T::SessionInterface::validators()
168 .contains(&validator)
169 .then_some(Default::default())
170 }
171}
172
173#[frame_support::pallet]
174pub mod pallet {
175 use crate::*;
176 use alloc::vec;
177 use frame_support::traits::{Hooks, UnixTime};
178 use frame_system::pallet_prelude::*;
179 use pallet_session::{historical, SessionManager};
180 use pallet_staking_async_rc_client::SessionReport;
181 use sp_runtime::{Perbill, Saturating};
182 use sp_staking::{
183 offence::{OffenceSeverity, OnOffenceHandler},
184 SessionIndex,
185 };
186
187 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
188
189 #[pallet::config]
190 pub trait Config: frame_system::Config {
191 type CurrencyBalance: sp_runtime::traits::AtLeast32BitUnsigned
193 + codec::FullCodec
194 + DecodeWithMemTracking
195 + codec::HasCompact<Type: DecodeWithMemTracking>
196 + Copy
197 + MaybeSerializeDeserialize
198 + core::fmt::Debug
199 + Default
200 + From<u64>
201 + TypeInfo
202 + Send
203 + Sync
204 + MaxEncodedLen;
205
206 type AssetHubOrigin: EnsureOrigin<Self::RuntimeOrigin>;
208
209 type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
211
212 type SendToAssetHub: SendToAssetHub<AccountId = Self::AccountId>;
214
215 type MinimumValidatorSetSize: Get<u32>;
217
218 type MaximumValidatorsWithPoints: Get<u32>;
232
233 type UnixTime: UnixTime;
235
236 type PointsPerBlock: Get<u32>;
238
239 type MaxOffenceBatchSize: Get<u32>;
246
247 type SessionInterface: SessionInterface<
249 ValidatorId = Self::AccountId,
250 AccountId = Self::AccountId,
251 >;
252
253 type Fallback: pallet_session::SessionManager<Self::AccountId>
260 + OnOffenceHandler<
261 Self::AccountId,
262 (Self::AccountId, sp_staking::Exposure<Self::AccountId, BalanceOf<Self>>),
263 Weight,
264 > + frame_support::traits::RewardsReporter<Self::AccountId>
265 + pallet_authorship::EventHandler<Self::AccountId, BlockNumberFor<Self>>;
266
267 type MaxSessionReportRetries: Get<u32>;
270 }
271
272 #[pallet::pallet]
273 #[pallet::storage_version(STORAGE_VERSION)]
274 pub struct Pallet<T>(_);
275
276 #[pallet::storage]
280 #[pallet::unbounded]
281 pub type ValidatorSet<T: Config> = StorageValue<_, (u32, Vec<T::AccountId>), OptionQuery>;
282
283 #[pallet::storage]
285 #[pallet::unbounded]
286 pub type IncompleteValidatorSetReport<T: Config> =
287 StorageValue<_, rc_client::ValidatorSetReport<T::AccountId>, OptionQuery>;
288
289 #[pallet::storage]
294 pub type ValidatorPoints<T: Config> =
295 StorageMap<_, Twox64Concat, T::AccountId, u32, ValueQuery>;
296
297 #[pallet::storage]
302 pub type Mode<T: Config> = StorageValue<_, OperatingMode, ValueQuery>;
303
304 #[pallet::storage]
313 pub type NextSessionChangesValidators<T: Config> = StorageValue<_, u32, OptionQuery>;
314
315 #[pallet::storage]
320 pub type ValidatorSetAppliedAt<T: Config> = StorageValue<_, SessionIndex, OptionQuery>;
321
322 #[pallet::storage]
327 #[pallet::unbounded]
328 pub type OutgoingSessionReport<T: Config> =
329 StorageValue<_, (SessionReport<T::AccountId>, u32), OptionQuery>;
330
331 pub struct OffenceSendQueue<T: Config>(core::marker::PhantomData<T>);
343
344 pub type QueuedOffenceOf<T> =
346 (SessionIndex, rc_client::Offence<<T as frame_system::Config>::AccountId>);
347 pub type QueuedOffencePageOf<T> =
349 BoundedVec<QueuedOffenceOf<T>, <T as Config>::MaxOffenceBatchSize>;
350
351 impl<T: Config> OffenceSendQueue<T> {
352 pub fn append(o: QueuedOffenceOf<T>) {
354 let mut index = OffenceSendQueueCursor::<T>::get();
355 match OffenceSendQueueOffences::<T>::try_mutate(index, |b| b.try_push(o.clone())) {
356 Ok(_) => {
357 },
359 Err(_) => {
360 debug_assert!(
361 !OffenceSendQueueOffences::<T>::contains_key(index + 1),
362 "next page should be empty"
363 );
364 index += 1;
365 OffenceSendQueueOffences::<T>::insert(
366 index,
367 BoundedVec::<_, _>::try_from(vec![o]).defensive_unwrap_or_default(),
368 );
369 OffenceSendQueueCursor::<T>::mutate(|i| *i += 1);
370 },
371 }
372 }
373
374 pub fn get_and_maybe_delete(op: impl FnOnce(QueuedOffencePageOf<T>) -> Result<(), ()>) {
376 let index = OffenceSendQueueCursor::<T>::get();
377 let page = OffenceSendQueueOffences::<T>::get(index);
378 let res = op(page);
379 match res {
380 Ok(_) => {
381 OffenceSendQueueOffences::<T>::remove(index);
382 OffenceSendQueueCursor::<T>::mutate(|i| *i = i.saturating_sub(1))
383 },
384 Err(_) => {
385 },
387 }
388 }
389
390 #[cfg(feature = "std")]
391 pub fn pages() -> u32 {
392 let last_page = if Self::last_page_empty() { 0 } else { 1 };
393 OffenceSendQueueCursor::<T>::get().saturating_add(last_page)
394 }
395
396 #[cfg(feature = "std")]
397 pub fn count() -> u32 {
398 let last_index = OffenceSendQueueCursor::<T>::get();
399 let last_page = OffenceSendQueueOffences::<T>::get(last_index);
400 let last_page_count = last_page.len() as u32;
401 last_index.saturating_mul(T::MaxOffenceBatchSize::get()) + last_page_count
402 }
403
404 #[cfg(feature = "std")]
405 fn last_page_empty() -> bool {
406 OffenceSendQueueOffences::<T>::get(OffenceSendQueueCursor::<T>::get()).is_empty()
407 }
408 }
409
410 #[pallet::storage]
412 #[pallet::unbounded]
413 pub(crate) type OffenceSendQueueOffences<T: Config> =
414 StorageMap<_, Twox64Concat, u32, QueuedOffencePageOf<T>, ValueQuery>;
415 #[pallet::storage]
417 pub(crate) type OffenceSendQueueCursor<T: Config> = StorageValue<_, u32, ValueQuery>;
418
419 #[pallet::genesis_config]
420 #[derive(frame_support::DefaultNoBound, frame_support::DebugNoBound)]
421 pub struct GenesisConfig<T: Config> {
422 pub operating_mode: OperatingMode,
424 pub _marker: core::marker::PhantomData<T>,
425 }
426
427 #[pallet::genesis_build]
428 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
429 fn build(&self) {
430 Mode::<T>::put(self.operating_mode.clone());
432 }
433 }
434
435 #[pallet::error]
436 pub enum Error<T> {
437 Blocked,
439 }
440
441 #[pallet::event]
442 #[pallet::generate_deposit(fn deposit_event)]
443 pub enum Event<T: Config> {
444 ValidatorSetReceived {
446 id: u32,
447 new_validator_set_count: u32,
448 prune_up_to: Option<SessionIndex>,
449 leftover: bool,
450 },
451 CouldNotMergeAndDropped,
456 SetTooSmallAndDropped,
459 Unexpected(UnexpectedKind),
462 SessionKeysUpdated { stash: T::AccountId, update: SessionKeysUpdate },
464 }
465
466 #[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, Debug)]
468 pub enum SessionKeysUpdate {
469 Set,
471 Purged,
473 }
474
475 #[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, RuntimeDebug)]
481 pub enum UnexpectedKind {
482 ReceivedValidatorSetWhilePassive,
484
485 UnexpectedModeTransition,
489
490 SessionReportSendFailed,
494
495 SessionReportDropped,
500
501 OffenceSendFailed,
505
506 ValidatorPointDropped,
513
514 InvalidKeysFromAssetHub,
519 }
520
521 #[pallet::call]
522 impl<T: Config> Pallet<T> {
523 #[pallet::call_index(0)]
524 #[pallet::weight(
525 T::DbWeight::get().reads_writes(2, 1)
532 )]
533 pub fn validator_set(
534 origin: OriginFor<T>,
535 report: rc_client::ValidatorSetReport<T::AccountId>,
536 ) -> DispatchResult {
537 log!(debug, "Received new validator set report {}", report);
539 T::AssetHubOrigin::ensure_origin_or_root(origin)?;
540
541 let mode = Mode::<T>::get();
543 ensure!(mode.can_accept_validator_set(), Error::<T>::Blocked);
544
545 let maybe_merged_report = match IncompleteValidatorSetReport::<T>::take() {
546 Some(old) => old.merge(report.clone()),
547 None => Ok(report),
548 };
549
550 if maybe_merged_report.is_err() {
551 Self::deposit_event(Event::CouldNotMergeAndDropped);
552 debug_assert!(
553 IncompleteValidatorSetReport::<T>::get().is_none(),
554 "we have ::take() it above, we don't want to keep the old data"
555 );
556 return Ok(());
557 }
558
559 let report = maybe_merged_report.expect("checked above; qed");
560
561 if report.leftover {
562 Self::deposit_event(Event::ValidatorSetReceived {
564 id: report.id,
565 new_validator_set_count: report.new_validator_set.len() as u32,
566 prune_up_to: report.prune_up_to,
567 leftover: report.leftover,
568 });
569 IncompleteValidatorSetReport::<T>::put(report);
570 } else {
571 let rc_client::ValidatorSetReport {
573 id,
574 leftover,
575 mut new_validator_set,
576 prune_up_to,
577 } = report;
578
579 new_validator_set.sort();
581 new_validator_set.dedup();
582
583 if (new_validator_set.len() as u32) < T::MinimumValidatorSetSize::get() {
584 Self::deposit_event(Event::SetTooSmallAndDropped);
585 debug_assert!(
586 IncompleteValidatorSetReport::<T>::get().is_none(),
587 "we have ::take() it above, we don't want to keep the old data"
588 );
589 return Ok(());
590 }
591
592 Self::deposit_event(Event::ValidatorSetReceived {
593 id,
594 new_validator_set_count: new_validator_set.len() as u32,
595 prune_up_to,
596 leftover,
597 });
598
599 ValidatorSet::<T>::put((id, new_validator_set));
601 if let Some(index) = prune_up_to {
602 T::SessionInterface::prune_up_to(index);
603 }
604 }
605
606 Ok(())
607 }
608
609 #[pallet::call_index(1)]
611 #[pallet::weight(T::DbWeight::get().writes(1))]
612 pub fn set_mode(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
613 T::AdminOrigin::ensure_origin(origin)?;
614 Self::do_set_mode(mode);
615 Ok(())
616 }
617
618 #[pallet::call_index(2)]
620 #[pallet::weight(T::DbWeight::get().writes(1))]
621 pub fn force_on_migration_end(origin: OriginFor<T>) -> DispatchResult {
622 T::AdminOrigin::ensure_origin(origin)?;
623 Self::on_migration_end();
624 Ok(())
625 }
626
627 #[pallet::call_index(3)]
635 #[pallet::weight(T::SessionInterface::set_keys_weight())]
636 pub fn set_keys_from_ah(
637 origin: OriginFor<T>,
638 stash: T::AccountId,
639 keys: Vec<u8>,
640 ) -> DispatchResult {
641 T::AssetHubOrigin::ensure_origin_or_root(origin)?;
642 log::info!(target: LOG_TARGET, "Received set_keys request from AssetHub for {stash:?}");
643
644 let session_keys =
646 match <<T as Config>::SessionInterface as SessionInterface>::Keys::decode(
647 &mut &keys[..],
648 ) {
649 Ok(keys) => keys,
650 Err(e) => {
651 log!(
654 warn,
655 "InvalidKeysFromAssetHub: failed to decode keys for {:?}: {:?}",
656 stash,
657 e
658 );
659 Self::deposit_event(Event::Unexpected(
660 UnexpectedKind::InvalidKeysFromAssetHub,
661 ));
662 return Ok(());
663 },
664 };
665
666 T::SessionInterface::set_keys(&stash, session_keys)?;
667 Self::deposit_event(Event::SessionKeysUpdated {
668 stash,
669 update: SessionKeysUpdate::Set,
670 });
671 Ok(())
672 }
673
674 #[pallet::call_index(4)]
679 #[pallet::weight(T::SessionInterface::purge_keys_weight())]
680 pub fn purge_keys_from_ah(origin: OriginFor<T>, stash: T::AccountId) -> DispatchResult {
681 T::AssetHubOrigin::ensure_origin_or_root(origin)?;
682 log::info!(target: LOG_TARGET, "Received purge_keys request from AssetHub for {stash:?}");
683
684 T::SessionInterface::purge_keys(&stash)?;
685 Self::deposit_event(Event::SessionKeysUpdated {
686 stash,
687 update: SessionKeysUpdate::Purged,
688 });
689 Ok(())
690 }
691 }
692
693 #[pallet::hooks]
694 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
695 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
696 let mut weight = Weight::zero();
697
698 let mode = Mode::<T>::get();
699 weight = weight.saturating_add(T::DbWeight::get().reads(1));
700 if mode != OperatingMode::Active {
701 return weight;
702 }
703
704 weight.saturating_accrue(T::DbWeight::get().reads(1));
706 if let Some((session_report, retries_left)) = OutgoingSessionReport::<T>::take() {
707 match T::SendToAssetHub::relay_session_report(session_report.clone()) {
708 Ok(()) => {
709 },
711 Err(()) => {
712 log!(error, "Failed to send session report to assethub");
713 Self::deposit_event(Event::<T>::Unexpected(
714 UnexpectedKind::SessionReportSendFailed,
715 ));
716 if let Some(new_retries_left) = retries_left.checked_sub(One::one()) {
717 OutgoingSessionReport::<T>::put((session_report, new_retries_left))
718 } else {
719 session_report.validator_points.into_iter().for_each(|(v, p)| {
722 ValidatorPoints::<T>::mutate(v, |existing_points| {
723 *existing_points = existing_points.defensive_saturating_add(p)
724 });
725 });
726
727 Self::deposit_event(Event::<T>::Unexpected(
728 UnexpectedKind::SessionReportDropped,
729 ));
730 }
731 },
732 }
733 }
734
735 weight.saturating_accrue(T::DbWeight::get().reads(2));
737 OffenceSendQueue::<T>::get_and_maybe_delete(|page| {
738 if page.is_empty() {
739 return Ok(())
740 }
741 T::SendToAssetHub::relay_new_offence_paged(page.into_inner()).inspect_err(|_| {
743 Self::deposit_event(Event::Unexpected(UnexpectedKind::OffenceSendFailed));
744 })
745 });
746
747 weight
748 }
749
750 fn integrity_test() {
751 assert!(T::MaxOffenceBatchSize::get() > 0, "Offence Batch size must be at least 1");
752 }
753 }
754
755 impl<T: Config>
756 historical::SessionManager<T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>>
757 for Pallet<T>
758 {
759 fn new_session(
760 new_index: sp_staking::SessionIndex,
761 ) -> Option<
762 Vec<(
763 <T as frame_system::Config>::AccountId,
764 sp_staking::Exposure<T::AccountId, BalanceOf<T>>,
765 )>,
766 > {
767 <Self as pallet_session::SessionManager<_>>::new_session(new_index)
768 .map(|v| v.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect())
769 }
770
771 fn new_session_genesis(
772 new_index: SessionIndex,
773 ) -> Option<Vec<(T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>)>> {
774 if Mode::<T>::get() == OperatingMode::Passive {
775 T::Fallback::new_session_genesis(new_index).map(|validators| {
776 validators.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect()
777 })
778 } else {
779 None
780 }
781 }
782
783 fn start_session(start_index: SessionIndex) {
784 <Self as pallet_session::SessionManager<_>>::start_session(start_index)
785 }
786
787 fn end_session(end_index: SessionIndex) {
788 <Self as pallet_session::SessionManager<_>>::end_session(end_index)
789 }
790 }
791
792 impl<T: Config> pallet_session::SessionManager<T::AccountId> for Pallet<T> {
793 fn new_session(session_index: u32) -> Option<Vec<T::AccountId>> {
794 match Mode::<T>::get() {
795 OperatingMode::Passive => T::Fallback::new_session(session_index),
796 OperatingMode::Buffered => None,
798 OperatingMode::Active => Self::do_new_session(),
799 }
800 }
801
802 fn start_session(session_index: u32) {
803 if Mode::<T>::get() == OperatingMode::Passive {
804 T::Fallback::start_session(session_index)
805 }
806 }
807
808 fn new_session_genesis(new_index: SessionIndex) -> Option<Vec<T::AccountId>> {
809 if Mode::<T>::get() == OperatingMode::Passive {
810 T::Fallback::new_session_genesis(new_index)
811 } else {
812 None
813 }
814 }
815
816 fn end_session(session_index: u32) {
817 match Mode::<T>::get() {
818 OperatingMode::Passive => T::Fallback::end_session(session_index),
819 OperatingMode::Buffered => (),
821 OperatingMode::Active => Self::do_end_session(session_index),
822 }
823 }
824 }
825
826 impl<T: Config>
827 OnOffenceHandler<
828 T::AccountId,
829 (T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>),
830 Weight,
831 > for Pallet<T>
832 {
833 fn on_offence(
834 offenders: &[OffenceDetails<
835 T::AccountId,
836 (T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>),
837 >],
838 slash_fraction: &[Perbill],
839 slash_session: SessionIndex,
840 ) -> Weight {
841 match Mode::<T>::get() {
842 OperatingMode::Passive => {
843 T::Fallback::on_offence(offenders, slash_fraction, slash_session)
845 },
846 OperatingMode::Buffered =>
847 Self::on_offence_buffered(offenders, slash_fraction, slash_session),
848 OperatingMode::Active =>
849 Self::on_offence_active(offenders, slash_fraction, slash_session),
850 }
851 }
852 }
853
854 impl<T: Config> RewardsReporter<T::AccountId> for Pallet<T> {
855 fn reward_by_ids(rewards: impl IntoIterator<Item = (T::AccountId, u32)>) {
856 match Mode::<T>::get() {
857 OperatingMode::Passive => T::Fallback::reward_by_ids(rewards),
858 OperatingMode::Buffered | OperatingMode::Active => Self::do_reward_by_ids(rewards),
859 }
860 }
861 }
862
863 impl<T: Config> pallet_authorship::EventHandler<T::AccountId, BlockNumberFor<T>> for Pallet<T> {
864 fn note_author(author: T::AccountId) {
865 match Mode::<T>::get() {
866 OperatingMode::Passive => T::Fallback::note_author(author),
867 OperatingMode::Buffered | OperatingMode::Active => Self::do_note_author(author),
868 }
869 }
870 }
871
872 impl<T: Config> Pallet<T> {
873 pub fn on_migration_start() {
882 debug_assert!(
883 Mode::<T>::get() == OperatingMode::Passive,
884 "we should only be called when in passive mode"
885 );
886 Self::do_set_mode(OperatingMode::Buffered);
887 }
888
889 pub fn on_migration_end() {
898 debug_assert!(
899 Mode::<T>::get() == OperatingMode::Buffered,
900 "we should only be called when in buffered mode"
901 );
902 Self::do_set_mode(OperatingMode::Active);
903
904 }
907
908 fn do_set_mode(new_mode: OperatingMode) {
909 let old_mode = Mode::<T>::get();
910 let unexpected = match new_mode {
911 OperatingMode::Passive => true,
913 OperatingMode::Buffered => old_mode != OperatingMode::Passive,
914 OperatingMode::Active => old_mode != OperatingMode::Buffered,
915 };
916
917 if unexpected {
919 log!(warn, "Unexpected mode transition from {:?} to {:?}", old_mode, new_mode);
920 Self::deposit_event(Event::Unexpected(UnexpectedKind::UnexpectedModeTransition));
921 }
922
923 Mode::<T>::put(new_mode);
925 }
926
927 fn do_new_session() -> Option<Vec<T::AccountId>> {
928 ValidatorSet::<T>::take().map(|(id, val_set)| {
929 NextSessionChangesValidators::<T>::put(id);
931 val_set
932 })
933 }
934
935 fn do_end_session(end_index: u32) {
936 let validator_points = ValidatorPoints::<T>::iter()
938 .drain()
939 .take(T::MaximumValidatorsWithPoints::get() as usize)
940 .collect::<Vec<_>>();
941
942 if ValidatorPoints::<T>::iter().next().is_some() {
944 Self::deposit_event(Event::<T>::Unexpected(UnexpectedKind::ValidatorPointDropped))
946 }
947
948 let activation_timestamp = NextSessionChangesValidators::<T>::take().map(|id| {
949 ValidatorSetAppliedAt::<T>::put(end_index + 1);
951 (T::UnixTime::now().as_millis().saturated_into::<u64>(), id)
953 });
954
955 let session_report = pallet_staking_async_rc_client::SessionReport {
956 end_index,
957 validator_points,
958 activation_timestamp,
959 leftover: false,
960 };
961
962 OutgoingSessionReport::<T>::put((session_report, T::MaxSessionReportRetries::get()));
964 }
965
966 fn do_reward_by_ids(rewards: impl IntoIterator<Item = (T::AccountId, u32)>) {
967 for (validator_id, points) in rewards {
968 ValidatorPoints::<T>::mutate(validator_id, |balance| {
969 balance.saturating_accrue(points);
970 });
971 }
972 }
973
974 fn do_note_author(author: T::AccountId) {
975 ValidatorPoints::<T>::mutate(author, |points| {
976 points.saturating_accrue(T::PointsPerBlock::get());
977 });
978 }
979
980 fn is_ongoing_offence(slash_session: SessionIndex) -> bool {
982 ValidatorSetAppliedAt::<T>::get()
983 .map(|start_session| slash_session >= start_session)
984 .unwrap_or(false)
985 }
986
987 fn on_offence_buffered(
989 offenders: &[OffenceDetailsOf<T>],
990 slash_fraction: &[Perbill],
991 slash_session: SessionIndex,
992 ) -> Weight {
993 let ongoing_offence = Self::is_ongoing_offence(slash_session);
994
995 offenders.iter().cloned().zip(slash_fraction).for_each(|(offence, fraction)| {
996 if ongoing_offence {
997 T::SessionInterface::report_offence(
999 offence.offender.0.clone(),
1000 OffenceSeverity(*fraction),
1001 );
1002 }
1003
1004 let (offender, _full_identification) = offence.offender;
1005 let reporters = offence.reporters;
1006
1007 OffenceSendQueue::<T>::append((
1009 slash_session,
1010 rc_client::Offence {
1011 offender: offender.clone(),
1012 reporters: reporters.into_iter().take(1).collect(),
1013 slash_fraction: *fraction,
1014 },
1015 ));
1016 });
1017
1018 T::DbWeight::get().reads_writes(1, 1)
1019 }
1020
1021 fn on_offence_active(
1023 offenders: &[OffenceDetailsOf<T>],
1024 slash_fraction: &[Perbill],
1025 slash_session: SessionIndex,
1026 ) -> Weight {
1027 let ongoing_offence = Self::is_ongoing_offence(slash_session);
1028
1029 offenders.iter().cloned().zip(slash_fraction).for_each(|(offence, fraction)| {
1030 if ongoing_offence {
1031 T::SessionInterface::report_offence(
1033 offence.offender.0.clone(),
1034 OffenceSeverity(*fraction),
1035 );
1036 }
1037
1038 let (offender, _full_identification) = offence.offender;
1039 let reporters = offence.reporters;
1040
1041 let offence = rc_client::Offence {
1044 offender,
1045 reporters: reporters.into_iter().take(1).collect(),
1046 slash_fraction: *fraction,
1047 };
1048 OffenceSendQueue::<T>::append((slash_session, offence))
1049 });
1050
1051 T::DbWeight::get().reads_writes(2, 2)
1052 }
1053 }
1054}
1055
1056#[cfg(test)]
1057mod keys_from_ah_tests {
1058 use super::*;
1059 use crate::mock::*;
1060 use codec::Encode;
1061 use frame_support::{assert_noop, assert_ok, hypothetically};
1062 use sp_runtime::DispatchError;
1063
1064 #[test]
1065 fn set_keys_from_ah() {
1066 new_test_ext().execute_with(|| {
1067 System::set_block_number(1);
1068 let stash = 42u64;
1069 let keys = MockSessionKeys { dummy: [1u8; 32] };
1070
1071 hypothetically!({
1073 SetKeysCalls::take();
1074 assert_ok!(StakingAsyncAhClient::set_keys_from_ah(
1075 RuntimeOrigin::root(),
1076 stash,
1077 keys.encode(),
1078 ));
1079 assert_eq!(SetKeysCalls::get(), vec![(stash, keys.clone())]);
1080 System::assert_has_event(
1081 Event::<Test>::SessionKeysUpdated { stash, update: SessionKeysUpdate::Set }
1082 .into(),
1083 );
1084 });
1085
1086 hypothetically!({
1088 SetKeysCalls::take();
1089 assert_noop!(
1090 StakingAsyncAhClient::set_keys_from_ah(
1091 RuntimeOrigin::signed(1),
1092 stash,
1093 keys.encode(),
1094 ),
1095 DispatchError::BadOrigin
1096 );
1097 assert!(SetKeysCalls::get().is_empty());
1098 });
1099
1100 hypothetically!({
1102 SetKeysCalls::take();
1103 assert_ok!(StakingAsyncAhClient::set_keys_from_ah(
1104 RuntimeOrigin::root(),
1105 stash,
1106 vec![1u8, 2, 3], ));
1108 assert!(SetKeysCalls::get().is_empty());
1109 System::assert_has_event(
1110 Event::<Test>::Unexpected(UnexpectedKind::InvalidKeysFromAssetHub).into(),
1111 );
1112 });
1113 });
1114 }
1115
1116 #[test]
1117 fn purge_keys_from_ah() {
1118 new_test_ext().execute_with(|| {
1119 System::set_block_number(1);
1120 let stash = 42u64;
1121
1122 hypothetically!({
1124 PurgeKeysCalls::take();
1125 assert_ok!(StakingAsyncAhClient::purge_keys_from_ah(RuntimeOrigin::root(), stash));
1126 assert_eq!(PurgeKeysCalls::get(), vec![stash]);
1127 System::assert_has_event(
1128 Event::<Test>::SessionKeysUpdated { stash, update: SessionKeysUpdate::Purged }
1129 .into(),
1130 );
1131 });
1132
1133 hypothetically!({
1135 PurgeKeysCalls::take();
1136 assert_noop!(
1137 StakingAsyncAhClient::purge_keys_from_ah(RuntimeOrigin::signed(1), stash),
1138 DispatchError::BadOrigin
1139 );
1140 assert!(PurgeKeysCalls::get().is_empty());
1141 });
1142 });
1143 }
1144}
1145
1146#[cfg(test)]
1147mod send_queue_tests {
1148 use frame_support::hypothetically;
1149 use sp_runtime::Perbill;
1150
1151 use super::*;
1152 use crate::mock::*;
1153
1154 fn status() -> (u32, Vec<u32>) {
1156 let mut sorted = OffenceSendQueueOffences::<Test>::iter().collect::<Vec<_>>();
1157 sorted.sort_by(|x, y| x.0.cmp(&y.0));
1158 (
1159 OffenceSendQueueCursor::<Test>::get(),
1160 sorted.into_iter().map(|(_, v)| v.len() as u32).collect(),
1161 )
1162 }
1163
1164 #[test]
1165 fn append_and_take() {
1166 new_test_ext().execute_with(|| {
1167 let o = (
1168 42,
1169 rc_client::Offence {
1170 offender: 42,
1171 reporters: vec![],
1172 slash_fraction: Perbill::from_percent(10),
1173 },
1174 );
1175 let page_size = <Test as Config>::MaxOffenceBatchSize::get();
1176 assert_eq!(page_size % 2, 0, "page size should be even");
1177
1178 assert_eq!(status(), (0, vec![]));
1179
1180 assert_eq!(OffenceSendQueue::<Test>::count(), 0);
1183 assert_eq!(OffenceSendQueue::<Test>::pages(), 0);
1184
1185 hypothetically!({
1187 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1188 assert_eq!(page.len(), 0);
1189 Err(())
1190 });
1191 assert_eq!(status(), (0, vec![]));
1192 });
1193
1194 hypothetically!({
1196 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1197 assert_eq!(page.len(), 0);
1198 Ok(())
1199 });
1200 assert_eq!(status(), (0, vec![]));
1201 });
1202
1203 for _ in 0..page_size / 2 {
1205 OffenceSendQueue::<Test>::append(o.clone());
1206 }
1207 assert_eq!(status(), (0, vec![page_size / 2]));
1208 assert_eq!(OffenceSendQueue::<Test>::count(), page_size / 2);
1209 assert_eq!(OffenceSendQueue::<Test>::pages(), 1);
1210
1211 hypothetically!({
1213 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1214 assert_eq!(page.len() as u32, page_size / 2);
1215 Err(())
1216 });
1217 assert_eq!(status(), (0, vec![page_size / 2]));
1218 });
1219
1220 hypothetically!({
1222 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1223 assert_eq!(page.len() as u32, page_size / 2);
1224 Ok(())
1225 });
1226 assert_eq!(status(), (0, vec![]));
1227 assert_eq!(OffenceSendQueue::<Test>::count(), 0);
1228 assert_eq!(OffenceSendQueue::<Test>::pages(), 0);
1229 });
1230
1231 for _ in 0..page_size / 2 {
1233 OffenceSendQueue::<Test>::append(o.clone());
1234 }
1235 assert_eq!(status(), (0, vec![page_size]));
1236 assert_eq!(OffenceSendQueue::<Test>::count(), page_size);
1237 assert_eq!(OffenceSendQueue::<Test>::pages(), 1);
1238
1239 hypothetically!({
1241 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1242 assert_eq!(page.len() as u32, page_size);
1243 Err(())
1244 });
1245 assert_eq!(status(), (0, vec![page_size]));
1246 });
1247
1248 hypothetically!({
1250 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1251 assert_eq!(page.len() as u32, page_size);
1252 Ok(())
1253 });
1254 assert_eq!(status(), (0, vec![]));
1255 });
1256
1257 OffenceSendQueue::<Test>::append(o.clone());
1259 assert_eq!(status(), (1, vec![page_size, 1]));
1260 assert_eq!(OffenceSendQueue::<Test>::count(), page_size + 1);
1261 assert_eq!(OffenceSendQueue::<Test>::pages(), 2);
1262
1263 hypothetically!({
1265 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1266 assert_eq!(page.len(), 1);
1267 Err(())
1268 });
1269 assert_eq!(status(), (1, vec![page_size, 1]));
1270 });
1271
1272 hypothetically!({
1274 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1275 assert_eq!(page.len(), 1);
1276 Ok(())
1277 });
1278 assert_eq!(status(), (0, vec![page_size]));
1279 });
1280 })
1281 }
1282}