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 Debug,
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 SessionKeysUpdateFailed {
467 stash: T::AccountId,
468 update: SessionKeysUpdate,
469 error: DispatchError,
470 },
471 }
472
473 #[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, Debug)]
475 pub enum SessionKeysUpdate {
476 Set,
478 Purged,
480 }
481
482 #[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, Debug)]
488 pub enum UnexpectedKind {
489 ReceivedValidatorSetWhilePassive,
491
492 UnexpectedModeTransition,
496
497 SessionReportSendFailed,
501
502 SessionReportDropped,
507
508 OffenceSendFailed,
512
513 ValidatorPointDropped,
520
521 InvalidKeysFromAssetHub,
526 }
527
528 #[pallet::call]
529 impl<T: Config> Pallet<T> {
530 #[pallet::call_index(0)]
531 #[pallet::weight(
532 T::DbWeight::get().reads_writes(2, 1)
539 )]
540 pub fn validator_set(
541 origin: OriginFor<T>,
542 report: rc_client::ValidatorSetReport<T::AccountId>,
543 ) -> DispatchResult {
544 log!(debug, "Received new validator set report {}", report);
546 T::AssetHubOrigin::ensure_origin_or_root(origin)?;
547
548 let mode = Mode::<T>::get();
550 ensure!(mode.can_accept_validator_set(), Error::<T>::Blocked);
551
552 let maybe_merged_report = match IncompleteValidatorSetReport::<T>::take() {
553 Some(old) => old.merge(report.clone()),
554 None => Ok(report),
555 };
556
557 if maybe_merged_report.is_err() {
558 Self::deposit_event(Event::CouldNotMergeAndDropped);
559 debug_assert!(
560 IncompleteValidatorSetReport::<T>::get().is_none(),
561 "we have ::take() it above, we don't want to keep the old data"
562 );
563 return Ok(());
564 }
565
566 let report = maybe_merged_report.expect("checked above; qed");
567
568 if report.leftover {
569 Self::deposit_event(Event::ValidatorSetReceived {
571 id: report.id,
572 new_validator_set_count: report.new_validator_set.len() as u32,
573 prune_up_to: report.prune_up_to,
574 leftover: report.leftover,
575 });
576 IncompleteValidatorSetReport::<T>::put(report);
577 } else {
578 let rc_client::ValidatorSetReport {
580 id,
581 leftover,
582 mut new_validator_set,
583 prune_up_to,
584 } = report;
585
586 new_validator_set.sort();
588 new_validator_set.dedup();
589
590 if (new_validator_set.len() as u32) < T::MinimumValidatorSetSize::get() {
591 Self::deposit_event(Event::SetTooSmallAndDropped);
592 debug_assert!(
593 IncompleteValidatorSetReport::<T>::get().is_none(),
594 "we have ::take() it above, we don't want to keep the old data"
595 );
596 return Ok(());
597 }
598
599 Self::deposit_event(Event::ValidatorSetReceived {
600 id,
601 new_validator_set_count: new_validator_set.len() as u32,
602 prune_up_to,
603 leftover,
604 });
605
606 ValidatorSet::<T>::put((id, new_validator_set));
608 if let Some(index) = prune_up_to {
609 T::SessionInterface::prune_up_to(index);
610 }
611 }
612
613 Ok(())
614 }
615
616 #[pallet::call_index(1)]
618 #[pallet::weight(T::DbWeight::get().writes(1))]
619 pub fn set_mode(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
620 T::AdminOrigin::ensure_origin(origin)?;
621 Self::do_set_mode(mode);
622 Ok(())
623 }
624
625 #[pallet::call_index(2)]
627 #[pallet::weight(T::DbWeight::get().writes(1))]
628 pub fn force_on_migration_end(origin: OriginFor<T>) -> DispatchResult {
629 T::AdminOrigin::ensure_origin(origin)?;
630 Self::on_migration_end();
631 Ok(())
632 }
633
634 #[pallet::call_index(3)]
642 #[pallet::weight(T::SessionInterface::set_keys_weight())]
643 pub fn set_keys_from_ah(
644 origin: OriginFor<T>,
645 stash: T::AccountId,
646 keys: Vec<u8>,
647 ) -> DispatchResult {
648 T::AssetHubOrigin::ensure_origin_or_root(origin)?;
649 log::info!(target: LOG_TARGET, "Received set_keys request from AssetHub for {stash:?}");
650
651 let session_keys =
653 match <<T as Config>::SessionInterface as SessionInterface>::Keys::decode(
654 &mut &keys[..],
655 ) {
656 Ok(keys) => keys,
657 Err(e) => {
658 log!(
661 warn,
662 "InvalidKeysFromAssetHub: failed to decode keys for {:?}: {:?}",
663 stash,
664 e
665 );
666 Self::deposit_event(Event::Unexpected(
667 UnexpectedKind::InvalidKeysFromAssetHub,
668 ));
669 return Ok(());
670 },
671 };
672
673 match T::SessionInterface::set_keys(&stash, session_keys) {
674 Ok(()) => Self::deposit_event(Event::SessionKeysUpdated {
675 stash,
676 update: SessionKeysUpdate::Set,
677 }),
678 Err(error) => {
679 log!(
680 warn,
681 "SessionKeysUpdateFailed: set_keys failed for {:?}: {:?}",
682 stash,
683 error
684 );
685 Self::deposit_event(Event::SessionKeysUpdateFailed {
686 stash,
687 update: SessionKeysUpdate::Set,
688 error,
689 });
690 },
691 }
692 Ok(())
693 }
694
695 #[pallet::call_index(4)]
700 #[pallet::weight(T::SessionInterface::purge_keys_weight())]
701 pub fn purge_keys_from_ah(origin: OriginFor<T>, stash: T::AccountId) -> DispatchResult {
702 T::AssetHubOrigin::ensure_origin_or_root(origin)?;
703 log::info!(target: LOG_TARGET, "Received purge_keys request from AssetHub for {stash:?}");
704
705 match T::SessionInterface::purge_keys(&stash) {
706 Ok(()) => Self::deposit_event(Event::SessionKeysUpdated {
707 stash,
708 update: SessionKeysUpdate::Purged,
709 }),
710 Err(error) => {
711 log!(
712 warn,
713 "SessionKeysUpdateFailed: purge_keys failed for {:?}: {:?}",
714 stash,
715 error
716 );
717 Self::deposit_event(Event::SessionKeysUpdateFailed {
718 stash,
719 update: SessionKeysUpdate::Purged,
720 error,
721 });
722 },
723 }
724 Ok(())
725 }
726 }
727
728 #[pallet::hooks]
729 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
730 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
731 let mut weight = Weight::zero();
732
733 let mode = Mode::<T>::get();
734 weight = weight.saturating_add(T::DbWeight::get().reads(1));
735 if mode != OperatingMode::Active {
736 return weight;
737 }
738
739 weight.saturating_accrue(T::DbWeight::get().reads(1));
741 if let Some((session_report, retries_left)) = OutgoingSessionReport::<T>::take() {
742 match T::SendToAssetHub::relay_session_report(session_report.clone()) {
743 Ok(()) => {
744 },
746 Err(()) => {
747 log!(error, "Failed to send session report to assethub");
748 Self::deposit_event(Event::<T>::Unexpected(
749 UnexpectedKind::SessionReportSendFailed,
750 ));
751 if let Some(new_retries_left) = retries_left.checked_sub(One::one()) {
752 OutgoingSessionReport::<T>::put((session_report, new_retries_left))
753 } else {
754 session_report.validator_points.into_iter().for_each(|(v, p)| {
757 ValidatorPoints::<T>::mutate(v, |existing_points| {
758 *existing_points = existing_points.defensive_saturating_add(p)
759 });
760 });
761
762 Self::deposit_event(Event::<T>::Unexpected(
763 UnexpectedKind::SessionReportDropped,
764 ));
765 }
766 },
767 }
768 }
769
770 weight.saturating_accrue(T::DbWeight::get().reads(2));
772 OffenceSendQueue::<T>::get_and_maybe_delete(|page| {
773 if page.is_empty() {
774 return Ok(());
775 }
776 T::SendToAssetHub::relay_new_offence_paged(page.into_inner()).inspect_err(|_| {
778 Self::deposit_event(Event::Unexpected(UnexpectedKind::OffenceSendFailed));
779 })
780 });
781
782 weight
783 }
784
785 fn integrity_test() {
786 assert!(T::MaxOffenceBatchSize::get() > 0, "Offence Batch size must be at least 1");
787 }
788 }
789
790 impl<T: Config>
791 historical::SessionManager<T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>>
792 for Pallet<T>
793 {
794 fn new_session(
795 new_index: sp_staking::SessionIndex,
796 ) -> Option<
797 Vec<(
798 <T as frame_system::Config>::AccountId,
799 sp_staking::Exposure<T::AccountId, BalanceOf<T>>,
800 )>,
801 > {
802 <Self as pallet_session::SessionManager<_>>::new_session(new_index)
803 .map(|v| v.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect())
804 }
805
806 fn new_session_genesis(
807 new_index: SessionIndex,
808 ) -> Option<Vec<(T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>)>> {
809 if Mode::<T>::get() == OperatingMode::Passive {
810 T::Fallback::new_session_genesis(new_index).map(|validators| {
811 validators.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect()
812 })
813 } else {
814 None
815 }
816 }
817
818 fn start_session(start_index: SessionIndex) {
819 <Self as pallet_session::SessionManager<_>>::start_session(start_index)
820 }
821
822 fn end_session(end_index: SessionIndex) {
823 <Self as pallet_session::SessionManager<_>>::end_session(end_index)
824 }
825 }
826
827 impl<T: Config> pallet_session::SessionManager<T::AccountId> for Pallet<T> {
828 fn new_session(session_index: u32) -> Option<Vec<T::AccountId>> {
829 match Mode::<T>::get() {
830 OperatingMode::Passive => T::Fallback::new_session(session_index),
831 OperatingMode::Buffered => None,
833 OperatingMode::Active => Self::do_new_session(),
834 }
835 }
836
837 fn start_session(session_index: u32) {
838 if Mode::<T>::get() == OperatingMode::Passive {
839 T::Fallback::start_session(session_index)
840 }
841 }
842
843 fn new_session_genesis(new_index: SessionIndex) -> Option<Vec<T::AccountId>> {
844 if Mode::<T>::get() == OperatingMode::Passive {
845 T::Fallback::new_session_genesis(new_index)
846 } else {
847 None
848 }
849 }
850
851 fn end_session(session_index: u32) {
852 match Mode::<T>::get() {
853 OperatingMode::Passive => T::Fallback::end_session(session_index),
854 OperatingMode::Buffered => (),
856 OperatingMode::Active => Self::do_end_session(session_index),
857 }
858 }
859 }
860
861 impl<T: Config>
862 OnOffenceHandler<
863 T::AccountId,
864 (T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>),
865 Weight,
866 > for Pallet<T>
867 {
868 fn on_offence(
869 offenders: &[OffenceDetails<
870 T::AccountId,
871 (T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>),
872 >],
873 slash_fraction: &[Perbill],
874 slash_session: SessionIndex,
875 ) -> Weight {
876 match Mode::<T>::get() {
877 OperatingMode::Passive => {
878 T::Fallback::on_offence(offenders, slash_fraction, slash_session)
880 },
881 OperatingMode::Buffered => {
882 Self::on_offence_buffered(offenders, slash_fraction, slash_session)
883 },
884 OperatingMode::Active => {
885 Self::on_offence_active(offenders, slash_fraction, slash_session)
886 },
887 }
888 }
889 }
890
891 impl<T: Config> RewardsReporter<T::AccountId> for Pallet<T> {
892 fn reward_by_ids(rewards: impl IntoIterator<Item = (T::AccountId, u32)>) {
893 match Mode::<T>::get() {
894 OperatingMode::Passive => T::Fallback::reward_by_ids(rewards),
895 OperatingMode::Buffered | OperatingMode::Active => Self::do_reward_by_ids(rewards),
896 }
897 }
898 }
899
900 impl<T: Config> pallet_authorship::EventHandler<T::AccountId, BlockNumberFor<T>> for Pallet<T> {
901 fn note_author(author: T::AccountId) {
902 match Mode::<T>::get() {
903 OperatingMode::Passive => T::Fallback::note_author(author),
904 OperatingMode::Buffered | OperatingMode::Active => Self::do_note_author(author),
905 }
906 }
907 }
908
909 impl<T: Config> Pallet<T> {
910 pub fn on_migration_start() {
919 debug_assert!(
920 Mode::<T>::get() == OperatingMode::Passive,
921 "we should only be called when in passive mode"
922 );
923 Self::do_set_mode(OperatingMode::Buffered);
924 }
925
926 pub fn on_migration_end() {
935 debug_assert!(
936 Mode::<T>::get() == OperatingMode::Buffered,
937 "we should only be called when in buffered mode"
938 );
939 Self::do_set_mode(OperatingMode::Active);
940
941 }
944
945 fn do_set_mode(new_mode: OperatingMode) {
946 let old_mode = Mode::<T>::get();
947 let unexpected = match new_mode {
948 OperatingMode::Passive => true,
950 OperatingMode::Buffered => old_mode != OperatingMode::Passive,
951 OperatingMode::Active => old_mode != OperatingMode::Buffered,
952 };
953
954 if unexpected {
956 log!(warn, "Unexpected mode transition from {:?} to {:?}", old_mode, new_mode);
957 Self::deposit_event(Event::Unexpected(UnexpectedKind::UnexpectedModeTransition));
958 }
959
960 Mode::<T>::put(new_mode);
962 }
963
964 fn do_new_session() -> Option<Vec<T::AccountId>> {
965 ValidatorSet::<T>::take().map(|(id, val_set)| {
966 NextSessionChangesValidators::<T>::put(id);
968 val_set
969 })
970 }
971
972 fn do_end_session(end_index: u32) {
973 let validator_points = ValidatorPoints::<T>::iter()
975 .drain()
976 .take(T::MaximumValidatorsWithPoints::get() as usize)
977 .collect::<Vec<_>>();
978
979 if ValidatorPoints::<T>::iter().next().is_some() {
981 Self::deposit_event(Event::<T>::Unexpected(UnexpectedKind::ValidatorPointDropped))
983 }
984
985 let activation_timestamp = NextSessionChangesValidators::<T>::take().map(|id| {
986 ValidatorSetAppliedAt::<T>::put(end_index + 1);
988 (T::UnixTime::now().as_millis().saturated_into::<u64>(), id)
990 });
991
992 let session_report = pallet_staking_async_rc_client::SessionReport {
993 end_index,
994 validator_points,
995 activation_timestamp,
996 leftover: false,
997 };
998
999 OutgoingSessionReport::<T>::put((session_report, T::MaxSessionReportRetries::get()));
1001 }
1002
1003 fn do_reward_by_ids(rewards: impl IntoIterator<Item = (T::AccountId, u32)>) {
1004 for (validator_id, points) in rewards {
1005 ValidatorPoints::<T>::mutate(validator_id, |balance| {
1006 balance.saturating_accrue(points);
1007 });
1008 }
1009 }
1010
1011 fn do_note_author(author: T::AccountId) {
1012 ValidatorPoints::<T>::mutate(author, |points| {
1013 points.saturating_accrue(T::PointsPerBlock::get());
1014 });
1015 }
1016
1017 fn is_ongoing_offence(slash_session: SessionIndex) -> bool {
1019 ValidatorSetAppliedAt::<T>::get()
1020 .map(|start_session| slash_session >= start_session)
1021 .unwrap_or(false)
1022 }
1023
1024 fn on_offence_buffered(
1026 offenders: &[OffenceDetailsOf<T>],
1027 slash_fraction: &[Perbill],
1028 slash_session: SessionIndex,
1029 ) -> Weight {
1030 let ongoing_offence = Self::is_ongoing_offence(slash_session);
1031
1032 offenders.iter().cloned().zip(slash_fraction).for_each(|(offence, fraction)| {
1033 if ongoing_offence {
1034 T::SessionInterface::report_offence(
1036 offence.offender.0.clone(),
1037 OffenceSeverity(*fraction),
1038 );
1039 }
1040
1041 let (offender, _full_identification) = offence.offender;
1042 let reporters = offence.reporters;
1043
1044 OffenceSendQueue::<T>::append((
1046 slash_session,
1047 rc_client::Offence {
1048 offender: offender.clone(),
1049 reporters: reporters.into_iter().take(1).collect(),
1050 slash_fraction: *fraction,
1051 },
1052 ));
1053 });
1054
1055 T::DbWeight::get().reads_writes(1, 1)
1056 }
1057
1058 fn on_offence_active(
1060 offenders: &[OffenceDetailsOf<T>],
1061 slash_fraction: &[Perbill],
1062 slash_session: SessionIndex,
1063 ) -> Weight {
1064 let ongoing_offence = Self::is_ongoing_offence(slash_session);
1065
1066 offenders.iter().cloned().zip(slash_fraction).for_each(|(offence, fraction)| {
1067 if ongoing_offence {
1068 T::SessionInterface::report_offence(
1070 offence.offender.0.clone(),
1071 OffenceSeverity(*fraction),
1072 );
1073 }
1074
1075 let (offender, _full_identification) = offence.offender;
1076 let reporters = offence.reporters;
1077
1078 let offence = rc_client::Offence {
1081 offender,
1082 reporters: reporters.into_iter().take(1).collect(),
1083 slash_fraction: *fraction,
1084 };
1085 OffenceSendQueue::<T>::append((slash_session, offence))
1086 });
1087
1088 T::DbWeight::get().reads_writes(2, 2)
1089 }
1090 }
1091}
1092
1093#[cfg(test)]
1094mod keys_from_ah_tests {
1095 use super::*;
1096 use crate::mock::*;
1097 use codec::Encode;
1098 use frame_support::{assert_noop, assert_ok, hypothetically};
1099 use sp_runtime::DispatchError;
1100
1101 #[test]
1102 fn set_keys_from_ah() {
1103 new_test_ext().execute_with(|| {
1104 System::set_block_number(1);
1105 let stash = 42u64;
1106 let keys = MockSessionKeys { dummy: [1u8; 32] };
1107
1108 hypothetically!({
1110 SetKeysCalls::take();
1111 assert_ok!(StakingAsyncAhClient::set_keys_from_ah(
1112 RuntimeOrigin::root(),
1113 stash,
1114 keys.encode(),
1115 ));
1116 assert_eq!(SetKeysCalls::get(), vec![(stash, keys.clone())]);
1117 System::assert_has_event(
1118 Event::<Test>::SessionKeysUpdated { stash, update: SessionKeysUpdate::Set }
1119 .into(),
1120 );
1121 });
1122
1123 hypothetically!({
1125 SetKeysCalls::take();
1126 assert_noop!(
1127 StakingAsyncAhClient::set_keys_from_ah(
1128 RuntimeOrigin::signed(1),
1129 stash,
1130 keys.encode(),
1131 ),
1132 DispatchError::BadOrigin
1133 );
1134 assert!(SetKeysCalls::get().is_empty());
1135 });
1136
1137 hypothetically!({
1139 SetKeysCalls::take();
1140 let error = DispatchError::Corruption;
1141 SetKeysError::set(Some(error));
1142 assert_ok!(StakingAsyncAhClient::set_keys_from_ah(
1143 RuntimeOrigin::root(),
1144 stash,
1145 keys.encode(),
1146 ));
1147 assert!(SetKeysCalls::get().is_empty());
1148 System::assert_has_event(
1149 Event::<Test>::SessionKeysUpdateFailed {
1150 stash,
1151 update: SessionKeysUpdate::Set,
1152 error,
1153 }
1154 .into(),
1155 );
1156 SetKeysError::take();
1157 });
1158
1159 hypothetically!({
1161 SetKeysCalls::take();
1162 assert_ok!(StakingAsyncAhClient::set_keys_from_ah(
1163 RuntimeOrigin::root(),
1164 stash,
1165 vec![1u8, 2, 3], ));
1167 assert!(SetKeysCalls::get().is_empty());
1168 System::assert_has_event(
1169 Event::<Test>::Unexpected(UnexpectedKind::InvalidKeysFromAssetHub).into(),
1170 );
1171 });
1172 });
1173 }
1174
1175 #[test]
1176 fn purge_keys_from_ah() {
1177 new_test_ext().execute_with(|| {
1178 System::set_block_number(1);
1179 let stash = 42u64;
1180
1181 hypothetically!({
1183 PurgeKeysCalls::take();
1184 assert_ok!(StakingAsyncAhClient::purge_keys_from_ah(RuntimeOrigin::root(), stash));
1185 assert_eq!(PurgeKeysCalls::get(), vec![stash]);
1186 System::assert_has_event(
1187 Event::<Test>::SessionKeysUpdated { stash, update: SessionKeysUpdate::Purged }
1188 .into(),
1189 );
1190 });
1191
1192 hypothetically!({
1194 PurgeKeysCalls::take();
1195 assert_noop!(
1196 StakingAsyncAhClient::purge_keys_from_ah(RuntimeOrigin::signed(1), stash),
1197 DispatchError::BadOrigin
1198 );
1199 assert!(PurgeKeysCalls::get().is_empty());
1200 });
1201
1202 hypothetically!({
1204 PurgeKeysCalls::take();
1205 let error = DispatchError::Corruption;
1206 PurgeKeysError::set(Some(error));
1207 assert_ok!(StakingAsyncAhClient::purge_keys_from_ah(RuntimeOrigin::root(), stash));
1208 assert!(PurgeKeysCalls::get().is_empty());
1209 System::assert_has_event(
1210 Event::<Test>::SessionKeysUpdateFailed {
1211 stash,
1212 update: SessionKeysUpdate::Purged,
1213 error,
1214 }
1215 .into(),
1216 );
1217 PurgeKeysError::take();
1218 });
1219 });
1220 }
1221}
1222
1223#[cfg(test)]
1224mod send_queue_tests {
1225 use frame_support::hypothetically;
1226 use sp_runtime::Perbill;
1227
1228 use super::*;
1229 use crate::mock::*;
1230
1231 fn status() -> (u32, Vec<u32>) {
1233 let mut sorted = OffenceSendQueueOffences::<Test>::iter().collect::<Vec<_>>();
1234 sorted.sort_by(|x, y| x.0.cmp(&y.0));
1235 (
1236 OffenceSendQueueCursor::<Test>::get(),
1237 sorted.into_iter().map(|(_, v)| v.len() as u32).collect(),
1238 )
1239 }
1240
1241 #[test]
1242 fn append_and_take() {
1243 new_test_ext().execute_with(|| {
1244 let o = (
1245 42,
1246 rc_client::Offence {
1247 offender: 42,
1248 reporters: vec![],
1249 slash_fraction: Perbill::from_percent(10),
1250 },
1251 );
1252 let page_size = <Test as Config>::MaxOffenceBatchSize::get();
1253 assert_eq!(page_size % 2, 0, "page size should be even");
1254
1255 assert_eq!(status(), (0, vec![]));
1256
1257 assert_eq!(OffenceSendQueue::<Test>::count(), 0);
1260 assert_eq!(OffenceSendQueue::<Test>::pages(), 0);
1261
1262 hypothetically!({
1264 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1265 assert_eq!(page.len(), 0);
1266 Err(())
1267 });
1268 assert_eq!(status(), (0, vec![]));
1269 });
1270
1271 hypothetically!({
1273 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1274 assert_eq!(page.len(), 0);
1275 Ok(())
1276 });
1277 assert_eq!(status(), (0, vec![]));
1278 });
1279
1280 for _ in 0..page_size / 2 {
1282 OffenceSendQueue::<Test>::append(o.clone());
1283 }
1284 assert_eq!(status(), (0, vec![page_size / 2]));
1285 assert_eq!(OffenceSendQueue::<Test>::count(), page_size / 2);
1286 assert_eq!(OffenceSendQueue::<Test>::pages(), 1);
1287
1288 hypothetically!({
1290 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1291 assert_eq!(page.len() as u32, page_size / 2);
1292 Err(())
1293 });
1294 assert_eq!(status(), (0, vec![page_size / 2]));
1295 });
1296
1297 hypothetically!({
1299 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1300 assert_eq!(page.len() as u32, page_size / 2);
1301 Ok(())
1302 });
1303 assert_eq!(status(), (0, vec![]));
1304 assert_eq!(OffenceSendQueue::<Test>::count(), 0);
1305 assert_eq!(OffenceSendQueue::<Test>::pages(), 0);
1306 });
1307
1308 for _ in 0..page_size / 2 {
1310 OffenceSendQueue::<Test>::append(o.clone());
1311 }
1312 assert_eq!(status(), (0, vec![page_size]));
1313 assert_eq!(OffenceSendQueue::<Test>::count(), page_size);
1314 assert_eq!(OffenceSendQueue::<Test>::pages(), 1);
1315
1316 hypothetically!({
1318 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1319 assert_eq!(page.len() as u32, page_size);
1320 Err(())
1321 });
1322 assert_eq!(status(), (0, vec![page_size]));
1323 });
1324
1325 hypothetically!({
1327 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1328 assert_eq!(page.len() as u32, page_size);
1329 Ok(())
1330 });
1331 assert_eq!(status(), (0, vec![]));
1332 });
1333
1334 OffenceSendQueue::<Test>::append(o.clone());
1336 assert_eq!(status(), (1, vec![page_size, 1]));
1337 assert_eq!(OffenceSendQueue::<Test>::count(), page_size + 1);
1338 assert_eq!(OffenceSendQueue::<Test>::pages(), 2);
1339
1340 hypothetically!({
1342 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1343 assert_eq!(page.len(), 1);
1344 Err(())
1345 });
1346 assert_eq!(status(), (1, vec![page_size, 1]));
1347 });
1348
1349 hypothetically!({
1351 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1352 assert_eq!(page.len(), 1);
1353 Ok(())
1354 });
1355 assert_eq!(status(), (0, vec![page_size]));
1356 });
1357 })
1358 }
1359}