// This file is part of Substrate. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! The client for AssetHub, intended to be used in the relay chain. //! //! The counter-part for this pallet is `pallet-staking-async-rc-client` on AssetHub. //! //! This documentation is divided into the following sections: //! //! 1. Incoming messages: the messages that we receive from the relay chian. //! 2. Outgoing messages: the messaged that we sent to the relay chain. //! 3. Local interfaces: the interfaces that we expose to other pallets in the runtime. //! //! ## Incoming Messages //! //! All incoming messages are handled via [`Call`]. They are all gated to be dispatched only by //! [`Config::AssetHubOrigin`]. The only one is: //! //! * [`Call::validator_set`]: A new validator set for a planning session index. //! //! ## Outgoing Messages //! //! All outgoing messages are handled by a single trait //! [`pallet_staking_async_rc_client::SendToAssetHub`]. They match the incoming messages of the //! `rc-client` pallet. //! //! ## Local Interfaces: //! //! Living on the relay chain, this pallet must: //! //! * Implement [`pallet_session::SessionManager`] (and historical variant thereof) to _give_ //! information to the session pallet. //! * Implements [`SessionInterface`] to _receive_ information from the session pallet //! * Implement [`sp_staking::offence::OnOffenceHandler`]. //! * Implement reward related APIs ([`frame_support::traits::RewardsReporter`]). //! //! ## Future Plans //! //! * Governance functions to force set validators. #![cfg_attr(not(feature = "std"), no_std)] pub use pallet::*; #[cfg(test)] pub mod mock; extern crate alloc; use alloc::vec::Vec; use frame_support::{ pallet_prelude::*, traits::{Defensive, DefensiveSaturating, RewardsReporter}, }; pub use pallet_staking_async_rc_client::SendToAssetHub; use pallet_staking_async_rc_client::{self as rc_client}; use sp_runtime::SaturatedConversion; use sp_staking::{ offence::{OffenceDetails, OffenceSeverity}, SessionIndex, }; /// The balance type seen from this pallet's PoV. pub type BalanceOf = ::CurrencyBalance; /// Type alias for offence details pub type OffenceDetailsOf = OffenceDetails< ::AccountId, ( ::AccountId, sp_staking::Exposure<::AccountId, BalanceOf>, ), >; const LOG_TARGET: &str = "runtime::staking-async::ah-client"; // syntactic sugar for logging. #[macro_export] macro_rules! log { ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => { log::$level!( target: $crate::LOG_TARGET, concat!("[{:?}] ⬇️ ", $patter), >::block_number() $(, $values)* ) }; } /// Interface to talk to the local session pallet. pub trait SessionInterface { /// The validator id type of the session pallet type ValidatorId: Clone; fn validators() -> Vec; /// prune up to the given session index. fn prune_up_to(index: SessionIndex); /// Report an offence. /// /// This is used to disable validators directly on the RC, until the next validator set. fn report_offence(offender: Self::ValidatorId, severity: OffenceSeverity); } impl SessionInterface for T { type ValidatorId = ::ValidatorId; fn validators() -> Vec { pallet_session::Pallet::::validators() } fn prune_up_to(index: SessionIndex) { pallet_session::historical::Pallet::::prune_up_to(index) } fn report_offence(offender: Self::ValidatorId, severity: OffenceSeverity) { pallet_session::Pallet::::report_offence(offender, severity) } } /// Represents the operating mode of the pallet. #[derive( Default, DecodeWithMemTracking, Encode, Decode, MaxEncodedLen, TypeInfo, Clone, PartialEq, Eq, RuntimeDebug, serde::Serialize, serde::Deserialize, )] pub enum OperatingMode { /// Fully delegated mode. /// /// In this mode, the pallet performs no core logic and forwards all relevant operations /// to the fallback implementation defined in the pallet's `Config::Fallback`. /// /// This mode is useful when staking is in synchronous mode and waiting for the signal to /// transition to asynchronous mode. #[default] Passive, /// Buffered mode for deferred execution. /// /// In this mode, offences are accepted and buffered for later transmission to AssetHub. /// However, session change reports are dropped. /// /// This mode is useful when the counterpart pallet `pallet-staking-async-rc-client` on /// AssetHub is not yet ready to process incoming messages. Buffered, /// Fully active mode. /// /// The pallet performs all core logic directly and handles messages immediately. /// /// This mode is useful when staking is ready to execute in asynchronous mode and the /// counterpart pallet `pallet-staking-async-rc-client` is ready to accept messages. Active, } impl OperatingMode { fn can_accept_validator_set(&self) -> bool { matches!(self, OperatingMode::Active) } } /// See `pallet_staking::DefaultExposureOf`. This type is the same, except it is duplicated here so /// that an rc-runtime can use it after `pallet-staking` is fully removed as a dependency. pub struct DefaultExposureOf(core::marker::PhantomData); impl sp_runtime::traits::Convert< T::AccountId, Option>>, > for DefaultExposureOf { fn convert( validator: T::AccountId, ) -> Option>> { T::SessionInterface::validators() .contains(&validator) .then_some(Default::default()) } } #[frame_support::pallet] pub mod pallet { use crate::*; use alloc::vec; use frame_support::traits::{Hooks, UnixTime}; use frame_system::pallet_prelude::*; use pallet_session::{historical, SessionManager}; use pallet_staking_async_rc_client::SessionReport; use sp_runtime::{Perbill, Saturating}; use sp_staking::{ offence::{OffenceSeverity, OnOffenceHandler}, SessionIndex, }; const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); #[pallet::config] pub trait Config: frame_system::Config { /// The balance type of the runtime's currency interface. type CurrencyBalance: sp_runtime::traits::AtLeast32BitUnsigned + codec::FullCodec + DecodeWithMemTracking + codec::HasCompact + Copy + MaybeSerializeDeserialize + core::fmt::Debug + Default + From + TypeInfo + Send + Sync + MaxEncodedLen; /// An origin type that ensures an incoming message is from asset hub. type AssetHubOrigin: EnsureOrigin; /// The origin that can control this pallet's operations. type AdminOrigin: EnsureOrigin; /// Our communication interface to AssetHub. type SendToAssetHub: SendToAssetHub; /// A safety measure that asserts an incoming validator set must be at least this large. type MinimumValidatorSetSize: Get; /// A safety measure that asserts when iterating over validator points (to be sent to AH), /// we don't iterate too many times. /// /// Validator may change session to session, and if session reports are not sent, validator /// points that we store may well grow beyond the size of the validator set. Yet, a too /// large of an upper bound may also exceed the maximum size of a single DMP message. /// Consult the test `message_queue_sizes` for more information. /// /// Note that in case a single session report is larger than a single DMP message, it might /// still be sent over if we use /// [`pallet_staking_async_rc_client::XCMSender::split_then_send`]. This will make the size /// of each individual message smaller, yet, it will still try and push them all to the /// queue at the same time. type MaximumValidatorsWithPoints: Get; /// A type that gives us a reliable unix timestamp. type UnixTime: UnixTime; /// Number of points to award a validator per block authored. type PointsPerBlock: Get; /// Maximum number of offences to batch in a single message to AssetHub. Actual sending /// happens `on_initialize`. Offences get infinite "retries", and are never dropped. /// /// A sensible value should be such that sending this batch is small enough to not exhaust /// the DMP queue. The size of a single offence is documented in `message_queue_sizes` test /// (74 bytes). type MaxOffenceBatchSize: Get; /// Interface to talk to the local Session pallet. type SessionInterface: SessionInterface; /// A fallback implementation to delegate logic to when the pallet is in /// [`OperatingMode::Passive`]. /// /// This type must implement the `historical::SessionManager` and `OnOffenceHandler` /// interface and is expected to behave as a stand-in for this pallet’s core logic when /// delegation is active. type Fallback: pallet_session::SessionManager + OnOffenceHandler< Self::AccountId, (Self::AccountId, sp_staking::Exposure>), Weight, > + frame_support::traits::RewardsReporter + pallet_authorship::EventHandler>; /// Maximum number of times we try to send a session report to AssetHub, after which, if /// sending still fails, we drop it. type MaxSessionReportRetries: Get; } #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); /// The queued validator sets for a given planning session index. /// /// This is received via a call from AssetHub. #[pallet::storage] #[pallet::unbounded] pub type ValidatorSet = StorageValue<_, (u32, Vec), OptionQuery>; /// An incomplete validator set report. #[pallet::storage] #[pallet::unbounded] pub type IncompleteValidatorSetReport = StorageValue<_, rc_client::ValidatorSetReport, OptionQuery>; /// All of the points of the validators. /// /// This is populated during a session, and is flushed and sent over via [`SendToAssetHub`] /// at each session end. #[pallet::storage] pub type ValidatorPoints = StorageMap<_, Twox64Concat, T::AccountId, u32, ValueQuery>; /// Indicates the current operating mode of the pallet. /// /// This value determines how the pallet behaves in response to incoming and outgoing messages, /// particularly whether it should execute logic directly, defer it, or delegate it entirely. #[pallet::storage] pub type Mode = StorageValue<_, OperatingMode, ValueQuery>; /// A storage value that is set when a `new_session` gives a new validator set to the session /// pallet, and is cleared on the next call. /// /// The inner u32 is the id of the said activated validator set. While not relevant here, good /// to know this is the planning era index of staking-async on AH. /// /// Once cleared, we know a validator set has been activated, and therefore we can send a /// timestamp to AH. #[pallet::storage] pub type NextSessionChangesValidators = StorageValue<_, u32, OptionQuery>; /// The session index at which the latest elected validator set was applied. /// /// This is used to determine if an offence, given a session index, is in the current active era /// or not. #[pallet::storage] pub type ValidatorSetAppliedAt = StorageValue<_, SessionIndex, OptionQuery>; /// A session report that is outgoing, and should be sent. /// /// This will be attempted to be sent, possibly on every `on_initialize` call, until it is sent, /// or the second value reaches zero, at which point we drop it. #[pallet::storage] #[pallet::unbounded] pub type OutgoingSessionReport = StorageValue<_, (SessionReport, u32), OptionQuery>; /// Wrapper struct for storing offences, and getting them back page by page. /// /// It has only two interfaces: /// /// * [`OffenceSendQueue::append`], to add a single offence. /// * [`OffenceSendQueue::get_and_maybe_delete`] which retrieves the last page. Depending on the /// closure, it may also delete that page. The returned value is indeed /// [`Config::MaxOffenceBatchSize`] or less items. /// /// Internally, it manages `OffenceSendQueueOffences` and `OffenceSendQueueCursor`, both of /// which should NEVER be used manually. pub struct OffenceSendQueue(core::marker::PhantomData); /// A single buffered offence in [`OffenceSendQueue`]. pub type QueuedOffenceOf = (SessionIndex, rc_client::Offence<::AccountId>); /// A page of buffered offences in [`OffenceSendQueue`]. pub type QueuedOffencePageOf = BoundedVec, ::MaxOffenceBatchSize>; impl OffenceSendQueue { /// Add a single offence to the queue. pub fn append(o: QueuedOffenceOf) { let mut index = OffenceSendQueueCursor::::get(); match OffenceSendQueueOffences::::try_mutate(index, |b| b.try_push(o.clone())) { Ok(_) => { // `index` had empty slot -- all good. }, Err(_) => { debug_assert!( !OffenceSendQueueOffences::::contains_key(index + 1), "next page should be empty" ); index += 1; OffenceSendQueueOffences::::insert( index, BoundedVec::<_, _>::try_from(vec![o]).defensive_unwrap_or_default(), ); OffenceSendQueueCursor::::mutate(|i| *i += 1); }, } } // Get the last page of offences, and delete it if `op` returns `Ok(())`. pub fn get_and_maybe_delete(op: impl FnOnce(QueuedOffencePageOf) -> Result<(), ()>) { let index = OffenceSendQueueCursor::::get(); let page = OffenceSendQueueOffences::::get(index); let res = op(page); match res { Ok(_) => { OffenceSendQueueOffences::::remove(index); OffenceSendQueueCursor::::mutate(|i| *i = i.saturating_sub(1)) }, Err(_) => { // nada }, } } #[cfg(feature = "std")] pub fn pages() -> u32 { let last_page = if Self::last_page_empty() { 0 } else { 1 }; OffenceSendQueueCursor::::get().saturating_add(last_page) } #[cfg(feature = "std")] pub fn count() -> u32 { let last_index = OffenceSendQueueCursor::::get(); let last_page = OffenceSendQueueOffences::::get(last_index); let last_page_count = last_page.len() as u32; last_index.saturating_mul(T::MaxOffenceBatchSize::get()) + last_page_count } #[cfg(feature = "std")] fn last_page_empty() -> bool { OffenceSendQueueOffences::::get(OffenceSendQueueCursor::::get()).is_empty() } } /// Internal storage item of [`OffenceSendQueue`]. Should not be used manually. #[pallet::storage] #[pallet::unbounded] pub(crate) type OffenceSendQueueOffences = StorageMap<_, Twox64Concat, u32, QueuedOffencePageOf, ValueQuery>; /// Internal storage item of [`OffenceSendQueue`]. Should not be used manually. #[pallet::storage] pub(crate) type OffenceSendQueueCursor = StorageValue<_, u32, ValueQuery>; #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound, frame_support::DebugNoBound)] pub struct GenesisConfig { /// The initial operating mode of the pallet. pub operating_mode: OperatingMode, pub _marker: core::marker::PhantomData, } #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { // Set the initial operating mode of the pallet. Mode::::put(self.operating_mode.clone()); } } #[pallet::error] pub enum Error { /// Could not process incoming message because incoming messages are blocked. Blocked, } #[pallet::event] #[pallet::generate_deposit(fn deposit_event)] pub enum Event { /// A new validator set has been received. ValidatorSetReceived { id: u32, new_validator_set_count: u32, prune_up_to: Option, leftover: bool, }, /// We could not merge, and therefore dropped a buffered message. /// /// Note that this event is more resembling an error, but we use an event because in this /// pallet we need to mutate storage upon some failures. CouldNotMergeAndDropped, /// The validator set received is way too small, as per /// [`Config::MinimumValidatorSetSize`]. SetTooSmallAndDropped, /// Something occurred that should never happen under normal operation. Logged as an event /// for fail-safe observability. Unexpected(UnexpectedKind), } /// Represents unexpected or invariant-breaking conditions encountered during execution. /// /// These variants are emitted as [`Event::Unexpected`] and indicate a defensive check has /// failed. While these should never occur under normal operation, they are useful for /// diagnosing issues in production or test environments. #[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, RuntimeDebug)] pub enum UnexpectedKind { /// A validator set was received while the pallet is in [`OperatingMode::Passive`]. ReceivedValidatorSetWhilePassive, /// An unexpected transition was applied between operating modes. /// /// Expected transitions are linear and forward-only: `Passive` → `Buffered` → `Active`. UnexpectedModeTransition, /// A session report failed to be sent. /// /// We will store, and retry it for a number of more block. SessionReportSendFailed, /// A session report failed enough times that we should drop it. /// /// We will retain the validator points, and send them over in the next session we receive /// from pallet-session. SessionReportDropped, /// An offence report failed to be sent. /// /// It will be retried again in the next block. We never drop them. OffenceSendFailed, /// Some validator points didn't make it to be included in the session report. Should /// never happen, and means: /// /// * a too low of a value is assigned to [`Config::MaximumValidatorsWithPoints`] /// * Those who are calling into our `RewardsReporter` likely have a bad view of the /// validator set, and are spamming us. ValidatorPointDropped, } #[pallet::call] impl Pallet { #[pallet::call_index(0)] #[pallet::weight( // Reads: // - OperatingMode // - IncompleteValidatorSetReport // Writes: // - IncompleteValidatorSetReport or ValidatorSet // ignoring `T::SessionInterface::prune_up_to` T::DbWeight::get().reads_writes(2, 1) )] pub fn validator_set( origin: OriginFor, report: rc_client::ValidatorSetReport, ) -> DispatchResult { // Ensure the origin is one of Root or whatever is representing AssetHub. log!(debug, "Received new validator set report {}", report); T::AssetHubOrigin::ensure_origin_or_root(origin)?; // Check the operating mode. let mode = Mode::::get(); ensure!(mode.can_accept_validator_set(), Error::::Blocked); let maybe_merged_report = match IncompleteValidatorSetReport::::take() { Some(old) => old.merge(report.clone()), None => Ok(report), }; if maybe_merged_report.is_err() { Self::deposit_event(Event::CouldNotMergeAndDropped); debug_assert!( IncompleteValidatorSetReport::::get().is_none(), "we have ::take() it above, we don't want to keep the old data" ); return Ok(()); } let report = maybe_merged_report.expect("checked above; qed"); if report.leftover { // buffer it, and nothing further to do. Self::deposit_event(Event::ValidatorSetReceived { id: report.id, new_validator_set_count: report.new_validator_set.len() as u32, prune_up_to: report.prune_up_to, leftover: report.leftover, }); IncompleteValidatorSetReport::::put(report); } else { // message is complete, process it. let rc_client::ValidatorSetReport { id, leftover, mut new_validator_set, prune_up_to, } = report; // ensure the validator set, deduplicated, is not too big. new_validator_set.sort(); new_validator_set.dedup(); if (new_validator_set.len() as u32) < T::MinimumValidatorSetSize::get() { Self::deposit_event(Event::SetTooSmallAndDropped); debug_assert!( IncompleteValidatorSetReport::::get().is_none(), "we have ::take() it above, we don't want to keep the old data" ); return Ok(()); } Self::deposit_event(Event::ValidatorSetReceived { id, new_validator_set_count: new_validator_set.len() as u32, prune_up_to, leftover, }); // Save the validator set. ValidatorSet::::put((id, new_validator_set)); if let Some(index) = prune_up_to { T::SessionInterface::prune_up_to(index); } } Ok(()) } /// Allows governance to force set the operating mode of the pallet. #[pallet::call_index(1)] #[pallet::weight(T::DbWeight::get().writes(1))] pub fn set_mode(origin: OriginFor, mode: OperatingMode) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; Self::do_set_mode(mode); Ok(()) } /// manually do what this pallet was meant to do at the end of the migration. #[pallet::call_index(2)] #[pallet::weight(T::DbWeight::get().writes(1))] pub fn force_on_migration_end(origin: OriginFor) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; Self::on_migration_end(); Ok(()) } } #[pallet::hooks] impl Hooks> for Pallet { fn on_initialize(_n: BlockNumberFor) -> Weight { let mut weight = Weight::zero(); let mode = Mode::::get(); weight = weight.saturating_add(T::DbWeight::get().reads(1)); if mode != OperatingMode::Active { return weight; } // if we have any pending session reports, send it. weight.saturating_accrue(T::DbWeight::get().reads(1)); if let Some((session_report, retries_left)) = OutgoingSessionReport::::take() { match T::SendToAssetHub::relay_session_report(session_report.clone()) { Ok(()) => { // report was sent, all good, it is already deleted. }, Err(()) => { log!(error, "Failed to send session report to assethub"); Self::deposit_event(Event::::Unexpected( UnexpectedKind::SessionReportSendFailed, )); if let Some(new_retries_left) = retries_left.checked_sub(One::one()) { OutgoingSessionReport::::put((session_report, new_retries_left)) } else { // recreate the validator points, so they will be sent in the next // report. session_report.validator_points.into_iter().for_each(|(v, p)| { ValidatorPoints::::mutate(v, |existing_points| { *existing_points = existing_points.defensive_saturating_add(p) }); }); Self::deposit_event(Event::::Unexpected( UnexpectedKind::SessionReportDropped, )); } }, } } // then, take a page from our send queue, and if present, send it. weight.saturating_accrue(T::DbWeight::get().reads(2)); OffenceSendQueue::::get_and_maybe_delete(|page| { if page.is_empty() { return Ok(()); } // send the page if not empty. If sending returns `Ok`, we delete this page. T::SendToAssetHub::relay_new_offence_paged(page.into_inner()).inspect_err(|_| { Self::deposit_event(Event::Unexpected(UnexpectedKind::OffenceSendFailed)); }) }); weight } fn integrity_test() { assert!(T::MaxOffenceBatchSize::get() > 0, "Offence Batch size must be at least 1"); } } impl historical::SessionManager>> for Pallet { fn new_session( new_index: sp_staking::SessionIndex, ) -> Option< Vec<( ::AccountId, sp_staking::Exposure>, )>, > { >::new_session(new_index) .map(|v| v.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect()) } fn new_session_genesis( new_index: SessionIndex, ) -> Option>)>> { if Mode::::get() == OperatingMode::Passive { T::Fallback::new_session_genesis(new_index).map(|validators| { validators.into_iter().map(|v| (v, sp_staking::Exposure::default())).collect() }) } else { None } } fn start_session(start_index: SessionIndex) { >::start_session(start_index) } fn end_session(end_index: SessionIndex) { >::end_session(end_index) } } impl pallet_session::SessionManager for Pallet { fn new_session(session_index: u32) -> Option> { match Mode::::get() { OperatingMode::Passive => T::Fallback::new_session(session_index), // In `Buffered` mode, we drop the session report and do nothing. OperatingMode::Buffered => None, OperatingMode::Active => Self::do_new_session(), } } fn start_session(session_index: u32) { if Mode::::get() == OperatingMode::Passive { T::Fallback::start_session(session_index) } } fn new_session_genesis(new_index: SessionIndex) -> Option> { if Mode::::get() == OperatingMode::Passive { T::Fallback::new_session_genesis(new_index) } else { None } } fn end_session(session_index: u32) { match Mode::::get() { OperatingMode::Passive => T::Fallback::end_session(session_index), // In `Buffered` mode, we drop the session report and do nothing. OperatingMode::Buffered => (), OperatingMode::Active => Self::do_end_session(session_index), } } } impl OnOffenceHandler< T::AccountId, (T::AccountId, sp_staking::Exposure>), Weight, > for Pallet { fn on_offence( offenders: &[OffenceDetails< T::AccountId, (T::AccountId, sp_staking::Exposure>), >], slash_fraction: &[Perbill], slash_session: SessionIndex, ) -> Weight { match Mode::::get() { OperatingMode::Passive => { // delegate to the fallback implementation. T::Fallback::on_offence(offenders, slash_fraction, slash_session) }, OperatingMode::Buffered => Self::on_offence_buffered(offenders, slash_fraction, slash_session), OperatingMode::Active => Self::on_offence_active(offenders, slash_fraction, slash_session), } } } impl RewardsReporter for Pallet { fn reward_by_ids(rewards: impl IntoIterator) { match Mode::::get() { OperatingMode::Passive => T::Fallback::reward_by_ids(rewards), OperatingMode::Buffered | OperatingMode::Active => Self::do_reward_by_ids(rewards), } } } impl pallet_authorship::EventHandler> for Pallet { fn note_author(author: T::AccountId) { match Mode::::get() { OperatingMode::Passive => T::Fallback::note_author(author), OperatingMode::Buffered | OperatingMode::Active => Self::do_note_author(author), } } } impl Pallet { /// Hook to be called when the AssetHub migration begins. /// /// This transitions the pallet into [`OperatingMode::Buffered`], meaning it will act as the /// primary staking module on the relay chain but will buffer outgoing messages instead of /// sending them to AssetHub. /// /// While in this mode, the pallet stops delegating to the fallback implementation and /// temporarily accumulates events for later processing. pub fn on_migration_start() { debug_assert!( Mode::::get() == OperatingMode::Passive, "we should only be called when in passive mode" ); Self::do_set_mode(OperatingMode::Buffered); } /// Hook to be called when the AssetHub migration is complete. /// /// This transitions the pallet into [`OperatingMode::Active`], meaning the counterpart /// pallet on AssetHub is ready to accept incoming messages, and this pallet can resume /// sending them. /// /// In this mode, the pallet becomes fully active and processes all staking-related events /// directly. pub fn on_migration_end() { debug_assert!( Mode::::get() == OperatingMode::Buffered, "we should only be called when in buffered mode" ); Self::do_set_mode(OperatingMode::Active); // Buffered offences will be processed gradually by on_initialize // using MaxOffenceBatchSize to prevent block overload. } fn do_set_mode(new_mode: OperatingMode) { let old_mode = Mode::::get(); let unexpected = match new_mode { // `Passive` is the initial state, and not expected to be set by the user. OperatingMode::Passive => true, OperatingMode::Buffered => old_mode != OperatingMode::Passive, OperatingMode::Active => old_mode != OperatingMode::Buffered, }; // this is a defensive check, and should never happen under normal operation. if unexpected { log!(warn, "Unexpected mode transition from {:?} to {:?}", old_mode, new_mode); Self::deposit_event(Event::Unexpected(UnexpectedKind::UnexpectedModeTransition)); } // apply new mode anyway. Mode::::put(new_mode); } fn do_new_session() -> Option> { ValidatorSet::::take().map(|(id, val_set)| { // store the id to be sent back in the next session back to AH NextSessionChangesValidators::::put(id); val_set }) } fn do_end_session(end_index: u32) { // take and delete all validator points, limited by `MaximumValidatorsWithPoints`. let validator_points = ValidatorPoints::::iter() .drain() .take(T::MaximumValidatorsWithPoints::get() as usize) .collect::>(); // If there were more validators than `MaximumValidatorsWithPoints`.. if ValidatorPoints::::iter().next().is_some() { // ..not much more we can do about it other than an event. Self::deposit_event(Event::::Unexpected(UnexpectedKind::ValidatorPointDropped)) } let activation_timestamp = NextSessionChangesValidators::::take().map(|id| { // keep track of starting session index at which the validator set was applied. ValidatorSetAppliedAt::::put(end_index + 1); // set the timestamp and the identifier of the validator set. (T::UnixTime::now().as_millis().saturated_into::(), id) }); let session_report = pallet_staking_async_rc_client::SessionReport { end_index, validator_points, activation_timestamp, leftover: false, }; // queue the session report to be sent. OutgoingSessionReport::::put((session_report, T::MaxSessionReportRetries::get())); } fn do_reward_by_ids(rewards: impl IntoIterator) { for (validator_id, points) in rewards { ValidatorPoints::::mutate(validator_id, |balance| { balance.saturating_accrue(points); }); } } fn do_note_author(author: T::AccountId) { ValidatorPoints::::mutate(author, |points| { points.saturating_accrue(T::PointsPerBlock::get()); }); } /// Check if an offence is from the active validator set. fn is_ongoing_offence(slash_session: SessionIndex) -> bool { ValidatorSetAppliedAt::::get() .map(|start_session| slash_session >= start_session) .unwrap_or(false) } /// Handle offences in Buffered mode. fn on_offence_buffered( offenders: &[OffenceDetailsOf], slash_fraction: &[Perbill], slash_session: SessionIndex, ) -> Weight { let ongoing_offence = Self::is_ongoing_offence(slash_session); offenders.iter().cloned().zip(slash_fraction).for_each(|(offence, fraction)| { if ongoing_offence { // report the offence to the session pallet. T::SessionInterface::report_offence( offence.offender.0.clone(), OffenceSeverity(*fraction), ); } let (offender, _full_identification) = offence.offender; let reporters = offence.reporters; // In `Buffered` mode, we buffer the offences for later processing. OffenceSendQueue::::append(( slash_session, rc_client::Offence { offender: offender.clone(), reporters: reporters.into_iter().take(1).collect(), slash_fraction: *fraction, }, )); }); T::DbWeight::get().reads_writes(1, 1) } /// Handle offences in Active mode. fn on_offence_active( offenders: &[OffenceDetailsOf], slash_fraction: &[Perbill], slash_session: SessionIndex, ) -> Weight { let ongoing_offence = Self::is_ongoing_offence(slash_session); offenders.iter().cloned().zip(slash_fraction).for_each(|(offence, fraction)| { if ongoing_offence { // report the offence to the session pallet. T::SessionInterface::report_offence( offence.offender.0.clone(), OffenceSeverity(*fraction), ); } let (offender, _full_identification) = offence.offender; let reporters = offence.reporters; // prepare an `Offence` instance for the XCM message. Note that we drop // the identification. let offence = rc_client::Offence { offender, reporters: reporters.into_iter().take(1).collect(), slash_fraction: *fraction, }; OffenceSendQueue::::append((slash_session, offence)) }); T::DbWeight::get().reads_writes(2, 2) } } } #[cfg(test)] mod send_queue_tests { use frame_support::hypothetically; use sp_runtime::Perbill; use super::*; use crate::mock::*; // (cursor, len_of_pages) fn status() -> (u32, Vec) { let mut sorted = OffenceSendQueueOffences::::iter().collect::>(); sorted.sort_by(|x, y| x.0.cmp(&y.0)); ( OffenceSendQueueCursor::::get(), sorted.into_iter().map(|(_, v)| v.len() as u32).collect(), ) } #[test] fn append_and_take() { new_test_ext().execute_with(|| { let o = ( 42, rc_client::Offence { offender: 42, reporters: vec![], slash_fraction: Perbill::from_percent(10), }, ); let page_size = ::MaxOffenceBatchSize::get(); assert_eq!(page_size % 2, 0, "page size should be even"); assert_eq!(status(), (0, vec![])); // --- when empty assert_eq!(OffenceSendQueue::::count(), 0); assert_eq!(OffenceSendQueue::::pages(), 0); // get and keep hypothetically!({ OffenceSendQueue::::get_and_maybe_delete(|page| { assert_eq!(page.len(), 0); Err(()) }); assert_eq!(status(), (0, vec![])); }); // get and delete hypothetically!({ OffenceSendQueue::::get_and_maybe_delete(|page| { assert_eq!(page.len(), 0); Ok(()) }); assert_eq!(status(), (0, vec![])); }); // -------- when 1 page half filled for _ in 0..page_size / 2 { OffenceSendQueue::::append(o.clone()); } assert_eq!(status(), (0, vec![page_size / 2])); assert_eq!(OffenceSendQueue::::count(), page_size / 2); assert_eq!(OffenceSendQueue::::pages(), 1); // get and keep hypothetically!({ OffenceSendQueue::::get_and_maybe_delete(|page| { assert_eq!(page.len() as u32, page_size / 2); Err(()) }); assert_eq!(status(), (0, vec![page_size / 2])); }); // get and delete hypothetically!({ OffenceSendQueue::::get_and_maybe_delete(|page| { assert_eq!(page.len() as u32, page_size / 2); Ok(()) }); assert_eq!(status(), (0, vec![])); assert_eq!(OffenceSendQueue::::count(), 0); assert_eq!(OffenceSendQueue::::pages(), 0); }); // -------- when 1 page full for _ in 0..page_size / 2 { OffenceSendQueue::::append(o.clone()); } assert_eq!(status(), (0, vec![page_size])); assert_eq!(OffenceSendQueue::::count(), page_size); assert_eq!(OffenceSendQueue::::pages(), 1); // get and keep hypothetically!({ OffenceSendQueue::::get_and_maybe_delete(|page| { assert_eq!(page.len() as u32, page_size); Err(()) }); assert_eq!(status(), (0, vec![page_size])); }); // get and delete hypothetically!({ OffenceSendQueue::::get_and_maybe_delete(|page| { assert_eq!(page.len() as u32, page_size); Ok(()) }); assert_eq!(status(), (0, vec![])); }); // -------- when more than 1 page full OffenceSendQueue::::append(o.clone()); assert_eq!(status(), (1, vec![page_size, 1])); assert_eq!(OffenceSendQueue::::count(), page_size + 1); assert_eq!(OffenceSendQueue::::pages(), 2); // get and keep hypothetically!({ OffenceSendQueue::::get_and_maybe_delete(|page| { assert_eq!(page.len(), 1); Err(()) }); assert_eq!(status(), (1, vec![page_size, 1])); }); // get and delete hypothetically!({ OffenceSendQueue::::get_and_maybe_delete(|page| { assert_eq!(page.len(), 1); Ok(()) }); assert_eq!(status(), (0, vec![page_size])); }); }) } }