1use core::marker::PhantomData;
21
22use crate::{
23 unsigned::MinerConfig, Config, ElectionCompute, Pallet, QueuedSolution, RawSolution,
24 ReadySolutionOf, SignedSubmissionIndices, SignedSubmissionNextIndex, SignedSubmissionsMap,
25 SnapshotMetadata, SolutionOf, SolutionOrSnapshotSize, Weight, WeightInfo,
26};
27use alloc::{
28 collections::{btree_map::BTreeMap, btree_set::BTreeSet},
29 vec::Vec,
30};
31use codec::{Decode, Encode, HasCompact};
32use core::cmp::Ordering;
33use frame_election_provider_support::NposSolution;
34use frame_support::traits::{
35 defensive_prelude::*, Currency, Get, OnUnbalanced, ReservableCurrency,
36};
37use frame_system::pallet_prelude::BlockNumberFor;
38use sp_arithmetic::traits::SaturatedConversion;
39use sp_core::bounded::BoundedVec;
40use sp_npos_elections::ElectionScore;
41use sp_runtime::{
42 traits::{Convert, Saturating, Zero},
43 FixedPointNumber, FixedPointOperand, FixedU128, Percent, RuntimeDebug,
44};
45
46#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo)]
50pub struct SignedSubmission<AccountId, Balance: HasCompact, Solution> {
51 pub who: AccountId,
53 pub deposit: Balance,
55 pub raw_solution: RawSolution<Solution>,
57 pub call_fee: Balance,
59}
60
61impl<AccountId, Balance, Solution> Ord for SignedSubmission<AccountId, Balance, Solution>
62where
63 AccountId: Ord,
64 Balance: Ord + HasCompact,
65 Solution: Ord,
66 RawSolution<Solution>: Ord,
67{
68 fn cmp(&self, other: &Self) -> Ordering {
69 self.raw_solution
70 .score
71 .cmp(&other.raw_solution.score)
72 .then_with(|| self.raw_solution.cmp(&other.raw_solution))
73 .then_with(|| self.deposit.cmp(&other.deposit))
74 .then_with(|| self.who.cmp(&other.who))
75 }
76}
77
78impl<AccountId, Balance, Solution> PartialOrd for SignedSubmission<AccountId, Balance, Solution>
79where
80 AccountId: Ord,
81 Balance: Ord + HasCompact,
82 Solution: Ord,
83 RawSolution<Solution>: Ord,
84{
85 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
86 Some(self.cmp(other))
87 }
88}
89
90pub type BalanceOf<T> =
91 <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
92pub type PositiveImbalanceOf<T> = <<T as Config>::Currency as Currency<
93 <T as frame_system::Config>::AccountId,
94>>::PositiveImbalance;
95pub type NegativeImbalanceOf<T> = <<T as Config>::Currency as Currency<
96 <T as frame_system::Config>::AccountId,
97>>::NegativeImbalance;
98pub type SignedSubmissionOf<T> = SignedSubmission<
99 <T as frame_system::Config>::AccountId,
100 BalanceOf<T>,
101 <<T as crate::Config>::MinerConfig as MinerConfig>::Solution,
102>;
103
104pub type SubmissionIndicesOf<T> =
107 BoundedVec<(ElectionScore, BlockNumberFor<T>, u32), <T as Config>::SignedMaxSubmissions>;
108
109pub enum InsertResult<T: Config> {
111 NotInserted,
114 Inserted,
116 InsertedEjecting(SignedSubmissionOf<T>),
119}
120
121#[cfg_attr(feature = "std", derive(frame_support::DebugNoBound))]
125pub struct SignedSubmissions<T: Config> {
126 indices: SubmissionIndicesOf<T>,
127 next_idx: u32,
128 insertion_overlay: BTreeMap<u32, SignedSubmissionOf<T>>,
129 deletion_overlay: BTreeSet<u32>,
130}
131
132impl<T: Config> SignedSubmissions<T> {
133 pub fn is_empty(&self) -> bool {
135 self.indices.is_empty()
136 }
137
138 pub fn len(&self) -> usize {
140 self.indices.len()
141 }
142
143 pub fn get() -> Self {
145 let submissions = SignedSubmissions {
146 indices: SignedSubmissionIndices::<T>::get(),
147 next_idx: SignedSubmissionNextIndex::<T>::get(),
148 insertion_overlay: BTreeMap::new(),
149 deletion_overlay: BTreeSet::new(),
150 };
151
152 debug_assert!(submissions
154 .indices
155 .iter()
156 .map(|(_, _, index)| index)
157 .copied()
158 .max()
159 .map_or(true, |max_idx| submissions.next_idx > max_idx,));
160 submissions
161 }
162
163 pub fn put(mut self) {
165 debug_assert!(self
167 .insertion_overlay
168 .keys()
169 .copied()
170 .max()
171 .map_or(true, |max_idx| self.next_idx > max_idx,));
172 debug_assert!(self
173 .indices
174 .iter()
175 .map(|(_, _, index)| index)
176 .copied()
177 .max()
178 .map_or(true, |max_idx| self.next_idx > max_idx,));
179
180 SignedSubmissionIndices::<T>::put(self.indices);
181 SignedSubmissionNextIndex::<T>::put(self.next_idx);
182 for key in self.deletion_overlay {
183 self.insertion_overlay.remove(&key);
184 SignedSubmissionsMap::<T>::remove(key);
185 }
186 for (key, value) in self.insertion_overlay {
187 SignedSubmissionsMap::<T>::insert(key, value);
188 }
189 }
190
191 fn get_submission(&self, index: u32) -> Option<SignedSubmissionOf<T>> {
193 if self.deletion_overlay.contains(&index) {
194 None
198 } else {
199 self.insertion_overlay
200 .get(&index)
201 .cloned()
202 .or_else(|| SignedSubmissionsMap::<T>::get(index))
203 }
204 }
205
206 fn swap_out_submission(
218 &mut self,
219 remove_pos: usize,
220 insert: Option<(ElectionScore, BlockNumberFor<T>, u32)>,
221 ) -> Option<SignedSubmissionOf<T>> {
222 if remove_pos >= self.indices.len() {
223 return None
224 }
225
226 let (_, _, remove_index) = self.indices.remove(remove_pos);
228
229 if let Some((insert_score, block_number, insert_idx)) = insert {
230 self.indices
231 .try_push((insert_score, block_number, insert_idx))
232 .expect("just removed an item, we must be under capacity; qed");
233 }
234
235 self.insertion_overlay.remove(&remove_index).or_else(|| {
236 (!self.deletion_overlay.contains(&remove_index))
237 .then(|| {
238 self.deletion_overlay.insert(remove_index);
239 SignedSubmissionsMap::<T>::get(remove_index)
240 })
241 .flatten()
242 })
243 }
244
245 pub fn pop_last(&mut self) -> Option<SignedSubmissionOf<T>> {
247 let best_index = self.indices.len().checked_sub(1)?;
248 self.swap_out_submission(best_index, None)
249 }
250
251 pub fn iter(&self) -> impl '_ + Iterator<Item = SignedSubmissionOf<T>> {
253 self.indices
254 .iter()
255 .filter_map(move |(_score, _bn, idx)| self.get_submission(*idx).defensive())
256 }
257
258 fn drain_submitted_order(mut self) -> impl Iterator<Item = SignedSubmissionOf<T>> {
266 let mut keys = SignedSubmissionsMap::<T>::iter_keys()
267 .filter(|k| {
268 if self.deletion_overlay.contains(k) {
269 SignedSubmissionsMap::<T>::remove(k);
271 false
272 } else {
273 true
274 }
275 })
276 .chain(self.insertion_overlay.keys().copied())
277 .collect::<Vec<_>>();
278 keys.sort();
279
280 SignedSubmissionIndices::<T>::kill();
281 SignedSubmissionNextIndex::<T>::kill();
282
283 keys.into_iter().filter_map(move |index| {
284 SignedSubmissionsMap::<T>::take(index).or_else(|| self.insertion_overlay.remove(&index))
285 })
286 }
287
288 pub fn decode_len() -> Option<usize> {
294 SignedSubmissionIndices::<T>::decode_len()
295 }
296
297 pub fn insert(&mut self, submission: SignedSubmissionOf<T>) -> InsertResult<T> {
302 debug_assert!(!self.indices.iter().map(|(_, _, x)| x).any(|&idx| idx == self.next_idx));
304 let block_number = frame_system::Pallet::<T>::block_number();
305
306 let maybe_weakest = match self.indices.try_push((
307 submission.raw_solution.score,
308 block_number,
309 self.next_idx,
310 )) {
311 Ok(_) => None,
312 Err(_) => {
313 let weakest_score = match self.indices.iter().next().defensive() {
315 None => return InsertResult::NotInserted,
316 Some((score, _, _)) => *score,
317 };
318 let threshold = T::BetterSignedThreshold::get();
319
320 if !submission.raw_solution.score.strict_threshold_better(weakest_score, threshold)
322 {
323 return InsertResult::NotInserted
324 }
325
326 self.swap_out_submission(
327 0, Some((submission.raw_solution.score, block_number, self.next_idx)),
329 )
330 },
331 };
332
333 self.indices
336 .sort_by(|(score1, bn1, _), (score2, bn2, _)| match score1.cmp(score2) {
337 Ordering::Equal => bn1.cmp(&bn2).reverse(),
338 x => x,
339 });
340
341 debug_assert!(!self.insertion_overlay.contains_key(&self.next_idx));
343 self.insertion_overlay.insert(self.next_idx, submission);
344 debug_assert!(!self.deletion_overlay.contains(&self.next_idx));
345 self.next_idx += 1;
346 match maybe_weakest {
347 Some(weakest) => InsertResult::InsertedEjecting(weakest),
348 None => InsertResult::Inserted,
349 }
350 }
351}
352
353pub struct GeometricDepositBase<Balance, Fixed, Inc> {
358 _marker: (PhantomData<Balance>, PhantomData<Fixed>, PhantomData<Inc>),
359}
360
361impl<Balance, Fixed, Inc> Convert<usize, Balance> for GeometricDepositBase<Balance, Fixed, Inc>
362where
363 Balance: FixedPointOperand,
364 Fixed: Get<Balance>,
365 Inc: Get<Percent>,
366{
367 fn convert(queue_len: usize) -> Balance {
373 let increase_factor: FixedU128 = FixedU128::from_u32(1) + Inc::get().into();
374
375 increase_factor.saturating_pow(queue_len).saturating_mul_int(Fixed::get())
376 }
377}
378
379impl<T: Config> Pallet<T> {
380 pub fn signed_submissions() -> SignedSubmissions<T> {
382 SignedSubmissions::<T>::get()
383 }
384
385 pub fn finalize_signed_phase() -> bool {
396 let (weight, found_solution) = Self::finalize_signed_phase_internal();
397 Self::register_weight(weight);
398 found_solution
399 }
400
401 pub(crate) fn finalize_signed_phase_internal() -> (Weight, bool) {
403 let mut all_submissions = Self::signed_submissions();
404 let mut found_solution = false;
405 let mut weight = T::DbWeight::get().reads(1);
406
407 let SolutionOrSnapshotSize { voters, targets } =
408 SnapshotMetadata::<T>::get().unwrap_or_default();
409
410 while let Some(best) = all_submissions.pop_last() {
411 log!(
412 debug,
413 "finalized_signed: trying to verify from {:?} score {:?}",
414 best.who,
415 best.raw_solution.score
416 );
417 let SignedSubmission { raw_solution, who, deposit, call_fee } = best;
418 let active_voters = raw_solution.solution.voter_count() as u32;
419 let feasibility_weight = {
420 let desired_targets =
422 crate::DesiredTargets::<T>::get().defensive_unwrap_or_default();
423 T::WeightInfo::feasibility_check(voters, targets, active_voters, desired_targets)
424 };
425
426 weight = weight.saturating_add(feasibility_weight);
428 match Self::feasibility_check(raw_solution, ElectionCompute::Signed) {
429 Ok(ready_solution) => {
430 Self::finalize_signed_phase_accept_solution(
431 ready_solution,
432 &who,
433 deposit,
434 call_fee,
435 );
436 found_solution = true;
437 log!(debug, "finalized_signed: found a valid solution");
438
439 weight = weight
440 .saturating_add(T::WeightInfo::finalize_signed_phase_accept_solution());
441 break
442 },
443 Err(_) => {
444 log!(warn, "finalized_signed: invalid signed submission found, slashing.");
445 Self::finalize_signed_phase_reject_solution(&who, deposit);
446 weight = weight
447 .saturating_add(T::WeightInfo::finalize_signed_phase_reject_solution());
448 },
449 }
450 }
451
452 let discarded = all_submissions.len();
455 let mut refund_count = 0;
456 let max_refunds = T::SignedMaxRefunds::get();
457
458 for SignedSubmission { who, deposit, call_fee, .. } in
459 all_submissions.drain_submitted_order()
460 {
461 if refund_count < max_refunds {
462 let positive_imbalance = T::Currency::deposit_creating(&who, call_fee);
464 T::RewardHandler::on_unbalanced(positive_imbalance);
465 refund_count += 1;
466 }
467
468 let _remaining = T::Currency::unreserve(&who, deposit);
470 debug_assert!(_remaining.is_zero());
471 weight = weight.saturating_add(T::DbWeight::get().reads_writes(1, 2));
472 }
473
474 debug_assert!(!SignedSubmissionIndices::<T>::exists());
475 debug_assert!(!SignedSubmissionNextIndex::<T>::exists());
476 debug_assert!(SignedSubmissionsMap::<T>::iter().next().is_none());
477
478 log!(
479 debug,
480 "closed signed phase, found solution? {}, discarded {}",
481 found_solution,
482 discarded
483 );
484
485 (weight, found_solution)
486 }
487 pub fn finalize_signed_phase_accept_solution(
493 ready_solution: ReadySolutionOf<T::MinerConfig>,
494 who: &T::AccountId,
495 deposit: BalanceOf<T>,
496 call_fee: BalanceOf<T>,
497 ) {
498 QueuedSolution::<T>::put(ready_solution);
500
501 let reward = T::SignedRewardBase::get();
502 Self::deposit_event(crate::Event::Rewarded { account: who.clone(), value: reward });
504
505 let _remaining = T::Currency::unreserve(who, deposit);
507 debug_assert!(_remaining.is_zero());
508
509 let positive_imbalance =
511 T::Currency::deposit_creating(who, reward.saturating_add(call_fee));
512 T::RewardHandler::on_unbalanced(positive_imbalance);
513 }
514
515 pub fn finalize_signed_phase_reject_solution(who: &T::AccountId, deposit: BalanceOf<T>) {
521 Self::deposit_event(crate::Event::Slashed { account: who.clone(), value: deposit });
522 let (negative_imbalance, _remaining) = T::Currency::slash_reserved(who, deposit);
523 debug_assert!(_remaining.is_zero());
524 T::SlashHandler::on_unbalanced(negative_imbalance);
525 }
526
527 pub fn solution_weight_of(
529 raw_solution: &RawSolution<SolutionOf<T::MinerConfig>>,
530 size: SolutionOrSnapshotSize,
531 ) -> Weight {
532 T::MinerConfig::solution_weight(
533 size.voters,
534 size.targets,
535 raw_solution.solution.voter_count() as u32,
536 raw_solution.solution.unique_targets().len() as u32,
537 )
538 }
539
540 pub fn deposit_for(
548 raw_solution: &RawSolution<SolutionOf<T::MinerConfig>>,
549 size: SolutionOrSnapshotSize,
550 ) -> BalanceOf<T> {
551 let encoded_len: u32 = raw_solution.encoded_size().saturated_into();
552 let encoded_len_balance: BalanceOf<T> = encoded_len.into();
553 let feasibility_weight = Self::solution_weight_of(raw_solution, size);
554
555 let len_deposit = T::SignedDepositByte::get().saturating_mul(encoded_len_balance);
556 let weight_deposit = T::SignedDepositWeight::get()
557 .saturating_mul(feasibility_weight.ref_time().saturated_into());
558
559 T::SignedDepositBase::convert(Self::signed_submissions().len())
560 .saturating_add(len_deposit)
561 .saturating_add(weight_deposit)
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use crate::{
569 mock::*, CurrentPhase, ElectionCompute, ElectionError, Error, Event, Perbill, Phase, Round,
570 };
571 use frame_election_provider_support::bounds::ElectionBoundsBuilder;
572 use frame_support::{assert_noop, assert_ok, assert_storage_noop};
573 use sp_runtime::Percent;
574
575 #[test]
576 fn cannot_submit_on_different_round() {
577 ExtBuilder::default().build_and_execute(|| {
578 roll_to_round(5);
580 assert_eq!(Round::<Runtime>::get(), 5);
581
582 roll_to_signed();
583 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
584
585 MultiPhase::create_snapshot().unwrap();
587 let mut solution = raw_solution();
588
589 solution.round = Round::<Runtime>::get() - 1;
591
592 assert_noop!(
593 MultiPhase::submit(RuntimeOrigin::signed(10), Box::new(solution)),
594 Error::<Runtime>::PreDispatchDifferentRound,
595 );
596
597 MultiPhase::create_snapshot().unwrap();
599 let mut solution = raw_solution();
600 solution.round = Round::<Runtime>::get() + 1;
601
602 assert_noop!(
603 MultiPhase::submit(RuntimeOrigin::signed(10), Box::new(solution)),
604 Error::<Runtime>::PreDispatchDifferentRound,
605 );
606 })
607 }
608
609 #[test]
610 fn cannot_submit_too_early() {
611 ExtBuilder::default().build_and_execute(|| {
612 roll_to(2);
613 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
614
615 MultiPhase::create_snapshot().unwrap();
617 let solution = raw_solution();
618
619 assert_noop!(
620 MultiPhase::submit(RuntimeOrigin::signed(10), Box::new(solution)),
621 Error::<Runtime>::PreDispatchEarlySubmission,
622 );
623
624 crate::Snapshot::<Runtime>::kill();
626 crate::SnapshotMetadata::<Runtime>::kill();
627 crate::DesiredTargets::<Runtime>::kill();
628 })
629 }
630
631 #[test]
632 fn data_provider_should_respect_target_limits() {
633 ExtBuilder::default().build_and_execute(|| {
634 let new_bounds = ElectionBoundsBuilder::default().targets_count(2.into()).build();
636 ElectionsBounds::set(new_bounds);
637 DataProviderAllowBadData::set(true);
639
640 assert_noop!(
641 MultiPhase::create_snapshot(),
642 ElectionError::DataProvider("Ensure targets bounds: bounds exceeded."),
643 );
644 })
645 }
646
647 #[test]
648 fn data_provider_should_respect_voter_limits() {
649 ExtBuilder::default().build_and_execute(|| {
650 let new_bounds = ElectionBoundsBuilder::default().voters_count(2.into()).build();
652 ElectionsBounds::set(new_bounds);
653 DataProviderAllowBadData::set(true);
655
656 assert_noop!(
657 MultiPhase::create_snapshot(),
658 ElectionError::DataProvider("Ensure voters bounds: bounds exceeded."),
659 );
660 })
661 }
662
663 #[test]
664 fn desired_targets_greater_than_max_winners() {
665 ExtBuilder::default().build_and_execute(|| {
666 DesiredTargets::set(4);
668 MaxWinners::set(3);
669
670 assert_noop!(
673 MultiPhase::create_snapshot_external(),
674 ElectionError::DataProvider("desired_targets must not be greater than MaxWinners."),
675 );
676 })
677 }
678
679 #[test]
680 fn should_pay_deposit() {
681 ExtBuilder::default().build_and_execute(|| {
682 roll_to_signed();
683 assert!(CurrentPhase::<Runtime>::get().is_signed());
684
685 let solution = raw_solution();
686 assert_eq!(balances(&99), (100, 0));
687
688 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
689
690 assert_eq!(balances(&99), (95, 5));
691 assert_eq!(MultiPhase::signed_submissions().iter().next().unwrap().deposit, 5);
692
693 assert_eq!(
694 multi_phase_events(),
695 vec![
696 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
697 Event::SolutionStored {
698 compute: ElectionCompute::Signed,
699 origin: Some(99),
700 prev_ejected: false
701 }
702 ]
703 );
704 })
705 }
706
707 #[test]
708 fn good_solution_is_rewarded() {
709 ExtBuilder::default().build_and_execute(|| {
710 roll_to_signed();
711 assert!(CurrentPhase::<Runtime>::get().is_signed());
712
713 let solution = raw_solution();
714 assert_eq!(balances(&99), (100, 0));
715
716 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
717 assert_eq!(balances(&99), (95, 5));
718
719 assert!(MultiPhase::finalize_signed_phase());
720 assert_eq!(balances(&99), (100 + 7 + 8, 0));
721
722 assert_eq!(
723 multi_phase_events(),
724 vec![
725 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
726 Event::SolutionStored {
727 compute: ElectionCompute::Signed,
728 origin: Some(99),
729 prev_ejected: false
730 },
731 Event::Rewarded { account: 99, value: 7 }
732 ]
733 );
734 })
735 }
736
737 #[test]
738 fn bad_solution_is_slashed() {
739 ExtBuilder::default().build_and_execute(|| {
740 roll_to_signed();
741 assert!(CurrentPhase::<Runtime>::get().is_signed());
742
743 let mut solution = raw_solution();
744 assert_eq!(balances(&99), (100, 0));
745
746 solution.score.minimal_stake += 1;
748
749 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
750 assert_eq!(balances(&99), (95, 5));
751
752 assert!(!MultiPhase::finalize_signed_phase());
754 assert_eq!(balances(&99), (95, 0));
756
757 assert_eq!(
758 multi_phase_events(),
759 vec![
760 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
761 Event::SolutionStored {
762 compute: ElectionCompute::Signed,
763 origin: Some(99),
764 prev_ejected: false
765 },
766 Event::Slashed { account: 99, value: 5 }
767 ]
768 );
769 })
770 }
771
772 #[test]
773 fn suppressed_solution_gets_bond_back() {
774 ExtBuilder::default().build_and_execute(|| {
775 roll_to_signed();
776 assert!(CurrentPhase::<Runtime>::get().is_signed());
777
778 let mut solution = raw_solution();
779 assert_eq!(balances(&99), (100, 0));
780 assert_eq!(balances(&999), (100, 0));
781
782 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution.clone())));
784
785 solution.score.minimal_stake -= 1;
787 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(999), Box::new(solution)));
788 assert_eq!(balances(&99), (95, 5));
789 assert_eq!(balances(&999), (95, 5));
790
791 assert!(MultiPhase::finalize_signed_phase());
793
794 assert_eq!(balances(&99), (100 + 7 + 8, 0));
796 assert_eq!(balances(&999), (100 + 8, 0));
798 assert_eq!(
799 multi_phase_events(),
800 vec![
801 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
802 Event::SolutionStored {
803 compute: ElectionCompute::Signed,
804 origin: Some(99),
805 prev_ejected: false
806 },
807 Event::SolutionStored {
808 compute: ElectionCompute::Signed,
809 origin: Some(999),
810 prev_ejected: false
811 },
812 Event::Rewarded { account: 99, value: 7 }
813 ]
814 );
815 })
816 }
817
818 #[test]
819 fn cannot_submit_worse_with_full_queue() {
820 ExtBuilder::default().build_and_execute(|| {
821 roll_to_signed();
822 assert!(CurrentPhase::<Runtime>::get().is_signed());
823
824 for s in 0..SignedMaxSubmissions::get() {
825 let solution = RawSolution {
827 score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
828 ..Default::default()
829 };
830 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
831 }
832
833 let solution = RawSolution {
835 score: ElectionScore { minimal_stake: 4, ..Default::default() },
836 ..Default::default()
837 };
838
839 assert_noop!(
840 MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)),
841 Error::<Runtime>::SignedQueueFull,
842 );
843 })
844 }
845
846 #[test]
847 fn geometric_deposit_queue_size_works() {
848 let constant = vec![1000; 10];
849 let progression_10 = vec![1000, 1100, 1210, 1331, 1464, 1610, 1771, 1948, 2143, 2357];
851 let progression_40 = vec![1000, 1400, 1960, 2744, 3841, 5378, 7529, 10541, 14757, 20661];
852
853 let check_progressive_base_fee = |expected: &Vec<u64>| {
854 for s in 0..SignedMaxSubmissions::get() {
855 let account = 99 + s as u64;
856 Balances::make_free_balance_be(&account, 10000000);
857 let mut solution = raw_solution();
858 solution.score.minimal_stake -= s as u128;
859
860 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(account), Box::new(solution)));
861 assert_eq!(balances(&account).1, expected[s as usize])
862 }
863 };
864
865 ExtBuilder::default()
866 .signed_max_submission(10)
867 .signed_base_deposit(1000, true, Percent::from_percent(0))
868 .build_and_execute(|| {
869 roll_to_signed();
870 assert!(CurrentPhase::<Runtime>::get().is_signed());
871
872 check_progressive_base_fee(&constant);
873 });
874
875 ExtBuilder::default()
876 .signed_max_submission(10)
877 .signed_base_deposit(1000, true, Percent::from_percent(10))
878 .build_and_execute(|| {
879 roll_to_signed();
880 assert!(CurrentPhase::<Runtime>::get().is_signed());
881
882 check_progressive_base_fee(&progression_10);
883 });
884
885 ExtBuilder::default()
886 .signed_max_submission(10)
887 .signed_base_deposit(1000, true, Percent::from_percent(40))
888 .build_and_execute(|| {
889 roll_to_signed();
890 assert!(CurrentPhase::<Runtime>::get().is_signed());
891
892 check_progressive_base_fee(&progression_40);
893 });
894 }
895
896 #[test]
897 fn call_fee_refund_is_limited_by_signed_max_refunds() {
898 ExtBuilder::default().build_and_execute(|| {
899 roll_to_signed();
900 assert!(CurrentPhase::<Runtime>::get().is_signed());
901 assert_eq!(SignedMaxRefunds::get(), 1);
902 assert!(SignedMaxSubmissions::get() > 2);
903
904 for s in 0..SignedMaxSubmissions::get() {
905 let account = 99 + s as u64;
906 Balances::make_free_balance_be(&account, 100);
907 let mut solution = raw_solution();
909 solution.score.minimal_stake -= s as u128;
910
911 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(account), Box::new(solution)));
912 assert_eq!(balances(&account), (95, 5));
913 }
914
915 assert_ok!(MultiPhase::do_elect());
916
917 for s in 0..SignedMaxSubmissions::get() {
918 let account = 99 + s as u64;
919 if s == 0 {
921 assert_eq!(balances(&account), (100 + 8 + 7, 0))
923 } else if s == 1 {
924 assert_eq!(balances(&account), (100 + 8, 0))
926 } else {
927 assert_eq!(balances(&account), (100, 0));
929 }
930 }
931 assert_eq!(
932 multi_phase_events(),
933 vec![
934 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
935 Event::SolutionStored {
936 compute: ElectionCompute::Signed,
937 origin: Some(99),
938 prev_ejected: false
939 },
940 Event::SolutionStored {
941 compute: ElectionCompute::Signed,
942 origin: Some(100),
943 prev_ejected: false
944 },
945 Event::SolutionStored {
946 compute: ElectionCompute::Signed,
947 origin: Some(101),
948 prev_ejected: false
949 },
950 Event::SolutionStored {
951 compute: ElectionCompute::Signed,
952 origin: Some(102),
953 prev_ejected: false
954 },
955 Event::SolutionStored {
956 compute: ElectionCompute::Signed,
957 origin: Some(103),
958 prev_ejected: false
959 },
960 Event::Rewarded { account: 99, value: 7 },
961 Event::ElectionFinalized {
962 compute: ElectionCompute::Signed,
963 score: ElectionScore {
964 minimal_stake: 40,
965 sum_stake: 100,
966 sum_stake_squared: 5200
967 }
968 }
969 ]
970 );
971 });
972 }
973
974 #[test]
975 fn cannot_submit_worse_with_full_queue_depends_on_threshold() {
976 ExtBuilder::default()
977 .signed_max_submission(1)
978 .better_signed_threshold(Perbill::from_percent(20))
979 .build_and_execute(|| {
980 roll_to_signed();
981 assert!(CurrentPhase::<Runtime>::get().is_signed());
982
983 let mut solution = RawSolution {
984 score: ElectionScore {
985 minimal_stake: 5u128,
986 sum_stake: 0u128,
987 sum_stake_squared: 10u128,
988 },
989 ..Default::default()
990 };
991 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
992
993 solution = RawSolution {
995 score: ElectionScore {
996 minimal_stake: 5u128,
997 sum_stake: 0u128,
998 sum_stake_squared: 9u128,
999 },
1000 ..Default::default()
1001 };
1002
1003 assert_noop!(
1004 MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)),
1005 Error::<Runtime>::SignedQueueFull,
1006 );
1007
1008 solution = RawSolution {
1010 score: ElectionScore {
1011 minimal_stake: 5u128,
1012 sum_stake: 0u128,
1013 sum_stake_squared: 7u128,
1014 },
1015 ..Default::default()
1016 };
1017
1018 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1019 assert_eq!(
1020 multi_phase_events(),
1021 vec![
1022 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1023 Event::SolutionStored {
1024 compute: ElectionCompute::Signed,
1025 origin: Some(99),
1026 prev_ejected: false
1027 },
1028 Event::SolutionStored {
1029 compute: ElectionCompute::Signed,
1030 origin: Some(99),
1031 prev_ejected: true
1032 }
1033 ]
1034 );
1035 })
1036 }
1037
1038 #[test]
1039 fn weakest_is_removed_if_better_provided() {
1040 ExtBuilder::default().build_and_execute(|| {
1041 roll_to_signed();
1042 assert!(CurrentPhase::<Runtime>::get().is_signed());
1043
1044 for s in 0..SignedMaxSubmissions::get() {
1045 let account = 99 + s as u64;
1046 Balances::make_free_balance_be(&account, 100);
1047 let solution = RawSolution {
1049 score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
1050 ..Default::default()
1051 };
1052 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(account), Box::new(solution)));
1053 assert_eq!(balances(&account), (95, 5));
1054 }
1055
1056 assert_eq!(
1057 MultiPhase::signed_submissions()
1058 .iter()
1059 .map(|s| s.raw_solution.score.minimal_stake)
1060 .collect::<Vec<_>>(),
1061 vec![5, 6, 7, 8, 9]
1062 );
1063
1064 let solution = RawSolution {
1066 score: ElectionScore { minimal_stake: 20, ..Default::default() },
1067 ..Default::default()
1068 };
1069 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(999), Box::new(solution)));
1070
1071 assert_eq!(
1073 MultiPhase::signed_submissions()
1074 .iter()
1075 .map(|s| s.raw_solution.score.minimal_stake)
1076 .collect::<Vec<_>>(),
1077 vec![6, 7, 8, 9, 20]
1078 );
1079
1080 assert_eq!(balances(&(99 + 0)), (100, 0));
1082 })
1083 }
1084
1085 #[test]
1086 fn replace_weakest_by_score_works() {
1087 ExtBuilder::default().signed_max_submission(3).build_and_execute(|| {
1088 roll_to_signed();
1089 assert!(CurrentPhase::<Runtime>::get().is_signed());
1090
1091 for s in 1..SignedMaxSubmissions::get() {
1092 let solution = RawSolution {
1094 score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
1095 ..Default::default()
1096 };
1097 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1098 }
1099
1100 let solution = RawSolution {
1101 score: ElectionScore { minimal_stake: 4, ..Default::default() },
1102 ..Default::default()
1103 };
1104 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1105
1106 assert_eq!(
1107 MultiPhase::signed_submissions()
1108 .iter()
1109 .map(|s| s.raw_solution.score.minimal_stake)
1110 .collect::<Vec<_>>(),
1111 vec![4, 6, 7],
1112 );
1113
1114 let solution = RawSolution {
1116 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1117 ..Default::default()
1118 };
1119 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1120
1121 assert_eq!(
1123 MultiPhase::signed_submissions()
1124 .iter()
1125 .map(|s| s.raw_solution.score.minimal_stake)
1126 .collect::<Vec<_>>(),
1127 vec![5, 6, 7],
1128 );
1129 })
1130 }
1131
1132 #[test]
1133 fn early_ejected_solution_gets_bond_back() {
1134 ExtBuilder::default().signed_deposit(2, 0, 0).build_and_execute(|| {
1135 roll_to_signed();
1136 assert!(CurrentPhase::<Runtime>::get().is_signed());
1137
1138 for s in 0..SignedMaxSubmissions::get() {
1139 let solution = RawSolution {
1141 score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
1142 ..Default::default()
1143 };
1144 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1145 }
1146
1147 assert_eq!(balances(&99).1, 2 * 5);
1148 assert_eq!(balances(&999).1, 0);
1149
1150 let solution = RawSolution {
1152 score: ElectionScore { minimal_stake: 20, ..Default::default() },
1153 ..Default::default()
1154 };
1155 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(999), Box::new(solution)));
1156
1157 assert_eq!(balances(&99).1, 2 * 4);
1159 assert_eq!(balances(&999).1, 2);
1160 })
1161 }
1162
1163 #[test]
1164 fn equally_good_solution_is_not_accepted_when_queue_full() {
1165 ExtBuilder::default().signed_max_submission(3).build_and_execute(|| {
1167 roll_to_signed();
1168 assert!(CurrentPhase::<Runtime>::get().is_signed());
1169
1170 for i in 0..SignedMaxSubmissions::get() {
1171 let solution = RawSolution {
1172 score: ElectionScore { minimal_stake: (5 + i).into(), ..Default::default() },
1173 ..Default::default()
1174 };
1175 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1176 }
1177
1178 assert_eq!(
1179 MultiPhase::signed_submissions()
1180 .iter()
1181 .map(|s| s.raw_solution.score.minimal_stake)
1182 .collect::<Vec<_>>(),
1183 vec![5, 6, 7]
1184 );
1185
1186 let solution = RawSolution {
1188 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1189 ..Default::default()
1190 };
1191 assert_noop!(
1192 MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)),
1193 Error::<Runtime>::SignedQueueFull,
1194 );
1195 })
1196 }
1197
1198 #[test]
1199 fn equally_good_solution_is_accepted_when_queue_not_full() {
1200 ExtBuilder::default().signed_max_submission(3).build_and_execute(|| {
1202 roll_to(15);
1203 assert!(CurrentPhase::<Runtime>::get().is_signed());
1204
1205 let solution = RawSolution {
1206 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1207 ..Default::default()
1208 };
1209 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1210
1211 assert_eq!(
1212 MultiPhase::signed_submissions()
1213 .iter()
1214 .map(|s| (s.who, s.raw_solution.score.minimal_stake,))
1215 .collect::<Vec<_>>(),
1216 vec![(99, 5)]
1217 );
1218
1219 roll_to(16);
1220 let solution = RawSolution {
1221 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1222 ..Default::default()
1223 };
1224 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(999), Box::new(solution)));
1225
1226 assert_eq!(
1227 MultiPhase::signed_submissions()
1228 .iter()
1229 .map(|s| (s.who, s.raw_solution.score.minimal_stake,))
1230 .collect::<Vec<_>>(),
1231 vec![(999, 5), (99, 5)]
1232 );
1233
1234 let solution = RawSolution {
1235 score: ElectionScore { minimal_stake: 6, ..Default::default() },
1236 ..Default::default()
1237 };
1238 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(9999), Box::new(solution)));
1239
1240 assert_eq!(
1241 MultiPhase::signed_submissions()
1242 .iter()
1243 .map(|s| (s.who, s.raw_solution.score.minimal_stake,))
1244 .collect::<Vec<_>>(),
1245 vec![(999, 5), (99, 5), (9999, 6)]
1246 );
1247 })
1248 }
1249
1250 #[test]
1251 fn all_equal_score() {
1252 ExtBuilder::default().signed_max_submission(3).build_and_execute(|| {
1254 roll_to(15);
1255 assert!(CurrentPhase::<Runtime>::get().is_signed());
1256
1257 for i in 0..SignedMaxSubmissions::get() {
1258 roll_to((15 + i).into());
1259 let solution = raw_solution();
1260 assert_ok!(MultiPhase::submit(
1261 RuntimeOrigin::signed(100 + i as AccountId),
1262 Box::new(solution)
1263 ));
1264 }
1265
1266 assert_eq!(
1267 MultiPhase::signed_submissions()
1268 .iter()
1269 .map(|s| (s.who, s.raw_solution.score.minimal_stake))
1270 .collect::<Vec<_>>(),
1271 vec![(102, 40), (101, 40), (100, 40)]
1272 );
1273
1274 roll_to(25);
1275
1276 assert_eq!(
1278 multi_phase_events(),
1279 vec![
1280 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1281 Event::SolutionStored {
1282 compute: ElectionCompute::Signed,
1283 origin: Some(100),
1284 prev_ejected: false
1285 },
1286 Event::SolutionStored {
1287 compute: ElectionCompute::Signed,
1288 origin: Some(101),
1289 prev_ejected: false
1290 },
1291 Event::SolutionStored {
1292 compute: ElectionCompute::Signed,
1293 origin: Some(102),
1294 prev_ejected: false
1295 },
1296 Event::Rewarded { account: 100, value: 7 },
1297 Event::PhaseTransitioned {
1298 from: Phase::Signed,
1299 to: Phase::Unsigned((true, 25)),
1300 round: 1
1301 },
1302 ]
1303 );
1304 })
1305 }
1306
1307 #[test]
1308 fn all_in_one_signed_submission_scenario() {
1309 ExtBuilder::default().build_and_execute(|| {
1314 roll_to_signed();
1315 assert!(CurrentPhase::<Runtime>::get().is_signed());
1316
1317 assert_eq!(balances(&99), (100, 0));
1318 assert_eq!(balances(&999), (100, 0));
1319 assert_eq!(balances(&9999), (100, 0));
1320
1321 let solution = raw_solution();
1322
1323 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution.clone())));
1325
1326 let mut solution_999 = solution.clone();
1328 solution_999.score.minimal_stake += 1;
1329 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(999), Box::new(solution_999)));
1330
1331 let mut solution_9999 = solution.clone();
1334 solution_9999.score.minimal_stake -= 1;
1335 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(9999), Box::new(solution_9999)));
1336
1337 assert_eq!(
1338 MultiPhase::signed_submissions().iter().map(|x| x.who).collect::<Vec<_>>(),
1339 vec![9999, 99, 999]
1340 );
1341
1342 assert!(MultiPhase::finalize_signed_phase());
1344
1345 assert_eq!(balances(&99), (100 + 7 + 8, 0));
1347 assert_eq!(balances(&999), (95, 0));
1349 assert_eq!(balances(&9999), (100 + 8, 0));
1351 assert_eq!(
1352 multi_phase_events(),
1353 vec![
1354 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1355 Event::SolutionStored {
1356 compute: ElectionCompute::Signed,
1357 origin: Some(99),
1358 prev_ejected: false
1359 },
1360 Event::SolutionStored {
1361 compute: ElectionCompute::Signed,
1362 origin: Some(999),
1363 prev_ejected: false
1364 },
1365 Event::SolutionStored {
1366 compute: ElectionCompute::Signed,
1367 origin: Some(9999),
1368 prev_ejected: false
1369 },
1370 Event::Slashed { account: 999, value: 5 },
1371 Event::Rewarded { account: 99, value: 7 }
1372 ]
1373 );
1374 })
1375 }
1376
1377 #[test]
1378 fn cannot_consume_too_much_future_weight() {
1379 ExtBuilder::default()
1380 .signed_weight(Weight::from_parts(40, u64::MAX))
1381 .mock_weight_info(MockedWeightInfo::Basic)
1382 .build_and_execute(|| {
1383 roll_to_signed();
1384 assert!(CurrentPhase::<Runtime>::get().is_signed());
1385
1386 let (raw, witness, _) = MultiPhase::mine_solution().unwrap();
1387 let solution_weight = <Runtime as MinerConfig>::solution_weight(
1388 witness.voters,
1389 witness.targets,
1390 raw.solution.voter_count() as u32,
1391 raw.solution.unique_targets().len() as u32,
1392 );
1393 assert_eq!(solution_weight, Weight::from_parts(35, 0));
1395 assert_eq!(raw.solution.voter_count(), 5);
1396 assert_eq!(
1397 <Runtime as Config>::SignedMaxWeight::get(),
1398 Weight::from_parts(40, u64::MAX)
1399 );
1400
1401 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(raw.clone())));
1402
1403 <SignedMaxWeight>::set(Weight::from_parts(30, u64::MAX));
1404
1405 assert_noop!(
1408 MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(raw)),
1409 Error::<Runtime>::SignedTooMuchWeight,
1410 );
1411 })
1412 }
1413
1414 #[test]
1415 fn insufficient_deposit_does_not_store_submission() {
1416 ExtBuilder::default().build_and_execute(|| {
1417 roll_to_signed();
1418 assert!(CurrentPhase::<Runtime>::get().is_signed());
1419
1420 let solution = raw_solution();
1421
1422 assert_eq!(balances(&123), (0, 0));
1423 assert_noop!(
1424 MultiPhase::submit(RuntimeOrigin::signed(123), Box::new(solution)),
1425 Error::<Runtime>::SignedCannotPayDeposit,
1426 );
1427
1428 assert_eq!(balances(&123), (0, 0));
1429 })
1430 }
1431
1432 #[test]
1435 fn insufficient_deposit_with_full_queue_works_properly() {
1436 ExtBuilder::default().build_and_execute(|| {
1437 roll_to_signed();
1438 assert!(CurrentPhase::<Runtime>::get().is_signed());
1439
1440 for s in 0..SignedMaxSubmissions::get() {
1441 let solution = RawSolution {
1443 score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
1444 ..Default::default()
1445 };
1446 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1447 }
1448
1449 let solution = RawSolution {
1451 score: ElectionScore {
1452 minimal_stake: (5 + SignedMaxSubmissions::get()).into(),
1453 ..Default::default()
1454 },
1455 ..Default::default()
1456 };
1457
1458 assert_eq!(balances(&123), (0, 0));
1459 assert_noop!(
1460 MultiPhase::submit(RuntimeOrigin::signed(123), Box::new(solution)),
1461 Error::<Runtime>::SignedCannotPayDeposit,
1462 );
1463
1464 assert_eq!(balances(&123), (0, 0));
1465 })
1466 }
1467
1468 #[test]
1469 fn finalize_signed_phase_is_idempotent_given_no_submissions() {
1470 ExtBuilder::default().build_and_execute(|| {
1471 for block_number in 0..25 {
1472 roll_to(block_number);
1473
1474 assert_eq!(SignedSubmissions::<Runtime>::decode_len().unwrap_or_default(), 0);
1475 assert_storage_noop!(MultiPhase::finalize_signed_phase_internal());
1476 }
1477 })
1478 }
1479
1480 #[test]
1481 fn finalize_signed_phase_is_idempotent_given_submissions() {
1482 ExtBuilder::default().build_and_execute(|| {
1483 roll_to_signed();
1484 assert!(CurrentPhase::<Runtime>::get().is_signed());
1485
1486 let solution = raw_solution();
1487
1488 assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1490
1491 assert!(MultiPhase::finalize_signed_phase());
1493
1494 assert_storage_noop!(MultiPhase::finalize_signed_phase());
1496
1497 assert_eq!(
1498 multi_phase_events(),
1499 vec![
1500 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1501 Event::SolutionStored {
1502 compute: ElectionCompute::Signed,
1503 origin: Some(99),
1504 prev_ejected: false
1505 },
1506 Event::Rewarded { account: 99, value: 7 }
1507 ]
1508 );
1509 })
1510 }
1511}