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::{
72 offence::{OffenceDetails, OffenceSeverity},
73 SessionIndex,
74};
75
76pub type BalanceOf<T> = <T as Config>::CurrencyBalance;
78
79pub type OffenceDetailsOf<T> = OffenceDetails<
81 <T as frame_system::Config>::AccountId,
82 (
83 <T as frame_system::Config>::AccountId,
84 sp_staking::Exposure<<T as frame_system::Config>::AccountId, BalanceOf<T>>,
85 ),
86>;
87
88const LOG_TARGET: &str = "runtime::staking-async::ah-client";
89
90#[macro_export]
92macro_rules! log {
93 ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
94 log::$level!(
95 target: $crate::LOG_TARGET,
96 concat!("[{:?}] ⬇️ ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
97 )
98 };
99}
100
101pub trait SessionInterface {
103 type ValidatorId: Clone;
105
106 fn validators() -> Vec<Self::ValidatorId>;
107
108 fn prune_up_to(index: SessionIndex);
110
111 fn report_offence(offender: Self::ValidatorId, severity: OffenceSeverity);
115}
116
117impl<T: Config + pallet_session::Config + pallet_session::historical::Config> SessionInterface
118 for T
119{
120 type ValidatorId = <T as pallet_session::Config>::ValidatorId;
121
122 fn validators() -> Vec<Self::ValidatorId> {
123 pallet_session::Pallet::<T>::validators()
124 }
125
126 fn prune_up_to(index: SessionIndex) {
127 pallet_session::historical::Pallet::<T>::prune_up_to(index)
128 }
129 fn report_offence(offender: Self::ValidatorId, severity: OffenceSeverity) {
130 pallet_session::Pallet::<T>::report_offence(offender, severity)
131 }
132}
133
134#[derive(
136 Default,
137 DecodeWithMemTracking,
138 Encode,
139 Decode,
140 MaxEncodedLen,
141 TypeInfo,
142 Clone,
143 PartialEq,
144 Eq,
145 RuntimeDebug,
146 serde::Serialize,
147 serde::Deserialize,
148)]
149pub enum OperatingMode {
150 #[default]
158 Passive,
159
160 Buffered,
168
169 Active,
176}
177
178impl OperatingMode {
179 fn can_accept_validator_set(&self) -> bool {
180 matches!(self, OperatingMode::Active)
181 }
182}
183
184pub struct DefaultExposureOf<T>(core::marker::PhantomData<T>);
187
188impl<T: Config>
189 sp_runtime::traits::Convert<
190 T::AccountId,
191 Option<sp_staking::Exposure<T::AccountId, BalanceOf<T>>>,
192 > for DefaultExposureOf<T>
193{
194 fn convert(
195 validator: T::AccountId,
196 ) -> Option<sp_staking::Exposure<T::AccountId, BalanceOf<T>>> {
197 T::SessionInterface::validators()
198 .contains(&validator)
199 .then_some(Default::default())
200 }
201}
202
203#[frame_support::pallet]
204pub mod pallet {
205 use crate::*;
206 use alloc::vec;
207 use frame_support::traits::{Hooks, UnixTime};
208 use frame_system::pallet_prelude::*;
209 use pallet_session::{historical, SessionManager};
210 use pallet_staking_async_rc_client::SessionReport;
211 use sp_runtime::{Perbill, Saturating};
212 use sp_staking::{
213 offence::{OffenceSeverity, OnOffenceHandler},
214 SessionIndex,
215 };
216
217 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
218
219 #[pallet::config]
220 pub trait Config: frame_system::Config {
221 type CurrencyBalance: sp_runtime::traits::AtLeast32BitUnsigned
223 + codec::FullCodec
224 + DecodeWithMemTracking
225 + codec::HasCompact<Type: DecodeWithMemTracking>
226 + Copy
227 + MaybeSerializeDeserialize
228 + core::fmt::Debug
229 + Default
230 + From<u64>
231 + TypeInfo
232 + Send
233 + Sync
234 + MaxEncodedLen;
235
236 type AssetHubOrigin: EnsureOrigin<Self::RuntimeOrigin>;
238
239 type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
241
242 type SendToAssetHub: SendToAssetHub<AccountId = Self::AccountId>;
244
245 type MinimumValidatorSetSize: Get<u32>;
247
248 type MaximumValidatorsWithPoints: Get<u32>;
262
263 type UnixTime: UnixTime;
265
266 type PointsPerBlock: Get<u32>;
268
269 type MaxOffenceBatchSize: Get<u32>;
276
277 type SessionInterface: SessionInterface<ValidatorId = Self::AccountId>;
279
280 type Fallback: pallet_session::SessionManager<Self::AccountId>
287 + OnOffenceHandler<
288 Self::AccountId,
289 (Self::AccountId, sp_staking::Exposure<Self::AccountId, BalanceOf<Self>>),
290 Weight,
291 > + frame_support::traits::RewardsReporter<Self::AccountId>
292 + pallet_authorship::EventHandler<Self::AccountId, BlockNumberFor<Self>>;
293
294 type MaxSessionReportRetries: Get<u32>;
297 }
298
299 #[pallet::pallet]
300 #[pallet::storage_version(STORAGE_VERSION)]
301 pub struct Pallet<T>(_);
302
303 #[pallet::storage]
307 #[pallet::unbounded]
308 pub type ValidatorSet<T: Config> = StorageValue<_, (u32, Vec<T::AccountId>), OptionQuery>;
309
310 #[pallet::storage]
312 #[pallet::unbounded]
313 pub type IncompleteValidatorSetReport<T: Config> =
314 StorageValue<_, rc_client::ValidatorSetReport<T::AccountId>, OptionQuery>;
315
316 #[pallet::storage]
321 pub type ValidatorPoints<T: Config> =
322 StorageMap<_, Twox64Concat, T::AccountId, u32, ValueQuery>;
323
324 #[pallet::storage]
329 pub type Mode<T: Config> = StorageValue<_, OperatingMode, ValueQuery>;
330
331 #[pallet::storage]
340 pub type NextSessionChangesValidators<T: Config> = StorageValue<_, u32, OptionQuery>;
341
342 #[pallet::storage]
347 pub type ValidatorSetAppliedAt<T: Config> = StorageValue<_, SessionIndex, OptionQuery>;
348
349 #[pallet::storage]
354 #[pallet::unbounded]
355 pub type OutgoingSessionReport<T: Config> =
356 StorageValue<_, (SessionReport<T::AccountId>, u32), OptionQuery>;
357
358 pub struct OffenceSendQueue<T: Config>(core::marker::PhantomData<T>);
370
371 pub type QueuedOffenceOf<T> =
373 (SessionIndex, rc_client::Offence<<T as frame_system::Config>::AccountId>);
374 pub type QueuedOffencePageOf<T> =
376 BoundedVec<QueuedOffenceOf<T>, <T as Config>::MaxOffenceBatchSize>;
377
378 impl<T: Config> OffenceSendQueue<T> {
379 pub fn append(o: QueuedOffenceOf<T>) {
381 let mut index = OffenceSendQueueCursor::<T>::get();
382 match OffenceSendQueueOffences::<T>::try_mutate(index, |b| b.try_push(o.clone())) {
383 Ok(_) => {
384 },
386 Err(_) => {
387 debug_assert!(
388 !OffenceSendQueueOffences::<T>::contains_key(index + 1),
389 "next page should be empty"
390 );
391 index += 1;
392 OffenceSendQueueOffences::<T>::insert(
393 index,
394 BoundedVec::<_, _>::try_from(vec![o]).defensive_unwrap_or_default(),
395 );
396 OffenceSendQueueCursor::<T>::mutate(|i| *i += 1);
397 },
398 }
399 }
400
401 pub fn get_and_maybe_delete(op: impl FnOnce(QueuedOffencePageOf<T>) -> Result<(), ()>) {
403 let index = OffenceSendQueueCursor::<T>::get();
404 let page = OffenceSendQueueOffences::<T>::get(index);
405 let res = op(page);
406 match res {
407 Ok(_) => {
408 OffenceSendQueueOffences::<T>::remove(index);
409 OffenceSendQueueCursor::<T>::mutate(|i| *i = i.saturating_sub(1))
410 },
411 Err(_) => {
412 },
414 }
415 }
416
417 #[cfg(feature = "std")]
418 pub fn pages() -> u32 {
419 let last_page = if Self::last_page_empty() { 0 } else { 1 };
420 OffenceSendQueueCursor::<T>::get().saturating_add(last_page)
421 }
422
423 #[cfg(feature = "std")]
424 pub fn count() -> u32 {
425 let last_index = OffenceSendQueueCursor::<T>::get();
426 let last_page = OffenceSendQueueOffences::<T>::get(last_index);
427 let last_page_count = last_page.len() as u32;
428 last_index.saturating_mul(T::MaxOffenceBatchSize::get()) + last_page_count
429 }
430
431 #[cfg(feature = "std")]
432 fn last_page_empty() -> bool {
433 OffenceSendQueueOffences::<T>::get(OffenceSendQueueCursor::<T>::get()).is_empty()
434 }
435 }
436
437 #[pallet::storage]
439 #[pallet::unbounded]
440 pub(crate) type OffenceSendQueueOffences<T: Config> =
441 StorageMap<_, Twox64Concat, u32, QueuedOffencePageOf<T>, ValueQuery>;
442 #[pallet::storage]
444 pub(crate) type OffenceSendQueueCursor<T: Config> = StorageValue<_, u32, ValueQuery>;
445
446 #[pallet::genesis_config]
447 #[derive(frame_support::DefaultNoBound, frame_support::DebugNoBound)]
448 pub struct GenesisConfig<T: Config> {
449 pub operating_mode: OperatingMode,
451 pub _marker: core::marker::PhantomData<T>,
452 }
453
454 #[pallet::genesis_build]
455 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
456 fn build(&self) {
457 Mode::<T>::put(self.operating_mode.clone());
459 }
460 }
461
462 #[pallet::error]
463 pub enum Error<T> {
464 Blocked,
466 }
467
468 #[pallet::event]
469 #[pallet::generate_deposit(fn deposit_event)]
470 pub enum Event<T: Config> {
471 ValidatorSetReceived {
473 id: u32,
474 new_validator_set_count: u32,
475 prune_up_to: Option<SessionIndex>,
476 leftover: bool,
477 },
478 CouldNotMergeAndDropped,
483 SetTooSmallAndDropped,
486 Unexpected(UnexpectedKind),
489 }
490
491 #[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, RuntimeDebug)]
497 pub enum UnexpectedKind {
498 ReceivedValidatorSetWhilePassive,
500
501 UnexpectedModeTransition,
505
506 SessionReportSendFailed,
510
511 SessionReportDropped,
516
517 OffenceSendFailed,
521
522 ValidatorPointDropped,
529 }
530
531 #[pallet::call]
532 impl<T: Config> Pallet<T> {
533 #[pallet::call_index(0)]
534 #[pallet::weight(
535 T::DbWeight::get().reads_writes(2, 1)
542 )]
543 pub fn validator_set(
544 origin: OriginFor<T>,
545 report: rc_client::ValidatorSetReport<T::AccountId>,
546 ) -> DispatchResult {
547 log!(debug, "Received new validator set report {}", report);
549 T::AssetHubOrigin::ensure_origin_or_root(origin)?;
550
551 let mode = Mode::<T>::get();
553 ensure!(mode.can_accept_validator_set(), Error::<T>::Blocked);
554
555 let maybe_merged_report = match IncompleteValidatorSetReport::<T>::take() {
556 Some(old) => old.merge(report.clone()),
557 None => Ok(report),
558 };
559
560 if maybe_merged_report.is_err() {
561 Self::deposit_event(Event::CouldNotMergeAndDropped);
562 debug_assert!(
563 IncompleteValidatorSetReport::<T>::get().is_none(),
564 "we have ::take() it above, we don't want to keep the old data"
565 );
566 return Ok(());
567 }
568
569 let report = maybe_merged_report.expect("checked above; qed");
570
571 if report.leftover {
572 Self::deposit_event(Event::ValidatorSetReceived {
574 id: report.id,
575 new_validator_set_count: report.new_validator_set.len() as u32,
576 prune_up_to: report.prune_up_to,
577 leftover: report.leftover,
578 });
579 IncompleteValidatorSetReport::<T>::put(report);
580 } else {
581 let rc_client::ValidatorSetReport {
583 id,
584 leftover,
585 mut new_validator_set,
586 prune_up_to,
587 } = report;
588
589 new_validator_set.sort();
591 new_validator_set.dedup();
592
593 if (new_validator_set.len() as u32) < T::MinimumValidatorSetSize::get() {
594 Self::deposit_event(Event::SetTooSmallAndDropped);
595 debug_assert!(
596 IncompleteValidatorSetReport::<T>::get().is_none(),
597 "we have ::take() it above, we don't want to keep the old data"
598 );
599 return Ok(());
600 }
601
602 Self::deposit_event(Event::ValidatorSetReceived {
603 id,
604 new_validator_set_count: new_validator_set.len() as u32,
605 prune_up_to,
606 leftover,
607 });
608
609 ValidatorSet::<T>::put((id, new_validator_set));
611 if let Some(index) = prune_up_to {
612 T::SessionInterface::prune_up_to(index);
613 }
614 }
615
616 Ok(())
617 }
618
619 #[pallet::call_index(1)]
621 #[pallet::weight(T::DbWeight::get().writes(1))]
622 pub fn set_mode(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
623 T::AdminOrigin::ensure_origin(origin)?;
624 Self::do_set_mode(mode);
625 Ok(())
626 }
627
628 #[pallet::call_index(2)]
630 #[pallet::weight(T::DbWeight::get().writes(1))]
631 pub fn force_on_migration_end(origin: OriginFor<T>) -> DispatchResult {
632 T::AdminOrigin::ensure_origin(origin)?;
633 Self::on_migration_end();
634 Ok(())
635 }
636 }
637
638 #[pallet::hooks]
639 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
640 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
641 let mut weight = Weight::zero();
642
643 let mode = Mode::<T>::get();
644 weight = weight.saturating_add(T::DbWeight::get().reads(1));
645 if mode != OperatingMode::Active {
646 return weight;
647 }
648
649 weight.saturating_accrue(T::DbWeight::get().reads(1));
651 if let Some((session_report, retries_left)) = OutgoingSessionReport::<T>::take() {
652 match T::SendToAssetHub::relay_session_report(session_report.clone()) {
653 Ok(()) => {
654 },
656 Err(()) => {
657 log!(error, "Failed to send session report to assethub");
658 Self::deposit_event(Event::<T>::Unexpected(
659 UnexpectedKind::SessionReportSendFailed,
660 ));
661 if let Some(new_retries_left) = retries_left.checked_sub(One::one()) {
662 OutgoingSessionReport::<T>::put((session_report, new_retries_left))
663 } else {
664 session_report.validator_points.into_iter().for_each(|(v, p)| {
667 ValidatorPoints::<T>::mutate(v, |existing_points| {
668 *existing_points = existing_points.defensive_saturating_add(p)
669 });
670 });
671
672 Self::deposit_event(Event::<T>::Unexpected(
673 UnexpectedKind::SessionReportDropped,
674 ));
675 }
676 },
677 }
678 }
679
680 weight.saturating_accrue(T::DbWeight::get().reads(2));
682 OffenceSendQueue::<T>::get_and_maybe_delete(|page| {
683 if page.is_empty() {
684 return Ok(())
685 }
686 T::SendToAssetHub::relay_new_offence_paged(page.into_inner()).inspect_err(|_| {
688 Self::deposit_event(Event::Unexpected(UnexpectedKind::OffenceSendFailed));
689 })
690 });
691
692 weight
693 }
694
695 fn integrity_test() {
696 assert!(T::MaxOffenceBatchSize::get() > 0, "Offence Batch size must be at least 1");
697 }
698 }
699
700 impl<T: Config>
701 historical::SessionManager<T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>>
702 for Pallet<T>
703 {
704 fn new_session(
705 new_index: sp_staking::SessionIndex,
706 ) -> Option<
707 Vec<(
708 <T as frame_system::Config>::AccountId,
709 sp_staking::Exposure<T::AccountId, BalanceOf<T>>,
710 )>,
711 > {
712 <Self as pallet_session::SessionManager<_>>::new_session(new_index)
713 .map(|v| v.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect())
714 }
715
716 fn new_session_genesis(
717 new_index: SessionIndex,
718 ) -> Option<Vec<(T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>)>> {
719 if Mode::<T>::get() == OperatingMode::Passive {
720 T::Fallback::new_session_genesis(new_index).map(|validators| {
721 validators.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect()
722 })
723 } else {
724 None
725 }
726 }
727
728 fn start_session(start_index: SessionIndex) {
729 <Self as pallet_session::SessionManager<_>>::start_session(start_index)
730 }
731
732 fn end_session(end_index: SessionIndex) {
733 <Self as pallet_session::SessionManager<_>>::end_session(end_index)
734 }
735 }
736
737 impl<T: Config> pallet_session::SessionManager<T::AccountId> for Pallet<T> {
738 fn new_session(session_index: u32) -> Option<Vec<T::AccountId>> {
739 match Mode::<T>::get() {
740 OperatingMode::Passive => T::Fallback::new_session(session_index),
741 OperatingMode::Buffered => None,
743 OperatingMode::Active => Self::do_new_session(),
744 }
745 }
746
747 fn start_session(session_index: u32) {
748 if Mode::<T>::get() == OperatingMode::Passive {
749 T::Fallback::start_session(session_index)
750 }
751 }
752
753 fn new_session_genesis(new_index: SessionIndex) -> Option<Vec<T::AccountId>> {
754 if Mode::<T>::get() == OperatingMode::Passive {
755 T::Fallback::new_session_genesis(new_index)
756 } else {
757 None
758 }
759 }
760
761 fn end_session(session_index: u32) {
762 match Mode::<T>::get() {
763 OperatingMode::Passive => T::Fallback::end_session(session_index),
764 OperatingMode::Buffered => (),
766 OperatingMode::Active => Self::do_end_session(session_index),
767 }
768 }
769 }
770
771 impl<T: Config>
772 OnOffenceHandler<
773 T::AccountId,
774 (T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>),
775 Weight,
776 > for Pallet<T>
777 {
778 fn on_offence(
779 offenders: &[OffenceDetails<
780 T::AccountId,
781 (T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>),
782 >],
783 slash_fraction: &[Perbill],
784 slash_session: SessionIndex,
785 ) -> Weight {
786 match Mode::<T>::get() {
787 OperatingMode::Passive => {
788 T::Fallback::on_offence(offenders, slash_fraction, slash_session)
790 },
791 OperatingMode::Buffered =>
792 Self::on_offence_buffered(offenders, slash_fraction, slash_session),
793 OperatingMode::Active =>
794 Self::on_offence_active(offenders, slash_fraction, slash_session),
795 }
796 }
797 }
798
799 impl<T: Config> RewardsReporter<T::AccountId> for Pallet<T> {
800 fn reward_by_ids(rewards: impl IntoIterator<Item = (T::AccountId, u32)>) {
801 match Mode::<T>::get() {
802 OperatingMode::Passive => T::Fallback::reward_by_ids(rewards),
803 OperatingMode::Buffered | OperatingMode::Active => Self::do_reward_by_ids(rewards),
804 }
805 }
806 }
807
808 impl<T: Config> pallet_authorship::EventHandler<T::AccountId, BlockNumberFor<T>> for Pallet<T> {
809 fn note_author(author: T::AccountId) {
810 match Mode::<T>::get() {
811 OperatingMode::Passive => T::Fallback::note_author(author),
812 OperatingMode::Buffered | OperatingMode::Active => Self::do_note_author(author),
813 }
814 }
815 }
816
817 impl<T: Config> Pallet<T> {
818 pub fn on_migration_start() {
827 debug_assert!(
828 Mode::<T>::get() == OperatingMode::Passive,
829 "we should only be called when in passive mode"
830 );
831 Self::do_set_mode(OperatingMode::Buffered);
832 }
833
834 pub fn on_migration_end() {
843 debug_assert!(
844 Mode::<T>::get() == OperatingMode::Buffered,
845 "we should only be called when in buffered mode"
846 );
847 Self::do_set_mode(OperatingMode::Active);
848
849 }
852
853 fn do_set_mode(new_mode: OperatingMode) {
854 let old_mode = Mode::<T>::get();
855 let unexpected = match new_mode {
856 OperatingMode::Passive => true,
858 OperatingMode::Buffered => old_mode != OperatingMode::Passive,
859 OperatingMode::Active => old_mode != OperatingMode::Buffered,
860 };
861
862 if unexpected {
864 log!(warn, "Unexpected mode transition from {:?} to {:?}", old_mode, new_mode);
865 Self::deposit_event(Event::Unexpected(UnexpectedKind::UnexpectedModeTransition));
866 }
867
868 Mode::<T>::put(new_mode);
870 }
871
872 fn do_new_session() -> Option<Vec<T::AccountId>> {
873 ValidatorSet::<T>::take().map(|(id, val_set)| {
874 NextSessionChangesValidators::<T>::put(id);
876 val_set
877 })
878 }
879
880 fn do_end_session(end_index: u32) {
881 let validator_points = ValidatorPoints::<T>::iter()
883 .drain()
884 .take(T::MaximumValidatorsWithPoints::get() as usize)
885 .collect::<Vec<_>>();
886
887 if ValidatorPoints::<T>::iter().next().is_some() {
889 Self::deposit_event(Event::<T>::Unexpected(UnexpectedKind::ValidatorPointDropped))
891 }
892
893 let activation_timestamp = NextSessionChangesValidators::<T>::take().map(|id| {
894 ValidatorSetAppliedAt::<T>::put(end_index + 1);
896 (T::UnixTime::now().as_millis().saturated_into::<u64>(), id)
898 });
899
900 let session_report = pallet_staking_async_rc_client::SessionReport {
901 end_index,
902 validator_points,
903 activation_timestamp,
904 leftover: false,
905 };
906
907 OutgoingSessionReport::<T>::put((session_report, T::MaxSessionReportRetries::get()));
909 }
910
911 fn do_reward_by_ids(rewards: impl IntoIterator<Item = (T::AccountId, u32)>) {
912 for (validator_id, points) in rewards {
913 ValidatorPoints::<T>::mutate(validator_id, |balance| {
914 balance.saturating_accrue(points);
915 });
916 }
917 }
918
919 fn do_note_author(author: T::AccountId) {
920 ValidatorPoints::<T>::mutate(author, |points| {
921 points.saturating_accrue(T::PointsPerBlock::get());
922 });
923 }
924
925 fn is_ongoing_offence(slash_session: SessionIndex) -> bool {
927 ValidatorSetAppliedAt::<T>::get()
928 .map(|start_session| slash_session >= start_session)
929 .unwrap_or(false)
930 }
931
932 fn on_offence_buffered(
934 offenders: &[OffenceDetailsOf<T>],
935 slash_fraction: &[Perbill],
936 slash_session: SessionIndex,
937 ) -> Weight {
938 let ongoing_offence = Self::is_ongoing_offence(slash_session);
939
940 offenders.iter().cloned().zip(slash_fraction).for_each(|(offence, fraction)| {
941 if ongoing_offence {
942 T::SessionInterface::report_offence(
944 offence.offender.0.clone(),
945 OffenceSeverity(*fraction),
946 );
947 }
948
949 let (offender, _full_identification) = offence.offender;
950 let reporters = offence.reporters;
951
952 OffenceSendQueue::<T>::append((
954 slash_session,
955 rc_client::Offence {
956 offender: offender.clone(),
957 reporters: reporters.into_iter().take(1).collect(),
958 slash_fraction: *fraction,
959 },
960 ));
961 });
962
963 T::DbWeight::get().reads_writes(1, 1)
964 }
965
966 fn on_offence_active(
968 offenders: &[OffenceDetailsOf<T>],
969 slash_fraction: &[Perbill],
970 slash_session: SessionIndex,
971 ) -> Weight {
972 let ongoing_offence = Self::is_ongoing_offence(slash_session);
973
974 offenders.iter().cloned().zip(slash_fraction).for_each(|(offence, fraction)| {
975 if ongoing_offence {
976 T::SessionInterface::report_offence(
978 offence.offender.0.clone(),
979 OffenceSeverity(*fraction),
980 );
981 }
982
983 let (offender, _full_identification) = offence.offender;
984 let reporters = offence.reporters;
985
986 let offence = rc_client::Offence {
989 offender,
990 reporters: reporters.into_iter().take(1).collect(),
991 slash_fraction: *fraction,
992 };
993 OffenceSendQueue::<T>::append((slash_session, offence))
994 });
995
996 T::DbWeight::get().reads_writes(2, 2)
997 }
998 }
999}
1000
1001#[cfg(test)]
1002mod send_queue_tests {
1003 use frame_support::hypothetically;
1004 use sp_runtime::Perbill;
1005
1006 use super::*;
1007 use crate::mock::*;
1008
1009 fn status() -> (u32, Vec<u32>) {
1011 let mut sorted = OffenceSendQueueOffences::<Test>::iter().collect::<Vec<_>>();
1012 sorted.sort_by(|x, y| x.0.cmp(&y.0));
1013 (
1014 OffenceSendQueueCursor::<Test>::get(),
1015 sorted.into_iter().map(|(_, v)| v.len() as u32).collect(),
1016 )
1017 }
1018
1019 #[test]
1020 fn append_and_take() {
1021 new_test_ext().execute_with(|| {
1022 let o = (
1023 42,
1024 rc_client::Offence {
1025 offender: 42,
1026 reporters: vec![],
1027 slash_fraction: Perbill::from_percent(10),
1028 },
1029 );
1030 let page_size = <Test as Config>::MaxOffenceBatchSize::get();
1031 assert_eq!(page_size % 2, 0, "page size should be even");
1032
1033 assert_eq!(status(), (0, vec![]));
1034
1035 assert_eq!(OffenceSendQueue::<Test>::count(), 0);
1038 assert_eq!(OffenceSendQueue::<Test>::pages(), 0);
1039
1040 hypothetically!({
1042 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1043 assert_eq!(page.len(), 0);
1044 Err(())
1045 });
1046 assert_eq!(status(), (0, vec![]));
1047 });
1048
1049 hypothetically!({
1051 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1052 assert_eq!(page.len(), 0);
1053 Ok(())
1054 });
1055 assert_eq!(status(), (0, vec![]));
1056 });
1057
1058 for _ in 0..page_size / 2 {
1060 OffenceSendQueue::<Test>::append(o.clone());
1061 }
1062 assert_eq!(status(), (0, vec![page_size / 2]));
1063 assert_eq!(OffenceSendQueue::<Test>::count(), page_size / 2);
1064 assert_eq!(OffenceSendQueue::<Test>::pages(), 1);
1065
1066 hypothetically!({
1068 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1069 assert_eq!(page.len() as u32, page_size / 2);
1070 Err(())
1071 });
1072 assert_eq!(status(), (0, vec![page_size / 2]));
1073 });
1074
1075 hypothetically!({
1077 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1078 assert_eq!(page.len() as u32, page_size / 2);
1079 Ok(())
1080 });
1081 assert_eq!(status(), (0, vec![]));
1082 assert_eq!(OffenceSendQueue::<Test>::count(), 0);
1083 assert_eq!(OffenceSendQueue::<Test>::pages(), 0);
1084 });
1085
1086 for _ in 0..page_size / 2 {
1088 OffenceSendQueue::<Test>::append(o.clone());
1089 }
1090 assert_eq!(status(), (0, vec![page_size]));
1091 assert_eq!(OffenceSendQueue::<Test>::count(), page_size);
1092 assert_eq!(OffenceSendQueue::<Test>::pages(), 1);
1093
1094 hypothetically!({
1096 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1097 assert_eq!(page.len() as u32, page_size);
1098 Err(())
1099 });
1100 assert_eq!(status(), (0, vec![page_size]));
1101 });
1102
1103 hypothetically!({
1105 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1106 assert_eq!(page.len() as u32, page_size);
1107 Ok(())
1108 });
1109 assert_eq!(status(), (0, vec![]));
1110 });
1111
1112 OffenceSendQueue::<Test>::append(o.clone());
1114 assert_eq!(status(), (1, vec![page_size, 1]));
1115 assert_eq!(OffenceSendQueue::<Test>::count(), page_size + 1);
1116 assert_eq!(OffenceSendQueue::<Test>::pages(), 2);
1117
1118 hypothetically!({
1120 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1121 assert_eq!(page.len(), 1);
1122 Err(())
1123 });
1124 assert_eq!(status(), (1, vec![page_size, 1]));
1125 });
1126
1127 hypothetically!({
1129 OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1130 assert_eq!(page.len(), 1);
1131 Ok(())
1132 });
1133 assert_eq!(status(), (0, vec![page_size]));
1134 });
1135 })
1136 }
1137}