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
58#[cfg(test)]
59pub mod mock;
60
61#[cfg(feature = "runtime-benchmarks")]
62pub mod benchmarking;
63pub mod weights;
64
65pub use weights::WeightInfo;
66
67extern crate alloc;
68use alloc::{collections::BTreeMap, vec::Vec};
69use frame_support::{pallet_prelude::*, traits::RewardsReporter};
70use pallet_staking_async_rc_client::{self as rc_client};
71use sp_staking::{
72	offence::{OffenceDetails, OffenceSeverity},
73	SessionIndex,
74};
75
76/// The balance type seen from this pallet's PoV.
77pub type BalanceOf<T> = <T as Config>::CurrencyBalance;
78
79/// Type alias for offence details
80pub 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// syntactic sugar for logging.
91#[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
101/// The interface to communicate to asset hub.
102///
103/// This trait should only encapsulate our outgoing communications. Any incoming message is handled
104/// with `Call`s.
105///
106/// In a real runtime, this is implemented via XCM calls, much like how the coretime pallet works.
107/// In a test runtime, it can be wired to direct function call.
108pub trait SendToAssetHub {
109	/// The validator account ids.
110	type AccountId;
111
112	/// Report a session change to AssetHub.
113	fn relay_session_report(session_report: rc_client::SessionReport<Self::AccountId>);
114
115	/// Report new offences.
116	fn relay_new_offence(
117		session_index: SessionIndex,
118		offences: Vec<rc_client::Offence<Self::AccountId>>,
119	);
120}
121
122/// A no-op implementation of [`SendToAssetHub`].
123#[cfg(feature = "std")]
124impl SendToAssetHub for () {
125	type AccountId = u64;
126
127	fn relay_session_report(_session_report: rc_client::SessionReport<Self::AccountId>) {
128		panic!("relay_session_report not implemented");
129	}
130
131	fn relay_new_offence(
132		_session_index: SessionIndex,
133		_offences: Vec<rc_client::Offence<Self::AccountId>>,
134	) {
135		panic!("relay_new_offence not implemented");
136	}
137}
138
139/// Interface to talk to the local session pallet.
140pub trait SessionInterface {
141	/// The validator id type of the session pallet
142	type ValidatorId: Clone;
143
144	fn validators() -> Vec<Self::ValidatorId>;
145
146	/// prune up to the given session index.
147	fn prune_up_to(index: SessionIndex);
148
149	/// Report an offence.
150	///
151	/// This is used to disable validators directly on the RC, until the next validator set.
152	fn report_offence(offender: Self::ValidatorId, severity: OffenceSeverity);
153}
154
155impl<T: Config + pallet_session::Config + pallet_session::historical::Config> SessionInterface
156	for T
157{
158	type ValidatorId = <T as pallet_session::Config>::ValidatorId;
159
160	fn validators() -> Vec<Self::ValidatorId> {
161		pallet_session::Pallet::<T>::validators()
162	}
163
164	fn prune_up_to(index: SessionIndex) {
165		pallet_session::historical::Pallet::<T>::prune_up_to(index)
166	}
167	fn report_offence(offender: Self::ValidatorId, severity: OffenceSeverity) {
168		pallet_session::Pallet::<T>::report_offence(offender, severity)
169	}
170}
171
172/// Represents the operating mode of the pallet.
173#[derive(
174	Default,
175	DecodeWithMemTracking,
176	Encode,
177	Decode,
178	MaxEncodedLen,
179	TypeInfo,
180	Clone,
181	PartialEq,
182	Eq,
183	RuntimeDebug,
184	serde::Serialize,
185	serde::Deserialize,
186)]
187pub enum OperatingMode {
188	/// Fully delegated mode.
189	///
190	/// In this mode, the pallet performs no core logic and forwards all relevant operations
191	/// to the fallback implementation defined in the pallet's `Config::Fallback`.
192	///
193	/// This mode is useful when staking is in synchronous mode and waiting for the signal to
194	/// transition to asynchronous mode.
195	#[default]
196	Passive,
197
198	/// Buffered mode for deferred execution.
199	///
200	/// In this mode, offences are accepted and buffered for later transmission to AssetHub.
201	/// However, session change reports are dropped.
202	///
203	/// This mode is useful when the counterpart pallet `pallet-staking-async-rc-client` on
204	/// AssetHub is not yet ready to process incoming messages.
205	Buffered,
206
207	/// Fully active mode.
208	///
209	/// The pallet performs all core logic directly and handles messages immediately.
210	///
211	/// This mode is useful when staking is ready to execute in asynchronous mode and the
212	/// counterpart pallet `pallet-staking-async-rc-client` is ready to accept messages.
213	Active,
214}
215
216impl OperatingMode {
217	fn can_accept_validator_set(&self) -> bool {
218		matches!(self, OperatingMode::Active)
219	}
220}
221
222/// See `pallet_staking::DefaultExposureOf`. This type is the same, except it is duplicated here so
223/// that an rc-runtime can use it after `pallet-staking` is fully removed as a dependency.
224pub struct DefaultExposureOf<T>(core::marker::PhantomData<T>);
225
226impl<T: Config>
227	sp_runtime::traits::Convert<
228		T::AccountId,
229		Option<sp_staking::Exposure<T::AccountId, BalanceOf<T>>>,
230	> for DefaultExposureOf<T>
231{
232	fn convert(
233		validator: T::AccountId,
234	) -> Option<sp_staking::Exposure<T::AccountId, BalanceOf<T>>> {
235		T::SessionInterface::validators()
236			.contains(&validator)
237			.then_some(Default::default())
238	}
239}
240
241#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
242pub struct BufferedOffence<AccountId> {
243	// rc_client::Offence takes multiple reporters, but in practice there is only one. In this
244	// pallet, we assume this is the case and store only the first reporter or none if empty.
245	pub reporter: Option<AccountId>,
246	pub slash_fraction: sp_runtime::Perbill,
247}
248
249/// A map of buffered offences, keyed by session index and then by offender account id.
250pub type BufferedOffencesMap<T> = BTreeMap<
251	SessionIndex,
252	BTreeMap<
253		<T as frame_system::Config>::AccountId,
254		BufferedOffence<<T as frame_system::Config>::AccountId>,
255	>,
256>;
257
258#[frame_support::pallet]
259pub mod pallet {
260	use crate::*;
261	use alloc::vec;
262	use frame_support::traits::{Hooks, UnixTime};
263	use frame_system::pallet_prelude::*;
264	use pallet_session::{historical, SessionManager};
265	use sp_runtime::{Perbill, Saturating};
266	use sp_staking::{
267		offence::{OffenceSeverity, OnOffenceHandler},
268		SessionIndex,
269	};
270
271	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
272
273	#[pallet::config]
274	pub trait Config: frame_system::Config {
275		/// The balance type of the runtime's currency interface.
276		type CurrencyBalance: sp_runtime::traits::AtLeast32BitUnsigned
277			+ codec::FullCodec
278			+ DecodeWithMemTracking
279			+ codec::HasCompact<Type: DecodeWithMemTracking>
280			+ Copy
281			+ MaybeSerializeDeserialize
282			+ core::fmt::Debug
283			+ Default
284			+ From<u64>
285			+ TypeInfo
286			+ Send
287			+ Sync
288			+ MaxEncodedLen;
289
290		/// An origin type that ensures an incoming message is from asset hub.
291		type AssetHubOrigin: EnsureOrigin<Self::RuntimeOrigin>;
292
293		/// The origin that can control this pallet's operations.
294		type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
295
296		/// Our communication interface to AssetHub.
297		type SendToAssetHub: SendToAssetHub<AccountId = Self::AccountId>;
298
299		/// A safety measure that asserts an incoming validator set must be at least this large.
300		type MinimumValidatorSetSize: Get<u32>;
301
302		/// A type that gives us a reliable unix timestamp.
303		type UnixTime: UnixTime;
304
305		/// Number of points to award a validator per block authored.
306		type PointsPerBlock: Get<u32>;
307
308		/// Maximum number of offences to batch in a single message to AssetHub.
309		///
310		/// Used during `Active` mode to limit batch size when processing buffered offences
311		/// in `on_initialize`. During `Buffered` mode, offences are accumulated without batching.
312		/// When transitioning from `Buffered` to `Active` mode (via `on_migration_end`),
313		/// buffered offences remain stored and are processed gradually by `on_initialize`
314		/// using this batch size limit to prevent block overload.
315		///
316		/// **Performance characteristics**
317		/// - Base cost: ~30.9ms (XCM infrastructure overhead)
318		/// - Per-offence cost: ~0.073ms (linear scaling)
319		/// - At batch size 50: ~34.6ms total (~1.7% of 2-second compute allowance)
320		type MaxOffenceBatchSize: Get<u32>;
321
322		/// Interface to talk to the local Session pallet.
323		type SessionInterface: SessionInterface<ValidatorId = Self::AccountId>;
324
325		/// A fallback implementation to delegate logic to when the pallet is in
326		/// [`OperatingMode::Passive`].
327		///
328		/// This type must implement the `historical::SessionManager` and `OnOffenceHandler`
329		/// interface and is expected to behave as a stand-in for this pallet’s core logic when
330		/// delegation is active.
331		type Fallback: pallet_session::SessionManager<Self::AccountId>
332			+ OnOffenceHandler<
333				Self::AccountId,
334				(Self::AccountId, sp_staking::Exposure<Self::AccountId, BalanceOf<Self>>),
335				Weight,
336			> + frame_support::traits::RewardsReporter<Self::AccountId>
337			+ pallet_authorship::EventHandler<Self::AccountId, BlockNumberFor<Self>>;
338
339		/// Information on runtime weights.
340		type WeightInfo: WeightInfo;
341	}
342
343	#[pallet::pallet]
344	#[pallet::storage_version(STORAGE_VERSION)]
345	pub struct Pallet<T>(_);
346
347	/// The queued validator sets for a given planning session index.
348	///
349	/// This is received via a call from AssetHub.
350	#[pallet::storage]
351	#[pallet::unbounded]
352	pub type ValidatorSet<T: Config> = StorageValue<_, (u32, Vec<T::AccountId>), OptionQuery>;
353
354	/// An incomplete validator set report.
355	#[pallet::storage]
356	#[pallet::unbounded]
357	pub type IncompleteValidatorSetReport<T: Config> =
358		StorageValue<_, rc_client::ValidatorSetReport<T::AccountId>, OptionQuery>;
359
360	/// All of the points of the validators.
361	///
362	/// This is populated during a session, and is flushed and sent over via [`SendToAssetHub`]
363	/// at each session end.
364	#[pallet::storage]
365	pub type ValidatorPoints<T: Config> =
366		StorageMap<_, Twox64Concat, T::AccountId, u32, ValueQuery>;
367
368	/// Indicates the current operating mode of the pallet.
369	///
370	/// This value determines how the pallet behaves in response to incoming and outgoing messages,
371	/// particularly whether it should execute logic directly, defer it, or delegate it entirely.
372	#[pallet::storage]
373	pub type Mode<T: Config> = StorageValue<_, OperatingMode, ValueQuery>;
374
375	/// A storage value that is set when a `new_session` gives a new validator set to the session
376	/// pallet, and is cleared on the next call.
377	///
378	/// The inner u32 is the id of the said activated validator set. While not relevant here, good
379	/// to know this is the planning era index of staking-async on AH.
380	///
381	/// Once cleared, we know a validator set has been activated, and therefore we can send a
382	/// timestamp to AH.
383	#[pallet::storage]
384	pub type NextSessionChangesValidators<T: Config> = StorageValue<_, u32, OptionQuery>;
385
386	/// The session index at which the latest elected validator set was applied.
387	///
388	/// This is used to determine if an offence, given a session index, is in the current active era
389	/// or not.
390	#[pallet::storage]
391	pub type ValidatorSetAppliedAt<T: Config> = StorageValue<_, SessionIndex, OptionQuery>;
392
393	/// Offences collected while in [`OperatingMode::Buffered`] mode.
394	///
395	/// These are temporarily stored and sent once the pallet switches to [`OperatingMode::Active`].
396	/// For each offender, only the highest `slash_fraction` is kept.
397	///
398	/// Internally stores as a nested BTreeMap:
399	/// `session_index -> (offender -> (reporter, slash_fraction))`.
400	///
401	/// Note: While the [`rc_client::Offence`] type includes a list of reporters, in practice there
402	/// is only one. In this pallet, we assume this is the case and store only the first reporter.
403	#[pallet::storage]
404	#[pallet::unbounded]
405	pub type BufferedOffences<T: Config> = StorageValue<_, BufferedOffencesMap<T>, ValueQuery>;
406
407	#[pallet::genesis_config]
408	#[derive(frame_support::DefaultNoBound, frame_support::DebugNoBound)]
409	pub struct GenesisConfig<T: Config> {
410		/// The initial operating mode of the pallet.
411		pub operating_mode: OperatingMode,
412		pub _marker: core::marker::PhantomData<T>,
413	}
414
415	#[pallet::genesis_build]
416	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
417		fn build(&self) {
418			// Set the initial operating mode of the pallet.
419			Mode::<T>::put(self.operating_mode.clone());
420		}
421	}
422
423	#[pallet::error]
424	pub enum Error<T> {
425		/// Could not process incoming message because incoming messages are blocked.
426		Blocked,
427	}
428
429	#[pallet::event]
430	#[pallet::generate_deposit(fn deposit_event)]
431	pub enum Event<T: Config> {
432		/// A new validator set has been received.
433		ValidatorSetReceived {
434			id: u32,
435			new_validator_set_count: u32,
436			prune_up_to: Option<SessionIndex>,
437			leftover: bool,
438		},
439		/// We could not merge, and therefore dropped a buffered message.
440		///
441		/// Note that this event is more resembling an error, but we use an event because in this
442		/// pallet we need to mutate storage upon some failures.
443		CouldNotMergeAndDropped,
444		/// The validator set received is way too small, as per
445		/// [`Config::MinimumValidatorSetSize`].
446		SetTooSmallAndDropped,
447		/// Something occurred that should never happen under normal operation. Logged as an event
448		/// for fail-safe observability.
449		Unexpected(UnexpectedKind),
450	}
451
452	/// Represents unexpected or invariant-breaking conditions encountered during execution.
453	///
454	/// These variants are emitted as [`Event::Unexpected`] and indicate a defensive check has
455	/// failed. While these should never occur under normal operation, they are useful for
456	/// diagnosing issues in production or test environments.
457	#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, RuntimeDebug)]
458	pub enum UnexpectedKind {
459		/// A validator set was received while the pallet is in [`OperatingMode::Passive`].
460		ReceivedValidatorSetWhilePassive,
461
462		/// An unexpected transition was applied between operating modes.
463		///
464		/// Expected transitions are linear and forward-only: `Passive` → `Buffered` → `Active`.
465		UnexpectedModeTransition,
466	}
467
468	#[pallet::call]
469	impl<T: Config> Pallet<T> {
470		#[pallet::call_index(0)]
471		#[pallet::weight(
472			// Reads:
473			// - OperatingMode
474			// - IncompleteValidatorSetReport
475			// Writes:
476			// - IncompleteValidatorSetReport or ValidatorSet
477			// ignoring `T::SessionInterface::prune_up_to`
478			T::DbWeight::get().reads_writes(2, 1)
479		)]
480		pub fn validator_set(
481			origin: OriginFor<T>,
482			report: rc_client::ValidatorSetReport<T::AccountId>,
483		) -> DispatchResult {
484			// Ensure the origin is one of Root or whatever is representing AssetHub.
485			log!(debug, "Received new validator set report {}", report);
486			T::AssetHubOrigin::ensure_origin_or_root(origin)?;
487
488			// Check the operating mode.
489			let mode = Mode::<T>::get();
490			ensure!(mode.can_accept_validator_set(), Error::<T>::Blocked);
491
492			let maybe_merged_report = match IncompleteValidatorSetReport::<T>::take() {
493				Some(old) => old.merge(report.clone()),
494				None => Ok(report),
495			};
496
497			if maybe_merged_report.is_err() {
498				Self::deposit_event(Event::CouldNotMergeAndDropped);
499				debug_assert!(
500					IncompleteValidatorSetReport::<T>::get().is_none(),
501					"we have ::take() it above, we don't want to keep the old data"
502				);
503				return Ok(());
504			}
505
506			let report = maybe_merged_report.expect("checked above; qed");
507
508			if report.leftover {
509				// buffer it, and nothing further to do.
510				Self::deposit_event(Event::ValidatorSetReceived {
511					id: report.id,
512					new_validator_set_count: report.new_validator_set.len() as u32,
513					prune_up_to: report.prune_up_to,
514					leftover: report.leftover,
515				});
516				IncompleteValidatorSetReport::<T>::put(report);
517			} else {
518				// message is complete, process it.
519				let rc_client::ValidatorSetReport {
520					id,
521					leftover,
522					mut new_validator_set,
523					prune_up_to,
524				} = report;
525
526				// ensure the validator set, deduplicated, is not too big.
527				new_validator_set.sort();
528				new_validator_set.dedup();
529
530				if (new_validator_set.len() as u32) < T::MinimumValidatorSetSize::get() {
531					Self::deposit_event(Event::SetTooSmallAndDropped);
532					debug_assert!(
533						IncompleteValidatorSetReport::<T>::get().is_none(),
534						"we have ::take() it above, we don't want to keep the old data"
535					);
536					return Ok(());
537				}
538
539				Self::deposit_event(Event::ValidatorSetReceived {
540					id,
541					new_validator_set_count: new_validator_set.len() as u32,
542					prune_up_to,
543					leftover,
544				});
545
546				// Save the validator set.
547				ValidatorSet::<T>::put((id, new_validator_set));
548				if let Some(index) = prune_up_to {
549					T::SessionInterface::prune_up_to(index);
550				}
551			}
552
553			Ok(())
554		}
555
556		/// Allows governance to force set the operating mode of the pallet.
557		#[pallet::call_index(1)]
558		#[pallet::weight(T::DbWeight::get().writes(1))]
559		pub fn set_mode(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
560			T::AdminOrigin::ensure_origin(origin)?;
561			Self::do_set_mode(mode);
562			Ok(())
563		}
564
565		/// manually do what this pallet was meant to do at the end of the migration.
566		#[pallet::call_index(2)]
567		#[pallet::weight(T::DbWeight::get().writes(1))]
568		pub fn force_on_migration_end(origin: OriginFor<T>) -> DispatchResult {
569			T::AdminOrigin::ensure_origin(origin)?;
570			Self::on_migration_end();
571			Ok(())
572		}
573	}
574
575	#[pallet::hooks]
576	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
577		fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
578			let mut weight = Weight::zero();
579
580			let mode = Mode::<T>::get();
581			weight = weight.saturating_add(T::DbWeight::get().reads(1));
582			if mode != OperatingMode::Active {
583				return weight;
584			}
585
586			// Check if we have any buffered offences to send
587			let buffered_offences = BufferedOffences::<T>::get();
588			weight = weight.saturating_add(T::DbWeight::get().reads(1));
589			if buffered_offences.is_empty() {
590				return weight;
591			}
592
593			let processing_weight = Self::process_buffered_offences();
594			weight = weight.saturating_add(processing_weight);
595
596			weight
597		}
598	}
599
600	impl<T: Config>
601		historical::SessionManager<T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>>
602		for Pallet<T>
603	{
604		fn new_session(
605			new_index: sp_staking::SessionIndex,
606		) -> Option<
607			Vec<(
608				<T as frame_system::Config>::AccountId,
609				sp_staking::Exposure<T::AccountId, BalanceOf<T>>,
610			)>,
611		> {
612			<Self as pallet_session::SessionManager<_>>::new_session(new_index)
613				.map(|v| v.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect())
614		}
615
616		fn new_session_genesis(
617			new_index: SessionIndex,
618		) -> Option<Vec<(T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>)>> {
619			if Mode::<T>::get() == OperatingMode::Passive {
620				T::Fallback::new_session_genesis(new_index).map(|validators| {
621					validators.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect()
622				})
623			} else {
624				None
625			}
626		}
627
628		fn start_session(start_index: SessionIndex) {
629			<Self as pallet_session::SessionManager<_>>::start_session(start_index)
630		}
631
632		fn end_session(end_index: SessionIndex) {
633			<Self as pallet_session::SessionManager<_>>::end_session(end_index)
634		}
635	}
636
637	impl<T: Config> pallet_session::SessionManager<T::AccountId> for Pallet<T> {
638		fn new_session(session_index: u32) -> Option<Vec<T::AccountId>> {
639			match Mode::<T>::get() {
640				OperatingMode::Passive => T::Fallback::new_session(session_index),
641				// In `Buffered` mode, we drop the session report and do nothing.
642				OperatingMode::Buffered => None,
643				OperatingMode::Active => Self::do_new_session(),
644			}
645		}
646
647		fn start_session(session_index: u32) {
648			if Mode::<T>::get() == OperatingMode::Passive {
649				T::Fallback::start_session(session_index)
650			}
651		}
652
653		fn new_session_genesis(new_index: SessionIndex) -> Option<Vec<T::AccountId>> {
654			if Mode::<T>::get() == OperatingMode::Passive {
655				T::Fallback::new_session_genesis(new_index)
656			} else {
657				None
658			}
659		}
660
661		fn end_session(session_index: u32) {
662			match Mode::<T>::get() {
663				OperatingMode::Passive => T::Fallback::end_session(session_index),
664				// In `Buffered` mode, we drop the session report and do nothing.
665				OperatingMode::Buffered => (),
666				OperatingMode::Active => Self::do_end_session(session_index),
667			}
668		}
669	}
670
671	impl<T: Config>
672		OnOffenceHandler<
673			T::AccountId,
674			(T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>),
675			Weight,
676		> for Pallet<T>
677	{
678		fn on_offence(
679			offenders: &[OffenceDetails<
680				T::AccountId,
681				(T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>),
682			>],
683			slash_fraction: &[Perbill],
684			slash_session: SessionIndex,
685		) -> Weight {
686			match Mode::<T>::get() {
687				OperatingMode::Passive => {
688					// delegate to the fallback implementation.
689					T::Fallback::on_offence(offenders, slash_fraction, slash_session)
690				},
691				OperatingMode::Buffered =>
692					Self::on_offence_buffered(offenders, slash_fraction, slash_session),
693				OperatingMode::Active =>
694					Self::on_offence_active(offenders, slash_fraction, slash_session),
695			}
696		}
697	}
698
699	impl<T: Config> RewardsReporter<T::AccountId> for Pallet<T> {
700		fn reward_by_ids(rewards: impl IntoIterator<Item = (T::AccountId, u32)>) {
701			match Mode::<T>::get() {
702				OperatingMode::Passive => T::Fallback::reward_by_ids(rewards),
703				OperatingMode::Buffered | OperatingMode::Active => Self::do_reward_by_ids(rewards),
704			}
705		}
706	}
707
708	impl<T: Config> pallet_authorship::EventHandler<T::AccountId, BlockNumberFor<T>> for Pallet<T> {
709		fn note_author(author: T::AccountId) {
710			match Mode::<T>::get() {
711				OperatingMode::Passive => T::Fallback::note_author(author),
712				OperatingMode::Buffered | OperatingMode::Active => Self::do_note_author(author),
713			}
714		}
715	}
716
717	impl<T: Config> Pallet<T> {
718		/// Hook to be called when the AssetHub migration begins.
719		///
720		/// This transitions the pallet into [`OperatingMode::Buffered`], meaning it will act as the
721		/// primary staking module on the relay chain but will buffer outgoing messages instead of
722		/// sending them to AssetHub.
723		///
724		/// While in this mode, the pallet stops delegating to the fallback implementation and
725		/// temporarily accumulates events for later processing.
726		pub fn on_migration_start() {
727			debug_assert!(
728				Mode::<T>::get() == OperatingMode::Passive,
729				"we should only be called when in passive mode"
730			);
731			Self::do_set_mode(OperatingMode::Buffered);
732		}
733
734		/// Hook to be called when the AssetHub migration is complete.
735		///
736		/// This transitions the pallet into [`OperatingMode::Active`], meaning the counterpart
737		/// pallet on AssetHub is ready to accept incoming messages, and this pallet can resume
738		/// sending them.
739		///
740		/// In this mode, the pallet becomes fully active and processes all staking-related events
741		/// directly.
742		pub fn on_migration_end() {
743			debug_assert!(
744				Mode::<T>::get() == OperatingMode::Buffered,
745				"we should only be called when in buffered mode"
746			);
747			Self::do_set_mode(OperatingMode::Active);
748
749			// Buffered offences will be processed gradually by on_initialize
750			// using MaxOffenceBatchSize to prevent block overload.
751		}
752
753		fn do_set_mode(new_mode: OperatingMode) {
754			let old_mode = Mode::<T>::get();
755			let unexpected = match new_mode {
756				// `Passive` is the initial state, and not expected to be set by the user.
757				OperatingMode::Passive => true,
758				OperatingMode::Buffered => old_mode != OperatingMode::Passive,
759				OperatingMode::Active => old_mode != OperatingMode::Buffered,
760			};
761
762			// this is a defensive check, and should never happen under normal operation.
763			if unexpected {
764				log!(warn, "Unexpected mode transition from {:?} to {:?}", old_mode, new_mode);
765				Self::deposit_event(Event::Unexpected(UnexpectedKind::UnexpectedModeTransition));
766			}
767
768			// apply new mode anyway.
769			Mode::<T>::put(new_mode);
770		}
771
772		fn do_new_session() -> Option<Vec<T::AccountId>> {
773			ValidatorSet::<T>::take().map(|(id, val_set)| {
774				// store the id to be sent back in the next session back to AH
775				NextSessionChangesValidators::<T>::put(id);
776				val_set
777			})
778		}
779
780		fn do_end_session(session_index: u32) {
781			use sp_runtime::SaturatedConversion;
782
783			let validator_points = ValidatorPoints::<T>::iter().drain().collect::<Vec<_>>();
784			let activation_timestamp = NextSessionChangesValidators::<T>::take().map(|id| {
785				// keep track of starting session index at which the validator set was applied.
786				ValidatorSetAppliedAt::<T>::put(session_index + 1);
787				// set the timestamp and the identifier of the validator set.
788				(T::UnixTime::now().as_millis().saturated_into::<u64>(), id)
789			});
790
791			let session_report = pallet_staking_async_rc_client::SessionReport {
792				end_index: session_index,
793				validator_points,
794				activation_timestamp,
795				leftover: false,
796			};
797
798			T::SendToAssetHub::relay_session_report(session_report);
799		}
800
801		fn do_reward_by_ids(rewards: impl IntoIterator<Item = (T::AccountId, u32)>) {
802			for (validator_id, points) in rewards {
803				ValidatorPoints::<T>::mutate(validator_id, |balance| {
804					balance.saturating_accrue(points);
805				});
806			}
807		}
808
809		fn do_note_author(author: T::AccountId) {
810			ValidatorPoints::<T>::mutate(author, |points| {
811				points.saturating_accrue(T::PointsPerBlock::get());
812			});
813		}
814
815		/// Process buffered offences and send them to AssetHub in batches.
816		pub(crate) fn process_buffered_offences() -> Weight {
817			let max_batch_size = T::MaxOffenceBatchSize::get() as usize;
818
819			// Process and remove offences one session at a time
820			let offences_sent = BufferedOffences::<T>::mutate(|buffered| {
821				let first_session_key = buffered.keys().next().copied()?;
822
823				let session_map = buffered.get_mut(&first_session_key)?;
824
825				// Take up to max_batch_size offences from this session
826				let keys_to_drain: Vec<_> =
827					session_map.keys().take(max_batch_size).cloned().collect();
828
829				let offences_to_send: Vec<_> = keys_to_drain
830					.into_iter()
831					.filter_map(|key| {
832						session_map.remove(&key).map(|offence| rc_client::Offence {
833							offender: key,
834							reporters: offence.reporter.into_iter().collect(),
835							slash_fraction: offence.slash_fraction,
836						})
837					})
838					.collect();
839
840				if !offences_to_send.is_empty() {
841					// Remove the entire session if it's now empty
842					if session_map.is_empty() {
843						buffered.remove(&first_session_key);
844						log!(debug, "Cleared all offences for session {}", first_session_key);
845					}
846
847					Some((first_session_key, offences_to_send))
848				} else {
849					None
850				}
851			});
852
853			if let Some((slash_session, offences_to_send)) = offences_sent {
854				log!(
855					info,
856					"Sending {} buffered offences for session {} to AssetHub",
857					offences_to_send.len(),
858					slash_session
859				);
860
861				let batch_size = offences_to_send.len();
862				T::SendToAssetHub::relay_new_offence(slash_session, offences_to_send);
863
864				T::WeightInfo::process_buffered_offences(batch_size as u32)
865			} else {
866				Weight::zero()
867			}
868		}
869
870		/// Check if an offence is from the active validator set.
871		fn is_ongoing_offence(slash_session: SessionIndex) -> bool {
872			ValidatorSetAppliedAt::<T>::get()
873				.map(|start_session| slash_session >= start_session)
874				.unwrap_or(false)
875		}
876
877		/// Handle offences in Buffered mode.
878		fn on_offence_buffered(
879			offenders: &[OffenceDetailsOf<T>],
880			slash_fraction: &[Perbill],
881			slash_session: SessionIndex,
882		) -> Weight {
883			let ongoing_offence = Self::is_ongoing_offence(slash_session);
884
885			let _: Vec<_> = offenders
886				.iter()
887				.cloned()
888				.zip(slash_fraction)
889				.map(|(offence, fraction)| {
890					if ongoing_offence {
891						// report the offence to the session pallet.
892						T::SessionInterface::report_offence(
893							offence.offender.0.clone(),
894							OffenceSeverity(*fraction),
895						);
896					}
897
898					let (offender, _full_identification) = offence.offender;
899					let reporters = offence.reporters;
900
901					// In `Buffered` mode, we buffer the offences for later processing.
902					// We only keep the highest slash fraction for each offender per session.
903					BufferedOffences::<T>::mutate(|buffered| {
904						let session_offences = buffered.entry(slash_session).or_default();
905						let entry = session_offences.entry(offender);
906
907						entry
908							.and_modify(|existing| {
909								if existing.slash_fraction < *fraction {
910									*existing = BufferedOffence {
911										reporter: reporters.first().cloned(),
912										slash_fraction: *fraction,
913									};
914								}
915							})
916							.or_insert(BufferedOffence {
917								reporter: reporters.first().cloned(),
918								slash_fraction: *fraction,
919							});
920					});
921
922					// Return unit for the map operation
923				})
924				.collect();
925
926			Weight::zero()
927		}
928
929		/// Handle offences in Active mode.
930		fn on_offence_active(
931			offenders: &[OffenceDetailsOf<T>],
932			slash_fraction: &[Perbill],
933			slash_session: SessionIndex,
934		) -> Weight {
935			let ongoing_offence = Self::is_ongoing_offence(slash_session);
936
937			let offenders_and_slashes_message: Vec<_> = offenders
938				.iter()
939				.cloned()
940				.zip(slash_fraction)
941				.map(|(offence, fraction)| {
942					if ongoing_offence {
943						// report the offence to the session pallet.
944						T::SessionInterface::report_offence(
945							offence.offender.0.clone(),
946							OffenceSeverity(*fraction),
947						);
948					}
949
950					let (offender, _full_identification) = offence.offender;
951					let reporters = offence.reporters;
952
953					// prepare an `Offence` instance for the XCM message. Note that we drop
954					// the identification.
955					rc_client::Offence { offender, reporters, slash_fraction: *fraction }
956				})
957				.collect();
958
959			// Send offence report to Asset Hub
960			if !offenders_and_slashes_message.is_empty() {
961				log!(info, "sending offence report to AH");
962				T::SendToAssetHub::relay_new_offence(slash_session, offenders_and_slashes_message);
963			}
964
965			Weight::zero()
966		}
967	}
968}