1#![cfg_attr(not(feature = "std"), no_std)]
55
56pub use pallet::*;
57
58extern crate alloc;
59use alloc::vec::Vec;
60use frame_support::{pallet_prelude::*, traits::RewardsReporter};
61use pallet_staking_async_rc_client::{self as rc_client};
62use sp_staking::{
63 offence::{OffenceDetails, OffenceSeverity},
64 SessionIndex,
65};
66
67pub type BalanceOf<T> = <T as Config>::CurrencyBalance;
69
70const LOG_TARGET: &str = "runtime::staking-async::ah-client";
71
72#[macro_export]
74macro_rules! log {
75 ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
76 log::$level!(
77 target: $crate::LOG_TARGET,
78 concat!("[{:?}] ⬇️ ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
79 )
80 };
81}
82
83pub trait SendToAssetHub {
91 type AccountId;
93
94 fn relay_session_report(session_report: rc_client::SessionReport<Self::AccountId>);
96
97 fn relay_new_offence(
99 session_index: SessionIndex,
100 offences: Vec<rc_client::Offence<Self::AccountId>>,
101 );
102}
103
104#[cfg(feature = "std")]
106impl SendToAssetHub for () {
107 type AccountId = u64;
108
109 fn relay_session_report(_session_report: rc_client::SessionReport<Self::AccountId>) {
110 panic!("relay_session_report not implemented");
111 }
112
113 fn relay_new_offence(
114 _session_index: SessionIndex,
115 _offences: Vec<rc_client::Offence<Self::AccountId>>,
116 ) {
117 panic!("relay_new_offence not implemented");
118 }
119}
120
121pub trait SessionInterface {
123 type ValidatorId: Clone;
125
126 fn validators() -> Vec<Self::ValidatorId>;
127
128 fn prune_up_to(index: SessionIndex);
130
131 fn report_offence(offender: Self::ValidatorId, severity: OffenceSeverity);
135}
136
137impl<T: Config + pallet_session::Config + pallet_session::historical::Config> SessionInterface
138 for T
139{
140 type ValidatorId = <T as pallet_session::Config>::ValidatorId;
141
142 fn validators() -> Vec<Self::ValidatorId> {
143 pallet_session::Pallet::<T>::validators()
144 }
145
146 fn prune_up_to(index: SessionIndex) {
147 pallet_session::historical::Pallet::<T>::prune_up_to(index)
148 }
149 fn report_offence(offender: Self::ValidatorId, severity: OffenceSeverity) {
150 pallet_session::Pallet::<T>::report_offence(offender, severity)
151 }
152}
153
154#[derive(
156 Default,
157 DecodeWithMemTracking,
158 Encode,
159 Decode,
160 MaxEncodedLen,
161 TypeInfo,
162 Clone,
163 PartialEq,
164 Eq,
165 RuntimeDebug,
166 serde::Serialize,
167 serde::Deserialize,
168)]
169pub enum OperatingMode {
170 #[default]
178 Passive,
179
180 Buffered,
188
189 Active,
196}
197
198impl OperatingMode {
199 fn can_accept_validator_set(&self) -> bool {
200 matches!(self, OperatingMode::Active)
201 }
202}
203
204pub struct DefaultExposureOf<T>(core::marker::PhantomData<T>);
207
208impl<T: Config>
209 sp_runtime::traits::Convert<
210 T::AccountId,
211 Option<sp_staking::Exposure<T::AccountId, BalanceOf<T>>>,
212 > for DefaultExposureOf<T>
213{
214 fn convert(
215 validator: T::AccountId,
216 ) -> Option<sp_staking::Exposure<T::AccountId, BalanceOf<T>>> {
217 T::SessionInterface::validators()
218 .contains(&validator)
219 .then_some(Default::default())
220 }
221}
222
223#[frame_support::pallet]
224pub mod pallet {
225 use crate::*;
226 use alloc::vec;
227 use frame_support::traits::UnixTime;
228 use frame_system::pallet_prelude::*;
229 use pallet_session::{historical, SessionManager};
230 use sp_runtime::{Perbill, Saturating};
231 use sp_staking::{
232 offence::{OffenceSeverity, OnOffenceHandler},
233 SessionIndex,
234 };
235
236 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
237
238 #[pallet::config]
239 pub trait Config: frame_system::Config {
240 type CurrencyBalance: sp_runtime::traits::AtLeast32BitUnsigned
242 + codec::FullCodec
243 + DecodeWithMemTracking
244 + codec::HasCompact<Type: DecodeWithMemTracking>
245 + Copy
246 + MaybeSerializeDeserialize
247 + core::fmt::Debug
248 + Default
249 + From<u64>
250 + TypeInfo
251 + Send
252 + Sync
253 + MaxEncodedLen;
254
255 type AssetHubOrigin: EnsureOrigin<Self::RuntimeOrigin>;
257
258 type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
260
261 type SendToAssetHub: SendToAssetHub<AccountId = Self::AccountId>;
263
264 type MinimumValidatorSetSize: Get<u32>;
266
267 type UnixTime: UnixTime;
269
270 type PointsPerBlock: Get<u32>;
272
273 type SessionInterface: SessionInterface<ValidatorId = Self::AccountId>;
275
276 type Fallback: pallet_session::SessionManager<Self::AccountId>
283 + OnOffenceHandler<
284 Self::AccountId,
285 (Self::AccountId, sp_staking::Exposure<Self::AccountId, BalanceOf<Self>>),
286 Weight,
287 > + frame_support::traits::RewardsReporter<Self::AccountId>
288 + pallet_authorship::EventHandler<Self::AccountId, BlockNumberFor<Self>>;
289 }
290
291 #[pallet::pallet]
292 #[pallet::storage_version(STORAGE_VERSION)]
293 pub struct Pallet<T>(_);
294
295 #[pallet::storage]
299 #[pallet::unbounded]
300 pub type ValidatorSet<T: Config> = StorageValue<_, (u32, Vec<T::AccountId>), OptionQuery>;
301
302 #[pallet::storage]
304 #[pallet::unbounded]
305 pub type IncompleteValidatorSetReport<T: Config> =
306 StorageValue<_, rc_client::ValidatorSetReport<T::AccountId>, OptionQuery>;
307
308 #[pallet::storage]
313 pub type ValidatorPoints<T: Config> =
314 StorageMap<_, Twox64Concat, T::AccountId, u32, ValueQuery>;
315
316 #[pallet::storage]
321 pub type Mode<T: Config> = StorageValue<_, OperatingMode, ValueQuery>;
322
323 #[pallet::storage]
332 pub type NextSessionChangesValidators<T: Config> = StorageValue<_, u32, OptionQuery>;
333
334 #[pallet::storage]
339 pub type ValidatorSetAppliedAt<T: Config> = StorageValue<_, SessionIndex, OptionQuery>;
340
341 #[pallet::storage]
351 #[pallet::unbounded]
352 pub type BufferedOffences<T: Config> =
353 StorageValue<_, Vec<(SessionIndex, Vec<rc_client::Offence<T::AccountId>>)>, ValueQuery>;
354
355 #[pallet::genesis_config]
356 #[derive(frame_support::DefaultNoBound, frame_support::DebugNoBound)]
357 pub struct GenesisConfig<T: Config> {
358 pub operating_mode: OperatingMode,
360 pub _marker: core::marker::PhantomData<T>,
361 }
362
363 #[pallet::genesis_build]
364 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
365 fn build(&self) {
366 Mode::<T>::put(self.operating_mode.clone());
368 }
369 }
370
371 #[pallet::error]
372 pub enum Error<T> {
373 Blocked,
375 }
376
377 #[pallet::event]
378 #[pallet::generate_deposit(fn deposit_event)]
379 pub enum Event<T: Config> {
380 ValidatorSetReceived {
382 id: u32,
383 new_validator_set_count: u32,
384 prune_up_to: Option<SessionIndex>,
385 leftover: bool,
386 },
387 CouldNotMergeAndDropped,
392 SetTooSmallAndDropped,
395 Unexpected(UnexpectedKind),
398 }
399
400 #[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, RuntimeDebug)]
406 pub enum UnexpectedKind {
407 ReceivedValidatorSetWhilePassive,
409
410 UnexpectedModeTransition,
414 }
415
416 #[pallet::call]
417 impl<T: Config> Pallet<T> {
418 #[pallet::call_index(0)]
419 #[pallet::weight(
420 T::DbWeight::get().reads_writes(2, 1)
427 )]
428 pub fn validator_set(
429 origin: OriginFor<T>,
430 report: rc_client::ValidatorSetReport<T::AccountId>,
431 ) -> DispatchResult {
432 log!(debug, "Received new validator set report {}", report);
434 T::AssetHubOrigin::ensure_origin_or_root(origin)?;
435
436 let mode = Mode::<T>::get();
438 ensure!(mode.can_accept_validator_set(), Error::<T>::Blocked);
439
440 let maybe_merged_report = match IncompleteValidatorSetReport::<T>::take() {
441 Some(old) => old.merge(report.clone()),
442 None => Ok(report),
443 };
444
445 if maybe_merged_report.is_err() {
446 Self::deposit_event(Event::CouldNotMergeAndDropped);
447 debug_assert!(
448 IncompleteValidatorSetReport::<T>::get().is_none(),
449 "we have ::take() it above, we don't want to keep the old data"
450 );
451 return Ok(());
452 }
453
454 let report = maybe_merged_report.expect("checked above; qed");
455
456 if report.leftover {
457 Self::deposit_event(Event::ValidatorSetReceived {
459 id: report.id,
460 new_validator_set_count: report.new_validator_set.len() as u32,
461 prune_up_to: report.prune_up_to,
462 leftover: report.leftover,
463 });
464 IncompleteValidatorSetReport::<T>::put(report);
465 } else {
466 let rc_client::ValidatorSetReport {
468 id,
469 leftover,
470 mut new_validator_set,
471 prune_up_to,
472 } = report;
473
474 new_validator_set.sort();
476 new_validator_set.dedup();
477
478 if (new_validator_set.len() as u32) < T::MinimumValidatorSetSize::get() {
479 Self::deposit_event(Event::SetTooSmallAndDropped);
480 debug_assert!(
481 IncompleteValidatorSetReport::<T>::get().is_none(),
482 "we have ::take() it above, we don't want to keep the old data"
483 );
484 return Ok(());
485 }
486
487 Self::deposit_event(Event::ValidatorSetReceived {
488 id,
489 new_validator_set_count: new_validator_set.len() as u32,
490 prune_up_to,
491 leftover,
492 });
493
494 ValidatorSet::<T>::put((id, new_validator_set));
496 if let Some(index) = prune_up_to {
497 T::SessionInterface::prune_up_to(index);
498 }
499 }
500
501 Ok(())
502 }
503
504 #[pallet::call_index(1)]
506 #[pallet::weight(T::DbWeight::get().writes(1))]
507 pub fn set_mode(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
508 T::AdminOrigin::ensure_origin(origin)?;
509 Self::do_set_mode(mode);
510 Ok(())
511 }
512 }
513
514 impl<T: Config>
515 historical::SessionManager<T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>>
516 for Pallet<T>
517 {
518 fn new_session(
519 new_index: sp_staking::SessionIndex,
520 ) -> Option<
521 Vec<(
522 <T as frame_system::Config>::AccountId,
523 sp_staking::Exposure<T::AccountId, BalanceOf<T>>,
524 )>,
525 > {
526 <Self as pallet_session::SessionManager<_>>::new_session(new_index)
527 .map(|v| v.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect())
528 }
529
530 fn new_session_genesis(
531 new_index: SessionIndex,
532 ) -> Option<Vec<(T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>)>> {
533 if Mode::<T>::get() == OperatingMode::Passive {
534 T::Fallback::new_session_genesis(new_index).map(|validators| {
535 validators.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect()
536 })
537 } else {
538 None
539 }
540 }
541
542 fn start_session(start_index: SessionIndex) {
543 <Self as pallet_session::SessionManager<_>>::start_session(start_index)
544 }
545
546 fn end_session(end_index: SessionIndex) {
547 <Self as pallet_session::SessionManager<_>>::end_session(end_index)
548 }
549 }
550
551 impl<T: Config> pallet_session::SessionManager<T::AccountId> for Pallet<T> {
552 fn new_session(session_index: u32) -> Option<Vec<T::AccountId>> {
553 match Mode::<T>::get() {
554 OperatingMode::Passive => T::Fallback::new_session(session_index),
555 OperatingMode::Buffered => None,
557 OperatingMode::Active => Self::do_new_session(),
558 }
559 }
560
561 fn start_session(session_index: u32) {
562 if Mode::<T>::get() == OperatingMode::Passive {
563 T::Fallback::start_session(session_index)
564 }
565 }
566
567 fn new_session_genesis(new_index: SessionIndex) -> Option<Vec<T::AccountId>> {
568 if Mode::<T>::get() == OperatingMode::Passive {
569 T::Fallback::new_session_genesis(new_index)
570 } else {
571 None
572 }
573 }
574
575 fn end_session(session_index: u32) {
576 match Mode::<T>::get() {
577 OperatingMode::Passive => T::Fallback::end_session(session_index),
578 OperatingMode::Buffered => (),
580 OperatingMode::Active => Self::do_end_session(session_index),
581 }
582 }
583 }
584
585 impl<T: Config>
586 OnOffenceHandler<
587 T::AccountId,
588 (T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>),
589 Weight,
590 > for Pallet<T>
591 {
592 fn on_offence(
593 offenders: &[OffenceDetails<
594 T::AccountId,
595 (T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>),
596 >],
597 slash_fraction: &[Perbill],
598 slash_session: SessionIndex,
599 ) -> Weight {
600 let mode = Mode::<T>::get();
601 if mode == OperatingMode::Passive {
602 return T::Fallback::on_offence(offenders, slash_fraction, slash_session);
604 }
605
606 let ongoing_offence = ValidatorSetAppliedAt::<T>::get()
608 .map(|start_session| slash_session >= start_session)
609 .unwrap_or(false);
610
611 let mut offenders_and_slashes = Vec::new();
612
613 for (offence, fraction) in offenders.iter().cloned().zip(slash_fraction) {
615 if ongoing_offence {
616 T::SessionInterface::report_offence(
618 offence.offender.0.clone(),
619 OffenceSeverity(*fraction),
620 );
621 }
622
623 let (offender, _full_identification) = offence.offender;
626 let reporters = offence.reporters;
627 offenders_and_slashes.push(rc_client::Offence {
628 offender,
629 reporters,
630 slash_fraction: *fraction,
631 });
632 }
633
634 match mode {
635 OperatingMode::Buffered => {
636 BufferedOffences::<T>::mutate(|buffered| {
637 buffered.push((slash_session, offenders_and_slashes.clone()));
638 });
639 log!(info, "Buffered offences: {:?}", offenders_and_slashes);
640 },
641 OperatingMode::Active => {
642 log!(info, "sending offence report to AH");
643 T::SendToAssetHub::relay_new_offence(slash_session, offenders_and_slashes);
644 },
645 _ => (),
646 }
647
648 Weight::zero()
649 }
650 }
651
652 impl<T: Config> RewardsReporter<T::AccountId> for Pallet<T> {
653 fn reward_by_ids(rewards: impl IntoIterator<Item = (T::AccountId, u32)>) {
654 match Mode::<T>::get() {
655 OperatingMode::Passive => T::Fallback::reward_by_ids(rewards),
656 OperatingMode::Buffered | OperatingMode::Active => Self::do_reward_by_ids(rewards),
657 }
658 }
659 }
660
661 impl<T: Config> pallet_authorship::EventHandler<T::AccountId, BlockNumberFor<T>> for Pallet<T> {
662 fn note_author(author: T::AccountId) {
663 match Mode::<T>::get() {
664 OperatingMode::Passive => T::Fallback::note_author(author),
665 OperatingMode::Buffered | OperatingMode::Active => Self::do_note_author(author),
666 }
667 }
668 }
669
670 impl<T: Config> Pallet<T> {
671 pub fn on_migration_start() {
680 debug_assert!(
681 Mode::<T>::get() == OperatingMode::Passive,
682 "we should only be called when in passive mode"
683 );
684 Self::do_set_mode(OperatingMode::Buffered);
685 }
686
687 pub fn on_migration_end() {
696 debug_assert!(
697 Mode::<T>::get() == OperatingMode::Buffered,
698 "we should only be called when in buffered mode"
699 );
700 Self::do_set_mode(OperatingMode::Active);
701
702 BufferedOffences::<T>::take().into_iter().for_each(|(slash_session, offences)| {
704 T::SendToAssetHub::relay_new_offence(slash_session, offences)
705 });
706 }
707
708 fn do_set_mode(new_mode: OperatingMode) {
709 let old_mode = Mode::<T>::get();
710 let unexpected = match new_mode {
711 OperatingMode::Passive => true,
713 OperatingMode::Buffered => old_mode != OperatingMode::Passive,
714 OperatingMode::Active => old_mode != OperatingMode::Buffered,
715 };
716
717 if unexpected {
719 log!(warn, "Unexpected mode transition from {:?} to {:?}", old_mode, new_mode);
720 Self::deposit_event(Event::Unexpected(UnexpectedKind::UnexpectedModeTransition));
721 }
722
723 Mode::<T>::put(new_mode);
725 }
726
727 fn do_new_session() -> Option<Vec<T::AccountId>> {
728 ValidatorSet::<T>::take().map(|(id, val_set)| {
729 NextSessionChangesValidators::<T>::put(id);
731 val_set
732 })
733 }
734
735 fn do_end_session(session_index: u32) {
736 use sp_runtime::SaturatedConversion;
737
738 let validator_points = ValidatorPoints::<T>::iter().drain().collect::<Vec<_>>();
739 let activation_timestamp = NextSessionChangesValidators::<T>::take().map(|id| {
740 ValidatorSetAppliedAt::<T>::put(session_index + 1);
742 (T::UnixTime::now().as_millis().saturated_into::<u64>(), id)
744 });
745
746 let session_report = pallet_staking_async_rc_client::SessionReport {
747 end_index: session_index,
748 validator_points,
749 activation_timestamp,
750 leftover: false,
751 };
752
753 T::SendToAssetHub::relay_session_report(session_report);
754 }
755
756 fn do_reward_by_ids(rewards: impl IntoIterator<Item = (T::AccountId, u32)>) {
757 for (validator_id, points) in rewards {
758 ValidatorPoints::<T>::mutate(validator_id, |balance| {
759 balance.saturating_accrue(points);
760 });
761 }
762 }
763
764 fn do_note_author(author: T::AccountId) {
765 ValidatorPoints::<T>::mutate(author, |points| {
766 points.saturating_accrue(T::PointsPerBlock::get());
767 });
768 }
769 }
770}