1use crate::{
56 types::SolutionOf,
57 verifier::{AsynchronousVerifier, SolutionDataProvider, Status, VerificationResult},
58};
59use codec::{Decode, Encode, MaxEncodedLen};
60use frame_election_provider_support::PageIndex;
61use frame_support::{
62 dispatch::DispatchResultWithPostInfo,
63 pallet_prelude::{StorageDoubleMap, ValueQuery, *},
64 traits::{
65 tokens::{
66 fungible::{Inspect, Mutate, MutateHold},
67 Fortitude, Precision,
68 },
69 Defensive, DefensiveSaturating, EstimateCallFee,
70 },
71 BoundedVec, Twox64Concat,
72};
73use frame_system::{ensure_signed, pallet_prelude::*};
74use scale_info::TypeInfo;
75use sp_io::MultiRemovalResults;
76use sp_npos_elections::ElectionScore;
77use sp_runtime::{traits::Saturating, Perbill};
78use sp_std::prelude::*;
79
80pub use crate::weights::traits::pallet_election_provider_multi_block_signed::*;
82pub use pallet::*;
84
85#[cfg(feature = "runtime-benchmarks")]
86mod benchmarking;
87
88pub(crate) type SignedWeightsOf<T> = <T as crate::signed::Config>::WeightInfo;
89
90#[cfg(test)]
91mod tests;
92
93type BalanceOf<T> =
94 <<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
95
96#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Default, DebugNoBound)]
98#[cfg_attr(test, derive(frame_support::PartialEqNoBound, frame_support::EqNoBound))]
99#[codec(mel_bound(T: Config))]
100#[scale_info(skip_type_params(T))]
101pub struct SubmissionMetadata<T: Config> {
102 deposit: BalanceOf<T>,
104 fee: BalanceOf<T>,
106 reward: BalanceOf<T>,
108 claimed_score: ElectionScore,
110 pages: BoundedVec<bool, T::Pages>,
112}
113
114impl<T: Config> SolutionDataProvider for Pallet<T> {
115 type Solution = SolutionOf<T::MinerConfig>;
116
117 fn get_page(page: PageIndex) -> Self::Solution {
122 let current_round = Self::current_round();
123 Submissions::<T>::leader(current_round)
124 .defensive()
125 .and_then(|(who, _score)| {
126 sublog!(
127 debug,
128 "signed",
129 "returning page {} of {:?}'s submission as leader.",
130 page,
131 who
132 );
133 Submissions::<T>::get_page_of(current_round, &who, page)
134 })
135 .unwrap_or_default()
136 }
137
138 fn get_score() -> ElectionScore {
140 let current_round = Self::current_round();
141 Submissions::<T>::leader(current_round)
142 .defensive()
143 .inspect(|(_who, score)| {
144 sublog!(
145 debug,
146 "signed",
147 "returning score {:?} of current leader for round {}.",
148 score,
149 current_round
150 );
151 })
152 .map(|(_who, score)| score)
153 .unwrap_or_default()
154 }
155
156 fn report_result(result: crate::verifier::VerificationResult) {
157 debug_assert!(matches!(<T::Verifier as AsynchronousVerifier>::status(), Status::Nothing));
159 let current_round = Self::current_round();
160
161 match result {
162 VerificationResult::Queued => {
163 if let Some((winner, metadata)) =
166 Submissions::<T>::take_leader_with_data(Self::current_round()).defensive()
167 {
168 let reward = metadata.reward.saturating_add(metadata.fee);
170 let _r = T::Currency::mint_into(&winner, reward);
171 debug_assert!(_r.is_ok());
172 Self::deposit_event(Event::<T>::Rewarded(
173 current_round,
174 winner.clone(),
175 reward,
176 ));
177
178 let _res = T::Currency::release(
180 &HoldReason::SignedSubmission.into(),
181 &winner,
182 metadata.deposit,
183 Precision::BestEffort,
184 );
185 debug_assert!(_res.is_ok());
186 }
187 },
188 VerificationResult::Rejected => {
189 Self::handle_solution_rejection(current_round);
190 },
191 }
192 }
193}
194
195pub trait CalculateBaseDeposit<Balance> {
200 fn calculate_base_deposit(existing_submitters: usize) -> Balance;
201}
202
203impl<Balance, G: Get<Balance>> CalculateBaseDeposit<Balance> for G {
204 fn calculate_base_deposit(_existing_submitters: usize) -> Balance {
205 G::get()
206 }
207}
208
209pub trait CalculatePageDeposit<Balance> {
214 fn calculate_page_deposit(existing_submitters: usize, page_size: usize) -> Balance;
215}
216
217impl<Balance: From<u32> + Saturating, G: Get<Balance>> CalculatePageDeposit<Balance> for G {
218 fn calculate_page_deposit(_existing_submitters: usize, page_size: usize) -> Balance {
219 let page_size: Balance = (page_size as u32).into();
220 G::get().saturating_mul(page_size)
221 }
222}
223
224#[frame_support::pallet]
225pub mod pallet {
226 use super::*;
227
228 #[pallet::config]
229 #[pallet::disable_frame_system_supertrait_check]
230 pub trait Config: crate::Config {
231 type Currency: Inspect<Self::AccountId>
233 + Mutate<Self::AccountId>
234 + MutateHold<Self::AccountId, Reason: From<HoldReason>>;
235
236 type DepositBase: CalculateBaseDeposit<BalanceOf<Self>>;
238
239 type DepositPerPage: CalculatePageDeposit<BalanceOf<Self>>;
241
242 type InvulnerableDeposit: Get<BalanceOf<Self>>;
244
245 type RewardBase: Get<BalanceOf<Self>>;
247
248 type MaxSubmissions: Get<u32>;
251
252 type BailoutGraceRatio: Get<Perbill>;
258
259 type EjectGraceRatio: Get<Perbill>;
264
265 type EstimateCallFee: EstimateCallFee<Call<Self>, BalanceOf<Self>>;
268
269 type WeightInfo: WeightInfo;
271 }
272
273 #[pallet::composite_enum]
275 pub enum HoldReason {
276 #[codec(index = 0)]
278 SignedSubmission,
279 }
280
281 #[pallet::storage]
291 pub type Invulnerables<T: Config> =
292 StorageValue<_, BoundedVec<T::AccountId, ConstU32<16>>, ValueQuery>;
293
294 pub(crate) struct Submissions<T: Config>(sp_std::marker::PhantomData<T>);
329
330 #[pallet::storage]
331 pub type SortedScores<T: Config> = StorageMap<
332 _,
333 Twox64Concat,
334 u32,
335 BoundedVec<(T::AccountId, ElectionScore), T::MaxSubmissions>,
336 ValueQuery,
337 >;
338
339 #[pallet::storage]
341 type SubmissionStorage<T: Config> = StorageNMap<
342 _,
343 (
344 NMapKey<Twox64Concat, u32>,
345 NMapKey<Twox64Concat, T::AccountId>,
346 NMapKey<Twox64Concat, PageIndex>,
347 ),
348 SolutionOf<T::MinerConfig>,
349 OptionQuery,
350 >;
351
352 #[pallet::storage]
357 type SubmissionMetadataStorage<T: Config> =
358 StorageDoubleMap<_, Twox64Concat, u32, Twox64Concat, T::AccountId, SubmissionMetadata<T>>;
359
360 impl<T: Config> Submissions<T> {
361 fn mutate_checked<R, F: FnOnce() -> R>(_round: u32, mutate: F) -> R {
368 let result = mutate();
369
370 #[cfg(debug_assertions)]
371 {
372 assert!(Self::sanity_check_round(_round).is_ok());
373 assert!(Self::sanity_check_round(_round + 1).is_ok());
374 assert!(Self::sanity_check_round(_round.saturating_sub(1)).is_ok());
375 }
376
377 result
378 }
379
380 pub(crate) fn take_leader_with_data(
386 round: u32,
387 ) -> Option<(T::AccountId, SubmissionMetadata<T>)> {
388 Self::mutate_checked(round, || {
389 SortedScores::<T>::mutate(round, |sorted| sorted.pop()).and_then(
390 |(submitter, _score)| {
391 let r: MultiRemovalResults = SubmissionStorage::<T>::clear_prefix(
393 (round, &submitter),
394 u32::MAX,
395 None,
396 );
397 debug_assert!(r.unique <= T::Pages::get());
398
399 SubmissionMetadataStorage::<T>::take(round, &submitter)
400 .map(|metadata| (submitter, metadata))
401 },
402 )
403 })
404 }
405
406 pub(crate) fn take_submission_with_data(
412 round: u32,
413 who: &T::AccountId,
414 ) -> Option<SubmissionMetadata<T>> {
415 Self::mutate_checked(round, || {
416 let mut sorted_scores = SortedScores::<T>::get(round);
417 if let Some(index) = sorted_scores.iter().position(|(x, _)| x == who) {
418 sorted_scores.remove(index);
419 }
420 if sorted_scores.is_empty() {
421 SortedScores::<T>::remove(round);
422 } else {
423 SortedScores::<T>::insert(round, sorted_scores);
424 }
425
426 let r = SubmissionStorage::<T>::clear_prefix((round, who), u32::MAX, None);
428 debug_assert!(r.unique <= T::Pages::get());
429
430 SubmissionMetadataStorage::<T>::take(round, who)
431 })
432 }
433
434 fn try_register(
441 round: u32,
442 who: &T::AccountId,
443 metadata: SubmissionMetadata<T>,
444 ) -> Result<bool, DispatchError> {
445 Self::mutate_checked(round, || Self::try_register_inner(round, who, metadata))
446 }
447
448 fn try_register_inner(
449 round: u32,
450 who: &T::AccountId,
451 metadata: SubmissionMetadata<T>,
452 ) -> Result<bool, DispatchError> {
453 let mut sorted_scores = SortedScores::<T>::get(round);
454
455 let did_eject = if let Some(_) = sorted_scores.iter().position(|(x, _)| x == who) {
456 return Err(Error::<T>::Duplicate.into());
457 } else {
458 debug_assert!(!SubmissionMetadataStorage::<T>::contains_key(round, who));
460
461 let insert_idx = match sorted_scores
462 .binary_search_by_key(&metadata.claimed_score, |(_, y)| *y)
463 {
464 Ok(pos) => pos,
467 Err(pos) => pos,
469 };
470
471 let mut record = (who.clone(), metadata.claimed_score);
472 if sorted_scores.is_full() {
473 let remove_idx = sorted_scores
474 .iter()
475 .position(|(x, _)| !Pallet::<T>::is_invulnerable(x))
476 .ok_or(Error::<T>::QueueFull)?;
477 if insert_idx > remove_idx {
478 sp_std::mem::swap(&mut sorted_scores[remove_idx], &mut record);
480 sorted_scores[remove_idx..insert_idx].rotate_left(1);
486
487 let discarded = record.0;
488 let maybe_metadata =
489 SubmissionMetadataStorage::<T>::take(round, &discarded).defensive();
490 let _r = SubmissionStorage::<T>::clear_prefix(
492 (round, &discarded),
493 u32::MAX,
494 None,
495 );
496 debug_assert!(_r.unique <= T::Pages::get());
497
498 if let Some(metadata) = maybe_metadata {
499 Pallet::<T>::settle_deposit(
500 &discarded,
501 metadata.deposit,
502 T::EjectGraceRatio::get(),
503 );
504 }
505
506 Pallet::<T>::deposit_event(Event::<T>::Ejected(round, discarded));
507 true
508 } else {
509 return Err(Error::<T>::QueueFull.into())
511 }
512 } else {
513 sorted_scores
514 .try_insert(insert_idx, record)
515 .expect("length checked above; qed");
516 false
517 }
518 };
519
520 SortedScores::<T>::insert(round, sorted_scores);
521 SubmissionMetadataStorage::<T>::insert(round, who, metadata);
522 Ok(did_eject)
523 }
524
525 pub(crate) fn try_mutate_page(
533 round: u32,
534 who: &T::AccountId,
535 page: PageIndex,
536 maybe_solution: Option<Box<SolutionOf<T::MinerConfig>>>,
537 ) -> DispatchResultWithPostInfo {
538 Self::mutate_checked(round, || {
539 Self::try_mutate_page_inner(round, who, page, maybe_solution)
540 })
541 }
542
543 fn deposit_for(who: &T::AccountId, pages: usize) -> BalanceOf<T> {
545 if Pallet::<T>::is_invulnerable(who) {
546 T::InvulnerableDeposit::get()
547 } else {
548 let round = Pallet::<T>::current_round();
549 let queue_size = Self::submitters_count(round);
550 let base = T::DepositBase::calculate_base_deposit(queue_size);
551 let pages = T::DepositPerPage::calculate_page_deposit(queue_size, pages);
552 base.saturating_add(pages)
553 }
554 }
555
556 fn try_mutate_page_inner(
557 round: u32,
558 who: &T::AccountId,
559 page: PageIndex,
560 maybe_solution: Option<Box<SolutionOf<T::MinerConfig>>>,
561 ) -> DispatchResultWithPostInfo {
562 let mut metadata =
563 SubmissionMetadataStorage::<T>::get(round, who).ok_or(Error::<T>::NotRegistered)?;
564 ensure!(page < T::Pages::get(), Error::<T>::BadPageIndex);
565
566 if let Some(page_bit) = metadata.pages.get_mut(page as usize).defensive() {
569 *page_bit = maybe_solution.is_some();
570 }
571
572 let new_pages = metadata.pages.iter().filter(|x| **x).count();
574 let new_deposit = Self::deposit_for(&who, new_pages);
575 let old_deposit = metadata.deposit;
576 if new_deposit > old_deposit {
577 let to_reserve = new_deposit - old_deposit;
578 T::Currency::hold(&HoldReason::SignedSubmission.into(), who, to_reserve)?;
579 } else {
580 let to_unreserve = old_deposit - new_deposit;
581 let _res = T::Currency::release(
582 &HoldReason::SignedSubmission.into(),
583 who,
584 to_unreserve,
585 Precision::BestEffort,
586 );
587 debug_assert_eq!(_res, Ok(to_unreserve));
588 };
589 metadata.deposit = new_deposit;
590
591 if maybe_solution.is_some() {
595 let fee = T::EstimateCallFee::estimate_call_fee(
596 &Call::submit_page { page, maybe_solution: maybe_solution.clone() },
597 None.into(),
598 );
599 metadata.fee.saturating_accrue(fee);
600 }
601
602 SubmissionStorage::<T>::mutate_exists((round, who, page), |maybe_old_solution| {
603 *maybe_old_solution = maybe_solution.map(|s| *s)
604 });
605 SubmissionMetadataStorage::<T>::insert(round, who, metadata);
606 Ok(().into())
607 }
608
609 pub(crate) fn has_leader(round: u32) -> bool {
611 !SortedScores::<T>::get(round).is_empty()
612 }
613
614 pub(crate) fn leader(round: u32) -> Option<(T::AccountId, ElectionScore)> {
615 SortedScores::<T>::get(round).last().cloned()
616 }
617
618 pub(crate) fn submitters_count(round: u32) -> usize {
619 SortedScores::<T>::get(round).len()
620 }
621
622 pub(crate) fn get_page_of(
623 round: u32,
624 who: &T::AccountId,
625 page: PageIndex,
626 ) -> Option<SolutionOf<T::MinerConfig>> {
627 SubmissionStorage::<T>::get((round, who, &page))
628 }
629 }
630
631 #[allow(unused)]
632 #[cfg(any(feature = "try-runtime", test, feature = "runtime-benchmarks", debug_assertions))]
633 impl<T: Config> Submissions<T> {
634 pub(crate) fn sorted_submitters(round: u32) -> BoundedVec<T::AccountId, T::MaxSubmissions> {
635 use frame_support::traits::TryCollect;
636 SortedScores::<T>::get(round).into_iter().map(|(x, _)| x).try_collect().unwrap()
637 }
638
639 pub fn submissions_iter(
640 round: u32,
641 ) -> impl Iterator<Item = (T::AccountId, PageIndex, SolutionOf<T::MinerConfig>)> {
642 SubmissionStorage::<T>::iter_prefix((round,)).map(|((x, y), z)| (x, y, z))
643 }
644
645 pub fn metadata_iter(
646 round: u32,
647 ) -> impl Iterator<Item = (T::AccountId, SubmissionMetadata<T>)> {
648 SubmissionMetadataStorage::<T>::iter_prefix(round)
649 }
650
651 pub fn metadata_of(round: u32, who: T::AccountId) -> Option<SubmissionMetadata<T>> {
652 SubmissionMetadataStorage::<T>::get(round, who)
653 }
654
655 pub fn pages_of(
656 round: u32,
657 who: T::AccountId,
658 ) -> impl Iterator<Item = (PageIndex, SolutionOf<T::MinerConfig>)> {
659 SubmissionStorage::<T>::iter_prefix((round, who))
660 }
661
662 pub fn leaderboard(
663 round: u32,
664 ) -> BoundedVec<(T::AccountId, ElectionScore), T::MaxSubmissions> {
665 SortedScores::<T>::get(round)
666 }
667
668 pub(crate) fn ensure_killed(round: u32) -> DispatchResult {
671 ensure!(Self::metadata_iter(round).count() == 0, "metadata_iter not cleared.");
672 ensure!(Self::submissions_iter(round).count() == 0, "submissions_iter not cleared.");
673 ensure!(Self::sorted_submitters(round).len() == 0, "sorted_submitters not cleared.");
674
675 Ok(())
676 }
677
678 pub(crate) fn ensure_killed_with(who: &T::AccountId, round: u32) -> DispatchResult {
680 ensure!(
681 SubmissionMetadataStorage::<T>::get(round, who).is_none(),
682 "metadata not cleared."
683 );
684 ensure!(
685 SubmissionStorage::<T>::iter_prefix((round, who)).count() == 0,
686 "submissions not cleared."
687 );
688 ensure!(
689 SortedScores::<T>::get(round).iter().all(|(x, _)| x != who),
690 "sorted_submitters not cleared."
691 );
692
693 Ok(())
694 }
695
696 pub(crate) fn sanity_check_round(round: u32) -> DispatchResult {
698 use sp_std::collections::btree_set::BTreeSet;
699 let sorted_scores = SortedScores::<T>::get(round);
700 assert_eq!(
701 sorted_scores.clone().into_iter().map(|(x, _)| x).collect::<BTreeSet<_>>().len(),
702 sorted_scores.len()
703 );
704
705 let _ = SubmissionMetadataStorage::<T>::iter_prefix(round)
706 .map(|(submitter, meta)| {
707 let mut matches = SortedScores::<T>::get(round)
708 .into_iter()
709 .filter(|(who, _score)| who == &submitter)
710 .collect::<Vec<_>>();
711
712 ensure!(
713 matches.len() == 1,
714 "item existing in metadata but missing in sorted list.",
715 );
716
717 let (_, score) = matches.pop().expect("checked; qed");
718 ensure!(score == meta.claimed_score, "score mismatch");
719 Ok(())
720 })
721 .collect::<Result<Vec<_>, &'static str>>()?;
722
723 ensure!(
724 SubmissionStorage::<T>::iter_key_prefix((round,)).map(|(k1, _k2)| k1).all(
725 |submitter| SubmissionMetadataStorage::<T>::contains_key(round, submitter)
726 ),
727 "missing metadata of submitter"
728 );
729
730 for submitter in SubmissionStorage::<T>::iter_key_prefix((round,)).map(|(k1, _k2)| k1) {
731 let pages_count =
732 SubmissionStorage::<T>::iter_key_prefix((round, &submitter)).count();
733 let metadata = SubmissionMetadataStorage::<T>::get(round, submitter)
734 .expect("metadata checked to exist for all keys; qed");
735 let assumed_pages_count = metadata.pages.iter().filter(|x| **x).count();
736 ensure!(pages_count == assumed_pages_count, "wrong page count");
737 }
738
739 Ok(())
740 }
741 }
742
743 #[pallet::pallet]
744 pub struct Pallet<T>(PhantomData<T>);
745
746 #[pallet::event]
747 #[pallet::generate_deposit(pub(super) fn deposit_event)]
748 pub enum Event<T: Config> {
749 Registered(u32, T::AccountId, ElectionScore),
751 Stored(u32, T::AccountId, PageIndex),
753 Rewarded(u32, T::AccountId, BalanceOf<T>),
755 Slashed(u32, T::AccountId, BalanceOf<T>),
757 Ejected(u32, T::AccountId),
759 Discarded(u32, T::AccountId),
761 Bailed(u32, T::AccountId),
763 }
764
765 #[pallet::error]
766 pub enum Error<T> {
767 PhaseNotSigned,
769 Duplicate,
771 QueueFull,
773 BadPageIndex,
775 NotRegistered,
777 NoSubmission,
779 RoundNotOver,
781 BadWitnessData,
783 TooManyInvulnerables,
785 }
786
787 #[pallet::call]
788 impl<T: Config> Pallet<T> {
789 #[pallet::weight(SignedWeightsOf::<T>::register_eject())]
791 #[pallet::call_index(0)]
792 pub fn register(
793 origin: OriginFor<T>,
794 claimed_score: ElectionScore,
795 ) -> DispatchResultWithPostInfo {
796 let who = ensure_signed(origin)?;
797 ensure!(crate::Pallet::<T>::current_phase().is_signed(), Error::<T>::PhaseNotSigned);
798
799 let deposit = Submissions::<T>::deposit_for(&who, 0);
803 let reward = T::RewardBase::get();
804 let fee = T::EstimateCallFee::estimate_call_fee(
805 &Call::register { claimed_score },
806 None.into(),
807 );
808 let mut pages = BoundedVec::<_, _>::with_bounded_capacity(T::Pages::get() as usize);
809 pages.bounded_resize(T::Pages::get() as usize, false);
810
811 let new_metadata = SubmissionMetadata { claimed_score, deposit, reward, fee, pages };
812
813 T::Currency::hold(&HoldReason::SignedSubmission.into(), &who, deposit)?;
814 let round = Self::current_round();
815 let discarded = Submissions::<T>::try_register(round, &who, new_metadata)?;
816 Self::deposit_event(Event::<T>::Registered(round, who, claimed_score));
817
818 if discarded {
820 Ok(().into())
821 } else {
822 Ok(Some(SignedWeightsOf::<T>::register_not_full()).into())
823 }
824 }
825
826 #[pallet::weight(SignedWeightsOf::<T>::submit_page())]
835 #[pallet::call_index(1)]
836 pub fn submit_page(
837 origin: OriginFor<T>,
838 page: PageIndex,
839 maybe_solution: Option<Box<SolutionOf<T::MinerConfig>>>,
840 ) -> DispatchResultWithPostInfo {
841 let who = ensure_signed(origin)?;
842 ensure!(crate::Pallet::<T>::current_phase().is_signed(), Error::<T>::PhaseNotSigned);
843 let is_set = maybe_solution.is_some();
844
845 let round = Self::current_round();
846 Submissions::<T>::try_mutate_page(round, &who, page, maybe_solution)?;
847 Self::deposit_event(Event::<T>::Stored(round, who, page));
848
849 if is_set {
851 Ok(().into())
852 } else {
853 Ok(Some(SignedWeightsOf::<T>::unset_page()).into())
854 }
855 }
856
857 #[pallet::weight(SignedWeightsOf::<T>::bail())]
863 #[pallet::call_index(2)]
864 pub fn bail(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
865 let who = ensure_signed(origin)?;
866 ensure!(crate::Pallet::<T>::current_phase().is_signed(), Error::<T>::PhaseNotSigned);
867 let round = Self::current_round();
868 let metadata = Submissions::<T>::take_submission_with_data(round, &who)
869 .ok_or(Error::<T>::NoSubmission)?;
870
871 let deposit = metadata.deposit;
872 Self::settle_deposit(&who, deposit, T::BailoutGraceRatio::get());
873 Self::deposit_event(Event::<T>::Bailed(round, who));
874
875 Ok(None.into())
876 }
877
878 #[pallet::call_index(3)]
885 #[pallet::weight(SignedWeightsOf::<T>::clear_old_round_data(*witness_pages))]
886 pub fn clear_old_round_data(
887 origin: OriginFor<T>,
888 round: u32,
889 witness_pages: u32,
890 ) -> DispatchResultWithPostInfo {
891 let discarded = ensure_signed(origin)?;
892
893 let current_round = Self::current_round();
894 ensure!(round < current_round, Error::<T>::RoundNotOver);
896
897 let metadata = Submissions::<T>::take_submission_with_data(round, &discarded)
898 .ok_or(Error::<T>::NoSubmission)?;
899 ensure!(
900 metadata.pages.iter().filter(|p| **p).count() as u32 <= witness_pages,
901 Error::<T>::BadWitnessData
902 );
903
904 let _res = T::Currency::release(
906 &HoldReason::SignedSubmission.into(),
907 &discarded,
908 metadata.deposit,
909 Precision::BestEffort,
910 );
911 debug_assert_eq!(_res, Ok(metadata.deposit));
912
913 if Self::is_invulnerable(&discarded) {
915 let _r = T::Currency::mint_into(&discarded, metadata.fee);
916 debug_assert!(_r.is_ok());
917 }
918
919 Self::deposit_event(Event::<T>::Discarded(round, discarded));
920
921 Ok(None.into())
923 }
924
925 #[pallet::call_index(4)]
929 #[pallet::weight(T::DbWeight::get().writes(1))]
930 pub fn set_invulnerables(origin: OriginFor<T>, inv: Vec<T::AccountId>) -> DispatchResult {
931 <T as crate::Config>::AdminOrigin::ensure_origin(origin)?;
932 let bounded: BoundedVec<_, ConstU32<16>> =
933 inv.try_into().map_err(|_| Error::<T>::TooManyInvulnerables)?;
934 Invulnerables::<T>::set(bounded);
935 Ok(())
936 }
937 }
938
939 #[pallet::view_functions]
940 impl<T: Config> Pallet<T> {
941 pub fn deposit_for(who: T::AccountId, pages: u32) -> BalanceOf<T> {
947 Submissions::<T>::deposit_for(&who, pages as usize)
948 }
949 }
950
951 #[pallet::hooks]
952 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
953 fn on_initialize(_: BlockNumberFor<T>) -> Weight {
954 let weight_taken_into_account: Weight = Default::default();
957
958 if crate::Pallet::<T>::current_phase().is_signed_validation_opened_now() {
959 let maybe_leader = Submissions::<T>::leader(Self::current_round());
960 sublog!(
961 debug,
962 "signed",
963 "signed validation started, sending validation start signal? {:?}",
964 maybe_leader.is_some()
965 );
966
967 if maybe_leader.is_some() {
969 let _ = <T::Verifier as AsynchronousVerifier>::start().defensive();
972 }
973 }
974
975 weight_taken_into_account
976 }
977
978 #[cfg(feature = "try-runtime")]
979 fn try_state(n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
980 Self::do_try_state(n)
981 }
982 }
983}
984
985impl<T: Config> Pallet<T> {
986 #[cfg(any(feature = "try-runtime", test, feature = "runtime-benchmarks"))]
987 pub(crate) fn do_try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
988 Submissions::<T>::sanity_check_round(Self::current_round())
989 }
990
991 fn current_round() -> u32 {
992 crate::Pallet::<T>::round()
993 }
994
995 fn is_invulnerable(who: &T::AccountId) -> bool {
996 Invulnerables::<T>::get().contains(who)
997 }
998
999 fn settle_deposit(who: &T::AccountId, deposit: BalanceOf<T>, grace: Perbill) {
1000 let to_refund = grace * deposit;
1001 let to_slash = deposit.defensive_saturating_sub(to_refund);
1002
1003 let _res = T::Currency::release(
1004 &HoldReason::SignedSubmission.into(),
1005 who,
1006 to_refund,
1007 Precision::BestEffort,
1008 )
1009 .defensive();
1010 debug_assert_eq!(_res, Ok(to_refund));
1011
1012 let _res = T::Currency::burn_held(
1013 &HoldReason::SignedSubmission.into(),
1014 who,
1015 to_slash,
1016 Precision::BestEffort,
1017 Fortitude::Force,
1018 )
1019 .defensive();
1020 debug_assert_eq!(_res, Ok(to_slash));
1021 }
1022
1023 fn handle_solution_rejection(current_round: u32) {
1025 if let Some((loser, metadata)) =
1026 Submissions::<T>::take_leader_with_data(current_round).defensive()
1027 {
1028 let slash = metadata.deposit;
1030 let _res = T::Currency::burn_held(
1031 &HoldReason::SignedSubmission.into(),
1032 &loser,
1033 slash,
1034 Precision::BestEffort,
1035 Fortitude::Force,
1036 );
1037 debug_assert_eq!(_res, Ok(slash));
1038 Self::deposit_event(Event::<T>::Slashed(current_round, loser.clone(), slash));
1039 Invulnerables::<T>::mutate(|x| x.retain(|y| y != &loser));
1040
1041 if let crate::types::Phase::SignedValidation(remaining_blocks) =
1043 crate::Pallet::<T>::current_phase()
1044 {
1045 let actual_blocks_remaining = remaining_blocks.saturating_add(One::one());
1048 if actual_blocks_remaining >= T::Pages::get().into() {
1049 if Submissions::<T>::has_leader(current_round) {
1050 let _ = <T::Verifier as AsynchronousVerifier>::start().defensive();
1053 }
1054 } else {
1055 sublog!(
1056 warn,
1057 "signed",
1058 "SignedValidation phase has {:?} blocks remaining, which are insufficient for {} pages",
1059 actual_blocks_remaining,
1060 T::Pages::get()
1061 );
1062 }
1063 }
1064 } else {
1065 sublog!(
1067 warn,
1068 "signed",
1069 "Tried to slash but no leader was present for round {}",
1070 current_round
1071 );
1072 }
1073 }
1074}