1#![cfg_attr(not(feature = "std"), no_std)]
117
118extern crate alloc;
119use alloc::{vec, vec::Vec};
120use core::fmt::Display;
121use pezframe_support::{pezpallet_prelude::*, storage::transactional::with_transaction_opaque_err};
122use pezsp_runtime::{traits::Convert, Perbill, TransactionOutcome};
123use pezsp_staking::SessionIndex;
124use xcm::latest::{send_xcm, Location, SendError, SendXcm, Xcm};
125
126pub use pezpallet::*;
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), <pezframe_system::Pezpallet<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_type_name} message index {idx}, size: {:?}", xcm.encoded_size());
468 send_xcm::<Sender>(dest.clone(), xcm).map(|_| {
469 log::debug!(target: "runtime::staking-async::rc-client", "📨 Successfully sent {message_type_name} message part {idx} to relay chain");
470 }).inspect_err(|e| {
471 log::error!(target: "runtime::staking-async::rc-client", "📨 Failed to send {message_type_name} message to relay chain: {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
517 .into_iter()
518 .map(ToXcm::convert)
519 .collect::<Vec<_>>());
520 },
521 Err(SendError::ExceedsMaxMessageSize) => {
522 log::debug!(target: "runtime::staking-async::xcm", "📨 ExceedsMaxMessageSize -- reducing chunk_size");
523 chunk_size = chunk_size.saturating_div(2);
524 steps += 1;
525 if maybe_max_steps.is_some_and(|max_steps| steps > max_steps)
526 || chunk_size.is_zero()
527 {
528 log::error!(target: "runtime::staking-async::xcm", "📨 Exceeded max steps or chunk_size = 0");
529 return Err(SendError::ExceedsMaxMessageSize);
530 } else {
531 continue;
533 }
534 },
535 Err(other) => {
536 log::error!(target: "runtime::staking-async::xcm", "📨 other error -- cannot send XCM: {other:?}");
537 return Err(other);
538 },
539 }
540 }
541 }
542}
543
544pub trait AHStakingInterface {
549 type AccountId;
551 type MaxValidatorSet: Get<u32>;
553
554 fn on_relay_session_report(report: SessionReport<Self::AccountId>) -> Weight;
556
557 fn weigh_on_relay_session_report(report: &SessionReport<Self::AccountId>) -> Weight;
562
563 fn on_new_offences(
565 slash_session: SessionIndex,
566 offences: Vec<Offence<Self::AccountId>>,
567 ) -> Weight;
568
569 fn weigh_on_new_offences(offence_count: u32) -> Weight;
574}
575
576pub trait RcClientInterface {
578 type AccountId;
580
581 fn validator_set(new_validator_set: Vec<Self::AccountId>, id: u32, prune_up_tp: Option<u32>);
583}
584
585#[derive(Encode, Decode, DecodeWithMemTracking, Debug, Clone, PartialEq, TypeInfo)]
587pub struct Offence<AccountId> {
588 pub offender: AccountId,
590 pub reporters: Vec<AccountId>,
592 pub slash_fraction: Perbill,
594}
595
596#[pezframe_support::pezpallet]
597pub mod pezpallet {
598 use super::*;
599 use pezframe_system::pezpallet_prelude::{BlockNumberFor, *};
600
601 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
603
604 #[pezpallet::storage]
608 #[pezpallet::unbounded]
609 pub type IncompleteSessionReport<T: Config> =
610 StorageValue<_, SessionReport<T::AccountId>, OptionQuery>;
611
612 #[pezpallet::storage]
621 pub type LastSessionReportEndingIndex<T: Config> = StorageValue<_, SessionIndex, OptionQuery>;
622
623 #[pezpallet::storage]
628 #[pezpallet::unbounded]
631 pub type OutgoingValidatorSet<T: Config> =
632 StorageValue<_, (ValidatorSetReport<T::AccountId>, u32), OptionQuery>;
633
634 #[pezpallet::pezpallet]
635 #[pezpallet::storage_version(STORAGE_VERSION)]
636 pub struct Pezpallet<T>(_);
637
638 #[pezpallet::hooks]
639 impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T> {
640 fn on_initialize(_: BlockNumberFor<T>) -> Weight {
641 if let Some((report, retries_left)) = OutgoingValidatorSet::<T>::take() {
642 match T::SendToRelayChain::validator_set(report.clone()) {
643 Ok(()) => {
644 },
646 Err(()) => {
647 log!(error, "Failed to send validator set report to relay chain");
648 Self::deposit_event(Event::<T>::Unexpected(
649 UnexpectedKind::ValidatorSetSendFailed,
650 ));
651 if let Some(new_retries_left) = retries_left.checked_sub(One::one()) {
652 OutgoingValidatorSet::<T>::put((report, new_retries_left))
653 } else {
654 Self::deposit_event(Event::<T>::Unexpected(
655 UnexpectedKind::ValidatorSetDropped,
656 ));
657 }
658 },
659 }
660 }
661 T::DbWeight::get().reads_writes(1, 1)
662 }
663 }
664
665 #[pezpallet::config]
666 pub trait Config: pezframe_system::Config {
667 type RelayChainOrigin: EnsureOrigin<Self::RuntimeOrigin>;
671
672 type AHStakingInterface: AHStakingInterface<AccountId = Self::AccountId>;
674
675 type SendToRelayChain: SendToRelayChain<AccountId = Self::AccountId>;
677
678 type MaxValidatorSetRetries: Get<u32>;
682 }
683
684 #[pezpallet::event]
685 #[pezpallet::generate_deposit(pub(crate) fn deposit_event)]
686 pub enum Event<T: Config> {
687 SessionReportReceived {
689 end_index: SessionIndex,
690 activation_timestamp: Option<(u64, u32)>,
691 validator_points_counts: u32,
692 leftover: bool,
693 },
694 OffenceReceived { slash_session: SessionIndex, offences_count: u32 },
696 Unexpected(UnexpectedKind),
699 }
700
701 #[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, RuntimeDebug)]
707 pub enum UnexpectedKind {
708 SessionReportIntegrityFailed,
710 ValidatorSetIntegrityFailed,
712 SessionSkipped,
714 SessionAlreadyProcessed,
717 ValidatorSetSendFailed,
721 ValidatorSetDropped,
723 }
724
725 impl<T: Config> RcClientInterface for Pezpallet<T> {
726 type AccountId = T::AccountId;
727
728 fn validator_set(
729 new_validator_set: Vec<Self::AccountId>,
730 id: u32,
731 prune_up_tp: Option<u32>,
732 ) {
733 let report = ValidatorSetReport::new_terminal(new_validator_set, id, prune_up_tp);
734 OutgoingValidatorSet::<T>::put((report, T::MaxValidatorSetRetries::get()));
736 }
737 }
738
739 #[pezpallet::call]
740 impl<T: Config> Pezpallet<T> {
741 #[pezpallet::call_index(0)]
743 #[pezpallet::weight(
744 T::DbWeight::get().reads_writes(2, 2) + T::AHStakingInterface::weigh_on_relay_session_report(report)
747 )]
748 pub fn relay_session_report(
749 origin: OriginFor<T>,
750 report: SessionReport<T::AccountId>,
751 ) -> DispatchResultWithPostInfo {
752 log!(debug, "Received session report: {}", report);
753 T::RelayChainOrigin::ensure_origin_or_root(origin)?;
754 let local_weight = T::DbWeight::get().reads_writes(2, 2);
755
756 match LastSessionReportEndingIndex::<T>::get() {
757 None => {
758 },
760 Some(last) if report.end_index == last + 1 => {
761 },
763 Some(last) if report.end_index > last + 1 => {
764 Self::deposit_event(Event::Unexpected(UnexpectedKind::SessionSkipped));
766 log!(
767 warn,
768 "Session report end index is more than expected. last_index={:?}, report.index={:?}",
769 last,
770 report.end_index
771 );
772 },
773 Some(past) => {
774 log!(
775 error,
776 "Session report end index is not valid. last_index={:?}, report.index={:?}",
777 past,
778 report.end_index
779 );
780 Self::deposit_event(Event::Unexpected(UnexpectedKind::SessionAlreadyProcessed));
781 IncompleteSessionReport::<T>::kill();
782 return Ok(Some(local_weight).into());
783 },
784 }
785
786 Self::deposit_event(Event::SessionReportReceived {
787 end_index: report.end_index,
788 activation_timestamp: report.activation_timestamp,
789 validator_points_counts: report.validator_points.len() as u32,
790 leftover: report.leftover,
791 });
792
793 let maybe_new_session_report = match IncompleteSessionReport::<T>::take() {
795 Some(old) => old.merge(report.clone()),
796 None => Ok(report),
797 };
798
799 if let Err(e) = maybe_new_session_report {
800 Self::deposit_event(Event::Unexpected(e));
801 debug_assert!(
802 IncompleteSessionReport::<T>::get().is_none(),
803 "we have ::take() it above, we don't want to keep the old data"
804 );
805 return Ok(().into());
806 }
807 let new_session_report = maybe_new_session_report.expect("checked above; qed");
808
809 if new_session_report.leftover {
810 IncompleteSessionReport::<T>::put(new_session_report);
812 Ok(().into())
813 } else {
814 LastSessionReportEndingIndex::<T>::put(new_session_report.end_index);
816 let weight = T::AHStakingInterface::on_relay_session_report(new_session_report);
817 Ok((Some(local_weight + weight)).into())
818 }
819 }
820
821 #[pezpallet::call_index(1)]
822 #[pezpallet::weight(
823 T::AHStakingInterface::weigh_on_new_offences(offences.len() as u32)
824 )]
825 pub fn relay_new_offence_paged(
826 origin: OriginFor<T>,
827 offences: Vec<(SessionIndex, Offence<T::AccountId>)>,
828 ) -> DispatchResultWithPostInfo {
829 T::RelayChainOrigin::ensure_origin_or_root(origin)?;
830 log!(info, "Received new page of {} offences", offences.len());
831
832 let mut offences_by_session =
833 alloc::collections::BTreeMap::<SessionIndex, Vec<Offence<T::AccountId>>>::new();
834 for (session_index, offence) in offences {
835 offences_by_session.entry(session_index).or_default().push(offence);
836 }
837
838 let mut weight: Weight = Default::default();
839 for (slash_session, offences) in offences_by_session {
840 Self::deposit_event(Event::OffenceReceived {
841 slash_session,
842 offences_count: offences.len() as u32,
843 });
844 let new_weight = T::AHStakingInterface::on_new_offences(slash_session, offences);
845 weight.saturating_accrue(new_weight)
846 }
847
848 Ok(Some(weight).into())
849 }
850 }
851}