1#![cfg_attr(not(feature = "std"), no_std)]
117
118extern crate alloc;
119use alloc::{vec, vec::Vec};
120use core::fmt::Display;
121use frame_support::{pallet_prelude::*, storage::transactional::with_transaction_opaque_err};
122use sp_runtime::{traits::Convert, Perbill, TransactionOutcome};
123use sp_staking::SessionIndex;
124use xcm::latest::{send_xcm, Location, SendError, SendXcm, Xcm};
125
126pub use pallet::*;
128
129const LOG_TARGET: &str = "runtime::staking-async::rc-client";
130
131#[macro_export]
133macro_rules! log {
134	($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
135		log::$level!(
136			target: $crate::LOG_TARGET,
137			concat!("[{:?}] โฌ๏ธ ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
138		)
139	};
140}
141
142pub trait SendToRelayChain {
150	type AccountId;
152
153	#[allow(clippy::result_unit_err)]
155	fn validator_set(report: ValidatorSetReport<Self::AccountId>) -> Result<(), ()>;
156}
157
158#[cfg(feature = "std")]
159impl SendToRelayChain for () {
160	type AccountId = u64;
161	fn validator_set(_report: ValidatorSetReport<Self::AccountId>) -> Result<(), ()> {
162		unimplemented!();
163	}
164}
165
166pub trait SendToAssetHub {
174	type AccountId;
176
177	#[allow(clippy::result_unit_err)]
181	fn relay_session_report(session_report: SessionReport<Self::AccountId>) -> Result<(), ()>;
182
183	#[allow(clippy::result_unit_err)]
184	fn relay_new_offence_paged(
185		offences: Vec<(SessionIndex, Offence<Self::AccountId>)>,
186	) -> Result<(), ()>;
187}
188
189#[cfg(feature = "std")]
191impl SendToAssetHub for () {
192	type AccountId = u64;
193
194	fn relay_session_report(_session_report: SessionReport<Self::AccountId>) -> Result<(), ()> {
195		unimplemented!();
196	}
197
198	fn relay_new_offence_paged(
199		_offences: Vec<(SessionIndex, Offence<Self::AccountId>)>,
200	) -> Result<(), ()> {
201		unimplemented!()
202	}
203}
204
205#[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, TypeInfo)]
206pub struct ValidatorSetReport<AccountId> {
208	pub new_validator_set: Vec<AccountId>,
210	pub id: u32,
218	pub prune_up_to: Option<SessionIndex>,
223	pub leftover: bool,
225}
226
227impl<AccountId: core::fmt::Debug> core::fmt::Debug for ValidatorSetReport<AccountId> {
228	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
229		f.debug_struct("ValidatorSetReport")
230			.field("new_validator_set", &self.new_validator_set)
231			.field("id", &self.id)
232			.field("prune_up_to", &self.prune_up_to)
233			.field("leftover", &self.leftover)
234			.finish()
235	}
236}
237
238impl<AccountId> core::fmt::Display for ValidatorSetReport<AccountId> {
239	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
240		f.debug_struct("ValidatorSetReport")
241			.field("new_validator_set", &self.new_validator_set.len())
242			.field("id", &self.id)
243			.field("prune_up_to", &self.prune_up_to)
244			.field("leftover", &self.leftover)
245			.finish()
246	}
247}
248
249impl<AccountId> ValidatorSetReport<AccountId> {
250	pub fn new_terminal(
253		new_validator_set: Vec<AccountId>,
254		id: u32,
255		prune_up_to: Option<SessionIndex>,
256	) -> Self {
257		Self { new_validator_set, id, prune_up_to, leftover: false }
258	}
259
260	pub fn merge(mut self, other: Self) -> Result<Self, UnexpectedKind> {
262		if self.id != other.id || self.prune_up_to != other.prune_up_to {
263			return Err(UnexpectedKind::ValidatorSetIntegrityFailed);
265		}
266		self.new_validator_set.extend(other.new_validator_set);
267		self.leftover = other.leftover;
268		Ok(self)
269	}
270
271	pub fn split(self, chunk_size: usize) -> Vec<Self>
273	where
274		AccountId: Clone,
275	{
276		let splitted_points = self.new_validator_set.chunks(chunk_size.max(1)).map(|x| x.to_vec());
277		let mut parts = splitted_points
278			.into_iter()
279			.map(|new_validator_set| Self { new_validator_set, leftover: true, ..self })
280			.collect::<Vec<_>>();
281		if let Some(x) = parts.last_mut() {
282			x.leftover = false
283		}
284		parts
285	}
286}
287
288#[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, TypeInfo, MaxEncodedLen)]
289pub struct SessionReport<AccountId> {
291	pub end_index: SessionIndex,
295	pub validator_points: Vec<(AccountId, u32)>,
299	pub activation_timestamp: Option<(u64, u32)>,
307	pub leftover: bool,
317}
318
319impl<AccountId: core::fmt::Debug> core::fmt::Debug for SessionReport<AccountId> {
320	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
321		f.debug_struct("SessionReport")
322			.field("end_index", &self.end_index)
323			.field("validator_points", &self.validator_points)
324			.field("activation_timestamp", &self.activation_timestamp)
325			.field("leftover", &self.leftover)
326			.finish()
327	}
328}
329
330impl<AccountId> core::fmt::Display for SessionReport<AccountId> {
331	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
332		f.debug_struct("SessionReport")
333			.field("end_index", &self.end_index)
334			.field("validator_points", &self.validator_points.len())
335			.field("activation_timestamp", &self.activation_timestamp)
336			.field("leftover", &self.leftover)
337			.finish()
338	}
339}
340
341impl<AccountId> SessionReport<AccountId> {
342	pub fn new_terminal(
345		end_index: SessionIndex,
346		validator_points: Vec<(AccountId, u32)>,
347		activation_timestamp: Option<(u64, u32)>,
348	) -> Self {
349		Self { end_index, validator_points, activation_timestamp, leftover: false }
350	}
351
352	pub fn merge(mut self, other: Self) -> Result<Self, UnexpectedKind> {
354		if self.end_index != other.end_index ||
355			self.activation_timestamp != other.activation_timestamp
356		{
357			return Err(UnexpectedKind::SessionReportIntegrityFailed);
359		}
360		self.validator_points.extend(other.validator_points);
361		self.leftover = other.leftover;
362		Ok(self)
363	}
364
365	pub fn split(self, chunk_size: usize) -> Vec<Self>
367	where
368		AccountId: Clone,
369	{
370		let splitted_points = self.validator_points.chunks(chunk_size.max(1)).map(|x| x.to_vec());
371		let mut parts = splitted_points
372			.into_iter()
373			.map(|validator_points| Self { validator_points, leftover: true, ..self })
374			.collect::<Vec<_>>();
375		if let Some(x) = parts.last_mut() {
376			x.leftover = false
377		}
378		parts
379	}
380}
381
382#[allow(clippy::len_without_is_empty)]
386pub trait SplittableMessage: Sized {
387	fn split_by(self, chunk_size: usize) -> Vec<Self>;
389
390	fn len(&self) -> usize;
392}
393
394impl<AccountId: Clone> SplittableMessage for SessionReport<AccountId> {
395	fn split_by(self, chunk_size: usize) -> Vec<Self> {
396		self.split(chunk_size)
397	}
398	fn len(&self) -> usize {
399		self.validator_points.len()
400	}
401}
402
403impl<AccountId: Clone> SplittableMessage for ValidatorSetReport<AccountId> {
404	fn split_by(self, chunk_size: usize) -> Vec<Self> {
405		self.split(chunk_size)
406	}
407	fn len(&self) -> usize {
408		self.new_validator_set.len()
409	}
410}
411
412pub struct XCMSender<Sender, Destination, Message, ToXcm>(
418	core::marker::PhantomData<(Sender, Destination, Message, ToXcm)>,
419);
420
421impl<Sender, Destination, Message, ToXcm> XCMSender<Sender, Destination, Message, ToXcm>
422where
423	Sender: SendXcm,
424	Destination: Get<Location>,
425	Message: Clone + Encode,
426	ToXcm: Convert<Message, Xcm<()>>,
427{
428	#[allow(clippy::result_unit_err)]
433	pub fn send(message: Message) -> Result<(), ()> {
434		let xcm = ToXcm::convert(message);
435		let dest = Destination::get();
436		send_xcm::<Sender>(dest, xcm).map(|_| ()).map_err(|_| ())
438	}
439}
440
441impl<Sender, Destination, Message, ToXcm> XCMSender<Sender, Destination, Message, ToXcm>
442where
443	Sender: SendXcm,
444	Destination: Get<Location>,
445	Message: SplittableMessage + Display + Clone + Encode,
446	ToXcm: Convert<Message, Xcm<()>>,
447{
448	#[deprecated(
455		note = "all staking related VMP messages should fit the single message limits. Should not be used."
456	)]
457	#[allow(clippy::result_unit_err)]
458	pub fn split_then_send(message: Message, maybe_max_steps: Option<u32>) -> Result<(), ()> {
459		let message_type_name = core::any::type_name::<Message>();
460		let dest = Destination::get();
461		let xcms = Self::prepare(message, maybe_max_steps).map_err(|e| {
462			log::error!(target: "runtime::staking-async::rc-client", "๐จ Failed to split message {}: {:?}", message_type_name, e);
463		})?;
464
465		match with_transaction_opaque_err(|| {
466			let all_sent = xcms.into_iter().enumerate().try_for_each(|(idx, xcm)| {
467				log::debug!(target: "runtime::staking-async::rc-client", "๐จ sending {} message index {}, size: {:?}", message_type_name, idx, xcm.encoded_size());
468				send_xcm::<Sender>(dest.clone(), xcm).map(|_| {
469					log::debug!(target: "runtime::staking-async::rc-client", "๐จ Successfully sent {} message part {} to relay chain", message_type_name,  idx);
470				}).inspect_err(|e| {
471					log::error!(target: "runtime::staking-async::rc-client", "๐จ Failed to send {} message to relay chain: {:?}", message_type_name, e);
472				})
473			});
474
475			match all_sent {
476				Ok(()) => TransactionOutcome::Commit(Ok(())),
477				Err(send_err) => TransactionOutcome::Rollback(Err(send_err)),
478			}
479		}) {
480			Ok(inner) => inner.map_err(|_| ()),
482			Err(_) => Err(()),
484		}
485	}
486
487	fn prepare(message: Message, maybe_max_steps: Option<u32>) -> Result<Vec<Xcm<()>>, SendError> {
488		let mut chunk_size = message.len();
490		let mut steps = 0;
491
492		loop {
493			let current_messages = message.clone().split_by(chunk_size);
494
495			let first_message = if let Some(r) = current_messages.first() {
497				r
498			} else {
499				log::debug!(target: "runtime::staking-async::xcm", "๐จ unexpected: no messages to send");
500				return Ok(vec![]);
501			};
502
503			log::debug!(
504				target: "runtime::staking-async::xcm",
505				"๐จ step: {:?}, chunk_size: {:?}, message_size: {:?}",
506				steps,
507				chunk_size,
508				first_message.encoded_size(),
509			);
510
511			let first_xcm = ToXcm::convert(first_message.clone());
512			match <Sender as SendXcm>::validate(&mut Some(Destination::get()), &mut Some(first_xcm))
513			{
514				Ok((_ticket, price)) => {
515					log::debug!(target: "runtime::staking-async::xcm", "๐จ validated, price: {:?}", price);
516					return Ok(current_messages.into_iter().map(ToXcm::convert).collect::<Vec<_>>());
517				},
518				Err(SendError::ExceedsMaxMessageSize) => {
519					log::debug!(target: "runtime::staking-async::xcm", "๐จ ExceedsMaxMessageSize -- reducing chunk_size");
520					chunk_size = chunk_size.saturating_div(2);
521					steps += 1;
522					if maybe_max_steps.is_some_and(|max_steps| steps > max_steps) ||
523						chunk_size.is_zero()
524					{
525						log::error!(target: "runtime::staking-async::xcm", "๐จ Exceeded max steps or chunk_size = 0");
526						return Err(SendError::ExceedsMaxMessageSize);
527					} else {
528						continue;
530					}
531				},
532				Err(other) => {
533					log::error!(target: "runtime::staking-async::xcm", "๐จ other error -- cannot send XCM: {:?}", other);
534					return Err(other);
535				},
536			}
537		}
538	}
539}
540
541pub trait AHStakingInterface {
546	type AccountId;
548	type MaxValidatorSet: Get<u32>;
550
551	fn on_relay_session_report(report: SessionReport<Self::AccountId>) -> Weight;
553
554	fn weigh_on_relay_session_report(report: &SessionReport<Self::AccountId>) -> Weight;
559
560	fn on_new_offences(
562		slash_session: SessionIndex,
563		offences: Vec<Offence<Self::AccountId>>,
564	) -> Weight;
565
566	fn weigh_on_new_offences(offence_count: u32) -> Weight;
571}
572
573pub trait RcClientInterface {
575	type AccountId;
577
578	fn validator_set(new_validator_set: Vec<Self::AccountId>, id: u32, prune_up_tp: Option<u32>);
580}
581
582#[derive(Encode, Decode, DecodeWithMemTracking, Debug, Clone, PartialEq, TypeInfo)]
584pub struct Offence<AccountId> {
585	pub offender: AccountId,
587	pub reporters: Vec<AccountId>,
589	pub slash_fraction: Perbill,
591}
592
593#[frame_support::pallet]
594pub mod pallet {
595	use super::*;
596	use alloc::vec;
597	use frame_system::pallet_prelude::{BlockNumberFor, *};
598
599	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
601
602	#[pallet::storage]
606	#[pallet::unbounded]
607	pub type IncompleteSessionReport<T: Config> =
608		StorageValue<_, SessionReport<T::AccountId>, OptionQuery>;
609
610	#[pallet::storage]
619	pub type LastSessionReportEndingIndex<T: Config> = StorageValue<_, SessionIndex, OptionQuery>;
620
621	#[pallet::storage]
626	#[pallet::unbounded]
629	pub type OutgoingValidatorSet<T: Config> =
630		StorageValue<_, (ValidatorSetReport<T::AccountId>, u32), OptionQuery>;
631
632	#[pallet::pallet]
633	#[pallet::storage_version(STORAGE_VERSION)]
634	pub struct Pallet<T>(_);
635
636	#[pallet::hooks]
637	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
638		fn on_initialize(_: BlockNumberFor<T>) -> Weight {
639			if let Some((report, retries_left)) = OutgoingValidatorSet::<T>::take() {
640				match T::SendToRelayChain::validator_set(report.clone()) {
641					Ok(()) => {
642						},
644					Err(()) => {
645						log!(error, "Failed to send validator set report to relay chain");
646						Self::deposit_event(Event::<T>::Unexpected(
647							UnexpectedKind::ValidatorSetSendFailed,
648						));
649						if let Some(new_retries_left) = retries_left.checked_sub(One::one()) {
650							OutgoingValidatorSet::<T>::put((report, new_retries_left))
651						} else {
652							Self::deposit_event(Event::<T>::Unexpected(
653								UnexpectedKind::ValidatorSetDropped,
654							));
655						}
656					},
657				}
658			}
659			T::DbWeight::get().reads_writes(1, 1)
660		}
661	}
662
663	#[pallet::config]
664	pub trait Config: frame_system::Config {
665		type RelayChainOrigin: EnsureOrigin<Self::RuntimeOrigin>;
669
670		type AHStakingInterface: AHStakingInterface<AccountId = Self::AccountId>;
672
673		type SendToRelayChain: SendToRelayChain<AccountId = Self::AccountId>;
675
676		type MaxValidatorSetRetries: Get<u32>;
680	}
681
682	#[pallet::event]
683	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
684	pub enum Event<T: Config> {
685		SessionReportReceived {
687			end_index: SessionIndex,
688			activation_timestamp: Option<(u64, u32)>,
689			validator_points_counts: u32,
690			leftover: bool,
691		},
692		OffenceReceived { slash_session: SessionIndex, offences_count: u32 },
694		Unexpected(UnexpectedKind),
697	}
698
699	#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, RuntimeDebug)]
705	pub enum UnexpectedKind {
706		SessionReportIntegrityFailed,
708		ValidatorSetIntegrityFailed,
710		SessionSkipped,
712		SessionAlreadyProcessed,
715		ValidatorSetSendFailed,
719		ValidatorSetDropped,
721	}
722
723	impl<T: Config> RcClientInterface for Pallet<T> {
724		type AccountId = T::AccountId;
725
726		fn validator_set(
727			new_validator_set: Vec<Self::AccountId>,
728			id: u32,
729			prune_up_tp: Option<u32>,
730		) {
731			let report = ValidatorSetReport::new_terminal(new_validator_set, id, prune_up_tp);
732			OutgoingValidatorSet::<T>::put((report, T::MaxValidatorSetRetries::get()));
734		}
735	}
736
737	#[pallet::call]
738	impl<T: Config> Pallet<T> {
739		#[pallet::call_index(0)]
741		#[pallet::weight(
742			T::DbWeight::get().reads_writes(2, 2) + T::AHStakingInterface::weigh_on_relay_session_report(report)
745		)]
746		pub fn relay_session_report(
747			origin: OriginFor<T>,
748			report: SessionReport<T::AccountId>,
749		) -> DispatchResultWithPostInfo {
750			log!(debug, "Received session report: {}", report);
751			T::RelayChainOrigin::ensure_origin_or_root(origin)?;
752			let local_weight = T::DbWeight::get().reads_writes(2, 2);
753
754			match LastSessionReportEndingIndex::<T>::get() {
755				None => {
756					},
758				Some(last) if report.end_index == last + 1 => {
759					},
761				Some(last) if report.end_index > last + 1 => {
762					Self::deposit_event(Event::Unexpected(UnexpectedKind::SessionSkipped));
764					log!(
765						warn,
766						"Session report end index is more than expected. last_index={:?}, report.index={:?}",
767						last,
768						report.end_index
769					);
770				},
771				Some(past) => {
772					log!(
773						error,
774						"Session report end index is not valid. last_index={:?}, report.index={:?}",
775						past,
776						report.end_index
777					);
778					Self::deposit_event(Event::Unexpected(UnexpectedKind::SessionAlreadyProcessed));
779					IncompleteSessionReport::<T>::kill();
780					return Ok(Some(local_weight).into());
781				},
782			}
783
784			Self::deposit_event(Event::SessionReportReceived {
785				end_index: report.end_index,
786				activation_timestamp: report.activation_timestamp,
787				validator_points_counts: report.validator_points.len() as u32,
788				leftover: report.leftover,
789			});
790
791			let maybe_new_session_report = match IncompleteSessionReport::<T>::take() {
793				Some(old) => old.merge(report.clone()),
794				None => Ok(report),
795			};
796
797			if let Err(e) = maybe_new_session_report {
798				Self::deposit_event(Event::Unexpected(e));
799				debug_assert!(
800					IncompleteSessionReport::<T>::get().is_none(),
801					"we have ::take() it above, we don't want to keep the old data"
802				);
803				return Ok(().into());
804			}
805			let new_session_report = maybe_new_session_report.expect("checked above; qed");
806
807			if new_session_report.leftover {
808				IncompleteSessionReport::<T>::put(new_session_report);
810				Ok(().into())
811			} else {
812				LastSessionReportEndingIndex::<T>::put(new_session_report.end_index);
814				let weight = T::AHStakingInterface::on_relay_session_report(new_session_report);
815				Ok((Some(local_weight + weight)).into())
816			}
817		}
818
819		#[pallet::call_index(1)]
820		#[pallet::weight(
821			T::AHStakingInterface::weigh_on_new_offences(offences.len() as u32)
822		)]
823		pub fn relay_new_offence_paged(
824			origin: OriginFor<T>,
825			offences: Vec<(SessionIndex, Offence<T::AccountId>)>,
826		) -> DispatchResultWithPostInfo {
827			T::RelayChainOrigin::ensure_origin_or_root(origin)?;
828			log!(info, "Received new page of {} offences", offences.len());
829
830			let mut offences_by_session =
831				alloc::collections::BTreeMap::<SessionIndex, Vec<Offence<T::AccountId>>>::new();
832			for (session_index, offence) in offences {
833				offences_by_session.entry(session_index).or_default().push(offence);
834			}
835
836			let mut weight: Weight = Default::default();
837			for (slash_session, offences) in offences_by_session {
838				Self::deposit_event(Event::OffenceReceived {
839					slash_session,
840					offences_count: offences.len() as u32,
841				});
842				let new_weight = T::AHStakingInterface::on_new_offences(slash_session, offences);
843				weight.saturating_accrue(new_weight)
844			}
845
846			Ok(Some(weight).into())
847		}
848	}
849}