1#![cfg_attr(not(feature = "std"), no_std)]
243
244extern crate alloc;
245
246use alloc::{boxed::Box, vec::Vec};
247use codec::{Decode, DecodeWithMemTracking, Encode};
248use frame_election_provider_support::{
249 bounds::{CountBound, ElectionBounds, SizeBound},
250 BoundedSupports, BoundedSupportsOf, ElectionDataProvider, ElectionProvider,
251 InstantElectionProvider, NposSolution, PageIndex,
252};
253use frame_support::{
254 dispatch::DispatchClass,
255 ensure,
256 traits::{Currency, Get, OnUnbalanced, ReservableCurrency},
257 weights::Weight,
258 DefaultNoBound, EqNoBound, PartialEqNoBound,
259};
260use frame_system::{ensure_none, offchain::CreateBare, pallet_prelude::BlockNumberFor};
261use scale_info::TypeInfo;
262use sp_arithmetic::{
263 traits::{CheckedAdd, Zero},
264 UpperOf,
265};
266use sp_npos_elections::{ElectionScore, IdentifierT, Supports, VoteWeight};
267use sp_runtime::{
268 transaction_validity::{
269 InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
270 TransactionValidityError, ValidTransaction,
271 },
272 Debug, DispatchError, ModuleError, PerThing, Perbill, SaturatedConversion,
273};
274
275#[cfg(feature = "try-runtime")]
276use sp_runtime::TryRuntimeError;
277
278#[cfg(feature = "runtime-benchmarks")]
279mod benchmarking;
280#[cfg(test)]
281mod mock;
282#[cfg(all(test, feature = "remote-mining"))]
283mod remote_mining;
284#[macro_use]
285pub mod helpers;
286
287pub(crate) const SINGLE_PAGE: u32 = 0;
289const LOG_TARGET: &str = "runtime::election-provider";
290
291pub mod migrations;
292pub mod signed;
293pub mod unsigned;
294pub mod weights;
295
296pub use signed::{
297 BalanceOf, GeometricDepositBase, NegativeImbalanceOf, PositiveImbalanceOf, SignedSubmission,
298 SignedSubmissionOf, SignedSubmissions, SubmissionIndicesOf,
299};
300use unsigned::VoterOf;
301pub use unsigned::{Miner, MinerConfig};
302pub use weights::WeightInfo;
303
304pub type SolutionOf<T> = <T as MinerConfig>::Solution;
306pub type SolutionVoterIndexOf<T> = <SolutionOf<T> as NposSolution>::VoterIndex;
308pub type SolutionTargetIndexOf<T> = <SolutionOf<T> as NposSolution>::TargetIndex;
310pub type SolutionAccuracyOf<T> =
312 <SolutionOf<<T as crate::Config>::MinerConfig> as NposSolution>::Accuracy;
313pub type ReadySolutionOf<T> = ReadySolution<
315 <T as MinerConfig>::AccountId,
316 <T as MinerConfig>::MaxWinners,
317 <T as MinerConfig>::MaxBackersPerWinner,
318>;
319pub type FallbackErrorOf<T> = <<T as crate::Config>::Fallback as ElectionProvider>::Error;
321
322pub trait BenchmarkingConfig {
324 const VOTERS: [u32; 2];
326 const TARGETS: [u32; 2];
328 const ACTIVE_VOTERS: [u32; 2];
330 const DESIRED_TARGETS: [u32; 2];
332 const SNAPSHOT_MAXIMUM_VOTERS: u32;
334 const MINER_MAXIMUM_VOTERS: u32;
336 const MAXIMUM_TARGETS: u32;
338}
339
340#[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, DecodeWithMemTracking, Debug, TypeInfo)]
342pub enum Phase<Bn> {
343 Off,
345 Signed,
347 Unsigned((bool, Bn)),
358 Emergency,
362}
363
364impl<Bn> Default for Phase<Bn> {
365 fn default() -> Self {
366 Phase::Off
367 }
368}
369
370impl<Bn: PartialEq + Eq> Phase<Bn> {
371 pub fn is_emergency(&self) -> bool {
373 matches!(self, Phase::Emergency)
374 }
375
376 pub fn is_signed(&self) -> bool {
378 matches!(self, Phase::Signed)
379 }
380
381 pub fn is_unsigned(&self) -> bool {
383 matches!(self, Phase::Unsigned(_))
384 }
385
386 pub fn is_unsigned_open_at(&self, at: Bn) -> bool {
388 matches!(self, Phase::Unsigned((true, real)) if *real == at)
389 }
390
391 pub fn is_unsigned_open(&self) -> bool {
393 matches!(self, Phase::Unsigned((true, _)))
394 }
395
396 pub fn is_off(&self) -> bool {
398 matches!(self, Phase::Off)
399 }
400}
401
402#[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, DecodeWithMemTracking, Debug, TypeInfo)]
404pub enum ElectionCompute {
405 OnChain,
407 Signed,
409 Unsigned,
411 Fallback,
413 Emergency,
415}
416
417impl Default for ElectionCompute {
418 fn default() -> Self {
419 ElectionCompute::OnChain
420 }
421}
422
423#[derive(
430 PartialEq, Eq, Clone, Encode, Decode, DecodeWithMemTracking, Debug, PartialOrd, Ord, TypeInfo,
431)]
432pub struct RawSolution<S> {
433 pub solution: S,
435 pub score: ElectionScore,
437 pub round: u32,
439}
440
441impl<C: Default> Default for RawSolution<C> {
442 fn default() -> Self {
443 Self { round: 1, solution: Default::default(), score: Default::default() }
445 }
446}
447
448#[derive(
450 PartialEqNoBound, EqNoBound, Clone, Encode, Decode, Debug, DefaultNoBound, scale_info::TypeInfo,
451)]
452#[scale_info(skip_type_params(AccountId, MaxWinners, MaxBackersPerWinner))]
453pub struct ReadySolution<AccountId, MaxWinners, MaxBackersPerWinner>
454where
455 AccountId: IdentifierT,
456 MaxWinners: Get<u32>,
457 MaxBackersPerWinner: Get<u32>,
458{
459 pub supports: BoundedSupports<AccountId, MaxWinners, MaxBackersPerWinner>,
464 pub score: ElectionScore,
468 pub compute: ElectionCompute,
470}
471
472#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, Default, TypeInfo)]
477#[scale_info(skip_type_params(T))]
478pub struct RoundSnapshot<AccountId, VoterType> {
479 pub voters: Vec<VoterType>,
481 pub targets: Vec<AccountId>,
483}
484
485#[derive(
491 PartialEq, Eq, Clone, Copy, Encode, Decode, DecodeWithMemTracking, Debug, Default, TypeInfo,
492)]
493pub struct SolutionOrSnapshotSize {
494 #[codec(compact)]
496 pub voters: u32,
497 #[codec(compact)]
499 pub targets: u32,
500}
501
502#[derive(frame_support::DebugNoBound)]
506#[cfg_attr(feature = "runtime-benchmarks", derive(strum::IntoStaticStr))]
507pub enum ElectionError<T: Config> {
508 Feasibility(FeasibilityError),
510 Miner(unsigned::MinerError),
512 DataProvider(&'static str),
514 Fallback(FallbackErrorOf<T>),
516 MultiPageNotSupported,
519 NothingQueued,
521}
522
523impl<T: Config> PartialEq for ElectionError<T>
526where
527 FallbackErrorOf<T>: PartialEq,
528{
529 fn eq(&self, other: &Self) -> bool {
530 use ElectionError::*;
531 match (self, other) {
532 (Feasibility(x), Feasibility(y)) if x == y => true,
533 (Miner(x), Miner(y)) if x == y => true,
534 (DataProvider(x), DataProvider(y)) if x == y => true,
535 (Fallback(x), Fallback(y)) if x == y => true,
536 (MultiPageNotSupported, MultiPageNotSupported) => true,
537 (NothingQueued, NothingQueued) => true,
538 _ => false,
539 }
540 }
541}
542
543impl<T: Config> From<FeasibilityError> for ElectionError<T> {
544 fn from(e: FeasibilityError) -> Self {
545 ElectionError::Feasibility(e)
546 }
547}
548
549impl<T: Config> From<unsigned::MinerError> for ElectionError<T> {
550 fn from(e: unsigned::MinerError) -> Self {
551 ElectionError::Miner(e)
552 }
553}
554
555#[derive(Debug, Eq, PartialEq)]
557#[cfg_attr(feature = "runtime-benchmarks", derive(strum::IntoStaticStr))]
558pub enum FeasibilityError {
559 WrongWinnerCount,
561 SnapshotUnavailable,
566 NposElection(sp_npos_elections::Error),
568 InvalidVote,
570 InvalidVoter,
572 InvalidScore,
574 InvalidRound,
576 UntrustedScoreTooLow,
578 TooManyDesiredTargets,
580 BoundedConversionFailed,
584}
585
586impl From<sp_npos_elections::Error> for FeasibilityError {
587 fn from(e: sp_npos_elections::Error) -> Self {
588 FeasibilityError::NposElection(e)
589 }
590}
591
592pub use pallet::*;
593#[frame_support::pallet]
594pub mod pallet {
595 use super::*;
596 use frame_election_provider_support::{InstantElectionProvider, NposSolver};
597 use frame_support::{pallet_prelude::*, traits::EstimateCallFee};
598 use frame_system::pallet_prelude::*;
599 use sp_runtime::traits::Convert;
600
601 #[pallet::config]
602 pub trait Config: frame_system::Config + CreateBare<Call<Self>> {
603 #[allow(deprecated)]
604 type RuntimeEvent: From<Event<Self>>
605 + IsType<<Self as frame_system::Config>::RuntimeEvent>
606 + TryInto<Event<Self>>;
607
608 type Currency: ReservableCurrency<Self::AccountId> + Currency<Self::AccountId>;
610
611 type EstimateCallFee: EstimateCallFee<Call<Self>, BalanceOf<Self>>;
613
614 type UnsignedPhase: Get<BlockNumberFor<Self>>;
616 type SignedPhase: Get<BlockNumberFor<Self>>;
618
619 #[pallet::constant]
622 type BetterSignedThreshold: Get<Perbill>;
623
624 #[pallet::constant]
629 type OffchainRepeat: Get<BlockNumberFor<Self>>;
630
631 #[pallet::constant]
633 type MinerTxPriority: Get<TransactionPriority>;
634
635 type MinerConfig: crate::unsigned::MinerConfig<
640 AccountId = Self::AccountId,
641 MaxVotesPerVoter = <Self::DataProvider as ElectionDataProvider>::MaxVotesPerVoter,
642 MaxWinners = Self::MaxWinners,
643 MaxBackersPerWinner = Self::MaxBackersPerWinner,
644 >;
645
646 #[pallet::constant]
654 type SignedMaxSubmissions: Get<u32>;
655
656 #[pallet::constant]
662 type SignedMaxWeight: Get<Weight>;
663
664 #[pallet::constant]
666 type SignedMaxRefunds: Get<u32>;
667
668 #[pallet::constant]
670 type SignedRewardBase: Get<BalanceOf<Self>>;
671
672 #[pallet::constant]
674 type SignedDepositByte: Get<BalanceOf<Self>>;
675
676 #[pallet::constant]
678 type SignedDepositWeight: Get<BalanceOf<Self>>;
679
680 #[pallet::constant]
684 type MaxWinners: Get<u32>;
685
686 #[pallet::constant]
690 type MaxBackersPerWinner: Get<u32>;
691
692 type SignedDepositBase: Convert<usize, BalanceOf<Self>>;
695
696 type ElectionBounds: Get<ElectionBounds>;
698
699 type SlashHandler: OnUnbalanced<NegativeImbalanceOf<Self>>;
701
702 type RewardHandler: OnUnbalanced<PositiveImbalanceOf<Self>>;
704
705 type DataProvider: ElectionDataProvider<
707 AccountId = Self::AccountId,
708 BlockNumber = BlockNumberFor<Self>,
709 >;
710
711 type Fallback: InstantElectionProvider<
713 AccountId = Self::AccountId,
714 BlockNumber = BlockNumberFor<Self>,
715 DataProvider = Self::DataProvider,
716 MaxBackersPerWinner = Self::MaxBackersPerWinner,
717 MaxWinnersPerPage = Self::MaxWinners,
718 >;
719
720 type GovernanceFallback: InstantElectionProvider<
725 AccountId = Self::AccountId,
726 BlockNumber = BlockNumberFor<Self>,
727 DataProvider = Self::DataProvider,
728 MaxWinnersPerPage = Self::MaxWinners,
729 MaxBackersPerWinner = Self::MaxBackersPerWinner,
730 >;
731
732 type Solver: NposSolver<AccountId = Self::AccountId>;
734
735 type ForceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
738
739 type BenchmarkingConfig: BenchmarkingConfig;
741
742 type WeightInfo: WeightInfo;
744 }
745
746 #[pallet::extra_constants]
748 impl<T: Config> Pallet<T> {
749 #[pallet::constant_name(MinerMaxLength)]
750 fn max_length() -> u32 {
751 <T::MinerConfig as MinerConfig>::MaxLength::get()
752 }
753
754 #[pallet::constant_name(MinerMaxWeight)]
755 fn max_weight() -> Weight {
756 <T::MinerConfig as MinerConfig>::MaxWeight::get()
757 }
758
759 #[pallet::constant_name(MinerMaxVotesPerVoter)]
760 fn max_votes_per_voter() -> u32 {
761 <T::MinerConfig as MinerConfig>::MaxVotesPerVoter::get()
762 }
763
764 #[pallet::constant_name(MinerMaxWinners)]
765 fn max_winners() -> u32 {
766 <T::MinerConfig as MinerConfig>::MaxWinners::get()
767 }
768 }
769
770 #[pallet::hooks]
771 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
772 fn on_initialize(now: BlockNumberFor<T>) -> Weight {
773 let next_election = T::DataProvider::next_election_prediction(now).max(now);
774
775 let signed_deadline = T::SignedPhase::get() + T::UnsignedPhase::get();
776 let unsigned_deadline = T::UnsignedPhase::get();
777
778 let remaining = next_election - now;
779 let current_phase = CurrentPhase::<T>::get();
780
781 log!(
782 trace,
783 "current phase {:?}, next election {:?}, queued? {:?}, metadata: {:?}",
784 current_phase,
785 next_election,
786 QueuedSolution::<T>::get().map(|rs| (rs.supports.len(), rs.compute, rs.score)),
787 SnapshotMetadata::<T>::get()
788 );
789 match current_phase {
790 Phase::Off if remaining <= signed_deadline && remaining > unsigned_deadline => {
791 match Self::create_snapshot() {
793 Ok(_) => {
794 Self::phase_transition(Phase::Signed);
795 T::WeightInfo::on_initialize_open_signed()
796 },
797 Err(why) => {
798 log!(warn, "failed to open signed phase due to {:?}", why);
800 T::WeightInfo::on_initialize_nothing()
801 },
802 }
803 },
804 Phase::Signed | Phase::Off
805 if remaining <= unsigned_deadline && remaining > Zero::zero() =>
806 {
807 let (need_snapshot, enabled) = if current_phase == Phase::Signed {
810 let _ = Self::finalize_signed_phase();
820 (false, true)
824 } else {
825 (true, true)
828 };
829
830 if need_snapshot {
831 match Self::create_snapshot() {
832 Ok(_) => {
833 Self::phase_transition(Phase::Unsigned((enabled, now)));
834 T::WeightInfo::on_initialize_open_unsigned()
835 },
836 Err(why) => {
837 log!(warn, "failed to open unsigned phase due to {:?}", why);
838 T::WeightInfo::on_initialize_nothing()
839 },
840 }
841 } else {
842 Self::phase_transition(Phase::Unsigned((enabled, now)));
843 T::WeightInfo::on_initialize_open_unsigned()
844 }
845 },
846 _ => T::WeightInfo::on_initialize_nothing(),
847 }
848 }
849
850 fn offchain_worker(now: BlockNumberFor<T>) {
851 use sp_runtime::offchain::storage_lock::{BlockAndTime, StorageLock};
852
853 let mut lock =
857 StorageLock::<BlockAndTime<frame_system::Pallet<T>>>::with_block_deadline(
858 unsigned::OFFCHAIN_LOCK,
859 T::UnsignedPhase::get().saturated_into(),
860 );
861
862 match lock.try_lock() {
863 Ok(_guard) => {
864 Self::do_synchronized_offchain_worker(now);
865 },
866 Err(deadline) => {
867 log!(debug, "offchain worker lock not released, deadline is {:?}", deadline);
868 },
869 };
870 }
871
872 fn integrity_test() {
873 use core::mem::size_of;
874 assert!(size_of::<SolutionVoterIndexOf<T::MinerConfig>>() <= size_of::<usize>());
877 assert!(size_of::<SolutionTargetIndexOf<T::MinerConfig>>() <= size_of::<usize>());
878
879 let max_vote: usize = <SolutionOf<T::MinerConfig> as NposSolution>::LIMIT;
882
883 let maximum_chain_accuracy: Vec<UpperOf<SolutionAccuracyOf<T>>> = (0..max_vote)
885 .map(|_| {
886 <UpperOf<SolutionAccuracyOf<T>>>::from(
887 SolutionAccuracyOf::<T>::one().deconstruct(),
888 )
889 })
890 .collect();
891 let _: UpperOf<SolutionAccuracyOf<T>> = maximum_chain_accuracy
892 .iter()
893 .fold(Zero::zero(), |acc, x| acc.checked_add(x).unwrap());
894
895 assert_eq!(
901 <T::DataProvider as ElectionDataProvider>::MaxVotesPerVoter::get(),
902 <SolutionOf<T::MinerConfig> as NposSolution>::LIMIT as u32,
903 );
904
905 assert!(T::SignedMaxSubmissions::get() >= T::SignedMaxRefunds::get());
909 }
910
911 #[cfg(feature = "try-runtime")]
912 fn try_state(_n: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
913 Self::do_try_state()
914 }
915 }
916
917 #[pallet::call]
918 impl<T: Config> Pallet<T> {
919 #[pallet::call_index(0)]
934 #[pallet::weight((
935 T::WeightInfo::submit_unsigned(
936 witness.voters,
937 witness.targets,
938 raw_solution.solution.voter_count() as u32,
939 raw_solution.solution.unique_targets().len() as u32
940 ),
941 DispatchClass::Operational,
942 ))]
943 pub fn submit_unsigned(
944 origin: OriginFor<T>,
945 raw_solution: Box<RawSolution<SolutionOf<T::MinerConfig>>>,
946 witness: SolutionOrSnapshotSize,
947 ) -> DispatchResult {
948 ensure_none(origin)?;
949 let error_message = "Invalid unsigned submission must produce invalid block and \
950 deprive validator from their authoring reward.";
951
952 Self::unsigned_pre_dispatch_checks(&raw_solution).expect(error_message);
954
955 let SolutionOrSnapshotSize { voters, targets } =
957 SnapshotMetadata::<T>::get().expect(error_message);
958
959 assert!(voters as u32 == witness.voters, "{}", error_message);
961 assert!(targets as u32 == witness.targets, "{}", error_message);
962
963 let ready = Self::feasibility_check(*raw_solution, ElectionCompute::Unsigned)
964 .expect(error_message);
965
966 log!(debug, "queued unsigned solution with score {:?}", ready.score);
968 let ejected_a_solution = QueuedSolution::<T>::exists();
969 QueuedSolution::<T>::put(ready);
970 Self::deposit_event(Event::SolutionStored {
971 compute: ElectionCompute::Unsigned,
972 origin: None,
973 prev_ejected: ejected_a_solution,
974 });
975
976 Ok(())
977 }
978
979 #[pallet::call_index(1)]
985 #[pallet::weight(T::DbWeight::get().writes(1))]
986 pub fn set_minimum_untrusted_score(
987 origin: OriginFor<T>,
988 maybe_next_score: Option<ElectionScore>,
989 ) -> DispatchResult {
990 T::ForceOrigin::ensure_origin(origin)?;
991 MinimumUntrustedScore::<T>::set(maybe_next_score);
992 Ok(())
993 }
994
995 #[pallet::call_index(2)]
1004 #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))]
1005 pub fn set_emergency_election_result(
1006 origin: OriginFor<T>,
1007 supports: Supports<T::AccountId>,
1008 ) -> DispatchResult {
1009 T::ForceOrigin::ensure_origin(origin)?;
1010 ensure!(CurrentPhase::<T>::get().is_emergency(), Error::<T>::CallNotAllowed);
1011
1012 let supports: BoundedSupportsOf<Self> =
1014 supports.try_into().map_err(|_| Error::<T>::TooManyWinners)?;
1015
1016 let solution = ReadySolution {
1019 supports,
1020 score: Default::default(),
1021 compute: ElectionCompute::Emergency,
1022 };
1023
1024 Self::deposit_event(Event::SolutionStored {
1025 compute: ElectionCompute::Emergency,
1026 origin: None,
1027 prev_ejected: QueuedSolution::<T>::exists(),
1028 });
1029
1030 QueuedSolution::<T>::put(solution);
1031 Ok(())
1032 }
1033
1034 #[pallet::call_index(3)]
1044 #[pallet::weight(T::WeightInfo::submit())]
1045 pub fn submit(
1046 origin: OriginFor<T>,
1047 raw_solution: Box<RawSolution<SolutionOf<T::MinerConfig>>>,
1048 ) -> DispatchResult {
1049 let who = ensure_signed(origin)?;
1050
1051 ensure!(CurrentPhase::<T>::get().is_signed(), Error::<T>::PreDispatchEarlySubmission);
1053 ensure!(raw_solution.round == Round::<T>::get(), Error::<T>::PreDispatchDifferentRound);
1054
1055 let size = SnapshotMetadata::<T>::get().ok_or(Error::<T>::MissingSnapshotMetadata)?;
1061
1062 ensure!(
1063 Self::solution_weight_of(&raw_solution, size).all_lt(T::SignedMaxWeight::get()),
1064 Error::<T>::SignedTooMuchWeight,
1065 );
1066
1067 let deposit = Self::deposit_for(&raw_solution, size);
1069 let call_fee = {
1070 let call = Call::submit { raw_solution: raw_solution.clone() };
1071 T::EstimateCallFee::estimate_call_fee(&call, None::<Weight>.into())
1072 };
1073
1074 let submission = SignedSubmission {
1075 who: who.clone(),
1076 deposit,
1077 raw_solution: *raw_solution,
1078 call_fee,
1079 };
1080
1081 let mut signed_submissions = Self::signed_submissions();
1084 let maybe_removed = match signed_submissions.insert(submission) {
1085 signed::InsertResult::NotInserted => return Err(Error::<T>::SignedQueueFull.into()),
1088 signed::InsertResult::Inserted => None,
1089 signed::InsertResult::InsertedEjecting(weakest) => Some(weakest),
1090 };
1091
1092 T::Currency::reserve(&who, deposit).map_err(|_| Error::<T>::SignedCannotPayDeposit)?;
1094
1095 let ejected_a_solution = maybe_removed.is_some();
1096 if let Some(removed) = maybe_removed {
1098 let _remainder = T::Currency::unreserve(&removed.who, removed.deposit);
1099 debug_assert!(_remainder.is_zero());
1100 }
1101
1102 signed_submissions.put();
1103 Self::deposit_event(Event::SolutionStored {
1104 compute: ElectionCompute::Signed,
1105 origin: Some(who),
1106 prev_ejected: ejected_a_solution,
1107 });
1108 Ok(())
1109 }
1110
1111 #[pallet::call_index(4)]
1116 #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))]
1117 pub fn governance_fallback(origin: OriginFor<T>) -> DispatchResult {
1118 T::ForceOrigin::ensure_origin(origin)?;
1119 ensure!(CurrentPhase::<T>::get().is_emergency(), Error::<T>::CallNotAllowed);
1120
1121 let RoundSnapshot { voters, targets } =
1122 Snapshot::<T>::get().ok_or(Error::<T>::MissingSnapshotMetadata)?;
1123 let desired_targets =
1124 DesiredTargets::<T>::get().ok_or(Error::<T>::MissingSnapshotMetadata)?;
1125
1126 let supports = T::GovernanceFallback::instant_elect(voters, targets, desired_targets)
1127 .map_err(|e| {
1128 log!(error, "GovernanceFallback failed: {:?}", e);
1129 Error::<T>::FallbackFailed
1130 })?;
1131
1132 let solution = ReadySolution {
1133 supports,
1134 score: Default::default(),
1135 compute: ElectionCompute::Fallback,
1136 };
1137
1138 Self::deposit_event(Event::SolutionStored {
1139 compute: ElectionCompute::Fallback,
1140 origin: None,
1141 prev_ejected: QueuedSolution::<T>::exists(),
1142 });
1143
1144 QueuedSolution::<T>::put(solution);
1145 Ok(())
1146 }
1147 }
1148
1149 #[pallet::event]
1150 #[pallet::generate_deposit(pub(super) fn deposit_event)]
1151 pub enum Event<T: Config> {
1152 SolutionStored {
1160 compute: ElectionCompute,
1161 origin: Option<T::AccountId>,
1162 prev_ejected: bool,
1163 },
1164 ElectionFinalized { compute: ElectionCompute, score: ElectionScore },
1166 ElectionFailed,
1170 Rewarded { account: <T as frame_system::Config>::AccountId, value: BalanceOf<T> },
1172 Slashed { account: <T as frame_system::Config>::AccountId, value: BalanceOf<T> },
1174 PhaseTransitioned {
1176 from: Phase<BlockNumberFor<T>>,
1177 to: Phase<BlockNumberFor<T>>,
1178 round: u32,
1179 },
1180 }
1181
1182 #[pallet::error]
1184 pub enum Error<T> {
1185 PreDispatchEarlySubmission,
1187 PreDispatchWrongWinnerCount,
1189 PreDispatchWeakSubmission,
1191 SignedQueueFull,
1193 SignedCannotPayDeposit,
1195 SignedInvalidWitness,
1197 SignedTooMuchWeight,
1199 OcwCallWrongEra,
1201 MissingSnapshotMetadata,
1203 InvalidSubmissionIndex,
1205 CallNotAllowed,
1207 FallbackFailed,
1209 BoundNotMet,
1211 TooManyWinners,
1213 PreDispatchDifferentRound,
1215 }
1216
1217 #[pallet::validate_unsigned]
1218 impl<T: Config> ValidateUnsigned for Pallet<T> {
1219 type Call = Call<T>;
1220 fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
1221 if let Call::submit_unsigned { raw_solution, .. } = call {
1222 match source {
1224 TransactionSource::Local | TransactionSource::InBlock => { },
1225 _ => return InvalidTransaction::Call.into(),
1226 }
1227
1228 Self::unsigned_pre_dispatch_checks(raw_solution)
1229 .inspect_err(|err| {
1230 log!(debug, "unsigned transaction validation failed due to {:?}", err);
1231 })
1232 .map_err(dispatch_error_to_invalid)?;
1233
1234 ValidTransaction::with_tag_prefix("OffchainElection")
1235 .priority(
1237 T::MinerTxPriority::get()
1238 .saturating_add(raw_solution.score.minimal_stake.saturated_into()),
1239 )
1240 .and_provides(raw_solution.round)
1243 .longevity(T::UnsignedPhase::get().saturated_into::<u64>())
1245 .propagate(false)
1247 .build()
1248 } else {
1249 InvalidTransaction::Call.into()
1250 }
1251 }
1252
1253 fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
1254 if let Call::submit_unsigned { raw_solution, .. } = call {
1255 Self::unsigned_pre_dispatch_checks(raw_solution)
1256 .map_err(dispatch_error_to_invalid)
1257 .map_err(Into::into)
1258 } else {
1259 Err(InvalidTransaction::Call.into())
1260 }
1261 }
1262 }
1263
1264 #[pallet::type_value]
1265 pub fn DefaultForRound() -> u32 {
1266 1
1267 }
1268
1269 #[pallet::storage]
1276 pub type Round<T: Config> = StorageValue<_, u32, ValueQuery, DefaultForRound>;
1277
1278 #[pallet::storage]
1280 pub type CurrentPhase<T: Config> = StorageValue<_, Phase<BlockNumberFor<T>>, ValueQuery>;
1281
1282 #[pallet::storage]
1286 pub type QueuedSolution<T: Config> = StorageValue<_, ReadySolutionOf<T::MinerConfig>>;
1287
1288 #[pallet::storage]
1293 pub type Snapshot<T: Config> = StorageValue<_, RoundSnapshot<T::AccountId, VoterOf<T>>>;
1294
1295 #[pallet::storage]
1300 pub type DesiredTargets<T> = StorageValue<_, u32>;
1301
1302 #[pallet::storage]
1307 pub type SnapshotMetadata<T: Config> = StorageValue<_, SolutionOrSnapshotSize>;
1308
1309 #[pallet::storage]
1323 pub type SignedSubmissionNextIndex<T: Config> = StorageValue<_, u32, ValueQuery>;
1324
1325 #[pallet::storage]
1332 pub type SignedSubmissionIndices<T: Config> =
1333 StorageValue<_, SubmissionIndicesOf<T>, ValueQuery>;
1334
1335 #[pallet::storage]
1343 pub type SignedSubmissionsMap<T: Config> =
1344 StorageMap<_, Twox64Concat, u32, SignedSubmissionOf<T>, OptionQuery>;
1345
1346 #[pallet::storage]
1353 pub type MinimumUntrustedScore<T: Config> = StorageValue<_, ElectionScore>;
1354
1355 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
1359
1360 #[pallet::pallet]
1361 #[pallet::without_storage_info]
1362 #[pallet::storage_version(STORAGE_VERSION)]
1363 pub struct Pallet<T>(_);
1364}
1365
1366pub struct SnapshotWrapper<T>(core::marker::PhantomData<T>);
1369
1370impl<T: Config> SnapshotWrapper<T> {
1371 pub fn kill() {
1373 Snapshot::<T>::kill();
1374 SnapshotMetadata::<T>::kill();
1375 DesiredTargets::<T>::kill();
1376 }
1377 pub fn set(metadata: SolutionOrSnapshotSize, desired_targets: u32, buffer: &[u8]) {
1379 SnapshotMetadata::<T>::put(metadata);
1380 DesiredTargets::<T>::put(desired_targets);
1381 sp_io::storage::set(&Snapshot::<T>::hashed_key(), &buffer);
1382 }
1383
1384 #[cfg(feature = "try-runtime")]
1387 pub fn is_consistent() -> bool {
1388 let snapshots = [
1389 Snapshot::<T>::exists(),
1390 SnapshotMetadata::<T>::exists(),
1391 DesiredTargets::<T>::exists(),
1392 ];
1393
1394 snapshots.iter().skip(1).all(|v| snapshots[0] == *v)
1396 }
1397}
1398
1399impl<T: Config> Pallet<T> {
1400 pub fn round() -> u32 {
1407 Round::<T>::get()
1408 }
1409
1410 pub fn current_phase() -> Phase<BlockNumberFor<T>> {
1412 CurrentPhase::<T>::get()
1413 }
1414
1415 pub fn queued_solution() -> Option<ReadySolutionOf<T::MinerConfig>> {
1419 QueuedSolution::<T>::get()
1420 }
1421
1422 pub fn snapshot() -> Option<RoundSnapshot<T::AccountId, VoterOf<T>>> {
1427 Snapshot::<T>::get()
1428 }
1429
1430 pub fn desired_targets() -> Option<u32> {
1435 DesiredTargets::<T>::get()
1436 }
1437
1438 pub fn snapshot_metadata() -> Option<SolutionOrSnapshotSize> {
1443 SnapshotMetadata::<T>::get()
1444 }
1445
1446 pub fn minimum_untrusted_score() -> Option<ElectionScore> {
1451 MinimumUntrustedScore::<T>::get()
1452 }
1453
1454 fn do_synchronized_offchain_worker(now: BlockNumberFor<T>) {
1457 let current_phase = CurrentPhase::<T>::get();
1458 log!(trace, "lock for offchain worker acquired. Phase = {:?}", current_phase);
1459 match current_phase {
1460 Phase::Unsigned((true, opened)) if opened == now => {
1461 let initial_output = Self::ensure_offchain_repeat_frequency(now).and_then(|_| {
1463 unsigned::kill_ocw_solution::<T>();
1466 Self::mine_check_save_submit()
1467 });
1468 log!(debug, "initial offchain thread output: {:?}", initial_output);
1469 },
1470 Phase::Unsigned((true, opened)) if opened < now => {
1471 let resubmit_output = Self::ensure_offchain_repeat_frequency(now)
1474 .and_then(|_| Self::restore_or_compute_then_maybe_submit());
1475 log!(debug, "resubmit offchain thread output: {:?}", resubmit_output);
1476 },
1477 _ => {},
1478 }
1479 }
1480
1481 pub(crate) fn phase_transition(to: Phase<BlockNumberFor<T>>) {
1483 log!(info, "Starting phase {:?}, round {}.", to, Round::<T>::get());
1484 Self::deposit_event(Event::PhaseTransitioned {
1485 from: CurrentPhase::<T>::get(),
1486 to,
1487 round: Round::<T>::get(),
1488 });
1489 CurrentPhase::<T>::put(to);
1490 }
1491
1492 fn create_snapshot_internal(
1496 targets: Vec<T::AccountId>,
1497 voters: Vec<VoterOf<T>>,
1498 desired_targets: u32,
1499 ) {
1500 let metadata =
1501 SolutionOrSnapshotSize { voters: voters.len() as u32, targets: targets.len() as u32 };
1502 log!(info, "creating a snapshot with metadata {:?}", metadata);
1503
1504 let snapshot = RoundSnapshot::<T::AccountId, VoterOf<T>> { voters, targets };
1508 let size = snapshot.encoded_size();
1509 log!(debug, "snapshot pre-calculated size {:?}", size);
1510 let mut buffer = Vec::with_capacity(size);
1511 snapshot.encode_to(&mut buffer);
1512
1513 debug_assert_eq!(buffer, snapshot.encode());
1515 debug_assert!(buffer.len() == size && size == buffer.capacity());
1517
1518 SnapshotWrapper::<T>::set(metadata, desired_targets, &buffer);
1519 }
1520
1521 fn create_snapshot_external(
1527 ) -> Result<(Vec<T::AccountId>, Vec<VoterOf<T>>, u32), ElectionError<T>> {
1528 let election_bounds = T::ElectionBounds::get();
1529 let targets = T::DataProvider::electable_targets_stateless(election_bounds.targets)
1530 .and_then(|t| {
1531 election_bounds.ensure_targets_limits(
1532 CountBound(t.len() as u32),
1533 SizeBound(t.encoded_size() as u32),
1534 )?;
1535 Ok(t)
1536 })
1537 .map_err(ElectionError::DataProvider)?;
1538
1539 let voters = T::DataProvider::electing_voters_stateless(election_bounds.voters)
1540 .and_then(|v| {
1541 election_bounds.ensure_voters_limits(
1542 CountBound(v.len() as u32),
1543 SizeBound(v.encoded_size() as u32),
1544 )?;
1545 Ok(v)
1546 })
1547 .map_err(ElectionError::DataProvider)?;
1548
1549 let mut desired_targets = <Pallet<T> as ElectionProvider>::desired_targets_checked()
1550 .map_err(|e| ElectionError::DataProvider(e))?;
1551
1552 let max_desired_targets: u32 = targets.len() as u32;
1555 if desired_targets > max_desired_targets {
1556 log!(
1557 warn,
1558 "desired_targets: {} > targets.len(): {}, capping desired_targets",
1559 desired_targets,
1560 max_desired_targets
1561 );
1562 desired_targets = max_desired_targets;
1563 }
1564
1565 Ok((targets, voters, desired_targets))
1566 }
1567
1568 pub fn create_snapshot() -> Result<(), ElectionError<T>> {
1579 let (targets, voters, desired_targets) = Self::create_snapshot_external()?;
1581
1582 let internal_weight =
1584 T::WeightInfo::create_snapshot_internal(voters.len() as u32, targets.len() as u32);
1585 Self::create_snapshot_internal(targets, voters, desired_targets);
1586 Self::register_weight(internal_weight);
1587 Ok(())
1588 }
1589
1590 fn register_weight(weight: Weight) {
1594 frame_system::Pallet::<T>::register_extra_weight_unchecked(
1595 weight,
1596 DispatchClass::Mandatory,
1597 );
1598 }
1599
1600 pub fn feasibility_check(
1602 raw_solution: RawSolution<SolutionOf<T::MinerConfig>>,
1603 compute: ElectionCompute,
1604 ) -> Result<ReadySolutionOf<T::MinerConfig>, FeasibilityError> {
1605 let desired_targets =
1606 DesiredTargets::<T>::get().ok_or(FeasibilityError::SnapshotUnavailable)?;
1607
1608 let snapshot = Snapshot::<T>::get().ok_or(FeasibilityError::SnapshotUnavailable)?;
1609 let round = Round::<T>::get();
1610 let minimum_untrusted_score = MinimumUntrustedScore::<T>::get();
1611
1612 Miner::<T::MinerConfig>::feasibility_check(
1613 raw_solution,
1614 compute,
1615 desired_targets,
1616 snapshot,
1617 round,
1618 minimum_untrusted_score,
1619 )
1620 }
1621
1622 fn rotate_round() {
1628 Round::<T>::mutate(|r| *r += 1);
1630
1631 Self::phase_transition(Phase::Off);
1633
1634 SnapshotWrapper::<T>::kill();
1636 }
1637
1638 fn do_elect() -> Result<BoundedSupportsOf<Self>, ElectionError<T>> {
1639 let _ = Self::finalize_signed_phase();
1647
1648 QueuedSolution::<T>::take()
1649 .ok_or(ElectionError::<T>::NothingQueued)
1650 .or_else(|_| {
1651 log!(warn, "No solution queued, falling back to instant fallback.",);
1652
1653 #[cfg(feature = "runtime-benchmarks")]
1654 Self::asap();
1655
1656 let (voters, targets, desired_targets) = if T::Fallback::bother() {
1657 let RoundSnapshot { voters, targets } = Snapshot::<T>::get().ok_or(
1658 ElectionError::<T>::Feasibility(FeasibilityError::SnapshotUnavailable),
1659 )?;
1660 let desired_targets = DesiredTargets::<T>::get().ok_or(
1661 ElectionError::<T>::Feasibility(FeasibilityError::SnapshotUnavailable),
1662 )?;
1663 (voters, targets, desired_targets)
1664 } else {
1665 (Default::default(), Default::default(), Default::default())
1666 };
1667 T::Fallback::instant_elect(voters, targets, desired_targets)
1668 .map_err(|fe| ElectionError::Fallback(fe))
1669 .and_then(|supports| {
1670 Ok(ReadySolution {
1671 supports,
1672 score: Default::default(),
1673 compute: ElectionCompute::Fallback,
1674 })
1675 })
1676 })
1677 .map(|ReadySolution { compute, score, supports }| {
1678 Self::deposit_event(Event::ElectionFinalized { compute, score });
1679 log!(info, "Finalized election round with compute {:?}.", compute);
1680 supports
1681 })
1682 .map_err(|err| {
1683 Self::deposit_event(Event::ElectionFailed);
1684 log!(warn, "Failed to finalize election round. reason {:?}", err);
1685 err
1686 })
1687 }
1688
1689 fn weigh_supports(supports: &BoundedSupportsOf<Self>) {
1691 let active_voters = supports
1692 .iter()
1693 .map(|(_, x)| x)
1694 .fold(Zero::zero(), |acc, next| acc + next.voters.len() as u32);
1695 let desired_targets = supports.len() as u32;
1696 Self::register_weight(T::WeightInfo::elect_queued(active_voters, desired_targets));
1697 }
1698}
1699
1700#[cfg(feature = "try-runtime")]
1701impl<T: Config> Pallet<T> {
1702 fn do_try_state() -> Result<(), TryRuntimeError> {
1703 Self::try_state_snapshot()?;
1704 Self::try_state_signed_submissions_map()?;
1705 Self::try_state_phase_off()
1706 }
1707
1708 fn try_state_snapshot() -> Result<(), TryRuntimeError> {
1712 if SnapshotWrapper::<T>::is_consistent() {
1713 Ok(())
1714 } else {
1715 Err("If snapshot exists, metadata and desired targets should be set too. Otherwise, none should be set.".into())
1716 }
1717 }
1718
1719 fn try_state_signed_submissions_map() -> Result<(), TryRuntimeError> {
1724 let mut last_score: ElectionScore = Default::default();
1725 let indices = SignedSubmissionIndices::<T>::get();
1726
1727 for (i, indice) in indices.iter().enumerate() {
1728 let submission = SignedSubmissionsMap::<T>::get(indice.2);
1729 if submission.is_none() {
1730 return Err(
1731 "All signed submissions indices must be part of the submissions map".into()
1732 )
1733 }
1734
1735 if i == 0 {
1736 last_score = indice.0
1737 } else {
1738 if last_score.strict_better(indice.0) {
1739 return Err(
1740 "Signed submission indices vector must be ordered by election score".into()
1741 )
1742 }
1743 last_score = indice.0;
1744 }
1745 }
1746
1747 if SignedSubmissionsMap::<T>::iter().nth(indices.len()).is_some() {
1748 return Err(
1749 "Signed submissions map length should be the same as the indices vec length".into()
1750 )
1751 }
1752
1753 match SignedSubmissionNextIndex::<T>::get() {
1754 0 => Ok(()),
1755 next =>
1756 if SignedSubmissionsMap::<T>::get(next).is_some() {
1757 return Err(
1758 "The next submissions index should not be in the submissions maps already"
1759 .into(),
1760 )
1761 } else {
1762 Ok(())
1763 },
1764 }
1765 }
1766
1767 fn try_state_phase_off() -> Result<(), TryRuntimeError> {
1770 match CurrentPhase::<T>::get().is_off() {
1771 false => Ok(()),
1772 true =>
1773 if Snapshot::<T>::get().is_some() {
1774 Err("Snapshot must be none when in Phase::Off".into())
1775 } else {
1776 Ok(())
1777 },
1778 }
1779 }
1780}
1781
1782impl<T: Config> ElectionProvider for Pallet<T> {
1783 type AccountId = T::AccountId;
1784 type BlockNumber = BlockNumberFor<T>;
1785 type Error = ElectionError<T>;
1786 type MaxWinnersPerPage = T::MaxWinners;
1787 type MaxBackersPerWinner = T::MaxBackersPerWinner;
1788 type MaxBackersPerWinnerFinal = T::MaxBackersPerWinner;
1789 type Pages = sp_core::ConstU32<1>;
1790 type DataProvider = T::DataProvider;
1791
1792 fn elect(page: PageIndex) -> Result<BoundedSupportsOf<Self>, Self::Error> {
1793 ensure!(page == SINGLE_PAGE, ElectionError::<T>::MultiPageNotSupported);
1795
1796 let res = match Self::do_elect() {
1797 Ok(bounded_supports) => {
1798 Self::weigh_supports(&bounded_supports);
1800 Self::rotate_round();
1801 Ok(bounded_supports)
1802 },
1803 Err(why) => {
1804 log!(error, "Entering emergency mode: {:?}", why);
1805 Self::phase_transition(Phase::Emergency);
1806 Err(why)
1807 },
1808 };
1809
1810 log!(info, "ElectionProvider::elect({}) => {:?}", page, res.as_ref().map(|s| s.len()));
1811 res
1812 }
1813
1814 fn duration() -> Self::BlockNumber {
1815 let signed: BlockNumberFor<T> = T::SignedPhase::get().saturated_into();
1816 let unsigned: BlockNumberFor<T> = T::UnsignedPhase::get().saturated_into();
1817 signed + unsigned
1818 }
1819
1820 fn start() -> Result<(), Self::Error> {
1821 log!(
1822 warn,
1823 "we received signal, but this pallet works in the basis of legacy pull based election"
1824 );
1825 Ok(())
1826 }
1827
1828 fn status() -> Result<Option<Weight>, ()> {
1829 let has_queued = QueuedSolution::<T>::exists();
1830 let phase = CurrentPhase::<T>::get();
1831 match (phase, has_queued) {
1832 (Phase::Unsigned(_), true) => Ok(Some(Default::default())),
1834 (Phase::Off, _) => Err(()),
1835 _ => Ok(None),
1836 }
1837 }
1838
1839 #[cfg(feature = "runtime-benchmarks")]
1840 fn asap() {
1841 if !Snapshot::<T>::exists() {
1843 Self::create_snapshot()
1844 .inspect_err(|e| {
1845 crate::log!(error, "failed to create snapshot while asap-preparing: {:?}", e)
1846 })
1847 .unwrap()
1848 }
1849 }
1850}
1851
1852pub fn dispatch_error_to_invalid(error: DispatchError) -> InvalidTransaction {
1855 let error_number = match error {
1856 DispatchError::Module(ModuleError { error, .. }) => error[0],
1857 _ => 0,
1858 };
1859 InvalidTransaction::Custom(error_number)
1860}
1861
1862#[cfg(test)]
1863mod feasibility_check {
1864 use super::*;
1869 use crate::mock::{
1870 raw_solution, roll_to, EpochLength, ExtBuilder, MultiPhase, Runtime, SignedPhase,
1871 TargetIndex, UnsignedPhase, VoterIndex,
1872 };
1873 use frame_support::{assert_noop, assert_ok};
1874
1875 const COMPUTE: ElectionCompute = ElectionCompute::OnChain;
1876
1877 #[test]
1878 fn snapshot_is_there() {
1879 ExtBuilder::default().build_and_execute(|| {
1880 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1881 assert!(CurrentPhase::<Runtime>::get().is_signed());
1882 let solution = raw_solution();
1883
1884 SnapshotWrapper::<Runtime>::kill();
1887
1888 assert_noop!(
1889 MultiPhase::feasibility_check(solution, COMPUTE),
1890 FeasibilityError::SnapshotUnavailable
1891 );
1892 })
1893 }
1894
1895 #[test]
1896 fn round() {
1897 ExtBuilder::default().build_and_execute(|| {
1898 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1899 assert!(CurrentPhase::<Runtime>::get().is_signed());
1900
1901 let mut solution = raw_solution();
1902 solution.round += 1;
1903 assert_noop!(
1904 MultiPhase::feasibility_check(solution, COMPUTE),
1905 FeasibilityError::InvalidRound
1906 );
1907 })
1908 }
1909
1910 #[test]
1911 fn desired_targets_gets_capped() {
1912 ExtBuilder::default().desired_targets(8).build_and_execute(|| {
1913 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1914 assert!(CurrentPhase::<Runtime>::get().is_signed());
1915
1916 let raw = raw_solution();
1917
1918 assert_eq!(raw.solution.unique_targets().len(), 4);
1919 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 4);
1921
1922 assert_ok!(MultiPhase::feasibility_check(raw, COMPUTE));
1924 })
1925 }
1926
1927 #[test]
1928 fn less_than_desired_targets_fails() {
1929 ExtBuilder::default().desired_targets(8).build_and_execute(|| {
1930 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1931 assert!(CurrentPhase::<Runtime>::get().is_signed());
1932
1933 let mut raw = raw_solution();
1934
1935 assert_eq!(raw.solution.unique_targets().len(), 4);
1936 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 4);
1938
1939 raw.solution.votes1[0].1 = 4;
1941
1942 assert_noop!(
1944 MultiPhase::feasibility_check(raw, COMPUTE),
1945 FeasibilityError::WrongWinnerCount,
1946 );
1947 })
1948 }
1949
1950 #[test]
1951 fn winner_indices() {
1952 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
1953 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1954 assert!(CurrentPhase::<Runtime>::get().is_signed());
1955
1956 let mut raw = raw_solution();
1957 assert_eq!(Snapshot::<Runtime>::get().unwrap().targets.len(), 4);
1958 raw.solution
1964 .votes1
1965 .iter_mut()
1966 .filter(|(_, t)| *t == TargetIndex::from(3u16))
1967 .for_each(|(_, t)| *t += 1);
1968 raw.solution.votes2.iter_mut().for_each(|(_, [(t0, _)], t1)| {
1969 if *t0 == TargetIndex::from(3u16) {
1970 *t0 += 1
1971 };
1972 if *t1 == TargetIndex::from(3u16) {
1973 *t1 += 1
1974 };
1975 });
1976 assert_noop!(
1977 MultiPhase::feasibility_check(raw, COMPUTE),
1978 FeasibilityError::NposElection(sp_npos_elections::Error::SolutionInvalidIndex)
1979 );
1980 })
1981 }
1982
1983 #[test]
1984 fn voter_indices() {
1985 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
1987 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1988 assert!(CurrentPhase::<Runtime>::get().is_signed());
1989
1990 let mut solution = raw_solution();
1991 assert_eq!(Snapshot::<Runtime>::get().unwrap().voters.len(), 8);
1992 assert!(
1996 solution
1997 .solution
1998 .votes1
1999 .iter_mut()
2000 .filter(|(v, _)| *v == VoterIndex::from(7u32))
2001 .map(|(v, _)| *v = 8)
2002 .count() > 0
2003 );
2004 assert_noop!(
2005 MultiPhase::feasibility_check(solution, COMPUTE),
2006 FeasibilityError::NposElection(sp_npos_elections::Error::SolutionInvalidIndex),
2007 );
2008 })
2009 }
2010
2011 #[test]
2012 fn voter_votes() {
2013 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
2014 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
2015 assert!(CurrentPhase::<Runtime>::get().is_signed());
2016
2017 let mut solution = raw_solution();
2018 assert_eq!(Snapshot::<Runtime>::get().unwrap().voters.len(), 8);
2019 assert_eq!(
2024 solution
2025 .solution
2026 .votes1
2027 .iter_mut()
2028 .filter(|(v, t)| *v == 7 && *t == 3)
2029 .map(|(_, t)| *t = 2)
2030 .count(),
2031 1,
2032 );
2033 assert_noop!(
2034 MultiPhase::feasibility_check(solution, COMPUTE),
2035 FeasibilityError::InvalidVote,
2036 );
2037 })
2038 }
2039
2040 #[test]
2041 fn score() {
2042 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
2043 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
2044 assert!(CurrentPhase::<Runtime>::get().is_signed());
2045
2046 let mut solution = raw_solution();
2047 assert_eq!(Snapshot::<Runtime>::get().unwrap().voters.len(), 8);
2048
2049 solution.score.minimal_stake += 1;
2051
2052 assert_noop!(
2053 MultiPhase::feasibility_check(solution, COMPUTE),
2054 FeasibilityError::InvalidScore,
2055 );
2056 })
2057 }
2058}
2059
2060#[cfg(test)]
2061mod tests {
2062 use super::*;
2063 use crate::{
2064 mock::{
2065 multi_phase_events, raw_solution, roll_to, roll_to_signed, roll_to_unsigned,
2066 ElectionsBounds, ExtBuilder, MockWeightInfo, MockedWeightInfo, MultiPhase, Runtime,
2067 RuntimeOrigin, SignedMaxSubmissions, System, Voters,
2068 },
2069 Phase,
2070 };
2071 use frame_election_provider_support::bounds::ElectionBoundsBuilder;
2072 use frame_support::{assert_noop, assert_ok};
2073 use sp_npos_elections::{BalancingConfig, Support};
2074
2075 #[test]
2076 fn phase_rotation_works() {
2077 ExtBuilder::default().build_and_execute(|| {
2078 assert_eq!(System::block_number(), 0);
2083 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
2084 assert_eq!(Round::<Runtime>::get(), 1);
2085
2086 roll_to(4);
2087 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
2088 assert!(Snapshot::<Runtime>::get().is_none());
2089 assert_eq!(Round::<Runtime>::get(), 1);
2090
2091 roll_to_signed();
2092 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2093 assert_eq!(
2094 multi_phase_events(),
2095 vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }]
2096 );
2097 assert!(Snapshot::<Runtime>::get().is_some());
2098 assert_eq!(Round::<Runtime>::get(), 1);
2099
2100 roll_to(24);
2101 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2102 assert!(Snapshot::<Runtime>::get().is_some());
2103 assert_eq!(Round::<Runtime>::get(), 1);
2104
2105 roll_to_unsigned();
2106 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2107 assert_eq!(
2108 multi_phase_events(),
2109 vec![
2110 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2111 Event::PhaseTransitioned {
2112 from: Phase::Signed,
2113 to: Phase::Unsigned((true, 25)),
2114 round: 1
2115 },
2116 ],
2117 );
2118 assert!(Snapshot::<Runtime>::get().is_some());
2119
2120 roll_to(29);
2121 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2122 assert!(Snapshot::<Runtime>::get().is_some());
2123
2124 roll_to(30);
2125 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2126 assert!(Snapshot::<Runtime>::get().is_some());
2127
2128 roll_to(32);
2130 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2131 assert!(Snapshot::<Runtime>::get().is_some());
2132
2133 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2134
2135 assert!(CurrentPhase::<Runtime>::get().is_off());
2136 assert!(Snapshot::<Runtime>::get().is_none());
2137 assert_eq!(Round::<Runtime>::get(), 2);
2138
2139 roll_to(44);
2140 assert!(CurrentPhase::<Runtime>::get().is_off());
2141
2142 roll_to_signed();
2143 assert!(CurrentPhase::<Runtime>::get().is_signed());
2144
2145 roll_to(55);
2146 assert!(CurrentPhase::<Runtime>::get().is_unsigned_open_at(55));
2147
2148 assert_eq!(
2149 multi_phase_events(),
2150 vec![
2151 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2152 Event::PhaseTransitioned {
2153 from: Phase::Signed,
2154 to: Phase::Unsigned((true, 25)),
2155 round: 1
2156 },
2157 Event::ElectionFinalized {
2158 compute: ElectionCompute::Fallback,
2159 score: ElectionScore {
2160 minimal_stake: 0,
2161 sum_stake: 0,
2162 sum_stake_squared: 0
2163 }
2164 },
2165 Event::PhaseTransitioned {
2166 from: Phase::Unsigned((true, 25)),
2167 to: Phase::Off,
2168 round: 2
2169 },
2170 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 2 },
2171 Event::PhaseTransitioned {
2172 from: Phase::Signed,
2173 to: Phase::Unsigned((true, 55)),
2174 round: 2
2175 },
2176 ]
2177 );
2178 })
2179 }
2180
2181 #[test]
2182 fn signed_phase_void() {
2183 ExtBuilder::default().phases(0, 10).build_and_execute(|| {
2184 roll_to(15);
2185 assert!(CurrentPhase::<Runtime>::get().is_off());
2186
2187 roll_to(19);
2188 assert!(CurrentPhase::<Runtime>::get().is_off());
2189
2190 roll_to(20);
2191 assert!(CurrentPhase::<Runtime>::get().is_unsigned_open_at(20));
2192 assert!(Snapshot::<Runtime>::get().is_some());
2193
2194 roll_to(30);
2195 assert!(CurrentPhase::<Runtime>::get().is_unsigned_open_at(20));
2196
2197 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2198
2199 assert!(CurrentPhase::<Runtime>::get().is_off());
2200 assert!(Snapshot::<Runtime>::get().is_none());
2201
2202 assert_eq!(
2203 multi_phase_events(),
2204 vec![
2205 Event::PhaseTransitioned {
2206 from: Phase::Off,
2207 to: Phase::Unsigned((true, 20)),
2208 round: 1
2209 },
2210 Event::ElectionFinalized {
2211 compute: ElectionCompute::Fallback,
2212 score: ElectionScore {
2213 minimal_stake: 0,
2214 sum_stake: 0,
2215 sum_stake_squared: 0
2216 }
2217 },
2218 Event::PhaseTransitioned {
2219 from: Phase::Unsigned((true, 20)),
2220 to: Phase::Off,
2221 round: 2
2222 },
2223 ]
2224 );
2225 });
2226 }
2227
2228 #[test]
2229 fn unsigned_phase_void() {
2230 ExtBuilder::default().phases(10, 0).build_and_execute(|| {
2231 roll_to(15);
2232 assert!(CurrentPhase::<Runtime>::get().is_off());
2233
2234 roll_to(19);
2235 assert!(CurrentPhase::<Runtime>::get().is_off());
2236
2237 roll_to_signed();
2238 assert!(CurrentPhase::<Runtime>::get().is_signed());
2239 assert!(Snapshot::<Runtime>::get().is_some());
2240
2241 roll_to(30);
2242 assert!(CurrentPhase::<Runtime>::get().is_signed());
2243
2244 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2245
2246 assert!(CurrentPhase::<Runtime>::get().is_off());
2247 assert!(Snapshot::<Runtime>::get().is_none());
2248
2249 assert_eq!(
2250 multi_phase_events(),
2251 vec![
2252 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2253 Event::ElectionFinalized {
2254 compute: ElectionCompute::Fallback,
2255 score: ElectionScore {
2256 minimal_stake: 0,
2257 sum_stake: 0,
2258 sum_stake_squared: 0
2259 }
2260 },
2261 Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 },
2262 ]
2263 )
2264 });
2265 }
2266
2267 #[test]
2268 fn early_termination() {
2269 ExtBuilder::default().build_and_execute(|| {
2271 roll_to_signed();
2274 assert_eq!(
2275 multi_phase_events(),
2276 vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }]
2277 );
2278 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2279 assert_eq!(Round::<Runtime>::get(), 1);
2280
2281 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2283
2284 assert_eq!(
2286 multi_phase_events(),
2287 vec![
2288 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2289 Event::ElectionFinalized {
2290 compute: ElectionCompute::Fallback,
2291 score: Default::default()
2292 },
2293 Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 },
2294 ],
2295 );
2296 assert_eq!(Round::<Runtime>::get(), 2);
2298 assert!(Snapshot::<Runtime>::get().is_none());
2299 assert!(SnapshotMetadata::<Runtime>::get().is_none());
2300 assert!(DesiredTargets::<Runtime>::get().is_none());
2301 assert!(QueuedSolution::<Runtime>::get().is_none());
2302 assert!(MultiPhase::signed_submissions().is_empty());
2303 })
2304 }
2305
2306 #[test]
2307 fn early_termination_with_submissions() {
2308 ExtBuilder::default().build_and_execute(|| {
2310 roll_to_signed();
2313 assert_eq!(
2314 multi_phase_events(),
2315 vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }]
2316 );
2317 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2318 assert_eq!(Round::<Runtime>::get(), 1);
2319
2320 for s in 0..SignedMaxSubmissions::get() {
2322 let solution = RawSolution {
2323 score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
2324 ..Default::default()
2325 };
2326 assert_ok!(MultiPhase::submit(
2327 crate::mock::RuntimeOrigin::signed(99),
2328 Box::new(solution)
2329 ));
2330 }
2331
2332 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2334
2335 assert_eq!(Round::<Runtime>::get(), 2);
2337 assert!(Snapshot::<Runtime>::get().is_none());
2338 assert!(SnapshotMetadata::<Runtime>::get().is_none());
2339 assert!(DesiredTargets::<Runtime>::get().is_none());
2340 assert!(QueuedSolution::<Runtime>::get().is_none());
2341 assert!(MultiPhase::signed_submissions().is_empty());
2342
2343 assert_eq!(
2344 multi_phase_events(),
2345 vec![
2346 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2347 Event::SolutionStored {
2348 compute: ElectionCompute::Signed,
2349 origin: Some(99),
2350 prev_ejected: false
2351 },
2352 Event::SolutionStored {
2353 compute: ElectionCompute::Signed,
2354 origin: Some(99),
2355 prev_ejected: false
2356 },
2357 Event::SolutionStored {
2358 compute: ElectionCompute::Signed,
2359 origin: Some(99),
2360 prev_ejected: false
2361 },
2362 Event::SolutionStored {
2363 compute: ElectionCompute::Signed,
2364 origin: Some(99),
2365 prev_ejected: false
2366 },
2367 Event::SolutionStored {
2368 compute: ElectionCompute::Signed,
2369 origin: Some(99),
2370 prev_ejected: false
2371 },
2372 Event::Slashed { account: 99, value: 5 },
2373 Event::Slashed { account: 99, value: 5 },
2374 Event::Slashed { account: 99, value: 5 },
2375 Event::Slashed { account: 99, value: 5 },
2376 Event::Slashed { account: 99, value: 5 },
2377 Event::ElectionFinalized {
2378 compute: ElectionCompute::Fallback,
2379 score: ElectionScore {
2380 minimal_stake: 0,
2381 sum_stake: 0,
2382 sum_stake_squared: 0
2383 }
2384 },
2385 Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 },
2386 ]
2387 );
2388 })
2389 }
2390
2391 #[test]
2392 fn check_events_with_compute_signed() {
2393 ExtBuilder::default().build_and_execute(|| {
2394 roll_to_signed();
2395 assert!(CurrentPhase::<Runtime>::get().is_signed());
2396
2397 let solution = raw_solution();
2398 assert_ok!(MultiPhase::submit(
2399 crate::mock::RuntimeOrigin::signed(99),
2400 Box::new(solution)
2401 ));
2402
2403 roll_to(30);
2404 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2405
2406 assert_eq!(
2407 multi_phase_events(),
2408 vec![
2409 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2410 Event::SolutionStored {
2411 compute: ElectionCompute::Signed,
2412 origin: Some(99),
2413 prev_ejected: false
2414 },
2415 Event::Rewarded { account: 99, value: 7 },
2416 Event::PhaseTransitioned {
2417 from: Phase::Signed,
2418 to: Phase::Unsigned((true, 25)),
2419 round: 1
2420 },
2421 Event::ElectionFinalized {
2422 compute: ElectionCompute::Signed,
2423 score: ElectionScore {
2424 minimal_stake: 40,
2425 sum_stake: 100,
2426 sum_stake_squared: 5200
2427 }
2428 },
2429 Event::PhaseTransitioned {
2430 from: Phase::Unsigned((true, 25)),
2431 to: Phase::Off,
2432 round: 2
2433 },
2434 ],
2435 );
2436 })
2437 }
2438
2439 #[test]
2440 fn check_events_with_compute_unsigned() {
2441 ExtBuilder::default().build_and_execute(|| {
2442 roll_to_unsigned();
2443 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
2444
2445 assert!(Snapshot::<Runtime>::get().is_some());
2447 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 2);
2448
2449 let (solution, witness, _) = MultiPhase::mine_solution().unwrap();
2451
2452 assert!(QueuedSolution::<Runtime>::get().is_none());
2454 assert_ok!(MultiPhase::submit_unsigned(
2455 crate::mock::RuntimeOrigin::none(),
2456 Box::new(solution),
2457 witness
2458 ));
2459 assert!(QueuedSolution::<Runtime>::get().is_some());
2460
2461 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2462
2463 assert_eq!(
2464 multi_phase_events(),
2465 vec![
2466 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2467 Event::PhaseTransitioned {
2468 from: Phase::Signed,
2469 to: Phase::Unsigned((true, 25)),
2470 round: 1
2471 },
2472 Event::SolutionStored {
2473 compute: ElectionCompute::Unsigned,
2474 origin: None,
2475 prev_ejected: false
2476 },
2477 Event::ElectionFinalized {
2478 compute: ElectionCompute::Unsigned,
2479 score: ElectionScore {
2480 minimal_stake: 40,
2481 sum_stake: 100,
2482 sum_stake_squared: 5200
2483 }
2484 },
2485 Event::PhaseTransitioned {
2486 from: Phase::Unsigned((true, 25)),
2487 to: Phase::Off,
2488 round: 2
2489 },
2490 ],
2491 );
2492 })
2493 }
2494
2495 #[test]
2496 fn try_elect_multi_page_fails() {
2497 let prepare_election = || {
2498 roll_to_signed();
2499 assert!(Snapshot::<Runtime>::get().is_some());
2500
2501 let (solution, _, _) = MultiPhase::mine_solution().unwrap();
2503 assert_ok!(MultiPhase::submit(
2504 crate::mock::RuntimeOrigin::signed(99),
2505 Box::new(solution),
2506 ));
2507 roll_to(30);
2508 assert!(QueuedSolution::<Runtime>::get().is_some());
2509 };
2510
2511 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2512 prepare_election();
2513 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2515 });
2516
2517 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2518 prepare_election();
2519 assert_noop!(MultiPhase::elect(SINGLE_PAGE + 1), ElectionError::MultiPageNotSupported);
2521 })
2522 }
2523
2524 #[test]
2525 fn fallback_strategy_works() {
2526 ExtBuilder::default().onchain_fallback(true).build_and_execute(|| {
2527 roll_to_unsigned();
2528 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2529
2530 assert!(QueuedSolution::<Runtime>::get().is_none());
2532 let supports = MultiPhase::elect(SINGLE_PAGE).unwrap();
2533
2534 let expected_supports = vec![
2535 (30, Support { total: 40, voters: vec![(2, 5), (4, 5), (30, 30)] }),
2536 (40, Support { total: 60, voters: vec![(2, 5), (3, 10), (4, 5), (40, 40)] }),
2537 ]
2538 .try_into()
2539 .unwrap();
2540
2541 assert_eq!(supports, expected_supports);
2542
2543 assert_eq!(
2544 multi_phase_events(),
2545 vec![
2546 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2547 Event::PhaseTransitioned {
2548 from: Phase::Signed,
2549 to: Phase::Unsigned((true, 25)),
2550 round: 1
2551 },
2552 Event::ElectionFinalized {
2553 compute: ElectionCompute::Fallback,
2554 score: ElectionScore {
2555 minimal_stake: 0,
2556 sum_stake: 0,
2557 sum_stake_squared: 0
2558 }
2559 },
2560 Event::PhaseTransitioned {
2561 from: Phase::Unsigned((true, 25)),
2562 to: Phase::Off,
2563 round: 2
2564 },
2565 ]
2566 );
2567 });
2568
2569 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2570 roll_to_unsigned();
2571 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2572
2573 assert!(QueuedSolution::<Runtime>::get().is_none());
2575 assert_eq!(
2576 MultiPhase::elect(SINGLE_PAGE).unwrap_err(),
2577 ElectionError::Fallback("NoFallback.")
2578 );
2579 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Emergency);
2581 assert!(Snapshot::<Runtime>::get().is_some());
2583
2584 assert_eq!(
2585 multi_phase_events(),
2586 vec![
2587 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2588 Event::PhaseTransitioned {
2589 from: Phase::Signed,
2590 to: Phase::Unsigned((true, 25)),
2591 round: 1
2592 },
2593 Event::ElectionFailed,
2594 Event::PhaseTransitioned {
2595 from: Phase::Unsigned((true, 25)),
2596 to: Phase::Emergency,
2597 round: 1
2598 },
2599 ]
2600 );
2601 })
2602 }
2603
2604 #[test]
2605 fn governance_fallback_works() {
2606 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2607 roll_to_unsigned();
2608 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2609
2610 assert!(QueuedSolution::<Runtime>::get().is_none());
2612 assert_eq!(
2613 MultiPhase::elect(SINGLE_PAGE).unwrap_err(),
2614 ElectionError::Fallback("NoFallback.")
2615 );
2616
2617 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Emergency);
2619 assert!(QueuedSolution::<Runtime>::get().is_none());
2620 assert!(Snapshot::<Runtime>::get().is_some());
2621
2622 assert_noop!(
2624 MultiPhase::governance_fallback(RuntimeOrigin::signed(99)),
2625 DispatchError::BadOrigin
2626 );
2627
2628 assert_ok!(MultiPhase::governance_fallback(RuntimeOrigin::root()));
2630 assert!(QueuedSolution::<Runtime>::get().is_some());
2632 assert!(MultiPhase::elect(SINGLE_PAGE).is_ok());
2634 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
2635
2636 assert_eq!(
2637 multi_phase_events(),
2638 vec![
2639 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2640 Event::PhaseTransitioned {
2641 from: Phase::Signed,
2642 to: Phase::Unsigned((true, 25)),
2643 round: 1
2644 },
2645 Event::ElectionFailed,
2646 Event::PhaseTransitioned {
2647 from: Phase::Unsigned((true, 25)),
2648 to: Phase::Emergency,
2649 round: 1
2650 },
2651 Event::SolutionStored {
2652 compute: ElectionCompute::Fallback,
2653 origin: None,
2654 prev_ejected: false
2655 },
2656 Event::ElectionFinalized {
2657 compute: ElectionCompute::Fallback,
2658 score: Default::default()
2659 },
2660 Event::PhaseTransitioned { from: Phase::Emergency, to: Phase::Off, round: 2 },
2661 ]
2662 );
2663 })
2664 }
2665
2666 #[test]
2667 fn snapshot_too_big_truncate() {
2668 ExtBuilder::default().build_and_execute(|| {
2670 assert_eq!(Voters::get().len(), 8);
2672 let new_bounds = ElectionBoundsBuilder::default().voters_count(2.into()).build();
2674 ElectionsBounds::set(new_bounds);
2675
2676 roll_to_signed();
2678 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2679
2680 assert_eq!(
2681 SnapshotMetadata::<Runtime>::get().unwrap(),
2682 SolutionOrSnapshotSize { voters: 2, targets: 4 }
2683 );
2684 })
2685 }
2686
2687 #[test]
2688 fn untrusted_score_verification_is_respected() {
2689 ExtBuilder::default().build_and_execute(|| {
2690 roll_to_signed();
2691 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2692
2693 crate::mock::Balancing::set(Some(BalancingConfig { iterations: 2, tolerance: 0 }));
2695
2696 let (solution, _, _) = MultiPhase::mine_solution().unwrap();
2697 assert!(matches!(solution.score, ElectionScore { minimal_stake: 50, .. }));
2699
2700 MinimumUntrustedScore::<Runtime>::put(ElectionScore {
2701 minimal_stake: 49,
2702 ..Default::default()
2703 });
2704 assert_ok!(MultiPhase::feasibility_check(solution.clone(), ElectionCompute::Signed));
2705
2706 MinimumUntrustedScore::<Runtime>::put(ElectionScore {
2707 minimal_stake: 51,
2708 ..Default::default()
2709 });
2710 assert_noop!(
2711 MultiPhase::feasibility_check(solution, ElectionCompute::Signed),
2712 FeasibilityError::UntrustedScoreTooLow,
2713 );
2714 })
2715 }
2716
2717 #[test]
2718 fn number_of_voters_allowed_2sec_block() {
2719 assert_eq!(MockWeightInfo::get(), MockedWeightInfo::Real);
2721
2722 let all_voters: u32 = 10_000;
2723 let all_targets: u32 = 5_000;
2724 let desired: u32 = 1_000;
2725 let weight_with = |active| {
2726 <Runtime as Config>::WeightInfo::submit_unsigned(
2727 all_voters,
2728 all_targets,
2729 active,
2730 desired,
2731 )
2732 };
2733
2734 let mut active = 1;
2735 while weight_with(active)
2736 .all_lte(<Runtime as frame_system::Config>::BlockWeights::get().max_block) ||
2737 active == all_voters
2738 {
2739 active += 1;
2740 }
2741
2742 println!("can support {} voters to yield a weight of {}", active, weight_with(active));
2743 }
2744}