referrerpolicy=no-referrer-when-downgrade

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
38//! [`pallet_staking_async_rc_client::SendToAssetHub`]. They match the incoming messages of the
39//! `rc-client` pallet.
40//!
41//! ## Local Interfaces:
42//!
43//! Living on the relay chain, this pallet must:
44//!
45//! * Implement [`pallet_session::SessionManager`] (and historical variant thereof) to _give_
46//!   information to the session pallet.
47//! * Implements [`SessionInterface`] to _receive_ information from the session pallet
48//! * Implement [`sp_staking::offence::OnOffenceHandler`].
49//! * Implement reward related APIs ([`frame_support::traits::RewardsReporter`]).
50//!
51//! ## Future Plans
52//!
53//! * Governance functions to force set validators.
54
55#![cfg_attr(not(feature = "std"), no_std)]
56
57pub use pallet::*;
58
59#[cfg(test)]
60pub mod mock;
61
62extern crate alloc;
63use alloc::vec::Vec;
64use frame_support::{
65	pallet_prelude::*,
66	traits::{Defensive, DefensiveSaturating, RewardsReporter},
67};
68pub use pallet_staking_async_rc_client::SendToAssetHub;
69use pallet_staking_async_rc_client::{self as rc_client};
70use sp_runtime::SaturatedConversion;
71use sp_staking::offence::OffenceDetails;
72
73/// The balance type seen from this pallet's PoV.
74pub type BalanceOf<T> = <T as Config>::CurrencyBalance;
75
76/// Type alias for offence details
77pub type OffenceDetailsOf<T> = OffenceDetails<
78	<T as frame_system::Config>::AccountId,
79	(
80		<T as frame_system::Config>::AccountId,
81		sp_staking::Exposure<<T as frame_system::Config>::AccountId, BalanceOf<T>>,
82	),
83>;
84
85const LOG_TARGET: &str = "runtime::staking-async::ah-client";
86
87// syntactic sugar for logging.
88#[macro_export]
89macro_rules! log {
90	($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
91		log::$level!(
92			target: $crate::LOG_TARGET,
93			concat!("[{:?}] ⬇️ ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
94		)
95	};
96}
97
98/// Re-export `SessionInterface` from `pallet_session`.
99///
100/// This trait provides the interface to talk to the local session pallet for cross-chain
101/// session management.
102pub use pallet_session::SessionInterface;
103
104/// Represents the operating mode of the pallet.
105#[derive(
106	Default,
107	DecodeWithMemTracking,
108	Encode,
109	Decode,
110	MaxEncodedLen,
111	TypeInfo,
112	Clone,
113	PartialEq,
114	Eq,
115	Debug,
116	serde::Serialize,
117	serde::Deserialize,
118)]
119pub enum OperatingMode {
120	/// Fully delegated mode.
121	///
122	/// In this mode, the pallet performs no core logic and forwards all relevant operations
123	/// to the fallback implementation defined in the pallet's `Config::Fallback`.
124	///
125	/// This mode is useful when staking is in synchronous mode and waiting for the signal to
126	/// transition to asynchronous mode.
127	#[default]
128	Passive,
129
130	/// Buffered mode for deferred execution.
131	///
132	/// In this mode, offences are accepted and buffered for later transmission to AssetHub.
133	/// However, session change reports are dropped.
134	///
135	/// This mode is useful when the counterpart pallet `pallet-staking-async-rc-client` on
136	/// AssetHub is not yet ready to process incoming messages.
137	Buffered,
138
139	/// Fully active mode.
140	///
141	/// The pallet performs all core logic directly and handles messages immediately.
142	///
143	/// This mode is useful when staking is ready to execute in asynchronous mode and the
144	/// counterpart pallet `pallet-staking-async-rc-client` is ready to accept messages.
145	Active,
146}
147
148impl OperatingMode {
149	fn can_accept_validator_set(&self) -> bool {
150		matches!(self, OperatingMode::Active)
151	}
152}
153
154/// See `pallet_staking::DefaultExposureOf`. This type is the same, except it is duplicated here so
155/// that an rc-runtime can use it after `pallet-staking` is fully removed as a dependency.
156pub struct DefaultExposureOf<T>(core::marker::PhantomData<T>);
157
158impl<T: Config>
159	sp_runtime::traits::Convert<
160		T::AccountId,
161		Option<sp_staking::Exposure<T::AccountId, BalanceOf<T>>>,
162	> for DefaultExposureOf<T>
163{
164	fn convert(
165		validator: T::AccountId,
166	) -> Option<sp_staking::Exposure<T::AccountId, BalanceOf<T>>> {
167		T::SessionInterface::validators()
168			.contains(&validator)
169			.then_some(Default::default())
170	}
171}
172
173#[frame_support::pallet]
174pub mod pallet {
175	use crate::*;
176	use alloc::vec;
177	use frame_support::traits::{Hooks, UnixTime};
178	use frame_system::pallet_prelude::*;
179	use pallet_session::{historical, SessionManager};
180	use pallet_staking_async_rc_client::SessionReport;
181	use sp_runtime::{Perbill, Saturating};
182	use sp_staking::{
183		offence::{OffenceSeverity, OnOffenceHandler},
184		SessionIndex,
185	};
186
187	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
188
189	#[pallet::config]
190	pub trait Config: frame_system::Config {
191		/// The balance type of the runtime's currency interface.
192		type CurrencyBalance: sp_runtime::traits::AtLeast32BitUnsigned
193			+ codec::FullCodec
194			+ DecodeWithMemTracking
195			+ codec::HasCompact<Type: DecodeWithMemTracking>
196			+ Copy
197			+ MaybeSerializeDeserialize
198			+ core::fmt::Debug
199			+ Default
200			+ From<u64>
201			+ TypeInfo
202			+ Send
203			+ Sync
204			+ MaxEncodedLen;
205
206		/// An origin type that ensures an incoming message is from asset hub.
207		type AssetHubOrigin: EnsureOrigin<Self::RuntimeOrigin>;
208
209		/// The origin that can control this pallet's operations.
210		type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
211
212		/// Our communication interface to AssetHub.
213		type SendToAssetHub: SendToAssetHub<AccountId = Self::AccountId>;
214
215		/// A safety measure that asserts an incoming validator set must be at least this large.
216		type MinimumValidatorSetSize: Get<u32>;
217
218		/// A safety measure that asserts when iterating over validator points (to be sent to AH),
219		/// we don't iterate too many times.
220		///
221		/// Validator may change session to session, and if session reports are not sent, validator
222		/// points that we store may well grow beyond the size of the validator set. Yet, a too
223		/// large of an upper bound may also exceed the maximum size of a single DMP message.
224		/// Consult the test `message_queue_sizes` for more information.
225		///
226		/// Note that in case a single session report is larger than a single DMP message, it might
227		/// still be sent over if we use
228		/// [`pallet_staking_async_rc_client::XCMSender::split_then_send`]. This will make the size
229		/// of each individual message smaller, yet, it will still try and push them all to the
230		/// queue at the same time.
231		type MaximumValidatorsWithPoints: Get<u32>;
232
233		/// A type that gives us a reliable unix timestamp.
234		type UnixTime: UnixTime;
235
236		/// Number of points to award a validator per block authored.
237		type PointsPerBlock: Get<u32>;
238
239		/// Maximum number of offences to batch in a single message to AssetHub. Actual sending
240		/// happens `on_initialize`. Offences get infinite "retries", and are never dropped.
241		///
242		/// A sensible value should be such that sending this batch is small enough to not exhaust
243		/// the DMP queue. The size of a single offence is documented in `message_queue_sizes` test
244		/// (74 bytes).
245		type MaxOffenceBatchSize: Get<u32>;
246
247		/// Interface to talk to the local Session pallet.
248		type SessionInterface: SessionInterface<
249			ValidatorId = Self::AccountId,
250			AccountId = Self::AccountId,
251		>;
252
253		/// A fallback implementation to delegate logic to when the pallet is in
254		/// [`OperatingMode::Passive`].
255		///
256		/// This type must implement the `historical::SessionManager` and `OnOffenceHandler`
257		/// interface and is expected to behave as a stand-in for this pallet’s core logic when
258		/// delegation is active.
259		type Fallback: pallet_session::SessionManager<Self::AccountId>
260			+ OnOffenceHandler<
261				Self::AccountId,
262				(Self::AccountId, sp_staking::Exposure<Self::AccountId, BalanceOf<Self>>),
263				Weight,
264			> + frame_support::traits::RewardsReporter<Self::AccountId>
265			+ pallet_authorship::EventHandler<Self::AccountId, BlockNumberFor<Self>>;
266
267		/// Maximum number of times we try to send a session report to AssetHub, after which, if
268		/// sending still fails, we drop it.
269		type MaxSessionReportRetries: Get<u32>;
270	}
271
272	#[pallet::pallet]
273	#[pallet::storage_version(STORAGE_VERSION)]
274	pub struct Pallet<T>(_);
275
276	/// The queued validator sets for a given planning session index.
277	///
278	/// This is received via a call from AssetHub.
279	#[pallet::storage]
280	#[pallet::unbounded]
281	pub type ValidatorSet<T: Config> = StorageValue<_, (u32, Vec<T::AccountId>), OptionQuery>;
282
283	/// An incomplete validator set report.
284	#[pallet::storage]
285	#[pallet::unbounded]
286	pub type IncompleteValidatorSetReport<T: Config> =
287		StorageValue<_, rc_client::ValidatorSetReport<T::AccountId>, OptionQuery>;
288
289	/// All of the points of the validators.
290	///
291	/// This is populated during a session, and is flushed and sent over via [`SendToAssetHub`]
292	/// at each session end.
293	#[pallet::storage]
294	pub type ValidatorPoints<T: Config> =
295		StorageMap<_, Twox64Concat, T::AccountId, u32, ValueQuery>;
296
297	/// Indicates the current operating mode of the pallet.
298	///
299	/// This value determines how the pallet behaves in response to incoming and outgoing messages,
300	/// particularly whether it should execute logic directly, defer it, or delegate it entirely.
301	#[pallet::storage]
302	pub type Mode<T: Config> = StorageValue<_, OperatingMode, ValueQuery>;
303
304	/// A storage value that is set when a `new_session` gives a new validator set to the session
305	/// pallet, and is cleared on the next call.
306	///
307	/// The inner u32 is the id of the said activated validator set. While not relevant here, good
308	/// to know this is the planning era index of staking-async on AH.
309	///
310	/// Once cleared, we know a validator set has been activated, and therefore we can send a
311	/// timestamp to AH.
312	#[pallet::storage]
313	pub type NextSessionChangesValidators<T: Config> = StorageValue<_, u32, OptionQuery>;
314
315	/// The session index at which the latest elected validator set was applied.
316	///
317	/// This is used to determine if an offence, given a session index, is in the current active era
318	/// or not.
319	#[pallet::storage]
320	pub type ValidatorSetAppliedAt<T: Config> = StorageValue<_, SessionIndex, OptionQuery>;
321
322	/// A session report that is outgoing, and should be sent.
323	///
324	/// This will be attempted to be sent, possibly on every `on_initialize` call, until it is sent,
325	/// or the second value reaches zero, at which point we drop it.
326	#[pallet::storage]
327	#[pallet::unbounded]
328	pub type OutgoingSessionReport<T: Config> =
329		StorageValue<_, (SessionReport<T::AccountId>, u32), OptionQuery>;
330
331	/// Wrapper struct for storing offences, and getting them back page by page.
332	///
333	/// It has only two interfaces:
334	///
335	/// * [`OffenceSendQueue::append`], to add a single offence.
336	/// * [`OffenceSendQueue::get_and_maybe_delete`] which retrieves the last page. Depending on the
337	///   closure, it may also delete that page. The returned value is indeed
338	///   [`Config::MaxOffenceBatchSize`] or less items.
339	///
340	/// Internally, it manages `OffenceSendQueueOffences` and `OffenceSendQueueCursor`, both of
341	/// which should NEVER be used manually.
342	pub struct OffenceSendQueue<T: Config>(core::marker::PhantomData<T>);
343
344	/// A single buffered offence in [`OffenceSendQueue`].
345	pub type QueuedOffenceOf<T> =
346		(SessionIndex, rc_client::Offence<<T as frame_system::Config>::AccountId>);
347	/// A page of buffered offences in [`OffenceSendQueue`].
348	pub type QueuedOffencePageOf<T> =
349		BoundedVec<QueuedOffenceOf<T>, <T as Config>::MaxOffenceBatchSize>;
350
351	impl<T: Config> OffenceSendQueue<T> {
352		/// Add a single offence to the queue.
353		pub fn append(o: QueuedOffenceOf<T>) {
354			let mut index = OffenceSendQueueCursor::<T>::get();
355			match OffenceSendQueueOffences::<T>::try_mutate(index, |b| b.try_push(o.clone())) {
356				Ok(_) => {
357					// `index` had empty slot -- all good.
358				},
359				Err(_) => {
360					debug_assert!(
361						!OffenceSendQueueOffences::<T>::contains_key(index + 1),
362						"next page should be empty"
363					);
364					index += 1;
365					OffenceSendQueueOffences::<T>::insert(
366						index,
367						BoundedVec::<_, _>::try_from(vec![o]).defensive_unwrap_or_default(),
368					);
369					OffenceSendQueueCursor::<T>::mutate(|i| *i += 1);
370				},
371			}
372		}
373
374		// Get the last page of offences, and delete it if `op` returns `Ok(())`.
375		pub fn get_and_maybe_delete(op: impl FnOnce(QueuedOffencePageOf<T>) -> Result<(), ()>) {
376			let index = OffenceSendQueueCursor::<T>::get();
377			let page = OffenceSendQueueOffences::<T>::get(index);
378			let res = op(page);
379			match res {
380				Ok(_) => {
381					OffenceSendQueueOffences::<T>::remove(index);
382					OffenceSendQueueCursor::<T>::mutate(|i| *i = i.saturating_sub(1))
383				},
384				Err(_) => {
385					// nada
386				},
387			}
388		}
389
390		#[cfg(feature = "std")]
391		pub fn pages() -> u32 {
392			let last_page = if Self::last_page_empty() { 0 } else { 1 };
393			OffenceSendQueueCursor::<T>::get().saturating_add(last_page)
394		}
395
396		#[cfg(feature = "std")]
397		pub fn count() -> u32 {
398			let last_index = OffenceSendQueueCursor::<T>::get();
399			let last_page = OffenceSendQueueOffences::<T>::get(last_index);
400			let last_page_count = last_page.len() as u32;
401			last_index.saturating_mul(T::MaxOffenceBatchSize::get()) + last_page_count
402		}
403
404		#[cfg(feature = "std")]
405		fn last_page_empty() -> bool {
406			OffenceSendQueueOffences::<T>::get(OffenceSendQueueCursor::<T>::get()).is_empty()
407		}
408	}
409
410	/// Internal storage item of [`OffenceSendQueue`]. Should not be used manually.
411	#[pallet::storage]
412	#[pallet::unbounded]
413	pub(crate) type OffenceSendQueueOffences<T: Config> =
414		StorageMap<_, Twox64Concat, u32, QueuedOffencePageOf<T>, ValueQuery>;
415	/// Internal storage item of [`OffenceSendQueue`]. Should not be used manually.
416	#[pallet::storage]
417	pub(crate) type OffenceSendQueueCursor<T: Config> = StorageValue<_, u32, ValueQuery>;
418
419	#[pallet::genesis_config]
420	#[derive(frame_support::DefaultNoBound, frame_support::DebugNoBound)]
421	pub struct GenesisConfig<T: Config> {
422		/// The initial operating mode of the pallet.
423		pub operating_mode: OperatingMode,
424		pub _marker: core::marker::PhantomData<T>,
425	}
426
427	#[pallet::genesis_build]
428	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
429		fn build(&self) {
430			// Set the initial operating mode of the pallet.
431			Mode::<T>::put(self.operating_mode.clone());
432		}
433	}
434
435	#[pallet::error]
436	pub enum Error<T> {
437		/// Could not process incoming message because incoming messages are blocked.
438		Blocked,
439	}
440
441	#[pallet::event]
442	#[pallet::generate_deposit(fn deposit_event)]
443	pub enum Event<T: Config> {
444		/// A new validator set has been received.
445		ValidatorSetReceived {
446			id: u32,
447			new_validator_set_count: u32,
448			prune_up_to: Option<SessionIndex>,
449			leftover: bool,
450		},
451		/// We could not merge, and therefore dropped a buffered message.
452		///
453		/// Note that this event is more resembling an error, but we use an event because in this
454		/// pallet we need to mutate storage upon some failures.
455		CouldNotMergeAndDropped,
456		/// The validator set received is way too small, as per
457		/// [`Config::MinimumValidatorSetSize`].
458		SetTooSmallAndDropped,
459		/// Something occurred that should never happen under normal operation. Logged as an event
460		/// for fail-safe observability.
461		Unexpected(UnexpectedKind),
462		/// Session keys updated for a validator.
463		SessionKeysUpdated { stash: T::AccountId, update: SessionKeysUpdate },
464		/// Session key update from AssetHub failed on the relay chain.
465		/// Logged as an event for fail-safe observability.
466		SessionKeysUpdateFailed {
467			stash: T::AccountId,
468			update: SessionKeysUpdate,
469			error: DispatchError,
470		},
471	}
472
473	/// The type of session keys update received from AssetHub.
474	#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, Debug)]
475	pub enum SessionKeysUpdate {
476		/// Session keys have been set.
477		Set,
478		/// Session keys have been purged.
479		Purged,
480	}
481
482	/// Represents unexpected or invariant-breaking conditions encountered during execution.
483	///
484	/// These variants are emitted as [`Event::Unexpected`] and indicate a defensive check has
485	/// failed. While these should never occur under normal operation, they are useful for
486	/// diagnosing issues in production or test environments.
487	#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, Debug)]
488	pub enum UnexpectedKind {
489		/// A validator set was received while the pallet is in [`OperatingMode::Passive`].
490		ReceivedValidatorSetWhilePassive,
491
492		/// An unexpected transition was applied between operating modes.
493		///
494		/// Expected transitions are linear and forward-only: `Passive` → `Buffered` → `Active`.
495		UnexpectedModeTransition,
496
497		/// A session report failed to be sent.
498		///
499		/// We will store, and retry it for a number of more block.
500		SessionReportSendFailed,
501
502		/// A session report failed enough times that we should drop it.
503		///
504		/// We will retain the validator points, and send them over in the next session we receive
505		/// from pallet-session.
506		SessionReportDropped,
507
508		/// An offence report failed to be sent.
509		///
510		/// It will be retried again in the next block. We never drop them.
511		OffenceSendFailed,
512
513		/// Some validator points didn't make it to be included in the session report. Should
514		/// never happen, and means:
515		///
516		/// * a too low of a value is assigned to [`Config::MaximumValidatorsWithPoints`]
517		/// * Those who are calling into our `RewardsReporter` likely have a bad view of the
518		///   validator set, and are spamming us.
519		ValidatorPointDropped,
520
521		/// Session keys received from AssetHub failed to decode.
522		///
523		/// This should never happen since AssetHub validates keys before forwarding them.
524		/// If this occurs, it indicates a mismatch between AH and RC key types or a bug.
525		InvalidKeysFromAssetHub,
526	}
527
528	#[pallet::call]
529	impl<T: Config> Pallet<T> {
530		#[pallet::call_index(0)]
531		#[pallet::weight(
532			// Reads:
533			// - OperatingMode
534			// - IncompleteValidatorSetReport
535			// Writes:
536			// - IncompleteValidatorSetReport or ValidatorSet
537			// ignoring `T::SessionInterface::prune_up_to`
538			T::DbWeight::get().reads_writes(2, 1)
539		)]
540		pub fn validator_set(
541			origin: OriginFor<T>,
542			report: rc_client::ValidatorSetReport<T::AccountId>,
543		) -> DispatchResult {
544			// Ensure the origin is one of Root or whatever is representing AssetHub.
545			log!(debug, "Received new validator set report {}", report);
546			T::AssetHubOrigin::ensure_origin_or_root(origin)?;
547
548			// Check the operating mode.
549			let mode = Mode::<T>::get();
550			ensure!(mode.can_accept_validator_set(), Error::<T>::Blocked);
551
552			let maybe_merged_report = match IncompleteValidatorSetReport::<T>::take() {
553				Some(old) => old.merge(report.clone()),
554				None => Ok(report),
555			};
556
557			if maybe_merged_report.is_err() {
558				Self::deposit_event(Event::CouldNotMergeAndDropped);
559				debug_assert!(
560					IncompleteValidatorSetReport::<T>::get().is_none(),
561					"we have ::take() it above, we don't want to keep the old data"
562				);
563				return Ok(());
564			}
565
566			let report = maybe_merged_report.expect("checked above; qed");
567
568			if report.leftover {
569				// buffer it, and nothing further to do.
570				Self::deposit_event(Event::ValidatorSetReceived {
571					id: report.id,
572					new_validator_set_count: report.new_validator_set.len() as u32,
573					prune_up_to: report.prune_up_to,
574					leftover: report.leftover,
575				});
576				IncompleteValidatorSetReport::<T>::put(report);
577			} else {
578				// message is complete, process it.
579				let rc_client::ValidatorSetReport {
580					id,
581					leftover,
582					mut new_validator_set,
583					prune_up_to,
584				} = report;
585
586				// ensure the validator set, deduplicated, is not too big.
587				new_validator_set.sort();
588				new_validator_set.dedup();
589
590				if (new_validator_set.len() as u32) < T::MinimumValidatorSetSize::get() {
591					Self::deposit_event(Event::SetTooSmallAndDropped);
592					debug_assert!(
593						IncompleteValidatorSetReport::<T>::get().is_none(),
594						"we have ::take() it above, we don't want to keep the old data"
595					);
596					return Ok(());
597				}
598
599				Self::deposit_event(Event::ValidatorSetReceived {
600					id,
601					new_validator_set_count: new_validator_set.len() as u32,
602					prune_up_to,
603					leftover,
604				});
605
606				// Save the validator set.
607				ValidatorSet::<T>::put((id, new_validator_set));
608				if let Some(index) = prune_up_to {
609					T::SessionInterface::prune_up_to(index);
610				}
611			}
612
613			Ok(())
614		}
615
616		/// Allows governance to force set the operating mode of the pallet.
617		#[pallet::call_index(1)]
618		#[pallet::weight(T::DbWeight::get().writes(1))]
619		pub fn set_mode(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
620			T::AdminOrigin::ensure_origin(origin)?;
621			Self::do_set_mode(mode);
622			Ok(())
623		}
624
625		/// manually do what this pallet was meant to do at the end of the migration.
626		#[pallet::call_index(2)]
627		#[pallet::weight(T::DbWeight::get().writes(1))]
628		pub fn force_on_migration_end(origin: OriginFor<T>) -> DispatchResult {
629			T::AdminOrigin::ensure_origin(origin)?;
630			Self::on_migration_end();
631			Ok(())
632		}
633
634		/// Set session keys for a validator, forwarded from AssetHub.
635		///
636		/// This is called when a validator sets their session keys on AssetHub, which forwards
637		/// the request to the RelayChain via XCM.
638		///
639		/// AssetHub validates both keys and ownership proof before sending.
640		/// RC trusts AH's validation and does not re-validate.
641		#[pallet::call_index(3)]
642		#[pallet::weight(T::SessionInterface::set_keys_weight())]
643		pub fn set_keys_from_ah(
644			origin: OriginFor<T>,
645			stash: T::AccountId,
646			keys: Vec<u8>,
647		) -> DispatchResult {
648			T::AssetHubOrigin::ensure_origin_or_root(origin)?;
649			log::info!(target: LOG_TARGET, "Received set_keys request from AssetHub for {stash:?}");
650
651			// Decode the keys from bytes (AH already validated, this is just for type conversion)
652			let session_keys =
653				match <<T as Config>::SessionInterface as SessionInterface>::Keys::decode(
654					&mut &keys[..],
655				) {
656					Ok(keys) => keys,
657					Err(e) => {
658						// This should never happen since AH validates keys before forwarding.
659						// Returning Ok() allows the event to be observed for monitoring.
660						log!(
661							warn,
662							"InvalidKeysFromAssetHub: failed to decode keys for {:?}: {:?}",
663							stash,
664							e
665						);
666						Self::deposit_event(Event::Unexpected(
667							UnexpectedKind::InvalidKeysFromAssetHub,
668						));
669						return Ok(());
670					},
671				};
672
673			match T::SessionInterface::set_keys(&stash, session_keys) {
674				Ok(()) => Self::deposit_event(Event::SessionKeysUpdated {
675					stash,
676					update: SessionKeysUpdate::Set,
677				}),
678				Err(error) => {
679					log!(
680						warn,
681						"SessionKeysUpdateFailed: set_keys failed for {:?}: {:?}",
682						stash,
683						error
684					);
685					Self::deposit_event(Event::SessionKeysUpdateFailed {
686						stash,
687						update: SessionKeysUpdate::Set,
688						error,
689					});
690				},
691			}
692			Ok(())
693		}
694
695		/// Purge session keys for a validator, forwarded from AssetHub.
696		///
697		/// This is called when a validator purges their session keys on AssetHub, which forwards
698		/// the request to the RelayChain via XCM.
699		#[pallet::call_index(4)]
700		#[pallet::weight(T::SessionInterface::purge_keys_weight())]
701		pub fn purge_keys_from_ah(origin: OriginFor<T>, stash: T::AccountId) -> DispatchResult {
702			T::AssetHubOrigin::ensure_origin_or_root(origin)?;
703			log::info!(target: LOG_TARGET, "Received purge_keys request from AssetHub for {stash:?}");
704
705			match T::SessionInterface::purge_keys(&stash) {
706				Ok(()) => Self::deposit_event(Event::SessionKeysUpdated {
707					stash,
708					update: SessionKeysUpdate::Purged,
709				}),
710				Err(error) => {
711					log!(
712						warn,
713						"SessionKeysUpdateFailed: purge_keys failed for {:?}: {:?}",
714						stash,
715						error
716					);
717					Self::deposit_event(Event::SessionKeysUpdateFailed {
718						stash,
719						update: SessionKeysUpdate::Purged,
720						error,
721					});
722				},
723			}
724			Ok(())
725		}
726	}
727
728	#[pallet::hooks]
729	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
730		fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
731			let mut weight = Weight::zero();
732
733			let mode = Mode::<T>::get();
734			weight = weight.saturating_add(T::DbWeight::get().reads(1));
735			if mode != OperatingMode::Active {
736				return weight;
737			}
738
739			// if we have any pending session reports, send it.
740			weight.saturating_accrue(T::DbWeight::get().reads(1));
741			if let Some((session_report, retries_left)) = OutgoingSessionReport::<T>::take() {
742				match T::SendToAssetHub::relay_session_report(session_report.clone()) {
743					Ok(()) => {
744						// report was sent, all good, it is already deleted.
745					},
746					Err(()) => {
747						log!(error, "Failed to send session report to assethub");
748						Self::deposit_event(Event::<T>::Unexpected(
749							UnexpectedKind::SessionReportSendFailed,
750						));
751						if let Some(new_retries_left) = retries_left.checked_sub(One::one()) {
752							OutgoingSessionReport::<T>::put((session_report, new_retries_left))
753						} else {
754							// recreate the validator points, so they will be sent in the next
755							// report.
756							session_report.validator_points.into_iter().for_each(|(v, p)| {
757								ValidatorPoints::<T>::mutate(v, |existing_points| {
758									*existing_points = existing_points.defensive_saturating_add(p)
759								});
760							});
761
762							Self::deposit_event(Event::<T>::Unexpected(
763								UnexpectedKind::SessionReportDropped,
764							));
765						}
766					},
767				}
768			}
769
770			// then, take a page from our send queue, and if present, send it.
771			weight.saturating_accrue(T::DbWeight::get().reads(2));
772			OffenceSendQueue::<T>::get_and_maybe_delete(|page| {
773				if page.is_empty() {
774					return Ok(());
775				}
776				// send the page if not empty. If sending returns `Ok`, we delete this page.
777				T::SendToAssetHub::relay_new_offence_paged(page.into_inner()).inspect_err(|_| {
778					Self::deposit_event(Event::Unexpected(UnexpectedKind::OffenceSendFailed));
779				})
780			});
781
782			weight
783		}
784
785		fn integrity_test() {
786			assert!(T::MaxOffenceBatchSize::get() > 0, "Offence Batch size must be at least 1");
787		}
788	}
789
790	impl<T: Config>
791		historical::SessionManager<T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>>
792		for Pallet<T>
793	{
794		fn new_session(
795			new_index: sp_staking::SessionIndex,
796		) -> Option<
797			Vec<(
798				<T as frame_system::Config>::AccountId,
799				sp_staking::Exposure<T::AccountId, BalanceOf<T>>,
800			)>,
801		> {
802			<Self as pallet_session::SessionManager<_>>::new_session(new_index)
803				.map(|v| v.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect())
804		}
805
806		fn new_session_genesis(
807			new_index: SessionIndex,
808		) -> Option<Vec<(T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>)>> {
809			if Mode::<T>::get() == OperatingMode::Passive {
810				T::Fallback::new_session_genesis(new_index).map(|validators| {
811					validators.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect()
812				})
813			} else {
814				None
815			}
816		}
817
818		fn start_session(start_index: SessionIndex) {
819			<Self as pallet_session::SessionManager<_>>::start_session(start_index)
820		}
821
822		fn end_session(end_index: SessionIndex) {
823			<Self as pallet_session::SessionManager<_>>::end_session(end_index)
824		}
825	}
826
827	impl<T: Config> pallet_session::SessionManager<T::AccountId> for Pallet<T> {
828		fn new_session(session_index: u32) -> Option<Vec<T::AccountId>> {
829			match Mode::<T>::get() {
830				OperatingMode::Passive => T::Fallback::new_session(session_index),
831				// In `Buffered` mode, we drop the session report and do nothing.
832				OperatingMode::Buffered => None,
833				OperatingMode::Active => Self::do_new_session(),
834			}
835		}
836
837		fn start_session(session_index: u32) {
838			if Mode::<T>::get() == OperatingMode::Passive {
839				T::Fallback::start_session(session_index)
840			}
841		}
842
843		fn new_session_genesis(new_index: SessionIndex) -> Option<Vec<T::AccountId>> {
844			if Mode::<T>::get() == OperatingMode::Passive {
845				T::Fallback::new_session_genesis(new_index)
846			} else {
847				None
848			}
849		}
850
851		fn end_session(session_index: u32) {
852			match Mode::<T>::get() {
853				OperatingMode::Passive => T::Fallback::end_session(session_index),
854				// In `Buffered` mode, we drop the session report and do nothing.
855				OperatingMode::Buffered => (),
856				OperatingMode::Active => Self::do_end_session(session_index),
857			}
858		}
859	}
860
861	impl<T: Config>
862		OnOffenceHandler<
863			T::AccountId,
864			(T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>),
865			Weight,
866		> for Pallet<T>
867	{
868		fn on_offence(
869			offenders: &[OffenceDetails<
870				T::AccountId,
871				(T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>),
872			>],
873			slash_fraction: &[Perbill],
874			slash_session: SessionIndex,
875		) -> Weight {
876			match Mode::<T>::get() {
877				OperatingMode::Passive => {
878					// delegate to the fallback implementation.
879					T::Fallback::on_offence(offenders, slash_fraction, slash_session)
880				},
881				OperatingMode::Buffered => {
882					Self::on_offence_buffered(offenders, slash_fraction, slash_session)
883				},
884				OperatingMode::Active => {
885					Self::on_offence_active(offenders, slash_fraction, slash_session)
886				},
887			}
888		}
889	}
890
891	impl<T: Config> RewardsReporter<T::AccountId> for Pallet<T> {
892		fn reward_by_ids(rewards: impl IntoIterator<Item = (T::AccountId, u32)>) {
893			match Mode::<T>::get() {
894				OperatingMode::Passive => T::Fallback::reward_by_ids(rewards),
895				OperatingMode::Buffered | OperatingMode::Active => Self::do_reward_by_ids(rewards),
896			}
897		}
898	}
899
900	impl<T: Config> pallet_authorship::EventHandler<T::AccountId, BlockNumberFor<T>> for Pallet<T> {
901		fn note_author(author: T::AccountId) {
902			match Mode::<T>::get() {
903				OperatingMode::Passive => T::Fallback::note_author(author),
904				OperatingMode::Buffered | OperatingMode::Active => Self::do_note_author(author),
905			}
906		}
907	}
908
909	impl<T: Config> Pallet<T> {
910		/// Hook to be called when the AssetHub migration begins.
911		///
912		/// This transitions the pallet into [`OperatingMode::Buffered`], meaning it will act as the
913		/// primary staking module on the relay chain but will buffer outgoing messages instead of
914		/// sending them to AssetHub.
915		///
916		/// While in this mode, the pallet stops delegating to the fallback implementation and
917		/// temporarily accumulates events for later processing.
918		pub fn on_migration_start() {
919			debug_assert!(
920				Mode::<T>::get() == OperatingMode::Passive,
921				"we should only be called when in passive mode"
922			);
923			Self::do_set_mode(OperatingMode::Buffered);
924		}
925
926		/// Hook to be called when the AssetHub migration is complete.
927		///
928		/// This transitions the pallet into [`OperatingMode::Active`], meaning the counterpart
929		/// pallet on AssetHub is ready to accept incoming messages, and this pallet can resume
930		/// sending them.
931		///
932		/// In this mode, the pallet becomes fully active and processes all staking-related events
933		/// directly.
934		pub fn on_migration_end() {
935			debug_assert!(
936				Mode::<T>::get() == OperatingMode::Buffered,
937				"we should only be called when in buffered mode"
938			);
939			Self::do_set_mode(OperatingMode::Active);
940
941			// Buffered offences will be processed gradually by on_initialize
942			// using MaxOffenceBatchSize to prevent block overload.
943		}
944
945		fn do_set_mode(new_mode: OperatingMode) {
946			let old_mode = Mode::<T>::get();
947			let unexpected = match new_mode {
948				// `Passive` is the initial state, and not expected to be set by the user.
949				OperatingMode::Passive => true,
950				OperatingMode::Buffered => old_mode != OperatingMode::Passive,
951				OperatingMode::Active => old_mode != OperatingMode::Buffered,
952			};
953
954			// this is a defensive check, and should never happen under normal operation.
955			if unexpected {
956				log!(warn, "Unexpected mode transition from {:?} to {:?}", old_mode, new_mode);
957				Self::deposit_event(Event::Unexpected(UnexpectedKind::UnexpectedModeTransition));
958			}
959
960			// apply new mode anyway.
961			Mode::<T>::put(new_mode);
962		}
963
964		fn do_new_session() -> Option<Vec<T::AccountId>> {
965			ValidatorSet::<T>::take().map(|(id, val_set)| {
966				// store the id to be sent back in the next session back to AH
967				NextSessionChangesValidators::<T>::put(id);
968				val_set
969			})
970		}
971
972		fn do_end_session(end_index: u32) {
973			// take and delete all validator points, limited by `MaximumValidatorsWithPoints`.
974			let validator_points = ValidatorPoints::<T>::iter()
975				.drain()
976				.take(T::MaximumValidatorsWithPoints::get() as usize)
977				.collect::<Vec<_>>();
978
979			// If there were more validators than `MaximumValidatorsWithPoints`..
980			if ValidatorPoints::<T>::iter().next().is_some() {
981				// ..not much more we can do about it other than an event.
982				Self::deposit_event(Event::<T>::Unexpected(UnexpectedKind::ValidatorPointDropped))
983			}
984
985			let activation_timestamp = NextSessionChangesValidators::<T>::take().map(|id| {
986				// keep track of starting session index at which the validator set was applied.
987				ValidatorSetAppliedAt::<T>::put(end_index + 1);
988				// set the timestamp and the identifier of the validator set.
989				(T::UnixTime::now().as_millis().saturated_into::<u64>(), id)
990			});
991
992			let session_report = pallet_staking_async_rc_client::SessionReport {
993				end_index,
994				validator_points,
995				activation_timestamp,
996				leftover: false,
997			};
998
999			// queue the session report to be sent.
1000			OutgoingSessionReport::<T>::put((session_report, T::MaxSessionReportRetries::get()));
1001		}
1002
1003		fn do_reward_by_ids(rewards: impl IntoIterator<Item = (T::AccountId, u32)>) {
1004			for (validator_id, points) in rewards {
1005				ValidatorPoints::<T>::mutate(validator_id, |balance| {
1006					balance.saturating_accrue(points);
1007				});
1008			}
1009		}
1010
1011		fn do_note_author(author: T::AccountId) {
1012			ValidatorPoints::<T>::mutate(author, |points| {
1013				points.saturating_accrue(T::PointsPerBlock::get());
1014			});
1015		}
1016
1017		/// Check if an offence is from the active validator set.
1018		fn is_ongoing_offence(slash_session: SessionIndex) -> bool {
1019			ValidatorSetAppliedAt::<T>::get()
1020				.map(|start_session| slash_session >= start_session)
1021				.unwrap_or(false)
1022		}
1023
1024		/// Handle offences in Buffered mode.
1025		fn on_offence_buffered(
1026			offenders: &[OffenceDetailsOf<T>],
1027			slash_fraction: &[Perbill],
1028			slash_session: SessionIndex,
1029		) -> Weight {
1030			let ongoing_offence = Self::is_ongoing_offence(slash_session);
1031
1032			offenders.iter().cloned().zip(slash_fraction).for_each(|(offence, fraction)| {
1033				if ongoing_offence {
1034					// report the offence to the session pallet.
1035					T::SessionInterface::report_offence(
1036						offence.offender.0.clone(),
1037						OffenceSeverity(*fraction),
1038					);
1039				}
1040
1041				let (offender, _full_identification) = offence.offender;
1042				let reporters = offence.reporters;
1043
1044				// In `Buffered` mode, we buffer the offences for later processing.
1045				OffenceSendQueue::<T>::append((
1046					slash_session,
1047					rc_client::Offence {
1048						offender: offender.clone(),
1049						reporters: reporters.into_iter().take(1).collect(),
1050						slash_fraction: *fraction,
1051					},
1052				));
1053			});
1054
1055			T::DbWeight::get().reads_writes(1, 1)
1056		}
1057
1058		/// Handle offences in Active mode.
1059		fn on_offence_active(
1060			offenders: &[OffenceDetailsOf<T>],
1061			slash_fraction: &[Perbill],
1062			slash_session: SessionIndex,
1063		) -> Weight {
1064			let ongoing_offence = Self::is_ongoing_offence(slash_session);
1065
1066			offenders.iter().cloned().zip(slash_fraction).for_each(|(offence, fraction)| {
1067				if ongoing_offence {
1068					// report the offence to the session pallet.
1069					T::SessionInterface::report_offence(
1070						offence.offender.0.clone(),
1071						OffenceSeverity(*fraction),
1072					);
1073				}
1074
1075				let (offender, _full_identification) = offence.offender;
1076				let reporters = offence.reporters;
1077
1078				// prepare an `Offence` instance for the XCM message. Note that we drop
1079				// the identification.
1080				let offence = rc_client::Offence {
1081					offender,
1082					reporters: reporters.into_iter().take(1).collect(),
1083					slash_fraction: *fraction,
1084				};
1085				OffenceSendQueue::<T>::append((slash_session, offence))
1086			});
1087
1088			T::DbWeight::get().reads_writes(2, 2)
1089		}
1090	}
1091}
1092
1093#[cfg(test)]
1094mod keys_from_ah_tests {
1095	use super::*;
1096	use crate::mock::*;
1097	use codec::Encode;
1098	use frame_support::{assert_noop, assert_ok, hypothetically};
1099	use sp_runtime::DispatchError;
1100
1101	#[test]
1102	fn set_keys_from_ah() {
1103		new_test_ext().execute_with(|| {
1104			System::set_block_number(1);
1105			let stash = 42u64;
1106			let keys = MockSessionKeys { dummy: [1u8; 32] };
1107
1108			// success with root origin
1109			hypothetically!({
1110				SetKeysCalls::take();
1111				assert_ok!(StakingAsyncAhClient::set_keys_from_ah(
1112					RuntimeOrigin::root(),
1113					stash,
1114					keys.encode(),
1115				));
1116				assert_eq!(SetKeysCalls::get(), vec![(stash, keys.clone())]);
1117				System::assert_has_event(
1118					Event::<Test>::SessionKeysUpdated { stash, update: SessionKeysUpdate::Set }
1119						.into(),
1120				);
1121			});
1122
1123			// rejects bad origin
1124			hypothetically!({
1125				SetKeysCalls::take();
1126				assert_noop!(
1127					StakingAsyncAhClient::set_keys_from_ah(
1128						RuntimeOrigin::signed(1),
1129						stash,
1130						keys.encode(),
1131					),
1132					DispatchError::BadOrigin
1133				);
1134				assert!(SetKeysCalls::get().is_empty());
1135			});
1136
1137			// emits SessionKeysUpdateFailed when SessionInterface::set_keys fails
1138			hypothetically!({
1139				SetKeysCalls::take();
1140				let error = DispatchError::Corruption;
1141				SetKeysError::set(Some(error));
1142				assert_ok!(StakingAsyncAhClient::set_keys_from_ah(
1143					RuntimeOrigin::root(),
1144					stash,
1145					keys.encode(),
1146				));
1147				assert!(SetKeysCalls::get().is_empty());
1148				System::assert_has_event(
1149					Event::<Test>::SessionKeysUpdateFailed {
1150						stash,
1151						update: SessionKeysUpdate::Set,
1152						error,
1153					}
1154					.into(),
1155				);
1156				SetKeysError::take();
1157			});
1158
1159			// handles invalid keys gracefully
1160			hypothetically!({
1161				SetKeysCalls::take();
1162				assert_ok!(StakingAsyncAhClient::set_keys_from_ah(
1163					RuntimeOrigin::root(),
1164					stash,
1165					vec![1u8, 2, 3], // invalid encoding
1166				));
1167				assert!(SetKeysCalls::get().is_empty());
1168				System::assert_has_event(
1169					Event::<Test>::Unexpected(UnexpectedKind::InvalidKeysFromAssetHub).into(),
1170				);
1171			});
1172		});
1173	}
1174
1175	#[test]
1176	fn purge_keys_from_ah() {
1177		new_test_ext().execute_with(|| {
1178			System::set_block_number(1);
1179			let stash = 42u64;
1180
1181			// success with root origin
1182			hypothetically!({
1183				PurgeKeysCalls::take();
1184				assert_ok!(StakingAsyncAhClient::purge_keys_from_ah(RuntimeOrigin::root(), stash));
1185				assert_eq!(PurgeKeysCalls::get(), vec![stash]);
1186				System::assert_has_event(
1187					Event::<Test>::SessionKeysUpdated { stash, update: SessionKeysUpdate::Purged }
1188						.into(),
1189				);
1190			});
1191
1192			// rejects bad origin
1193			hypothetically!({
1194				PurgeKeysCalls::take();
1195				assert_noop!(
1196					StakingAsyncAhClient::purge_keys_from_ah(RuntimeOrigin::signed(1), stash),
1197					DispatchError::BadOrigin
1198				);
1199				assert!(PurgeKeysCalls::get().is_empty());
1200			});
1201
1202			// emits SessionKeysUpdateFailed when SessionInterface::purge_keys fails
1203			hypothetically!({
1204				PurgeKeysCalls::take();
1205				let error = DispatchError::Corruption;
1206				PurgeKeysError::set(Some(error));
1207				assert_ok!(StakingAsyncAhClient::purge_keys_from_ah(RuntimeOrigin::root(), stash));
1208				assert!(PurgeKeysCalls::get().is_empty());
1209				System::assert_has_event(
1210					Event::<Test>::SessionKeysUpdateFailed {
1211						stash,
1212						update: SessionKeysUpdate::Purged,
1213						error,
1214					}
1215					.into(),
1216				);
1217				PurgeKeysError::take();
1218			});
1219		});
1220	}
1221}
1222
1223#[cfg(test)]
1224mod send_queue_tests {
1225	use frame_support::hypothetically;
1226	use sp_runtime::Perbill;
1227
1228	use super::*;
1229	use crate::mock::*;
1230
1231	// (cursor, len_of_pages)
1232	fn status() -> (u32, Vec<u32>) {
1233		let mut sorted = OffenceSendQueueOffences::<Test>::iter().collect::<Vec<_>>();
1234		sorted.sort_by(|x, y| x.0.cmp(&y.0));
1235		(
1236			OffenceSendQueueCursor::<Test>::get(),
1237			sorted.into_iter().map(|(_, v)| v.len() as u32).collect(),
1238		)
1239	}
1240
1241	#[test]
1242	fn append_and_take() {
1243		new_test_ext().execute_with(|| {
1244			let o = (
1245				42,
1246				rc_client::Offence {
1247					offender: 42,
1248					reporters: vec![],
1249					slash_fraction: Perbill::from_percent(10),
1250				},
1251			);
1252			let page_size = <Test as Config>::MaxOffenceBatchSize::get();
1253			assert_eq!(page_size % 2, 0, "page size should be even");
1254
1255			assert_eq!(status(), (0, vec![]));
1256
1257			// --- when empty
1258
1259			assert_eq!(OffenceSendQueue::<Test>::count(), 0);
1260			assert_eq!(OffenceSendQueue::<Test>::pages(), 0);
1261
1262			// get and keep
1263			hypothetically!({
1264				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1265					assert_eq!(page.len(), 0);
1266					Err(())
1267				});
1268				assert_eq!(status(), (0, vec![]));
1269			});
1270
1271			// get and delete
1272			hypothetically!({
1273				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1274					assert_eq!(page.len(), 0);
1275					Ok(())
1276				});
1277				assert_eq!(status(), (0, vec![]));
1278			});
1279
1280			// -------- when 1 page half filled
1281			for _ in 0..page_size / 2 {
1282				OffenceSendQueue::<Test>::append(o.clone());
1283			}
1284			assert_eq!(status(), (0, vec![page_size / 2]));
1285			assert_eq!(OffenceSendQueue::<Test>::count(), page_size / 2);
1286			assert_eq!(OffenceSendQueue::<Test>::pages(), 1);
1287
1288			// get and keep
1289			hypothetically!({
1290				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1291					assert_eq!(page.len() as u32, page_size / 2);
1292					Err(())
1293				});
1294				assert_eq!(status(), (0, vec![page_size / 2]));
1295			});
1296
1297			// get and delete
1298			hypothetically!({
1299				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1300					assert_eq!(page.len() as u32, page_size / 2);
1301					Ok(())
1302				});
1303				assert_eq!(status(), (0, vec![]));
1304				assert_eq!(OffenceSendQueue::<Test>::count(), 0);
1305				assert_eq!(OffenceSendQueue::<Test>::pages(), 0);
1306			});
1307
1308			// -------- when 1 page full
1309			for _ in 0..page_size / 2 {
1310				OffenceSendQueue::<Test>::append(o.clone());
1311			}
1312			assert_eq!(status(), (0, vec![page_size]));
1313			assert_eq!(OffenceSendQueue::<Test>::count(), page_size);
1314			assert_eq!(OffenceSendQueue::<Test>::pages(), 1);
1315
1316			// get and keep
1317			hypothetically!({
1318				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1319					assert_eq!(page.len() as u32, page_size);
1320					Err(())
1321				});
1322				assert_eq!(status(), (0, vec![page_size]));
1323			});
1324
1325			// get and delete
1326			hypothetically!({
1327				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1328					assert_eq!(page.len() as u32, page_size);
1329					Ok(())
1330				});
1331				assert_eq!(status(), (0, vec![]));
1332			});
1333
1334			// -------- when more than 1 page full
1335			OffenceSendQueue::<Test>::append(o.clone());
1336			assert_eq!(status(), (1, vec![page_size, 1]));
1337			assert_eq!(OffenceSendQueue::<Test>::count(), page_size + 1);
1338			assert_eq!(OffenceSendQueue::<Test>::pages(), 2);
1339
1340			// get and keep
1341			hypothetically!({
1342				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1343					assert_eq!(page.len(), 1);
1344					Err(())
1345				});
1346				assert_eq!(status(), (1, vec![page_size, 1]));
1347			});
1348
1349			// get and delete
1350			hypothetically!({
1351				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1352					assert_eq!(page.len(), 1);
1353					Ok(())
1354				});
1355				assert_eq!(status(), (0, vec![page_size]));
1356			});
1357		})
1358	}
1359}