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