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		/// If set, records that a validator set was applied at this session index at genesis.
425		/// Allows offence-based validator disabling on relays not connected to an AssetHub.
426		/// Defaults to `None` (no-op).
427		pub validator_set_applied_at: Option<SessionIndex>,
428		pub _marker: core::marker::PhantomData<T>,
429	}
430
431	#[pallet::genesis_build]
432	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
433		fn build(&self) {
434			// Set the initial operating mode of the pallet.
435			Mode::<T>::put(self.operating_mode.clone());
436			if let Some(session) = self.validator_set_applied_at {
437				ValidatorSetAppliedAt::<T>::put(session);
438			}
439		}
440	}
441
442	#[pallet::error]
443	pub enum Error<T> {
444		/// Could not process incoming message because incoming messages are blocked.
445		Blocked,
446	}
447
448	#[pallet::event]
449	#[pallet::generate_deposit(fn deposit_event)]
450	pub enum Event<T: Config> {
451		/// A new validator set has been received.
452		ValidatorSetReceived {
453			id: u32,
454			new_validator_set_count: u32,
455			prune_up_to: Option<SessionIndex>,
456			leftover: bool,
457		},
458		/// We could not merge, and therefore dropped a buffered message.
459		///
460		/// Note that this event is more resembling an error, but we use an event because in this
461		/// pallet we need to mutate storage upon some failures.
462		CouldNotMergeAndDropped,
463		/// The validator set received is way too small, as per
464		/// [`Config::MinimumValidatorSetSize`].
465		SetTooSmallAndDropped,
466		/// Something occurred that should never happen under normal operation. Logged as an event
467		/// for fail-safe observability.
468		Unexpected(UnexpectedKind),
469		/// Session keys updated for a validator.
470		SessionKeysUpdated { stash: T::AccountId, update: SessionKeysUpdate },
471		/// Session key update from AssetHub failed on the relay chain.
472		/// Logged as an event for fail-safe observability.
473		SessionKeysUpdateFailed {
474			stash: T::AccountId,
475			update: SessionKeysUpdate,
476			error: DispatchError,
477		},
478	}
479
480	/// The type of session keys update received from AssetHub.
481	#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, Debug)]
482	pub enum SessionKeysUpdate {
483		/// Session keys have been set.
484		Set,
485		/// Session keys have been purged.
486		Purged,
487	}
488
489	/// Represents unexpected or invariant-breaking conditions encountered during execution.
490	///
491	/// These variants are emitted as [`Event::Unexpected`] and indicate a defensive check has
492	/// failed. While these should never occur under normal operation, they are useful for
493	/// diagnosing issues in production or test environments.
494	#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, Debug)]
495	pub enum UnexpectedKind {
496		/// A validator set was received while the pallet is in [`OperatingMode::Passive`].
497		ReceivedValidatorSetWhilePassive,
498
499		/// An unexpected transition was applied between operating modes.
500		///
501		/// Expected transitions are linear and forward-only: `Passive` → `Buffered` → `Active`.
502		UnexpectedModeTransition,
503
504		/// A session report failed to be sent.
505		///
506		/// We will store, and retry it for a number of more block.
507		SessionReportSendFailed,
508
509		/// A session report failed enough times that we should drop it.
510		///
511		/// We will retain the validator points, and send them over in the next session we receive
512		/// from pallet-session.
513		SessionReportDropped,
514
515		/// An offence report failed to be sent.
516		///
517		/// It will be retried again in the next block. We never drop them.
518		OffenceSendFailed,
519
520		/// Some validator points didn't make it to be included in the session report. Should
521		/// never happen, and means:
522		///
523		/// * a too low of a value is assigned to [`Config::MaximumValidatorsWithPoints`]
524		/// * Those who are calling into our `RewardsReporter` likely have a bad view of the
525		///   validator set, and are spamming us.
526		ValidatorPointDropped,
527
528		/// Session keys received from AssetHub failed to decode.
529		///
530		/// This should never happen since AssetHub validates keys before forwarding them.
531		/// If this occurs, it indicates a mismatch between AH and RC key types or a bug.
532		InvalidKeysFromAssetHub,
533	}
534
535	#[pallet::call]
536	impl<T: Config> Pallet<T> {
537		#[pallet::call_index(0)]
538		#[pallet::weight(
539			// Reads:
540			// - OperatingMode
541			// - IncompleteValidatorSetReport
542			// Writes:
543			// - IncompleteValidatorSetReport or ValidatorSet
544			// ignoring `T::SessionInterface::prune_up_to`
545			T::DbWeight::get().reads_writes(2, 1)
546		)]
547		pub fn validator_set(
548			origin: OriginFor<T>,
549			report: rc_client::ValidatorSetReport<T::AccountId>,
550		) -> DispatchResult {
551			// Ensure the origin is one of Root or whatever is representing AssetHub.
552			log!(debug, "Received new validator set report {}", report);
553			T::AssetHubOrigin::ensure_origin_or_root(origin)?;
554
555			// Check the operating mode.
556			let mode = Mode::<T>::get();
557			ensure!(mode.can_accept_validator_set(), Error::<T>::Blocked);
558
559			let maybe_merged_report = match IncompleteValidatorSetReport::<T>::take() {
560				Some(old) => old.merge(report.clone()),
561				None => Ok(report),
562			};
563
564			if maybe_merged_report.is_err() {
565				Self::deposit_event(Event::CouldNotMergeAndDropped);
566				debug_assert!(
567					IncompleteValidatorSetReport::<T>::get().is_none(),
568					"we have ::take() it above, we don't want to keep the old data"
569				);
570				return Ok(());
571			}
572
573			let report = maybe_merged_report.expect("checked above; qed");
574
575			if report.leftover {
576				// buffer it, and nothing further to do.
577				Self::deposit_event(Event::ValidatorSetReceived {
578					id: report.id,
579					new_validator_set_count: report.new_validator_set.len() as u32,
580					prune_up_to: report.prune_up_to,
581					leftover: report.leftover,
582				});
583				IncompleteValidatorSetReport::<T>::put(report);
584			} else {
585				// message is complete, process it.
586				let rc_client::ValidatorSetReport {
587					id,
588					leftover,
589					mut new_validator_set,
590					prune_up_to,
591				} = report;
592
593				// ensure the validator set, deduplicated, is not too big.
594				new_validator_set.sort();
595				new_validator_set.dedup();
596
597				if (new_validator_set.len() as u32) < T::MinimumValidatorSetSize::get() {
598					Self::deposit_event(Event::SetTooSmallAndDropped);
599					debug_assert!(
600						IncompleteValidatorSetReport::<T>::get().is_none(),
601						"we have ::take() it above, we don't want to keep the old data"
602					);
603					return Ok(());
604				}
605
606				Self::deposit_event(Event::ValidatorSetReceived {
607					id,
608					new_validator_set_count: new_validator_set.len() as u32,
609					prune_up_to,
610					leftover,
611				});
612
613				// Save the validator set.
614				ValidatorSet::<T>::put((id, new_validator_set));
615				if let Some(index) = prune_up_to {
616					T::SessionInterface::prune_up_to(index);
617				}
618			}
619
620			Ok(())
621		}
622
623		/// Allows governance to force set the operating mode of the pallet.
624		#[pallet::call_index(1)]
625		#[pallet::weight(T::DbWeight::get().writes(1))]
626		pub fn set_mode(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
627			T::AdminOrigin::ensure_origin(origin)?;
628			Self::do_set_mode(mode);
629			Ok(())
630		}
631
632		/// manually do what this pallet was meant to do at the end of the migration.
633		#[pallet::call_index(2)]
634		#[pallet::weight(T::DbWeight::get().writes(1))]
635		pub fn force_on_migration_end(origin: OriginFor<T>) -> DispatchResult {
636			T::AdminOrigin::ensure_origin(origin)?;
637			Self::on_migration_end();
638			Ok(())
639		}
640
641		/// Set session keys for a validator, forwarded from AssetHub.
642		///
643		/// This is called when a validator sets their session keys on AssetHub, which forwards
644		/// the request to the RelayChain via XCM.
645		///
646		/// AssetHub validates both keys and ownership proof before sending.
647		/// RC trusts AH's validation and does not re-validate.
648		#[pallet::call_index(3)]
649		#[pallet::weight(T::SessionInterface::set_keys_weight())]
650		pub fn set_keys_from_ah(
651			origin: OriginFor<T>,
652			stash: T::AccountId,
653			keys: Vec<u8>,
654		) -> DispatchResult {
655			T::AssetHubOrigin::ensure_origin_or_root(origin)?;
656			log::info!(target: LOG_TARGET, "Received set_keys request from AssetHub for {stash:?}");
657
658			// Decode the keys from bytes (AH already validated, this is just for type conversion)
659			let session_keys =
660				match <<T as Config>::SessionInterface as SessionInterface>::Keys::decode(
661					&mut &keys[..],
662				) {
663					Ok(keys) => keys,
664					Err(e) => {
665						// This should never happen since AH validates keys before forwarding.
666						// Returning Ok() allows the event to be observed for monitoring.
667						log!(
668							warn,
669							"InvalidKeysFromAssetHub: failed to decode keys for {:?}: {:?}",
670							stash,
671							e
672						);
673						Self::deposit_event(Event::Unexpected(
674							UnexpectedKind::InvalidKeysFromAssetHub,
675						));
676						return Ok(());
677					},
678				};
679
680			match T::SessionInterface::set_keys(&stash, session_keys) {
681				Ok(()) => Self::deposit_event(Event::SessionKeysUpdated {
682					stash,
683					update: SessionKeysUpdate::Set,
684				}),
685				Err(error) => {
686					log!(
687						warn,
688						"SessionKeysUpdateFailed: set_keys failed for {:?}: {:?}",
689						stash,
690						error
691					);
692					Self::deposit_event(Event::SessionKeysUpdateFailed {
693						stash,
694						update: SessionKeysUpdate::Set,
695						error,
696					});
697				},
698			}
699			Ok(())
700		}
701
702		/// Purge session keys for a validator, forwarded from AssetHub.
703		///
704		/// This is called when a validator purges their session keys on AssetHub, which forwards
705		/// the request to the RelayChain via XCM.
706		#[pallet::call_index(4)]
707		#[pallet::weight(T::SessionInterface::purge_keys_weight())]
708		pub fn purge_keys_from_ah(origin: OriginFor<T>, stash: T::AccountId) -> DispatchResult {
709			T::AssetHubOrigin::ensure_origin_or_root(origin)?;
710			log::info!(target: LOG_TARGET, "Received purge_keys request from AssetHub for {stash:?}");
711
712			match T::SessionInterface::purge_keys(&stash) {
713				Ok(()) => Self::deposit_event(Event::SessionKeysUpdated {
714					stash,
715					update: SessionKeysUpdate::Purged,
716				}),
717				Err(error) => {
718					log!(
719						warn,
720						"SessionKeysUpdateFailed: purge_keys failed for {:?}: {:?}",
721						stash,
722						error
723					);
724					Self::deposit_event(Event::SessionKeysUpdateFailed {
725						stash,
726						update: SessionKeysUpdate::Purged,
727						error,
728					});
729				},
730			}
731			Ok(())
732		}
733	}
734
735	#[pallet::hooks]
736	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
737		fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
738			let mut weight = Weight::zero();
739
740			let mode = Mode::<T>::get();
741			weight = weight.saturating_add(T::DbWeight::get().reads(1));
742			if mode != OperatingMode::Active {
743				return weight;
744			}
745
746			// if we have any pending session reports, send it.
747			weight.saturating_accrue(T::DbWeight::get().reads(1));
748			if let Some((session_report, retries_left)) = OutgoingSessionReport::<T>::take() {
749				match T::SendToAssetHub::relay_session_report(session_report.clone()) {
750					Ok(()) => {
751						// report was sent, all good, it is already deleted.
752					},
753					Err(()) => {
754						log!(error, "Failed to send session report to assethub");
755						Self::deposit_event(Event::<T>::Unexpected(
756							UnexpectedKind::SessionReportSendFailed,
757						));
758						if let Some(new_retries_left) = retries_left.checked_sub(One::one()) {
759							OutgoingSessionReport::<T>::put((session_report, new_retries_left))
760						} else {
761							// recreate the validator points, so they will be sent in the next
762							// report.
763							session_report.validator_points.into_iter().for_each(|(v, p)| {
764								ValidatorPoints::<T>::mutate(v, |existing_points| {
765									*existing_points = existing_points.defensive_saturating_add(p)
766								});
767							});
768
769							Self::deposit_event(Event::<T>::Unexpected(
770								UnexpectedKind::SessionReportDropped,
771							));
772						}
773					},
774				}
775			}
776
777			// then, take a page from our send queue, and if present, send it.
778			weight.saturating_accrue(T::DbWeight::get().reads(2));
779			OffenceSendQueue::<T>::get_and_maybe_delete(|page| {
780				if page.is_empty() {
781					return Ok(());
782				}
783				// send the page if not empty. If sending returns `Ok`, we delete this page.
784				T::SendToAssetHub::relay_new_offence_paged(page.into_inner()).inspect_err(|_| {
785					Self::deposit_event(Event::Unexpected(UnexpectedKind::OffenceSendFailed));
786				})
787			});
788
789			weight
790		}
791
792		fn integrity_test() {
793			assert!(T::MaxOffenceBatchSize::get() > 0, "Offence Batch size must be at least 1");
794		}
795	}
796
797	impl<T: Config>
798		historical::SessionManager<T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>>
799		for Pallet<T>
800	{
801		fn new_session(
802			new_index: sp_staking::SessionIndex,
803		) -> Option<
804			Vec<(
805				<T as frame_system::Config>::AccountId,
806				sp_staking::Exposure<T::AccountId, BalanceOf<T>>,
807			)>,
808		> {
809			<Self as pallet_session::SessionManager<_>>::new_session(new_index)
810				.map(|v| v.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect())
811		}
812
813		fn new_session_genesis(
814			new_index: SessionIndex,
815		) -> Option<Vec<(T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>)>> {
816			if Mode::<T>::get() == OperatingMode::Passive {
817				T::Fallback::new_session_genesis(new_index).map(|validators| {
818					validators.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect()
819				})
820			} else {
821				None
822			}
823		}
824
825		fn start_session(start_index: SessionIndex) {
826			<Self as pallet_session::SessionManager<_>>::start_session(start_index)
827		}
828
829		fn end_session(end_index: SessionIndex) {
830			<Self as pallet_session::SessionManager<_>>::end_session(end_index)
831		}
832	}
833
834	impl<T: Config> pallet_session::SessionManager<T::AccountId> for Pallet<T> {
835		fn new_session(session_index: u32) -> Option<Vec<T::AccountId>> {
836			match Mode::<T>::get() {
837				OperatingMode::Passive => T::Fallback::new_session(session_index),
838				// In `Buffered` mode, we drop the session report and do nothing.
839				OperatingMode::Buffered => None,
840				OperatingMode::Active => Self::do_new_session(),
841			}
842		}
843
844		fn start_session(session_index: u32) {
845			if Mode::<T>::get() == OperatingMode::Passive {
846				T::Fallback::start_session(session_index)
847			}
848		}
849
850		fn new_session_genesis(new_index: SessionIndex) -> Option<Vec<T::AccountId>> {
851			if Mode::<T>::get() == OperatingMode::Passive {
852				T::Fallback::new_session_genesis(new_index)
853			} else {
854				None
855			}
856		}
857
858		fn end_session(session_index: u32) {
859			match Mode::<T>::get() {
860				OperatingMode::Passive => T::Fallback::end_session(session_index),
861				// In `Buffered` mode, we drop the session report and do nothing.
862				OperatingMode::Buffered => (),
863				OperatingMode::Active => Self::do_end_session(session_index),
864			}
865		}
866	}
867
868	impl<T: Config>
869		OnOffenceHandler<
870			T::AccountId,
871			(T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>),
872			Weight,
873		> for Pallet<T>
874	{
875		fn on_offence(
876			offenders: &[OffenceDetails<
877				T::AccountId,
878				(T::AccountId, sp_staking::Exposure<T::AccountId, BalanceOf<T>>),
879			>],
880			slash_fraction: &[Perbill],
881			slash_session: SessionIndex,
882		) -> Weight {
883			match Mode::<T>::get() {
884				OperatingMode::Passive => {
885					// delegate to the fallback implementation.
886					T::Fallback::on_offence(offenders, slash_fraction, slash_session)
887				},
888				OperatingMode::Buffered => {
889					Self::on_offence_buffered(offenders, slash_fraction, slash_session)
890				},
891				OperatingMode::Active => {
892					Self::on_offence_active(offenders, slash_fraction, slash_session)
893				},
894			}
895		}
896	}
897
898	impl<T: Config> RewardsReporter<T::AccountId> for Pallet<T> {
899		fn reward_by_ids(rewards: impl IntoIterator<Item = (T::AccountId, u32)>) {
900			match Mode::<T>::get() {
901				OperatingMode::Passive => T::Fallback::reward_by_ids(rewards),
902				OperatingMode::Buffered | OperatingMode::Active => Self::do_reward_by_ids(rewards),
903			}
904		}
905	}
906
907	impl<T: Config> pallet_authorship::EventHandler<T::AccountId, BlockNumberFor<T>> for Pallet<T> {
908		fn note_author(author: T::AccountId) {
909			match Mode::<T>::get() {
910				OperatingMode::Passive => T::Fallback::note_author(author),
911				OperatingMode::Buffered | OperatingMode::Active => Self::do_note_author(author),
912			}
913		}
914	}
915
916	impl<T: Config> Pallet<T> {
917		/// Hook to be called when the AssetHub migration begins.
918		///
919		/// This transitions the pallet into [`OperatingMode::Buffered`], meaning it will act as the
920		/// primary staking module on the relay chain but will buffer outgoing messages instead of
921		/// sending them to AssetHub.
922		///
923		/// While in this mode, the pallet stops delegating to the fallback implementation and
924		/// temporarily accumulates events for later processing.
925		pub fn on_migration_start() {
926			debug_assert!(
927				Mode::<T>::get() == OperatingMode::Passive,
928				"we should only be called when in passive mode"
929			);
930			Self::do_set_mode(OperatingMode::Buffered);
931		}
932
933		/// Hook to be called when the AssetHub migration is complete.
934		///
935		/// This transitions the pallet into [`OperatingMode::Active`], meaning the counterpart
936		/// pallet on AssetHub is ready to accept incoming messages, and this pallet can resume
937		/// sending them.
938		///
939		/// In this mode, the pallet becomes fully active and processes all staking-related events
940		/// directly.
941		pub fn on_migration_end() {
942			debug_assert!(
943				Mode::<T>::get() == OperatingMode::Buffered,
944				"we should only be called when in buffered mode"
945			);
946			Self::do_set_mode(OperatingMode::Active);
947
948			// Buffered offences will be processed gradually by on_initialize
949			// using MaxOffenceBatchSize to prevent block overload.
950		}
951
952		fn do_set_mode(new_mode: OperatingMode) {
953			let old_mode = Mode::<T>::get();
954			let unexpected = match new_mode {
955				// `Passive` is the initial state, and not expected to be set by the user.
956				OperatingMode::Passive => true,
957				OperatingMode::Buffered => old_mode != OperatingMode::Passive,
958				OperatingMode::Active => old_mode != OperatingMode::Buffered,
959			};
960
961			// this is a defensive check, and should never happen under normal operation.
962			if unexpected {
963				log!(warn, "Unexpected mode transition from {:?} to {:?}", old_mode, new_mode);
964				Self::deposit_event(Event::Unexpected(UnexpectedKind::UnexpectedModeTransition));
965			}
966
967			// apply new mode anyway.
968			Mode::<T>::put(new_mode);
969		}
970
971		fn do_new_session() -> Option<Vec<T::AccountId>> {
972			ValidatorSet::<T>::take().map(|(id, val_set)| {
973				// store the id to be sent back in the next session back to AH
974				NextSessionChangesValidators::<T>::put(id);
975				val_set
976			})
977		}
978
979		fn do_end_session(end_index: u32) {
980			// take and delete all validator points, limited by `MaximumValidatorsWithPoints`.
981			let validator_points = ValidatorPoints::<T>::iter()
982				.drain()
983				.take(T::MaximumValidatorsWithPoints::get() as usize)
984				.collect::<Vec<_>>();
985
986			// If there were more validators than `MaximumValidatorsWithPoints`..
987			if ValidatorPoints::<T>::iter().next().is_some() {
988				// ..not much more we can do about it other than an event.
989				Self::deposit_event(Event::<T>::Unexpected(UnexpectedKind::ValidatorPointDropped))
990			}
991
992			let activation_timestamp = NextSessionChangesValidators::<T>::take().map(|id| {
993				// keep track of starting session index at which the validator set was applied.
994				ValidatorSetAppliedAt::<T>::put(end_index + 1);
995				// set the timestamp and the identifier of the validator set.
996				(T::UnixTime::now().as_millis().saturated_into::<u64>(), id)
997			});
998
999			let session_report = pallet_staking_async_rc_client::SessionReport {
1000				end_index,
1001				validator_points,
1002				activation_timestamp,
1003				leftover: false,
1004			};
1005
1006			// queue the session report to be sent.
1007			OutgoingSessionReport::<T>::put((session_report, T::MaxSessionReportRetries::get()));
1008		}
1009
1010		fn do_reward_by_ids(rewards: impl IntoIterator<Item = (T::AccountId, u32)>) {
1011			for (validator_id, points) in rewards {
1012				ValidatorPoints::<T>::mutate(validator_id, |balance| {
1013					balance.saturating_accrue(points);
1014				});
1015			}
1016		}
1017
1018		fn do_note_author(author: T::AccountId) {
1019			ValidatorPoints::<T>::mutate(author, |points| {
1020				points.saturating_accrue(T::PointsPerBlock::get());
1021			});
1022		}
1023
1024		/// Check if an offence is from the active validator set.
1025		fn is_ongoing_offence(slash_session: SessionIndex) -> bool {
1026			ValidatorSetAppliedAt::<T>::get()
1027				.map(|start_session| slash_session >= start_session)
1028				.unwrap_or(false)
1029		}
1030
1031		/// Handle offences in Buffered mode.
1032		fn on_offence_buffered(
1033			offenders: &[OffenceDetailsOf<T>],
1034			slash_fraction: &[Perbill],
1035			slash_session: SessionIndex,
1036		) -> Weight {
1037			let ongoing_offence = Self::is_ongoing_offence(slash_session);
1038
1039			offenders.iter().cloned().zip(slash_fraction).for_each(|(offence, fraction)| {
1040				if ongoing_offence {
1041					// report the offence to the session pallet.
1042					T::SessionInterface::report_offence(
1043						offence.offender.0.clone(),
1044						OffenceSeverity(*fraction),
1045					);
1046				}
1047
1048				let (offender, _full_identification) = offence.offender;
1049				let reporters = offence.reporters;
1050
1051				// In `Buffered` mode, we buffer the offences for later processing.
1052				OffenceSendQueue::<T>::append((
1053					slash_session,
1054					rc_client::Offence {
1055						offender: offender.clone(),
1056						reporters: reporters.into_iter().take(1).collect(),
1057						slash_fraction: *fraction,
1058					},
1059				));
1060			});
1061
1062			T::DbWeight::get().reads_writes(1, 1)
1063		}
1064
1065		/// Handle offences in Active mode.
1066		fn on_offence_active(
1067			offenders: &[OffenceDetailsOf<T>],
1068			slash_fraction: &[Perbill],
1069			slash_session: SessionIndex,
1070		) -> Weight {
1071			let ongoing_offence = Self::is_ongoing_offence(slash_session);
1072
1073			offenders.iter().cloned().zip(slash_fraction).for_each(|(offence, fraction)| {
1074				if ongoing_offence {
1075					// report the offence to the session pallet.
1076					T::SessionInterface::report_offence(
1077						offence.offender.0.clone(),
1078						OffenceSeverity(*fraction),
1079					);
1080				}
1081
1082				let (offender, _full_identification) = offence.offender;
1083				let reporters = offence.reporters;
1084
1085				// prepare an `Offence` instance for the XCM message. Note that we drop
1086				// the identification.
1087				let offence = rc_client::Offence {
1088					offender,
1089					reporters: reporters.into_iter().take(1).collect(),
1090					slash_fraction: *fraction,
1091				};
1092				OffenceSendQueue::<T>::append((slash_session, offence))
1093			});
1094
1095			T::DbWeight::get().reads_writes(2, 2)
1096		}
1097	}
1098}
1099
1100#[cfg(test)]
1101mod keys_from_ah_tests {
1102	use super::*;
1103	use crate::mock::*;
1104	use codec::Encode;
1105	use frame_support::{assert_noop, assert_ok, hypothetically};
1106	use sp_runtime::DispatchError;
1107
1108	#[test]
1109	fn set_keys_from_ah() {
1110		new_test_ext().execute_with(|| {
1111			System::set_block_number(1);
1112			let stash = 42u64;
1113			let keys = MockSessionKeys { dummy: [1u8; 32] };
1114
1115			// success with root origin
1116			hypothetically!({
1117				SetKeysCalls::take();
1118				assert_ok!(StakingAsyncAhClient::set_keys_from_ah(
1119					RuntimeOrigin::root(),
1120					stash,
1121					keys.encode(),
1122				));
1123				assert_eq!(SetKeysCalls::get(), vec![(stash, keys.clone())]);
1124				System::assert_has_event(
1125					Event::<Test>::SessionKeysUpdated { stash, update: SessionKeysUpdate::Set }
1126						.into(),
1127				);
1128			});
1129
1130			// rejects bad origin
1131			hypothetically!({
1132				SetKeysCalls::take();
1133				assert_noop!(
1134					StakingAsyncAhClient::set_keys_from_ah(
1135						RuntimeOrigin::signed(1),
1136						stash,
1137						keys.encode(),
1138					),
1139					DispatchError::BadOrigin
1140				);
1141				assert!(SetKeysCalls::get().is_empty());
1142			});
1143
1144			// emits SessionKeysUpdateFailed when SessionInterface::set_keys fails
1145			hypothetically!({
1146				SetKeysCalls::take();
1147				let error = DispatchError::Corruption;
1148				SetKeysError::set(Some(error));
1149				assert_ok!(StakingAsyncAhClient::set_keys_from_ah(
1150					RuntimeOrigin::root(),
1151					stash,
1152					keys.encode(),
1153				));
1154				assert!(SetKeysCalls::get().is_empty());
1155				System::assert_has_event(
1156					Event::<Test>::SessionKeysUpdateFailed {
1157						stash,
1158						update: SessionKeysUpdate::Set,
1159						error,
1160					}
1161					.into(),
1162				);
1163				SetKeysError::take();
1164			});
1165
1166			// handles invalid keys gracefully
1167			hypothetically!({
1168				SetKeysCalls::take();
1169				assert_ok!(StakingAsyncAhClient::set_keys_from_ah(
1170					RuntimeOrigin::root(),
1171					stash,
1172					vec![1u8, 2, 3], // invalid encoding
1173				));
1174				assert!(SetKeysCalls::get().is_empty());
1175				System::assert_has_event(
1176					Event::<Test>::Unexpected(UnexpectedKind::InvalidKeysFromAssetHub).into(),
1177				);
1178			});
1179		});
1180	}
1181
1182	#[test]
1183	fn purge_keys_from_ah() {
1184		new_test_ext().execute_with(|| {
1185			System::set_block_number(1);
1186			let stash = 42u64;
1187
1188			// success with root origin
1189			hypothetically!({
1190				PurgeKeysCalls::take();
1191				assert_ok!(StakingAsyncAhClient::purge_keys_from_ah(RuntimeOrigin::root(), stash));
1192				assert_eq!(PurgeKeysCalls::get(), vec![stash]);
1193				System::assert_has_event(
1194					Event::<Test>::SessionKeysUpdated { stash, update: SessionKeysUpdate::Purged }
1195						.into(),
1196				);
1197			});
1198
1199			// rejects bad origin
1200			hypothetically!({
1201				PurgeKeysCalls::take();
1202				assert_noop!(
1203					StakingAsyncAhClient::purge_keys_from_ah(RuntimeOrigin::signed(1), stash),
1204					DispatchError::BadOrigin
1205				);
1206				assert!(PurgeKeysCalls::get().is_empty());
1207			});
1208
1209			// emits SessionKeysUpdateFailed when SessionInterface::purge_keys fails
1210			hypothetically!({
1211				PurgeKeysCalls::take();
1212				let error = DispatchError::Corruption;
1213				PurgeKeysError::set(Some(error));
1214				assert_ok!(StakingAsyncAhClient::purge_keys_from_ah(RuntimeOrigin::root(), stash));
1215				assert!(PurgeKeysCalls::get().is_empty());
1216				System::assert_has_event(
1217					Event::<Test>::SessionKeysUpdateFailed {
1218						stash,
1219						update: SessionKeysUpdate::Purged,
1220						error,
1221					}
1222					.into(),
1223				);
1224				PurgeKeysError::take();
1225			});
1226		});
1227	}
1228}
1229
1230#[cfg(test)]
1231mod send_queue_tests {
1232	use frame_support::hypothetically;
1233	use sp_runtime::Perbill;
1234
1235	use super::*;
1236	use crate::mock::*;
1237
1238	// (cursor, len_of_pages)
1239	fn status() -> (u32, Vec<u32>) {
1240		let mut sorted = OffenceSendQueueOffences::<Test>::iter().collect::<Vec<_>>();
1241		sorted.sort_by(|x, y| x.0.cmp(&y.0));
1242		(
1243			OffenceSendQueueCursor::<Test>::get(),
1244			sorted.into_iter().map(|(_, v)| v.len() as u32).collect(),
1245		)
1246	}
1247
1248	#[test]
1249	fn append_and_take() {
1250		new_test_ext().execute_with(|| {
1251			let o = (
1252				42,
1253				rc_client::Offence {
1254					offender: 42,
1255					reporters: vec![],
1256					slash_fraction: Perbill::from_percent(10),
1257				},
1258			);
1259			let page_size = <Test as Config>::MaxOffenceBatchSize::get();
1260			assert_eq!(page_size % 2, 0, "page size should be even");
1261
1262			assert_eq!(status(), (0, vec![]));
1263
1264			// --- when empty
1265
1266			assert_eq!(OffenceSendQueue::<Test>::count(), 0);
1267			assert_eq!(OffenceSendQueue::<Test>::pages(), 0);
1268
1269			// get and keep
1270			hypothetically!({
1271				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1272					assert_eq!(page.len(), 0);
1273					Err(())
1274				});
1275				assert_eq!(status(), (0, vec![]));
1276			});
1277
1278			// get and delete
1279			hypothetically!({
1280				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1281					assert_eq!(page.len(), 0);
1282					Ok(())
1283				});
1284				assert_eq!(status(), (0, vec![]));
1285			});
1286
1287			// -------- when 1 page half filled
1288			for _ in 0..page_size / 2 {
1289				OffenceSendQueue::<Test>::append(o.clone());
1290			}
1291			assert_eq!(status(), (0, vec![page_size / 2]));
1292			assert_eq!(OffenceSendQueue::<Test>::count(), page_size / 2);
1293			assert_eq!(OffenceSendQueue::<Test>::pages(), 1);
1294
1295			// get and keep
1296			hypothetically!({
1297				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1298					assert_eq!(page.len() as u32, page_size / 2);
1299					Err(())
1300				});
1301				assert_eq!(status(), (0, vec![page_size / 2]));
1302			});
1303
1304			// get and delete
1305			hypothetically!({
1306				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1307					assert_eq!(page.len() as u32, page_size / 2);
1308					Ok(())
1309				});
1310				assert_eq!(status(), (0, vec![]));
1311				assert_eq!(OffenceSendQueue::<Test>::count(), 0);
1312				assert_eq!(OffenceSendQueue::<Test>::pages(), 0);
1313			});
1314
1315			// -------- when 1 page full
1316			for _ in 0..page_size / 2 {
1317				OffenceSendQueue::<Test>::append(o.clone());
1318			}
1319			assert_eq!(status(), (0, vec![page_size]));
1320			assert_eq!(OffenceSendQueue::<Test>::count(), page_size);
1321			assert_eq!(OffenceSendQueue::<Test>::pages(), 1);
1322
1323			// get and keep
1324			hypothetically!({
1325				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1326					assert_eq!(page.len() as u32, page_size);
1327					Err(())
1328				});
1329				assert_eq!(status(), (0, vec![page_size]));
1330			});
1331
1332			// get and delete
1333			hypothetically!({
1334				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1335					assert_eq!(page.len() as u32, page_size);
1336					Ok(())
1337				});
1338				assert_eq!(status(), (0, vec![]));
1339			});
1340
1341			// -------- when more than 1 page full
1342			OffenceSendQueue::<Test>::append(o.clone());
1343			assert_eq!(status(), (1, vec![page_size, 1]));
1344			assert_eq!(OffenceSendQueue::<Test>::count(), page_size + 1);
1345			assert_eq!(OffenceSendQueue::<Test>::pages(), 2);
1346
1347			// get and keep
1348			hypothetically!({
1349				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1350					assert_eq!(page.len(), 1);
1351					Err(())
1352				});
1353				assert_eq!(status(), (1, vec![page_size, 1]));
1354			});
1355
1356			// get and delete
1357			hypothetically!({
1358				OffenceSendQueue::<Test>::get_and_maybe_delete(|page| {
1359					assert_eq!(page.len(), 1);
1360					Ok(())
1361				});
1362				assert_eq!(status(), (0, vec![page_size]));
1363			});
1364		})
1365	}
1366}