1use crate::{
57 types::SolutionOf,
58 verifier::{AsynchronousVerifier, SolutionDataProvider, Status, VerificationResult},
59};
60use codec::{Decode, Encode, MaxEncodedLen};
61use frame_election_provider_support::PageIndex;
62use frame_support::{
63 dispatch::{DispatchResultWithPostInfo, GetDispatchInfo},
64 pallet_prelude::{StorageDoubleMap, ValueQuery, *},
65 traits::{
66 tokens::{
67 fungible::{
68 BalancedHold, Credit as FungibleCredit, Inspect, Mutate, MutateHold, Unbalanced,
69 },
70 Imbalance, Precision, Preservation,
71 },
72 Defensive, DefensiveSaturating, EstimateCallFee, EstimateFee, OnUnbalanced,
73 },
74 BoundedVec, Twox64Concat,
75};
76use frame_system::{ensure_signed, pallet_prelude::*};
77use scale_info::TypeInfo;
78use sp_io::MultiRemovalResults;
79use sp_npos_elections::ElectionScore;
80use sp_runtime::{
81 traits::{Saturating, Zero},
82 Perbill,
83};
84use sp_std::prelude::*;
85
86pub use crate::weights::traits::pallet_election_provider_multi_block_signed::*;
88pub use pallet::*;
90
91#[cfg(feature = "runtime-benchmarks")]
92mod benchmarking;
93
94pub(crate) type SignedWeightsOf<T> = <T as crate::signed::Config>::WeightInfo;
95
96#[cfg(test)]
97mod tests;
98
99type BalanceOf<T> =
100 <<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
101
102#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Default, DebugNoBound)]
104#[cfg_attr(test, derive(frame_support::PartialEqNoBound, frame_support::EqNoBound))]
105#[codec(mel_bound(T: Config))]
106#[scale_info(skip_type_params(T))]
107pub struct SubmissionMetadata<T: Config> {
108 deposit: BalanceOf<T>,
110 fee: BalanceOf<T>,
112 reward: BalanceOf<T>,
114 claimed_score: ElectionScore,
116 pages: BoundedVec<bool, T::Pages>,
118}
119
120pub const MAX_UNPAID_REWARDS: u32 = 16;
125
126#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, DebugNoBound)]
129#[cfg_attr(test, derive(frame_support::PartialEqNoBound, frame_support::EqNoBound))]
130#[codec(mel_bound(T: Config))]
131#[scale_info(skip_type_params(T))]
132pub struct UnpaidReward<T: Config> {
133 round: u32,
135 who: T::AccountId,
137 amount: BalanceOf<T>,
139}
140
141impl<T: Config> crate::types::SignedInterface for Pallet<T> {
142 fn has_leader(round: u32) -> bool {
143 Submissions::<T>::has_leader(round)
144 }
145}
146
147impl<T: Config> SolutionDataProvider for Pallet<T> {
148 type Solution = SolutionOf<T::MinerConfig>;
149
150 fn get_page(page: PageIndex) -> Self::Solution {
155 let current_round = Self::current_round();
156 Submissions::<T>::leader(current_round)
157 .defensive()
158 .and_then(|(who, _score)| {
159 sublog!(
160 debug,
161 "signed",
162 "returning page {} of {:?}'s submission as leader.",
163 page,
164 who
165 );
166 Submissions::<T>::get_page_of(current_round, &who, page)
167 })
168 .unwrap_or_default()
169 }
170
171 fn get_score() -> ElectionScore {
173 let current_round = Self::current_round();
174 Submissions::<T>::leader(current_round)
175 .defensive()
176 .inspect(|(_who, score)| {
177 sublog!(
178 debug,
179 "signed",
180 "returning score {:?} of current leader for round {}.",
181 score,
182 current_round
183 );
184 })
185 .map(|(_who, score)| score)
186 .unwrap_or_default()
187 }
188
189 fn report_result(result: crate::verifier::VerificationResult) {
190 debug_assert!(matches!(<T::Verifier as AsynchronousVerifier>::status(), Status::Nothing));
192 let current_round = Self::current_round();
193
194 match result {
195 VerificationResult::Queued => {
196 if let Some((winner, metadata)) =
199 Submissions::<T>::take_leader_with_data(Self::current_round()).defensive()
200 {
201 let reward =
203 metadata.reward.saturating_add(metadata.fee.min(T::MaxFeeRefund::get()));
204 Self::pay_reward(current_round, &winner, reward);
205
206 let _res = T::Currency::release(
208 &HoldReason::SignedSubmission.into(),
209 &winner,
210 metadata.deposit,
211 Precision::BestEffort,
212 );
213 debug_assert!(_res.is_ok());
214 }
215 },
216 VerificationResult::Rejected => {
217 Self::handle_solution_rejection(current_round);
218 },
219 }
220 }
221}
222
223pub trait CalculateBaseDeposit<Balance> {
228 fn calculate_base_deposit(existing_submitters: usize) -> Balance;
229}
230
231impl<Balance, G: Get<Balance>> CalculateBaseDeposit<Balance> for G {
232 fn calculate_base_deposit(_existing_submitters: usize) -> Balance {
233 G::get()
234 }
235}
236
237pub trait CalculatePageDeposit<Balance> {
242 fn calculate_page_deposit(existing_submitters: usize, page_size: usize) -> Balance;
243}
244
245impl<Balance: From<u32> + Saturating, G: Get<Balance>> CalculatePageDeposit<Balance> for G {
246 fn calculate_page_deposit(_existing_submitters: usize, page_size: usize) -> Balance {
247 let page_size: Balance = (page_size as u32).into();
248 G::get().saturating_mul(page_size)
249 }
250}
251
252pub trait RewardSource<AccountId, Balance> {
257 fn account() -> Option<AccountId>;
259
260 fn paid(amount: Balance);
262}
263
264pub struct ActivePot<P>(sp_std::marker::PhantomData<P>);
269
270impl<AccountId, Balance, P: Get<Option<AccountId>>> RewardSource<AccountId, Balance>
271 for ActivePot<P>
272{
273 fn account() -> Option<AccountId> {
274 P::get()
275 }
276
277 fn paid(_amount: Balance) {}
278}
279
280pub struct ReactivatingPot<P, Currency>(sp_std::marker::PhantomData<(P, Currency)>);
285
286impl<AccountId, Balance, P, Currency> RewardSource<AccountId, Balance>
287 for ReactivatingPot<P, Currency>
288where
289 P: Get<Option<AccountId>>,
290 Currency: Unbalanced<AccountId, Balance = Balance>,
291{
292 fn account() -> Option<AccountId> {
293 P::get()
294 }
295
296 fn paid(amount: Balance) {
297 Currency::reactivate(amount);
298 }
299}
300
301pub struct FullSubmissionFee<T, F>(PhantomData<(T, F)>);
309
310impl<T: Config, F: EstimateFee<BalanceOf<T>>> Get<BalanceOf<T>> for FullSubmissionFee<T, F> {
311 fn get() -> BalanceOf<T> {
312 let page_call = Call::<T>::submit_page { page: 0, maybe_solution: None };
314 let page_len = page_call
315 .encoded_size()
316 .saturating_add(SolutionOf::<T::MinerConfig>::max_encoded_len());
317 let per_page = F::estimate_fee(page_len as u32, &page_call.get_dispatch_info());
318
319 let register_call = Call::<T>::register { claimed_score: Default::default() };
320 let register = F::estimate_fee(
321 register_call.encoded_size() as u32,
322 ®ister_call.get_dispatch_info(),
323 );
324
325 register.saturating_add(per_page.saturating_mul(T::Pages::get().into()))
326 }
327}
328
329#[frame_support::pallet]
330pub mod pallet {
331 use super::*;
332
333 #[pallet::config]
334 #[pallet::disable_frame_system_supertrait_check]
335 pub trait Config: crate::Config {
336 type Currency: Inspect<Self::AccountId>
338 + Mutate<Self::AccountId>
339 + MutateHold<Self::AccountId, Reason: From<HoldReason>>
340 + BalancedHold<Self::AccountId>;
341
342 type DepositBase: CalculateBaseDeposit<BalanceOf<Self>>;
344
345 type DepositPerPage: CalculatePageDeposit<BalanceOf<Self>>;
347
348 type InvulnerableDeposit: Get<BalanceOf<Self>>;
350
351 type RewardBase: Get<BalanceOf<Self>>;
353
354 type MaxSubmissions: Get<u32>;
357
358 type BailoutGraceRatio: Get<Perbill>;
364
365 type EjectGraceRatio: Get<Perbill>;
370
371 type EstimateCallFee: EstimateCallFee<Call<Self>, BalanceOf<Self>>;
374
375 type Slash: OnUnbalanced<FungibleCredit<Self::AccountId, Self::Currency>>;
377
378 type RewardSource: RewardSource<Self::AccountId, BalanceOf<Self>>;
381
382 type MaxFeeRefund: Get<BalanceOf<Self>>;
392
393 type WeightInfo: WeightInfo;
395 }
396
397 #[pallet::composite_enum]
399 pub enum HoldReason {
400 #[codec(index = 0)]
402 SignedSubmission,
403 }
404
405 #[pallet::storage]
416 pub type Invulnerables<T: Config> =
417 StorageValue<_, BoundedVec<T::AccountId, ConstU32<16>>, ValueQuery>;
418
419 #[pallet::storage]
426 pub type UnpaidRewards<T: Config> =
427 StorageValue<_, BoundedVec<UnpaidReward<T>, ConstU32<MAX_UNPAID_REWARDS>>, ValueQuery>;
428
429 pub(crate) struct Submissions<T: Config>(sp_std::marker::PhantomData<T>);
464
465 #[pallet::storage]
466 pub type SortedScores<T: Config> = StorageMap<
467 _,
468 Twox64Concat,
469 u32,
470 BoundedVec<(T::AccountId, ElectionScore), T::MaxSubmissions>,
471 ValueQuery,
472 >;
473
474 #[pallet::storage]
476 type SubmissionStorage<T: Config> = StorageNMap<
477 _,
478 (
479 NMapKey<Twox64Concat, u32>,
480 NMapKey<Twox64Concat, T::AccountId>,
481 NMapKey<Twox64Concat, PageIndex>,
482 ),
483 SolutionOf<T::MinerConfig>,
484 OptionQuery,
485 >;
486
487 #[pallet::storage]
492 type SubmissionMetadataStorage<T: Config> =
493 StorageDoubleMap<_, Twox64Concat, u32, Twox64Concat, T::AccountId, SubmissionMetadata<T>>;
494
495 impl<T: Config> Submissions<T> {
496 fn mutate_checked<R, F: FnOnce() -> R>(_round: u32, mutate: F) -> R {
503 let result = mutate();
504
505 #[cfg(debug_assertions)]
506 {
507 assert!(Self::sanity_check_round(_round).is_ok());
508 assert!(Self::sanity_check_round(_round + 1).is_ok());
509 assert!(Self::sanity_check_round(_round.saturating_sub(1)).is_ok());
510 }
511
512 result
513 }
514
515 pub(crate) fn take_leader_with_data(
521 round: u32,
522 ) -> Option<(T::AccountId, SubmissionMetadata<T>)> {
523 Self::mutate_checked(round, || {
524 SortedScores::<T>::mutate(round, |sorted| sorted.pop()).and_then(
525 |(submitter, _score)| {
526 let r: MultiRemovalResults = SubmissionStorage::<T>::clear_prefix(
528 (round, &submitter),
529 u32::MAX,
530 None,
531 );
532 debug_assert!(r.unique <= T::Pages::get());
533
534 SubmissionMetadataStorage::<T>::take(round, &submitter)
535 .map(|metadata| (submitter, metadata))
536 },
537 )
538 })
539 }
540
541 pub(crate) fn take_submission_with_data(
547 round: u32,
548 who: &T::AccountId,
549 ) -> Option<SubmissionMetadata<T>> {
550 Self::mutate_checked(round, || {
551 let mut sorted_scores = SortedScores::<T>::get(round);
552 if let Some(index) = sorted_scores.iter().position(|(x, _)| x == who) {
553 sorted_scores.remove(index);
554 }
555 if sorted_scores.is_empty() {
556 SortedScores::<T>::remove(round);
557 } else {
558 SortedScores::<T>::insert(round, sorted_scores);
559 }
560
561 let r = SubmissionStorage::<T>::clear_prefix((round, who), u32::MAX, None);
563 debug_assert!(r.unique <= T::Pages::get());
564
565 SubmissionMetadataStorage::<T>::take(round, who)
566 })
567 }
568
569 fn try_register(
576 round: u32,
577 who: &T::AccountId,
578 metadata: SubmissionMetadata<T>,
579 ) -> Result<bool, DispatchError> {
580 Self::mutate_checked(round, || Self::try_register_inner(round, who, metadata))
581 }
582
583 fn try_register_inner(
584 round: u32,
585 who: &T::AccountId,
586 metadata: SubmissionMetadata<T>,
587 ) -> Result<bool, DispatchError> {
588 let mut sorted_scores = SortedScores::<T>::get(round);
589
590 let did_eject = if let Some(_) = sorted_scores.iter().position(|(x, _)| x == who) {
591 return Err(Error::<T>::Duplicate.into());
592 } else {
593 debug_assert!(!SubmissionMetadataStorage::<T>::contains_key(round, who));
595
596 let insert_idx = match sorted_scores
597 .binary_search_by_key(&metadata.claimed_score, |(_, y)| *y)
598 {
599 Ok(pos) => pos,
602 Err(pos) => pos,
604 };
605
606 let mut record = (who.clone(), metadata.claimed_score);
607 if sorted_scores.is_full() {
608 let remove_idx = sorted_scores
609 .iter()
610 .position(|(x, _)| !Pallet::<T>::is_invulnerable(x))
611 .ok_or(Error::<T>::QueueFull)?;
612 if insert_idx > remove_idx {
613 sp_std::mem::swap(&mut sorted_scores[remove_idx], &mut record);
615 sorted_scores[remove_idx..insert_idx].rotate_left(1);
621
622 let discarded = record.0;
623 let maybe_metadata =
624 SubmissionMetadataStorage::<T>::take(round, &discarded).defensive();
625 let _r = SubmissionStorage::<T>::clear_prefix(
627 (round, &discarded),
628 u32::MAX,
629 None,
630 );
631 debug_assert!(_r.unique <= T::Pages::get());
632
633 if let Some(metadata) = maybe_metadata {
634 Pallet::<T>::settle_deposit(
635 round,
636 &discarded,
637 metadata.deposit,
638 T::EjectGraceRatio::get(),
639 );
640 }
641
642 Pallet::<T>::deposit_event(Event::<T>::Ejected(round, discarded));
643 true
644 } else {
645 return Err(Error::<T>::QueueFull.into());
647 }
648 } else {
649 sorted_scores
650 .try_insert(insert_idx, record)
651 .expect("length checked above; qed");
652 false
653 }
654 };
655
656 SortedScores::<T>::insert(round, sorted_scores);
657 SubmissionMetadataStorage::<T>::insert(round, who, metadata);
658 Ok(did_eject)
659 }
660
661 pub(crate) fn try_mutate_page(
669 round: u32,
670 who: &T::AccountId,
671 page: PageIndex,
672 maybe_solution: Option<Box<SolutionOf<T::MinerConfig>>>,
673 ) -> DispatchResultWithPostInfo {
674 Self::mutate_checked(round, || {
675 Self::try_mutate_page_inner(round, who, page, maybe_solution)
676 })
677 }
678
679 fn deposit_for(who: &T::AccountId, pages: usize) -> BalanceOf<T> {
681 if Pallet::<T>::is_invulnerable(who) {
682 T::InvulnerableDeposit::get()
683 } else {
684 let round = Pallet::<T>::current_round();
685 let queue_size = Self::submitters_count(round);
686 let base = T::DepositBase::calculate_base_deposit(queue_size);
687 let pages = T::DepositPerPage::calculate_page_deposit(queue_size, pages);
688 base.saturating_add(pages)
689 }
690 }
691
692 fn try_mutate_page_inner(
693 round: u32,
694 who: &T::AccountId,
695 page: PageIndex,
696 maybe_solution: Option<Box<SolutionOf<T::MinerConfig>>>,
697 ) -> DispatchResultWithPostInfo {
698 let mut metadata =
699 SubmissionMetadataStorage::<T>::get(round, who).ok_or(Error::<T>::NotRegistered)?;
700 ensure!(page < T::Pages::get(), Error::<T>::BadPageIndex);
701
702 let was_set = metadata.pages.get(page as usize).copied().unwrap_or_default();
703
704 if let Some(page_bit) = metadata.pages.get_mut(page as usize).defensive() {
707 *page_bit = maybe_solution.is_some();
708 }
709
710 let new_pages = metadata.pages.iter().filter(|x| **x).count();
712 let new_deposit = Self::deposit_for(&who, new_pages);
713 let old_deposit = metadata.deposit;
714 if new_deposit > old_deposit {
715 let to_reserve = new_deposit - old_deposit;
716 T::Currency::hold(&HoldReason::SignedSubmission.into(), who, to_reserve)?;
717 } else {
718 let to_unreserve = old_deposit - new_deposit;
719 let _res = T::Currency::release(
720 &HoldReason::SignedSubmission.into(),
721 who,
722 to_unreserve,
723 Precision::BestEffort,
724 );
725 debug_assert_eq!(_res, Ok(to_unreserve));
726 };
727 metadata.deposit = new_deposit;
728
729 if maybe_solution.is_some() && !was_set {
735 let fee = T::EstimateCallFee::estimate_call_fee(
736 &Call::submit_page { page, maybe_solution: maybe_solution.clone() },
737 None.into(),
738 );
739 metadata.fee.saturating_accrue(fee);
740 }
741
742 SubmissionStorage::<T>::mutate_exists((round, who, page), |maybe_old_solution| {
743 *maybe_old_solution = maybe_solution.map(|s| *s)
744 });
745 SubmissionMetadataStorage::<T>::insert(round, who, metadata);
746 Ok(().into())
747 }
748
749 pub(crate) fn has_leader(round: u32) -> bool {
751 !SortedScores::<T>::get(round).is_empty()
752 }
753
754 pub(crate) fn leader(round: u32) -> Option<(T::AccountId, ElectionScore)> {
755 SortedScores::<T>::get(round).last().cloned()
756 }
757
758 pub(crate) fn submitters_count(round: u32) -> usize {
759 SortedScores::<T>::get(round).len()
760 }
761
762 pub(crate) fn get_page_of(
763 round: u32,
764 who: &T::AccountId,
765 page: PageIndex,
766 ) -> Option<SolutionOf<T::MinerConfig>> {
767 SubmissionStorage::<T>::get((round, who, &page))
768 }
769 }
770
771 #[allow(unused)]
772 #[cfg(any(feature = "try-runtime", test, feature = "runtime-benchmarks", debug_assertions))]
773 impl<T: Config> Submissions<T> {
774 pub(crate) fn sorted_submitters(round: u32) -> BoundedVec<T::AccountId, T::MaxSubmissions> {
775 use frame_support::traits::TryCollect;
776 SortedScores::<T>::get(round).into_iter().map(|(x, _)| x).try_collect().unwrap()
777 }
778
779 pub fn submissions_iter(
780 round: u32,
781 ) -> impl Iterator<Item = (T::AccountId, PageIndex, SolutionOf<T::MinerConfig>)> {
782 SubmissionStorage::<T>::iter_prefix((round,)).map(|((x, y), z)| (x, y, z))
783 }
784
785 pub fn metadata_iter(
786 round: u32,
787 ) -> impl Iterator<Item = (T::AccountId, SubmissionMetadata<T>)> {
788 SubmissionMetadataStorage::<T>::iter_prefix(round)
789 }
790
791 pub fn metadata_of(round: u32, who: T::AccountId) -> Option<SubmissionMetadata<T>> {
792 SubmissionMetadataStorage::<T>::get(round, who)
793 }
794
795 pub fn pages_of(
796 round: u32,
797 who: T::AccountId,
798 ) -> impl Iterator<Item = (PageIndex, SolutionOf<T::MinerConfig>)> {
799 SubmissionStorage::<T>::iter_prefix((round, who))
800 }
801
802 pub fn leaderboard(
803 round: u32,
804 ) -> BoundedVec<(T::AccountId, ElectionScore), T::MaxSubmissions> {
805 SortedScores::<T>::get(round)
806 }
807
808 pub(crate) fn ensure_killed(round: u32) -> DispatchResult {
811 ensure!(Self::metadata_iter(round).count() == 0, "metadata_iter not cleared.");
812 ensure!(Self::submissions_iter(round).count() == 0, "submissions_iter not cleared.");
813 ensure!(Self::sorted_submitters(round).len() == 0, "sorted_submitters not cleared.");
814
815 Ok(())
816 }
817
818 pub(crate) fn ensure_killed_with(who: &T::AccountId, round: u32) -> DispatchResult {
820 ensure!(
821 SubmissionMetadataStorage::<T>::get(round, who).is_none(),
822 "metadata not cleared."
823 );
824 ensure!(
825 SubmissionStorage::<T>::iter_prefix((round, who)).count() == 0,
826 "submissions not cleared."
827 );
828 ensure!(
829 SortedScores::<T>::get(round).iter().all(|(x, _)| x != who),
830 "sorted_submitters not cleared."
831 );
832
833 Ok(())
834 }
835
836 pub(crate) fn sanity_check_round(round: u32) -> DispatchResult {
838 use sp_std::collections::btree_set::BTreeSet;
839 let sorted_scores = SortedScores::<T>::get(round);
840 assert_eq!(
841 sorted_scores.clone().into_iter().map(|(x, _)| x).collect::<BTreeSet<_>>().len(),
842 sorted_scores.len()
843 );
844
845 let _ = SubmissionMetadataStorage::<T>::iter_prefix(round)
846 .map(|(submitter, meta)| {
847 let mut matches = SortedScores::<T>::get(round)
848 .into_iter()
849 .filter(|(who, _score)| who == &submitter)
850 .collect::<Vec<_>>();
851
852 ensure!(
853 matches.len() == 1,
854 "item existing in metadata but missing in sorted list.",
855 );
856
857 let (_, score) = matches.pop().expect("checked; qed");
858 ensure!(score == meta.claimed_score, "score mismatch");
859 Ok(())
860 })
861 .collect::<Result<Vec<_>, &'static str>>()?;
862
863 ensure!(
864 SubmissionStorage::<T>::iter_key_prefix((round,)).map(|(k1, _k2)| k1).all(
865 |submitter| SubmissionMetadataStorage::<T>::contains_key(round, submitter)
866 ),
867 "missing metadata of submitter"
868 );
869
870 for submitter in SubmissionStorage::<T>::iter_key_prefix((round,)).map(|(k1, _k2)| k1) {
871 let pages_count =
872 SubmissionStorage::<T>::iter_key_prefix((round, &submitter)).count();
873 let metadata = SubmissionMetadataStorage::<T>::get(round, submitter)
874 .expect("metadata checked to exist for all keys; qed");
875 let assumed_pages_count = metadata.pages.iter().filter(|x| **x).count();
876 ensure!(pages_count == assumed_pages_count, "wrong page count");
877 }
878
879 Ok(())
880 }
881 }
882
883 #[pallet::pallet]
884 pub struct Pallet<T>(PhantomData<T>);
885
886 #[pallet::event]
887 #[pallet::generate_deposit(pub(super) fn deposit_event)]
888 pub enum Event<T: Config> {
889 Registered(u32, T::AccountId, ElectionScore),
891 Stored(u32, T::AccountId, PageIndex),
893 Rewarded(u32, T::AccountId, BalanceOf<T>),
895 RewardPaymentDeferred(u32, T::AccountId, BalanceOf<T>),
898 UnpaidRewardEvicted(u32, T::AccountId, BalanceOf<T>),
901 FeeRefundFailed(u32, T::AccountId, BalanceOf<T>),
903 Slashed(u32, T::AccountId, BalanceOf<T>),
905 Ejected(u32, T::AccountId),
907 Discarded(u32, T::AccountId),
909 Bailed(u32, T::AccountId),
911 }
912
913 #[pallet::error]
914 pub enum Error<T> {
915 PhaseNotSigned,
917 Duplicate,
919 QueueFull,
921 BadPageIndex,
923 NotRegistered,
925 NoSubmission,
927 RoundNotOver,
929 BadWitnessData,
931 TooManyInvulnerables,
933 NoUnpaidReward,
935 PotStillDepleted,
937 }
938
939 #[pallet::call]
940 impl<T: Config> Pallet<T> {
941 #[pallet::weight(SignedWeightsOf::<T>::register_eject())]
943 #[pallet::call_index(0)]
944 pub fn register(
945 origin: OriginFor<T>,
946 claimed_score: ElectionScore,
947 ) -> DispatchResultWithPostInfo {
948 let who = ensure_signed(origin)?;
949 ensure!(crate::Pallet::<T>::current_phase().is_signed(), Error::<T>::PhaseNotSigned);
950
951 let deposit = Submissions::<T>::deposit_for(&who, 0);
955 let reward = T::RewardBase::get();
956 let fee = T::EstimateCallFee::estimate_call_fee(
957 &Call::register { claimed_score },
958 None.into(),
959 );
960 let mut pages = BoundedVec::<_, _>::with_bounded_capacity(T::Pages::get() as usize);
961 pages.bounded_resize(T::Pages::get() as usize, false);
962
963 let new_metadata = SubmissionMetadata { claimed_score, deposit, reward, fee, pages };
964
965 T::Currency::hold(&HoldReason::SignedSubmission.into(), &who, deposit)?;
966 let round = Self::current_round();
967 let discarded = Submissions::<T>::try_register(round, &who, new_metadata)?;
968 Self::deposit_event(Event::<T>::Registered(round, who, claimed_score));
969
970 if discarded {
972 Ok(().into())
973 } else {
974 Ok(Some(SignedWeightsOf::<T>::register_not_full()).into())
975 }
976 }
977
978 #[pallet::weight(SignedWeightsOf::<T>::submit_page())]
987 #[pallet::call_index(1)]
988 pub fn submit_page(
989 origin: OriginFor<T>,
990 page: PageIndex,
991 maybe_solution: Option<Box<SolutionOf<T::MinerConfig>>>,
992 ) -> DispatchResultWithPostInfo {
993 let who = ensure_signed(origin)?;
994 ensure!(crate::Pallet::<T>::current_phase().is_signed(), Error::<T>::PhaseNotSigned);
995 let is_set = maybe_solution.is_some();
996
997 let round = Self::current_round();
998 Submissions::<T>::try_mutate_page(round, &who, page, maybe_solution)?;
999 Self::deposit_event(Event::<T>::Stored(round, who, page));
1000
1001 if is_set {
1003 Ok(().into())
1004 } else {
1005 Ok(Some(SignedWeightsOf::<T>::unset_page()).into())
1006 }
1007 }
1008
1009 #[pallet::weight(SignedWeightsOf::<T>::bail())]
1015 #[pallet::call_index(2)]
1016 pub fn bail(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
1017 let who = ensure_signed(origin)?;
1018 ensure!(crate::Pallet::<T>::current_phase().is_signed(), Error::<T>::PhaseNotSigned);
1019 let round = Self::current_round();
1020 let metadata = Submissions::<T>::take_submission_with_data(round, &who)
1021 .ok_or(Error::<T>::NoSubmission)?;
1022
1023 let deposit = metadata.deposit;
1024 Self::settle_deposit(round, &who, deposit, T::BailoutGraceRatio::get());
1025 Self::deposit_event(Event::<T>::Bailed(round, who));
1026
1027 Ok(None.into())
1028 }
1029
1030 #[pallet::call_index(3)]
1037 #[pallet::weight(SignedWeightsOf::<T>::clear_old_round_data(*witness_pages))]
1038 pub fn clear_old_round_data(
1039 origin: OriginFor<T>,
1040 round: u32,
1041 witness_pages: u32,
1042 ) -> DispatchResultWithPostInfo {
1043 let discarded = ensure_signed(origin)?;
1044
1045 let current_round = Self::current_round();
1046 ensure!(round < current_round, Error::<T>::RoundNotOver);
1048
1049 let metadata = Submissions::<T>::take_submission_with_data(round, &discarded)
1050 .ok_or(Error::<T>::NoSubmission)?;
1051 ensure!(
1052 metadata.pages.iter().filter(|p| **p).count() as u32 <= witness_pages,
1053 Error::<T>::BadWitnessData
1054 );
1055
1056 let _res = T::Currency::release(
1058 &HoldReason::SignedSubmission.into(),
1059 &discarded,
1060 metadata.deposit,
1061 Precision::BestEffort,
1062 );
1063 debug_assert_eq!(_res, Ok(metadata.deposit));
1064
1065 if Self::is_invulnerable(&discarded) {
1067 Self::refund_fee(round, &discarded, metadata.fee.min(T::MaxFeeRefund::get()));
1068 }
1069
1070 Self::deposit_event(Event::<T>::Discarded(round, discarded));
1071
1072 Ok(None.into())
1074 }
1075
1076 #[pallet::call_index(4)]
1080 #[pallet::weight(T::DbWeight::get().writes(1))]
1081 pub fn set_invulnerables(origin: OriginFor<T>, inv: Vec<T::AccountId>) -> DispatchResult {
1082 <T as crate::Config>::AdminOrigin::ensure_origin(origin)?;
1083 let bounded: BoundedVec<_, ConstU32<16>> =
1084 inv.try_into().map_err(|_| Error::<T>::TooManyInvulnerables)?;
1085 Invulnerables::<T>::set(bounded);
1086 Ok(())
1087 }
1088
1089 #[pallet::call_index(5)]
1092 #[pallet::weight(SignedWeightsOf::<T>::claim_unpaid_reward())]
1093 pub fn claim_unpaid_reward(origin: OriginFor<T>, round: u32) -> DispatchResultWithPostInfo {
1094 let _ = ensure_signed(origin)?;
1095 let mut unpaid = UnpaidRewards::<T>::get();
1096 let idx = unpaid
1097 .iter()
1098 .position(|entry| entry.round == round)
1099 .ok_or(Error::<T>::NoUnpaidReward)?;
1100 let entry = unpaid[idx].clone();
1101
1102 Self::transfer_or_mint(&entry.who, entry.amount)
1103 .map_err(|_| Error::<T>::PotStillDepleted)?;
1104
1105 unpaid.remove(idx);
1106 UnpaidRewards::<T>::put(unpaid);
1107 Self::deposit_event(Event::<T>::Rewarded(entry.round, entry.who, entry.amount));
1108
1109 Ok(Pays::No.into())
1110 }
1111 }
1112
1113 #[pallet::view_functions]
1114 impl<T: Config> Pallet<T> {
1115 pub fn deposit_for(who: T::AccountId, pages: u32) -> BalanceOf<T> {
1121 Submissions::<T>::deposit_for(&who, pages as usize)
1122 }
1123 }
1124
1125 #[pallet::hooks]
1126 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
1127 #[cfg(feature = "try-runtime")]
1128 fn try_state(n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
1129 Self::do_try_state(n)
1130 }
1131 }
1132}
1133
1134impl<T: Config> Pallet<T> {
1135 #[cfg(any(feature = "try-runtime", test, feature = "runtime-benchmarks"))]
1136 pub(crate) fn do_try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
1137 Submissions::<T>::sanity_check_round(Self::current_round())
1138 }
1139
1140 fn current_round() -> u32 {
1141 crate::Pallet::<T>::round()
1142 }
1143
1144 fn is_invulnerable(who: &T::AccountId) -> bool {
1145 Invulnerables::<T>::get().contains(who)
1146 }
1147
1148 fn transfer_or_mint(to: &T::AccountId, amount: BalanceOf<T>) -> Result<(), ()> {
1150 if let Some(source) = T::RewardSource::account() {
1151 T::Currency::transfer(&source, to, amount, Preservation::Preserve).map_err(|_| ())?;
1152 T::RewardSource::paid(amount);
1153 } else {
1154 let _r = T::Currency::mint_into(to, amount);
1155 debug_assert!(_r.is_ok());
1156 }
1157 Ok(())
1158 }
1159
1160 fn pay_reward(round: u32, to: &T::AccountId, amount: BalanceOf<T>) {
1164 if Self::transfer_or_mint(to, amount).is_ok() {
1165 Self::deposit_event(Event::<T>::Rewarded(round, to.clone(), amount));
1166 return;
1167 }
1168
1169 sublog!(
1170 warn,
1171 "signed",
1172 "reward pot insufficient; deferring {:?} to {:?} for round {}",
1173 amount,
1174 to,
1175 round
1176 );
1177 let entry = UnpaidReward { round, who: to.clone(), amount };
1178 UnpaidRewards::<T>::mutate(|unpaid| {
1179 if unpaid.is_full() {
1180 let evicted = unpaid.remove(0);
1182 Self::deposit_event(Event::<T>::UnpaidRewardEvicted(
1183 evicted.round,
1184 evicted.who,
1185 evicted.amount,
1186 ));
1187 }
1188 let _ = unpaid.try_push(entry).defensive_proof("an element was just evicted; qed");
1189 });
1190 Self::deposit_event(Event::<T>::RewardPaymentDeferred(round, to.clone(), amount));
1191 }
1192
1193 fn refund_fee(round: u32, to: &T::AccountId, amount: BalanceOf<T>) {
1197 if Self::transfer_or_mint(to, amount).is_err() {
1198 sublog!(
1199 warn,
1200 "signed",
1201 "reward pot insufficient; fee refund of {:?} to {:?} not paid",
1202 amount,
1203 to
1204 );
1205 Self::deposit_event(Event::<T>::FeeRefundFailed(round, to.clone(), amount));
1206 }
1207 }
1208
1209 fn settle_deposit(round: u32, who: &T::AccountId, deposit: BalanceOf<T>, grace: Perbill) {
1210 let to_refund = grace * deposit;
1211 let to_slash = deposit.defensive_saturating_sub(to_refund);
1212
1213 let _res = T::Currency::release(
1214 &HoldReason::SignedSubmission.into(),
1215 who,
1216 to_refund,
1217 Precision::BestEffort,
1218 )
1219 .defensive();
1220 debug_assert_eq!(_res, Ok(to_refund));
1221
1222 let (credit, remainder) =
1223 T::Currency::slash(&HoldReason::SignedSubmission.into(), who, to_slash);
1224 debug_assert!(remainder.is_zero(), "the full deposit was held; slash must not be partial");
1225 let slashed = credit.peek();
1226 T::Slash::on_unbalanced(credit);
1227 if !slashed.is_zero() {
1228 Self::deposit_event(Event::<T>::Slashed(round, who.clone(), slashed));
1229 }
1230 }
1231
1232 fn handle_solution_rejection(current_round: u32) {
1234 if let Some((loser, metadata)) =
1235 Submissions::<T>::take_leader_with_data(current_round).defensive()
1236 {
1237 let slash = metadata.deposit;
1243 let (credit, remainder) =
1244 T::Currency::slash(&HoldReason::SignedSubmission.into(), &loser, slash);
1245 debug_assert!(
1246 remainder.is_zero(),
1247 "the full deposit was held; slash must not be partial"
1248 );
1249 let slashed = credit.peek();
1250 T::Slash::on_unbalanced(credit);
1251 if !slashed.is_zero() {
1252 Self::deposit_event(Event::<T>::Slashed(current_round, loser.clone(), slashed));
1253 }
1254
1255 if let crate::types::Phase::SignedValidation(remaining_blocks) =
1257 crate::Pallet::<T>::current_phase()
1258 {
1259 if remaining_blocks >= T::Pages::get().into() {
1262 if Submissions::<T>::has_leader(current_round) {
1263 let _ = <T::Verifier as AsynchronousVerifier>::start().defensive();
1266 }
1267 } else {
1268 sublog!(
1269 warn,
1270 "signed",
1271 "SignedValidation phase has {:?} blocks remaining, which are insufficient for {} pages",
1272 remaining_blocks,
1273 T::Pages::get()
1274 );
1275 }
1276 }
1277 } else {
1278 sublog!(
1280 warn,
1281 "signed",
1282 "Tried to slash but no leader was present for round {}",
1283 current_round
1284 );
1285 }
1286 }
1287}