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::*;
122use sp_runtime::{traits::Convert, Perbill};
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 fn validator_set(report: ValidatorSetReport<Self::AccountId>);
155}
156
157#[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, TypeInfo)]
158pub struct ValidatorSetReport<AccountId> {
160 pub new_validator_set: Vec<AccountId>,
162 pub id: u32,
170 pub prune_up_to: Option<SessionIndex>,
175 pub leftover: bool,
177}
178
179impl<AccountId: core::fmt::Debug> core::fmt::Debug for ValidatorSetReport<AccountId> {
180 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
181 f.debug_struct("ValidatorSetReport")
182 .field("new_validator_set", &self.new_validator_set)
183 .field("id", &self.id)
184 .field("prune_up_to", &self.prune_up_to)
185 .field("leftover", &self.leftover)
186 .finish()
187 }
188}
189
190impl<AccountId> core::fmt::Display for ValidatorSetReport<AccountId> {
191 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
192 f.debug_struct("ValidatorSetReport")
193 .field("new_validator_set", &self.new_validator_set.len())
194 .field("id", &self.id)
195 .field("prune_up_to", &self.prune_up_to)
196 .field("leftover", &self.leftover)
197 .finish()
198 }
199}
200
201impl<AccountId> ValidatorSetReport<AccountId> {
202 pub fn new_terminal(
205 new_validator_set: Vec<AccountId>,
206 id: u32,
207 prune_up_to: Option<SessionIndex>,
208 ) -> Self {
209 Self { new_validator_set, id, prune_up_to, leftover: false }
210 }
211
212 pub fn merge(mut self, other: Self) -> Result<Self, UnexpectedKind> {
214 if self.id != other.id || self.prune_up_to != other.prune_up_to {
215 return Err(UnexpectedKind::ValidatorSetIntegrityFailed);
217 }
218 self.new_validator_set.extend(other.new_validator_set);
219 self.leftover = other.leftover;
220 Ok(self)
221 }
222
223 pub fn split(self, chunk_size: usize) -> Vec<Self>
225 where
226 AccountId: Clone,
227 {
228 let splitted_points = self.new_validator_set.chunks(chunk_size.max(1)).map(|x| x.to_vec());
229 let mut parts = splitted_points
230 .into_iter()
231 .map(|new_validator_set| Self { new_validator_set, leftover: true, ..self })
232 .collect::<Vec<_>>();
233 if let Some(x) = parts.last_mut() {
234 x.leftover = false
235 }
236 parts
237 }
238}
239
240#[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, TypeInfo, MaxEncodedLen)]
241pub struct SessionReport<AccountId> {
243 pub end_index: SessionIndex,
247 pub validator_points: Vec<(AccountId, u32)>,
251 pub activation_timestamp: Option<(u64, u32)>,
259 pub leftover: bool,
269}
270
271impl<AccountId: core::fmt::Debug> core::fmt::Debug for SessionReport<AccountId> {
272 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
273 f.debug_struct("SessionReport")
274 .field("end_index", &self.end_index)
275 .field("validator_points", &self.validator_points)
276 .field("activation_timestamp", &self.activation_timestamp)
277 .field("leftover", &self.leftover)
278 .finish()
279 }
280}
281
282impl<AccountId> core::fmt::Display for SessionReport<AccountId> {
283 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
284 f.debug_struct("SessionReport")
285 .field("end_index", &self.end_index)
286 .field("validator_points", &self.validator_points.len())
287 .field("activation_timestamp", &self.activation_timestamp)
288 .field("leftover", &self.leftover)
289 .finish()
290 }
291}
292
293impl<AccountId> SessionReport<AccountId> {
294 pub fn new_terminal(
297 end_index: SessionIndex,
298 validator_points: Vec<(AccountId, u32)>,
299 activation_timestamp: Option<(u64, u32)>,
300 ) -> Self {
301 Self { end_index, validator_points, activation_timestamp, leftover: false }
302 }
303
304 pub fn merge(mut self, other: Self) -> Result<Self, UnexpectedKind> {
306 if self.end_index != other.end_index ||
307 self.activation_timestamp != other.activation_timestamp
308 {
309 return Err(UnexpectedKind::SessionReportIntegrityFailed);
311 }
312 self.validator_points.extend(other.validator_points);
313 self.leftover = other.leftover;
314 Ok(self)
315 }
316
317 pub fn split(self, chunk_size: usize) -> Vec<Self>
319 where
320 AccountId: Clone,
321 {
322 let splitted_points = self.validator_points.chunks(chunk_size.max(1)).map(|x| x.to_vec());
323 let mut parts = splitted_points
324 .into_iter()
325 .map(|validator_points| Self { validator_points, leftover: true, ..self })
326 .collect::<Vec<_>>();
327 if let Some(x) = parts.last_mut() {
328 x.leftover = false
329 }
330 parts
331 }
332}
333
334#[allow(clippy::len_without_is_empty)]
338pub trait SplittableMessage: Sized {
339 fn split_by(self, chunk_size: usize) -> Vec<Self>;
341
342 fn len(&self) -> usize;
344}
345
346impl<AccountId: Clone> SplittableMessage for SessionReport<AccountId> {
347 fn split_by(self, chunk_size: usize) -> Vec<Self> {
348 self.split(chunk_size)
349 }
350 fn len(&self) -> usize {
351 self.validator_points.len()
352 }
353}
354
355impl<AccountId: Clone> SplittableMessage for ValidatorSetReport<AccountId> {
356 fn split_by(self, chunk_size: usize) -> Vec<Self> {
357 self.split(chunk_size)
358 }
359 fn len(&self) -> usize {
360 self.new_validator_set.len()
361 }
362}
363
364pub struct XCMSender<Sender, Destination, Message, ToXcm>(
370 core::marker::PhantomData<(Sender, Destination, Message, ToXcm)>,
371);
372
373impl<Sender, Destination, Message, ToXcm> XCMSender<Sender, Destination, Message, ToXcm>
374where
375 Sender: SendXcm,
376 Destination: Get<Location>,
377 Message: SplittableMessage + Display + Clone + Encode,
378 ToXcm: Convert<Message, Xcm<()>>,
379{
380 pub fn split_then_send(message: Message, maybe_max_steps: Option<u32>) {
387 let message_type_name = core::any::type_name::<Message>();
388 let dest = Destination::get();
389 let xcms = match Self::prepare(message, maybe_max_steps) {
390 Ok(x) => x,
391 Err(e) => {
392 log::error!(target: "runtime::rc-client", "๐จ Failed to split message {}: {:?}", message_type_name, e);
393 return;
394 },
395 };
396
397 for (idx, xcm) in xcms.into_iter().enumerate() {
398 log::debug!(target: "runtime::rc-client", "๐จ sending {} message index {}, size: {:?}", message_type_name, idx, xcm.encoded_size());
399 let result = send_xcm::<Sender>(dest.clone(), xcm);
400 match result {
401 Ok(_) => {
402 log::debug!(target: "runtime::rc-client", "๐จ Successfully sent {} message part {} to relay chain", message_type_name, idx)
403 },
404 Err(e) => {
405 log::error!(target: "runtime::rc-client", "๐จ Failed to send {} message to relay chain: {:?}", message_type_name, e)
406 },
407 }
408 }
409 }
410
411 fn prepare(message: Message, maybe_max_steps: Option<u32>) -> Result<Vec<Xcm<()>>, SendError> {
412 let mut chunk_size = message.len();
414 let mut steps = 0;
415
416 loop {
417 let current_messages = message.clone().split_by(chunk_size);
418
419 let first_message = if let Some(r) = current_messages.first() {
421 r
422 } else {
423 log::debug!(target: "runtime::staking-async::xcm", "๐จ unexpected: no messages to send");
424 return Ok(vec![]);
425 };
426
427 log::debug!(
428 target: "runtime::staking-async::xcm",
429 "๐จ step: {:?}, chunk_size: {:?}, message_size: {:?}",
430 steps,
431 chunk_size,
432 first_message.encoded_size(),
433 );
434
435 let first_xcm = ToXcm::convert(first_message.clone());
436 match <Sender as SendXcm>::validate(&mut Some(Destination::get()), &mut Some(first_xcm))
437 {
438 Ok((_ticket, price)) => {
439 log::debug!(target: "runtime::staking-async::xcm", "๐จ validated, price: {:?}", price);
440 return Ok(current_messages.into_iter().map(ToXcm::convert).collect::<Vec<_>>());
441 },
442 Err(SendError::ExceedsMaxMessageSize) => {
443 log::debug!(target: "runtime::staking-async::xcm", "๐จ ExceedsMaxMessageSize -- reducing chunk_size");
444 chunk_size = chunk_size.saturating_div(2);
445 steps += 1;
446 if maybe_max_steps.is_some_and(|max_steps| steps > max_steps) ||
447 chunk_size.is_zero()
448 {
449 log::error!(target: "runtime::staking-async::xcm", "๐จ Exceeded max steps or chunk_size = 0");
450 return Err(SendError::ExceedsMaxMessageSize);
451 } else {
452 continue;
454 }
455 },
456 Err(other) => {
457 log::error!(target: "runtime::staking-async::xcm", "๐จ other error -- cannot send XCM: {:?}", other);
458 return Err(other);
459 },
460 }
461 }
462 }
463}
464
465pub trait AHStakingInterface {
470 type AccountId;
472 type MaxValidatorSet: Get<u32>;
474
475 fn on_relay_session_report(report: SessionReport<Self::AccountId>);
477
478 fn on_new_offences(slash_session: SessionIndex, offences: Vec<Offence<Self::AccountId>>);
482}
483
484pub trait RcClientInterface {
486 type AccountId;
488
489 fn validator_set(new_validator_set: Vec<Self::AccountId>, id: u32, prune_up_tp: Option<u32>);
491}
492
493#[derive(Encode, Decode, DecodeWithMemTracking, Debug, Clone, PartialEq, TypeInfo)]
495pub struct Offence<AccountId> {
496 pub offender: AccountId,
498 pub reporters: Vec<AccountId>,
500 pub slash_fraction: Perbill,
502}
503
504#[frame_support::pallet]
505pub mod pallet {
506 use super::*;
507 use alloc::vec;
508 use frame_system::pallet_prelude::*;
509
510 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
512
513 #[pallet::storage]
517 #[pallet::unbounded]
518 pub type IncompleteSessionReport<T: Config> =
519 StorageValue<_, SessionReport<T::AccountId>, OptionQuery>;
520
521 #[pallet::storage]
530 pub type LastSessionReportEndingIndex<T: Config> = StorageValue<_, SessionIndex, OptionQuery>;
531
532 #[pallet::pallet]
533 #[pallet::storage_version(STORAGE_VERSION)]
534 pub struct Pallet<T>(_);
535
536 #[pallet::config]
537 pub trait Config: frame_system::Config {
538 type RelayChainOrigin: EnsureOrigin<Self::RuntimeOrigin>;
542
543 type AHStakingInterface: AHStakingInterface<AccountId = Self::AccountId>;
545
546 type SendToRelayChain: SendToRelayChain<AccountId = Self::AccountId>;
548 }
549
550 #[pallet::event]
551 #[pallet::generate_deposit(pub(crate) fn deposit_event)]
552 pub enum Event<T: Config> {
553 SessionReportReceived {
555 end_index: SessionIndex,
556 activation_timestamp: Option<(u64, u32)>,
557 validator_points_counts: u32,
558 leftover: bool,
559 },
560 OffenceReceived { slash_session: SessionIndex, offences_count: u32 },
562 Unexpected(UnexpectedKind),
565 }
566
567 #[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, RuntimeDebug)]
573 pub enum UnexpectedKind {
574 SessionReportIntegrityFailed,
576 ValidatorSetIntegrityFailed,
578 SessionSkipped,
580 SessionAlreadyProcessed,
583 }
584
585 impl<T: Config> RcClientInterface for Pallet<T> {
586 type AccountId = T::AccountId;
587
588 fn validator_set(
589 new_validator_set: Vec<Self::AccountId>,
590 id: u32,
591 prune_up_tp: Option<u32>,
592 ) {
593 let report = ValidatorSetReport::new_terminal(new_validator_set, id, prune_up_tp);
594 T::SendToRelayChain::validator_set(report);
595 }
596 }
597
598 #[pallet::call]
599 impl<T: Config> Pallet<T> {
600 #[pallet::call_index(0)]
602 #[pallet::weight(
603 T::DbWeight::get().reads_writes(2, 2)
607 )]
608 pub fn relay_session_report(
609 origin: OriginFor<T>,
610 report: SessionReport<T::AccountId>,
611 ) -> DispatchResult {
612 log!(debug, "Received session report: {}", report);
613 T::RelayChainOrigin::ensure_origin_or_root(origin)?;
614
615 match LastSessionReportEndingIndex::<T>::get() {
616 None => {
617 },
619 Some(last) if report.end_index == last + 1 => {
620 },
622 Some(last) if report.end_index > last + 1 => {
623 Self::deposit_event(Event::Unexpected(UnexpectedKind::SessionSkipped));
625 log!(
626 warn,
627 "Session report end index is more than expected. last_index={:?}, report.index={:?}",
628 last,
629 report.end_index
630 );
631 },
632 Some(past) => {
633 log!(
634 error,
635 "Session report end index is not valid. last_index={:?}, report.index={:?}",
636 past,
637 report.end_index
638 );
639 Self::deposit_event(Event::Unexpected(UnexpectedKind::SessionAlreadyProcessed));
640 IncompleteSessionReport::<T>::kill();
641 return Ok(());
642 },
643 }
644
645 Self::deposit_event(Event::SessionReportReceived {
646 end_index: report.end_index,
647 activation_timestamp: report.activation_timestamp,
648 validator_points_counts: report.validator_points.len() as u32,
649 leftover: report.leftover,
650 });
651
652 let maybe_new_session_report = match IncompleteSessionReport::<T>::take() {
654 Some(old) => old.merge(report.clone()),
655 None => Ok(report),
656 };
657
658 if let Err(e) = maybe_new_session_report {
659 Self::deposit_event(Event::Unexpected(e));
660 debug_assert!(
661 IncompleteSessionReport::<T>::get().is_none(),
662 "we have ::take() it above, we don't want to keep the old data"
663 );
664 return Ok(());
665 }
666 let new_session_report = maybe_new_session_report.expect("checked above; qed");
667
668 if new_session_report.leftover {
669 IncompleteSessionReport::<T>::put(new_session_report);
671 } else {
672 LastSessionReportEndingIndex::<T>::put(new_session_report.end_index);
674 T::AHStakingInterface::on_relay_session_report(new_session_report);
675 }
676
677 Ok(())
678 }
679
680 #[pallet::call_index(1)]
682 #[pallet::weight(
683 Weight::default()
687 )]
688 pub fn relay_new_offence(
689 origin: OriginFor<T>,
690 slash_session: SessionIndex,
691 offences: Vec<Offence<T::AccountId>>,
692 ) -> DispatchResult {
693 log!(info, "Received new offence at slash_session: {:?}", slash_session);
694 T::RelayChainOrigin::ensure_origin_or_root(origin)?;
695
696 Self::deposit_event(Event::OffenceReceived {
697 slash_session,
698 offences_count: offences.len() as u32,
699 });
700
701 T::AHStakingInterface::on_new_offences(slash_session, offences);
702 Ok(())
703 }
704 }
705}