pallet_staking_async_ah_client/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! The client for AssetHub, intended to be used in the relay chain.
19//!
20//! The counter-part for this pallet is `pallet-staking-async-rc-client` on AssetHub.
21//!
22//! This documentation is divided into the following sections:
23//!
24//! 1. Incoming messages: the messages that we receive from the relay chian.
25//! 2. Outgoing messages: the messaged that we sent to the relay chain.
26//! 3. Local interfaces: the interfaces that we expose to other pallets in the runtime.
27//!
28//! ## Incoming Messages
29//!
30//! All incoming messages are handled via [`Call`]. They are all gated to be dispatched only by
31//! [`Config::AssetHubOrigin`]. The only one is:
32//!
33//! * [`Call::validator_set`]: A new validator set for a planning session index.
34//!
35//! ## Outgoing Messages
36//!
37//! All outgoing messages are handled by a single trait [`SendToAssetHub`]. They match the
38//! incoming messages of the `ah-client` pallet.
39//!
40//! ## Local Interfaces:
41//!
42//! Living on the relay chain, this pallet must:
43//!
44//! * Implement [`pallet_session::SessionManager`] (and historical variant thereof) to _give_
45//!   information to the session pallet.
46//! * Implements [`SessionInterface`] to _receive_ information from the session pallet
47//! * Implement [`sp_staking::offence::OnOffenceHandler`].
48//! * Implement reward related APIs ([`frame_support::traits::RewardsReporter`]).
49//!
50//! ## Future Plans
51//!
52//! * Governance functions to force set validators.
53
54#![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
67/// The balance type seen from this pallet's PoV.
68pub type BalanceOf<T> = <T as Config>::CurrencyBalance;
69
70const LOG_TARGET: &str = "runtime::staking-async::ah-client";
71
72// syntactic sugar for logging.
73#[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
83/// The interface to communicate to asset hub.
84///
85/// This trait should only encapsulate our outgoing communications. Any incoming message is handled
86/// with `Call`s.
87///
88/// In a real runtime, this is implemented via XCM calls, much like how the coretime pallet works.
89/// In a test runtime, it can be wired to direct function call.
90pub trait SendToAssetHub {
91	/// The validator account ids.
92	type AccountId;
93
94	/// Report a session change to AssetHub.
95	fn relay_session_report(session_report: rc_client::SessionReport<Self::AccountId>);
96
97	/// Report new offences.
98	fn relay_new_offence(
99		session_index: SessionIndex,
100		offences: Vec<rc_client::Offence<Self::AccountId>>,
101	);
102}
103
104/// A no-op implementation of [`SendToAssetHub`].
105#[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
121/// Interface to talk to the local session pallet.
122pub trait SessionInterface {
123	/// The validator id type of the session pallet
124	type ValidatorId: Clone;
125
126	fn validators() -> Vec<Self::ValidatorId>;
127
128	/// prune up to the given session index.
129	fn prune_up_to(index: SessionIndex);
130
131	/// Report an offence.
132	///
133	/// This is used to disable validators directly on the RC, until the next validator set.
134	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/// Represents the operating mode of the pallet.
155#[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	/// Fully delegated mode.
171	///
172	/// In this mode, the pallet performs no core logic and forwards all relevant operations
173	/// to the fallback implementation defined in the pallet's `Config::Fallback`.
174	///
175	/// This mode is useful when staking is in synchronous mode and waiting for the signal to
176	/// transition to asynchronous mode.
177	#[default]
178	Passive,
179
180	/// Buffered mode for deferred execution.
181	///
182	/// In this mode, offences are accepted and buffered for later transmission to AssetHub.
183	/// However, session change reports are dropped.
184	///
185	/// This mode is useful when the counterpart pallet `pallet-staking-async-rc-client` on
186	/// AssetHub is not yet ready to process incoming messages.
187	Buffered,
188
189	/// Fully active mode.
190	///
191	/// The pallet performs all core logic directly and handles messages immediately.
192	///
193	/// This mode is useful when staking is ready to execute in asynchronous mode and the
194	/// counterpart pallet `pallet-staking-async-rc-client` is ready to accept messages.
195	Active,
196}
197
198impl OperatingMode {
199	fn can_accept_validator_set(&self) -> bool {
200		matches!(self, OperatingMode::Active)
201	}
202}
203
204/// See `pallet_staking::DefaultExposureOf`. This type is the same, except it is duplicated here so
205/// that an rc-runtime can use it after `pallet-staking` is fully removed as a dependency.
206pub 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		/// The balance type of the runtime's currency interface.
241		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		/// An origin type that ensures an incoming message is from asset hub.
256		type AssetHubOrigin: EnsureOrigin<Self::RuntimeOrigin>;
257
258		/// The origin that can control this pallet's operations.
259		type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
260
261		/// Our communication interface to AssetHub.
262		type SendToAssetHub: SendToAssetHub<AccountId = Self::AccountId>;
263
264		/// A safety measure that asserts an incoming validator set must be at least this large.
265		type MinimumValidatorSetSize: Get<u32>;
266
267		/// A type that gives us a reliable unix timestamp.
268		type UnixTime: UnixTime;
269
270		/// Number of points to award a validator per block authored.
271		type PointsPerBlock: Get<u32>;
272
273		/// Interface to talk to the local Session pallet.
274		type SessionInterface: SessionInterface<ValidatorId = Self::AccountId>;
275
276		/// A fallback implementation to delegate logic to when the pallet is in
277		/// [`OperatingMode::Passive`].
278		///
279		/// This type must implement the `historical::SessionManager` and `OnOffenceHandler`
280		/// interface and is expected to behave as a stand-in for this pallet’s core logic when
281		/// delegation is active.
282		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	/// The queued validator sets for a given planning session index.
296	///
297	/// This is received via a call from AssetHub.
298	#[pallet::storage]
299	#[pallet::unbounded]
300	pub type ValidatorSet<T: Config> = StorageValue<_, (u32, Vec<T::AccountId>), OptionQuery>;
301
302	/// An incomplete validator set report.
303	#[pallet::storage]
304	#[pallet::unbounded]
305	pub type IncompleteValidatorSetReport<T: Config> =
306		StorageValue<_, rc_client::ValidatorSetReport<T::AccountId>, OptionQuery>;
307
308	/// All of the points of the validators.
309	///
310	/// This is populated during a session, and is flushed and sent over via [`SendToAssetHub`]
311	/// at each session end.
312	#[pallet::storage]
313	pub type ValidatorPoints<T: Config> =
314		StorageMap<_, Twox64Concat, T::AccountId, u32, ValueQuery>;
315
316	/// Indicates the current operating mode of the pallet.
317	///
318	/// This value determines how the pallet behaves in response to incoming and outgoing messages,
319	/// particularly whether it should execute logic directly, defer it, or delegate it entirely.
320	#[pallet::storage]
321	pub type Mode<T: Config> = StorageValue<_, OperatingMode, ValueQuery>;
322
323	/// A storage value that is set when a `new_session` gives a new validator set to the session
324	/// pallet, and is cleared on the next call.
325	///
326	/// The inner u32 is the id of the said activated validator set. While not relevant here, good
327	/// to know this is the planning era index of staking-async on AH.
328	///
329	/// Once cleared, we know a validator set has been activated, and therefore we can send a
330	/// timestamp to AH.
331	#[pallet::storage]
332	pub type NextSessionChangesValidators<T: Config> = StorageValue<_, u32, OptionQuery>;
333
334	/// The session index at which the latest elected validator set was applied.
335	///
336	/// This is used to determine if an offence, given a session index, is in the current active era
337	/// or not.
338	#[pallet::storage]
339	pub type ValidatorSetAppliedAt<T: Config> = StorageValue<_, SessionIndex, OptionQuery>;
340
341	/// Stores offences that have been received while the pallet is in [`OperatingMode::Buffered`]
342	/// mode.
343	///
344	/// These offences are collected and buffered for later processing when the pallet transitions
345	/// to [`OperatingMode::Active`]. This allows the system to defer slashing or reporting logic
346	/// until communication with the counterpart pallet on AssetHub is fully established.
347	///
348	/// This storage is only used in `Buffered` mode; in `Active` mode, offences are immediately
349	/// sent, and in `Passive` mode, they are delegated to the [`Config::Fallback`] implementation.
350	#[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		/// The initial operating mode of the pallet.
359		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			// Set the initial operating mode of the pallet.
367			Mode::<T>::put(self.operating_mode.clone());
368		}
369	}
370
371	#[pallet::error]
372	pub enum Error<T> {
373		/// Could not process incoming message because incoming messages are blocked.
374		Blocked,
375	}
376
377	#[pallet::event]
378	#[pallet::generate_deposit(fn deposit_event)]
379	pub enum Event<T: Config> {
380		/// A new validator set has been received.
381		ValidatorSetReceived {
382			id: u32,
383			new_validator_set_count: u32,
384			prune_up_to: Option<SessionIndex>,
385			leftover: bool,
386		},
387		/// We could not merge, and therefore dropped a buffered message.
388		///
389		/// Note that this event is more resembling an error, but we use an event because in this
390		/// pallet we need to mutate storage upon some failures.
391		CouldNotMergeAndDropped,
392		/// The validator set received is way too small, as per
393		/// [`Config::MinimumValidatorSetSize`].
394		SetTooSmallAndDropped,
395		/// Something occurred that should never happen under normal operation. Logged as an event
396		/// for fail-safe observability.
397		Unexpected(UnexpectedKind),
398	}
399
400	/// Represents unexpected or invariant-breaking conditions encountered during execution.
401	///
402	/// These variants are emitted as [`Event::Unexpected`] and indicate a defensive check has
403	/// failed. While these should never occur under normal operation, they are useful for
404	/// diagnosing issues in production or test environments.
405	#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, RuntimeDebug)]
406	pub enum UnexpectedKind {
407		/// A validator set was received while the pallet is in [`OperatingMode::Passive`].
408		ReceivedValidatorSetWhilePassive,
409
410		/// An unexpected transition was applied between operating modes.
411		///
412		/// Expected transitions are linear and forward-only: `Passive` → `Buffered` → `Active`.
413		UnexpectedModeTransition,
414	}
415
416	#[pallet::call]
417	impl<T: Config> Pallet<T> {
418		#[pallet::call_index(0)]
419		#[pallet::weight(
420			// Reads:
421			// - OperatingMode
422			// - IncompleteValidatorSetReport
423			// Writes:
424			// - IncompleteValidatorSetReport or ValidatorSet
425			// ignoring `T::SessionInterface::prune_up_to`
426			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			// Ensure the origin is one of Root or whatever is representing AssetHub.
433			log!(debug, "Received new validator set report {}", report);
434			T::AssetHubOrigin::ensure_origin_or_root(origin)?;
435
436			// Check the operating mode.
437			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				// buffer it, and nothing further to do.
458				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				// message is complete, process it.
467				let rc_client::ValidatorSetReport {
468					id,
469					leftover,
470					mut new_validator_set,
471					prune_up_to,
472				} = report;
473
474				// ensure the validator set, deduplicated, is not too big.
475				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				// Save the validator set.
495				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		/// Allows governance to force set the operating mode of the pallet.
505		#[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				// In `Buffered` mode, we drop the session report and do nothing.
556				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				// In `Buffered` mode, we drop the session report and do nothing.
579				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				// delegate to the fallback implementation.
603				return T::Fallback::on_offence(offenders, slash_fraction, slash_session);
604			}
605
606			// check if offence is from the active validator set.
607			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			// notify pallet-session about the offences
614			for (offence, fraction) in offenders.iter().cloned().zip(slash_fraction) {
615				if ongoing_offence {
616					// report the offence to the session pallet.
617					T::SessionInterface::report_offence(
618						offence.offender.0.clone(),
619						OffenceSeverity(*fraction),
620					);
621				}
622
623				// prepare an `Offence` instance for the XCM message. Note that we drop the
624				// identification.
625				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		/// Hook to be called when the AssetHub migration begins.
672		///
673		/// This transitions the pallet into [`OperatingMode::Buffered`], meaning it will act as the
674		/// primary staking module on the relay chain but will buffer outgoing messages instead of
675		/// sending them to AssetHub.
676		///
677		/// While in this mode, the pallet stops delegating to the fallback implementation and
678		/// temporarily accumulates events for later processing.
679		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		/// Hook to be called when the AssetHub migration is complete.
688		///
689		/// This transitions the pallet into [`OperatingMode::Active`], meaning the counterpart
690		/// pallet on AssetHub is ready to accept incoming messages, and this pallet can resume
691		/// sending them.
692		///
693		/// In this mode, the pallet becomes fully active and processes all staking-related events
694		/// directly.
695		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			// send all buffered offences to AssetHub.
703			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				// `Passive` is the initial state, and not expected to be set by the user.
712				OperatingMode::Passive => true,
713				OperatingMode::Buffered => old_mode != OperatingMode::Passive,
714				OperatingMode::Active => old_mode != OperatingMode::Buffered,
715			};
716
717			// this is a defensive check, and should never happen under normal operation.
718			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			// apply new mode anyway.
724			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				// store the id to be sent back in the next session back to AH
730				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				// keep track of starting session index at which the validator set was applied.
741				ValidatorSetAppliedAt::<T>::put(session_index + 1);
742				// set the timestamp and the identifier of the validator set.
743				(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}