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 #[allow(deprecated)]
1218 #[pallet::validate_unsigned]
1219 impl<T: Config> ValidateUnsigned for Pallet<T> {
1220 type Call = Call<T>;
1221 fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
1222 if let Call::submit_unsigned { raw_solution, .. } = call {
1223 match source {
1225 TransactionSource::Local | TransactionSource::InBlock => { },
1226 _ => return InvalidTransaction::Call.into(),
1227 }
1228
1229 Self::unsigned_pre_dispatch_checks(raw_solution)
1230 .inspect_err(|err| {
1231 log!(debug, "unsigned transaction validation failed due to {:?}", err);
1232 })
1233 .map_err(dispatch_error_to_invalid)?;
1234
1235 ValidTransaction::with_tag_prefix("OffchainElection")
1236 .priority(
1238 T::MinerTxPriority::get()
1239 .saturating_add(raw_solution.score.minimal_stake.saturated_into()),
1240 )
1241 .and_provides(raw_solution.round)
1244 .longevity(T::UnsignedPhase::get().saturated_into::<u64>())
1246 .propagate(false)
1248 .build()
1249 } else {
1250 InvalidTransaction::Call.into()
1251 }
1252 }
1253
1254 fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
1255 if let Call::submit_unsigned { raw_solution, .. } = call {
1256 Self::unsigned_pre_dispatch_checks(raw_solution)
1257 .map_err(dispatch_error_to_invalid)
1258 .map_err(Into::into)
1259 } else {
1260 Err(InvalidTransaction::Call.into())
1261 }
1262 }
1263 }
1264
1265 #[pallet::type_value]
1266 pub fn DefaultForRound() -> u32 {
1267 1
1268 }
1269
1270 #[pallet::storage]
1277 pub type Round<T: Config> = StorageValue<_, u32, ValueQuery, DefaultForRound>;
1278
1279 #[pallet::storage]
1281 pub type CurrentPhase<T: Config> = StorageValue<_, Phase<BlockNumberFor<T>>, ValueQuery>;
1282
1283 #[pallet::storage]
1287 pub type QueuedSolution<T: Config> = StorageValue<_, ReadySolutionOf<T::MinerConfig>>;
1288
1289 #[pallet::storage]
1294 pub type Snapshot<T: Config> = StorageValue<_, RoundSnapshot<T::AccountId, VoterOf<T>>>;
1295
1296 #[pallet::storage]
1301 pub type DesiredTargets<T> = StorageValue<_, u32>;
1302
1303 #[pallet::storage]
1308 pub type SnapshotMetadata<T: Config> = StorageValue<_, SolutionOrSnapshotSize>;
1309
1310 #[pallet::storage]
1324 pub type SignedSubmissionNextIndex<T: Config> = StorageValue<_, u32, ValueQuery>;
1325
1326 #[pallet::storage]
1333 pub type SignedSubmissionIndices<T: Config> =
1334 StorageValue<_, SubmissionIndicesOf<T>, ValueQuery>;
1335
1336 #[pallet::storage]
1344 pub type SignedSubmissionsMap<T: Config> =
1345 StorageMap<_, Twox64Concat, u32, SignedSubmissionOf<T>, OptionQuery>;
1346
1347 #[pallet::storage]
1354 pub type MinimumUntrustedScore<T: Config> = StorageValue<_, ElectionScore>;
1355
1356 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
1360
1361 #[pallet::pallet]
1362 #[pallet::without_storage_info]
1363 #[pallet::storage_version(STORAGE_VERSION)]
1364 pub struct Pallet<T>(_);
1365}
1366
1367pub struct SnapshotWrapper<T>(core::marker::PhantomData<T>);
1370
1371impl<T: Config> SnapshotWrapper<T> {
1372 pub fn kill() {
1374 Snapshot::<T>::kill();
1375 SnapshotMetadata::<T>::kill();
1376 DesiredTargets::<T>::kill();
1377 }
1378 pub fn set(metadata: SolutionOrSnapshotSize, desired_targets: u32, buffer: &[u8]) {
1380 SnapshotMetadata::<T>::put(metadata);
1381 DesiredTargets::<T>::put(desired_targets);
1382 sp_io::storage::set(&Snapshot::<T>::hashed_key(), &buffer);
1383 }
1384
1385 #[cfg(feature = "try-runtime")]
1388 pub fn is_consistent() -> bool {
1389 let snapshots = [
1390 Snapshot::<T>::exists(),
1391 SnapshotMetadata::<T>::exists(),
1392 DesiredTargets::<T>::exists(),
1393 ];
1394
1395 snapshots.iter().skip(1).all(|v| snapshots[0] == *v)
1397 }
1398}
1399
1400impl<T: Config> Pallet<T> {
1401 pub fn round() -> u32 {
1408 Round::<T>::get()
1409 }
1410
1411 pub fn current_phase() -> Phase<BlockNumberFor<T>> {
1413 CurrentPhase::<T>::get()
1414 }
1415
1416 pub fn queued_solution() -> Option<ReadySolutionOf<T::MinerConfig>> {
1420 QueuedSolution::<T>::get()
1421 }
1422
1423 pub fn snapshot() -> Option<RoundSnapshot<T::AccountId, VoterOf<T>>> {
1428 Snapshot::<T>::get()
1429 }
1430
1431 pub fn desired_targets() -> Option<u32> {
1436 DesiredTargets::<T>::get()
1437 }
1438
1439 pub fn snapshot_metadata() -> Option<SolutionOrSnapshotSize> {
1444 SnapshotMetadata::<T>::get()
1445 }
1446
1447 pub fn minimum_untrusted_score() -> Option<ElectionScore> {
1452 MinimumUntrustedScore::<T>::get()
1453 }
1454
1455 fn do_synchronized_offchain_worker(now: BlockNumberFor<T>) {
1458 let current_phase = CurrentPhase::<T>::get();
1459 log!(trace, "lock for offchain worker acquired. Phase = {:?}", current_phase);
1460 match current_phase {
1461 Phase::Unsigned((true, opened)) if opened == now => {
1462 let initial_output = Self::ensure_offchain_repeat_frequency(now).and_then(|_| {
1464 unsigned::kill_ocw_solution::<T>();
1467 Self::mine_check_save_submit()
1468 });
1469 log!(debug, "initial offchain thread output: {:?}", initial_output);
1470 },
1471 Phase::Unsigned((true, opened)) if opened < now => {
1472 let resubmit_output = Self::ensure_offchain_repeat_frequency(now)
1475 .and_then(|_| Self::restore_or_compute_then_maybe_submit());
1476 log!(debug, "resubmit offchain thread output: {:?}", resubmit_output);
1477 },
1478 _ => {},
1479 }
1480 }
1481
1482 pub(crate) fn phase_transition(to: Phase<BlockNumberFor<T>>) {
1484 log!(info, "Starting phase {:?}, round {}.", to, Round::<T>::get());
1485 Self::deposit_event(Event::PhaseTransitioned {
1486 from: CurrentPhase::<T>::get(),
1487 to,
1488 round: Round::<T>::get(),
1489 });
1490 CurrentPhase::<T>::put(to);
1491 }
1492
1493 fn create_snapshot_internal(
1497 targets: Vec<T::AccountId>,
1498 voters: Vec<VoterOf<T>>,
1499 desired_targets: u32,
1500 ) {
1501 let metadata =
1502 SolutionOrSnapshotSize { voters: voters.len() as u32, targets: targets.len() as u32 };
1503 log!(info, "creating a snapshot with metadata {:?}", metadata);
1504
1505 let snapshot = RoundSnapshot::<T::AccountId, VoterOf<T>> { voters, targets };
1509 let size = snapshot.encoded_size();
1510 log!(debug, "snapshot pre-calculated size {:?}", size);
1511 let mut buffer = Vec::with_capacity(size);
1512 snapshot.encode_to(&mut buffer);
1513
1514 debug_assert_eq!(buffer, snapshot.encode());
1516 debug_assert!(buffer.len() == size && size == buffer.capacity());
1518
1519 SnapshotWrapper::<T>::set(metadata, desired_targets, &buffer);
1520 }
1521
1522 fn create_snapshot_external(
1528 ) -> Result<(Vec<T::AccountId>, Vec<VoterOf<T>>, u32), ElectionError<T>> {
1529 let election_bounds = T::ElectionBounds::get();
1530 let targets = T::DataProvider::electable_targets_stateless(election_bounds.targets)
1531 .and_then(|t| {
1532 election_bounds.ensure_targets_limits(
1533 CountBound(t.len() as u32),
1534 SizeBound(t.encoded_size() as u32),
1535 )?;
1536 Ok(t)
1537 })
1538 .map_err(ElectionError::DataProvider)?;
1539
1540 let voters = T::DataProvider::electing_voters_stateless(election_bounds.voters)
1541 .and_then(|v| {
1542 election_bounds.ensure_voters_limits(
1543 CountBound(v.len() as u32),
1544 SizeBound(v.encoded_size() as u32),
1545 )?;
1546 Ok(v)
1547 })
1548 .map_err(ElectionError::DataProvider)?;
1549
1550 let mut desired_targets = <Pallet<T> as ElectionProvider>::desired_targets_checked()
1551 .map_err(|e| ElectionError::DataProvider(e))?;
1552
1553 let max_desired_targets: u32 = targets.len() as u32;
1556 if desired_targets > max_desired_targets {
1557 log!(
1558 warn,
1559 "desired_targets: {} > targets.len(): {}, capping desired_targets",
1560 desired_targets,
1561 max_desired_targets
1562 );
1563 desired_targets = max_desired_targets;
1564 }
1565
1566 Ok((targets, voters, desired_targets))
1567 }
1568
1569 pub fn create_snapshot() -> Result<(), ElectionError<T>> {
1580 let (targets, voters, desired_targets) = Self::create_snapshot_external()?;
1582
1583 let internal_weight =
1585 T::WeightInfo::create_snapshot_internal(voters.len() as u32, targets.len() as u32);
1586 Self::create_snapshot_internal(targets, voters, desired_targets);
1587 Self::register_weight(internal_weight);
1588 Ok(())
1589 }
1590
1591 fn register_weight(weight: Weight) {
1595 frame_system::Pallet::<T>::register_extra_weight_unchecked(
1596 weight,
1597 DispatchClass::Mandatory,
1598 );
1599 }
1600
1601 pub fn feasibility_check(
1603 raw_solution: RawSolution<SolutionOf<T::MinerConfig>>,
1604 compute: ElectionCompute,
1605 ) -> Result<ReadySolutionOf<T::MinerConfig>, FeasibilityError> {
1606 let desired_targets =
1607 DesiredTargets::<T>::get().ok_or(FeasibilityError::SnapshotUnavailable)?;
1608
1609 let snapshot = Snapshot::<T>::get().ok_or(FeasibilityError::SnapshotUnavailable)?;
1610 let round = Round::<T>::get();
1611 let minimum_untrusted_score = MinimumUntrustedScore::<T>::get();
1612
1613 Miner::<T::MinerConfig>::feasibility_check(
1614 raw_solution,
1615 compute,
1616 desired_targets,
1617 snapshot,
1618 round,
1619 minimum_untrusted_score,
1620 )
1621 }
1622
1623 fn rotate_round() {
1629 Round::<T>::mutate(|r| *r += 1);
1631
1632 Self::phase_transition(Phase::Off);
1634
1635 SnapshotWrapper::<T>::kill();
1637 }
1638
1639 fn do_elect() -> Result<BoundedSupportsOf<Self>, ElectionError<T>> {
1640 let _ = Self::finalize_signed_phase();
1648
1649 QueuedSolution::<T>::take()
1650 .ok_or(ElectionError::<T>::NothingQueued)
1651 .or_else(|_| {
1652 log!(warn, "No solution queued, falling back to instant fallback.",);
1653
1654 #[cfg(feature = "runtime-benchmarks")]
1655 Self::asap();
1656
1657 let (voters, targets, desired_targets) = if T::Fallback::bother() {
1658 let RoundSnapshot { voters, targets } = Snapshot::<T>::get().ok_or(
1659 ElectionError::<T>::Feasibility(FeasibilityError::SnapshotUnavailable),
1660 )?;
1661 let desired_targets = DesiredTargets::<T>::get().ok_or(
1662 ElectionError::<T>::Feasibility(FeasibilityError::SnapshotUnavailable),
1663 )?;
1664 (voters, targets, desired_targets)
1665 } else {
1666 (Default::default(), Default::default(), Default::default())
1667 };
1668 T::Fallback::instant_elect(voters, targets, desired_targets)
1669 .map_err(|fe| ElectionError::Fallback(fe))
1670 .and_then(|supports| {
1671 Ok(ReadySolution {
1672 supports,
1673 score: Default::default(),
1674 compute: ElectionCompute::Fallback,
1675 })
1676 })
1677 })
1678 .map(|ReadySolution { compute, score, supports }| {
1679 Self::deposit_event(Event::ElectionFinalized { compute, score });
1680 log!(info, "Finalized election round with compute {:?}.", compute);
1681 supports
1682 })
1683 .map_err(|err| {
1684 Self::deposit_event(Event::ElectionFailed);
1685 log!(warn, "Failed to finalize election round. reason {:?}", err);
1686 err
1687 })
1688 }
1689
1690 fn weigh_supports(supports: &BoundedSupportsOf<Self>) {
1692 let active_voters = supports
1693 .iter()
1694 .map(|(_, x)| x)
1695 .fold(Zero::zero(), |acc, next| acc + next.voters.len() as u32);
1696 let desired_targets = supports.len() as u32;
1697 Self::register_weight(T::WeightInfo::elect_queued(active_voters, desired_targets));
1698 }
1699}
1700
1701#[cfg(feature = "try-runtime")]
1702impl<T: Config> Pallet<T> {
1703 fn do_try_state() -> Result<(), TryRuntimeError> {
1704 Self::try_state_snapshot()?;
1705 Self::try_state_signed_submissions_map()?;
1706 Self::try_state_phase_off()
1707 }
1708
1709 fn try_state_snapshot() -> Result<(), TryRuntimeError> {
1713 if SnapshotWrapper::<T>::is_consistent() {
1714 Ok(())
1715 } else {
1716 Err("If snapshot exists, metadata and desired targets should be set too. Otherwise, none should be set.".into())
1717 }
1718 }
1719
1720 fn try_state_signed_submissions_map() -> Result<(), TryRuntimeError> {
1725 let mut last_score: ElectionScore = Default::default();
1726 let indices = SignedSubmissionIndices::<T>::get();
1727
1728 for (i, indice) in indices.iter().enumerate() {
1729 let submission = SignedSubmissionsMap::<T>::get(indice.2);
1730 if submission.is_none() {
1731 return Err(
1732 "All signed submissions indices must be part of the submissions map".into()
1733 );
1734 }
1735
1736 if i == 0 {
1737 last_score = indice.0
1738 } else {
1739 if last_score.strict_better(indice.0) {
1740 return Err(
1741 "Signed submission indices vector must be ordered by election score".into(),
1742 );
1743 }
1744 last_score = indice.0;
1745 }
1746 }
1747
1748 if SignedSubmissionsMap::<T>::iter().nth(indices.len()).is_some() {
1749 return Err(
1750 "Signed submissions map length should be the same as the indices vec length".into(),
1751 );
1752 }
1753
1754 match SignedSubmissionNextIndex::<T>::get() {
1755 0 => Ok(()),
1756 next => {
1757 if SignedSubmissionsMap::<T>::get(next).is_some() {
1758 return Err(
1759 "The next submissions index should not be in the submissions maps already"
1760 .into(),
1761 );
1762 } else {
1763 Ok(())
1764 }
1765 },
1766 }
1767 }
1768
1769 fn try_state_phase_off() -> Result<(), TryRuntimeError> {
1772 match CurrentPhase::<T>::get().is_off() {
1773 false => Ok(()),
1774 true => {
1775 if Snapshot::<T>::get().is_some() {
1776 Err("Snapshot must be none when in Phase::Off".into())
1777 } else {
1778 Ok(())
1779 }
1780 },
1781 }
1782 }
1783}
1784
1785impl<T: Config> ElectionProvider for Pallet<T> {
1786 type AccountId = T::AccountId;
1787 type BlockNumber = BlockNumberFor<T>;
1788 type Error = ElectionError<T>;
1789 type MaxWinnersPerPage = T::MaxWinners;
1790 type MaxBackersPerWinner = T::MaxBackersPerWinner;
1791 type MaxBackersPerWinnerFinal = T::MaxBackersPerWinner;
1792 type Pages = sp_core::ConstU32<1>;
1793 type DataProvider = T::DataProvider;
1794
1795 fn elect(page: PageIndex) -> Result<BoundedSupportsOf<Self>, Self::Error> {
1796 ensure!(page == SINGLE_PAGE, ElectionError::<T>::MultiPageNotSupported);
1798
1799 let res = match Self::do_elect() {
1800 Ok(bounded_supports) => {
1801 Self::weigh_supports(&bounded_supports);
1803 Self::rotate_round();
1804 Ok(bounded_supports)
1805 },
1806 Err(why) => {
1807 log!(error, "Entering emergency mode: {:?}", why);
1808 Self::phase_transition(Phase::Emergency);
1809 Err(why)
1810 },
1811 };
1812
1813 log!(info, "ElectionProvider::elect({}) => {:?}", page, res.as_ref().map(|s| s.len()));
1814 res
1815 }
1816
1817 fn duration() -> Self::BlockNumber {
1818 let signed: BlockNumberFor<T> = T::SignedPhase::get().saturated_into();
1819 let unsigned: BlockNumberFor<T> = T::UnsignedPhase::get().saturated_into();
1820 signed + unsigned
1821 }
1822
1823 fn start() -> Result<(), Self::Error> {
1824 log!(
1825 warn,
1826 "we received signal, but this pallet works in the basis of legacy pull based election"
1827 );
1828 Ok(())
1829 }
1830
1831 fn status() -> Result<Option<Weight>, ()> {
1832 let has_queued = QueuedSolution::<T>::exists();
1833 let phase = CurrentPhase::<T>::get();
1834 match (phase, has_queued) {
1835 (Phase::Unsigned(_), true) => Ok(Some(Default::default())),
1837 (Phase::Off, _) => Err(()),
1838 _ => Ok(None),
1839 }
1840 }
1841
1842 #[cfg(feature = "runtime-benchmarks")]
1843 fn asap() {
1844 if !Snapshot::<T>::exists() {
1846 Self::create_snapshot()
1847 .inspect_err(|e| {
1848 crate::log!(error, "failed to create snapshot while asap-preparing: {:?}", e)
1849 })
1850 .unwrap()
1851 }
1852 }
1853}
1854
1855pub fn dispatch_error_to_invalid(error: DispatchError) -> InvalidTransaction {
1858 let error_number = match error {
1859 DispatchError::Module(ModuleError { error, .. }) => error[0],
1860 _ => 0,
1861 };
1862 InvalidTransaction::Custom(error_number)
1863}
1864
1865#[cfg(test)]
1866mod feasibility_check {
1867 use super::*;
1872 use crate::mock::{
1873 raw_solution, roll_to, EpochLength, ExtBuilder, MultiPhase, Runtime, SignedPhase,
1874 TargetIndex, UnsignedPhase, VoterIndex,
1875 };
1876 use frame_support::{assert_noop, assert_ok};
1877
1878 const COMPUTE: ElectionCompute = ElectionCompute::OnChain;
1879
1880 #[test]
1881 fn snapshot_is_there() {
1882 ExtBuilder::default().build_and_execute(|| {
1883 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1884 assert!(CurrentPhase::<Runtime>::get().is_signed());
1885 let solution = raw_solution();
1886
1887 SnapshotWrapper::<Runtime>::kill();
1890
1891 assert_noop!(
1892 MultiPhase::feasibility_check(solution, COMPUTE),
1893 FeasibilityError::SnapshotUnavailable
1894 );
1895 })
1896 }
1897
1898 #[test]
1899 fn round() {
1900 ExtBuilder::default().build_and_execute(|| {
1901 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1902 assert!(CurrentPhase::<Runtime>::get().is_signed());
1903
1904 let mut solution = raw_solution();
1905 solution.round += 1;
1906 assert_noop!(
1907 MultiPhase::feasibility_check(solution, COMPUTE),
1908 FeasibilityError::InvalidRound
1909 );
1910 })
1911 }
1912
1913 #[test]
1914 fn desired_targets_gets_capped() {
1915 ExtBuilder::default().desired_targets(8).build_and_execute(|| {
1916 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1917 assert!(CurrentPhase::<Runtime>::get().is_signed());
1918
1919 let raw = raw_solution();
1920
1921 assert_eq!(raw.solution.unique_targets().len(), 4);
1922 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 4);
1924
1925 assert_ok!(MultiPhase::feasibility_check(raw, COMPUTE));
1927 })
1928 }
1929
1930 #[test]
1931 fn less_than_desired_targets_fails() {
1932 ExtBuilder::default().desired_targets(8).build_and_execute(|| {
1933 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1934 assert!(CurrentPhase::<Runtime>::get().is_signed());
1935
1936 let mut raw = raw_solution();
1937
1938 assert_eq!(raw.solution.unique_targets().len(), 4);
1939 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 4);
1941
1942 raw.solution.votes1[0].1 = 4;
1944
1945 assert_noop!(
1947 MultiPhase::feasibility_check(raw, COMPUTE),
1948 FeasibilityError::WrongWinnerCount,
1949 );
1950 })
1951 }
1952
1953 #[test]
1954 fn winner_indices() {
1955 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
1956 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1957 assert!(CurrentPhase::<Runtime>::get().is_signed());
1958
1959 let mut raw = raw_solution();
1960 assert_eq!(Snapshot::<Runtime>::get().unwrap().targets.len(), 4);
1961 raw.solution
1967 .votes1
1968 .iter_mut()
1969 .filter(|(_, t)| *t == TargetIndex::from(3u16))
1970 .for_each(|(_, t)| *t += 1);
1971 raw.solution.votes2.iter_mut().for_each(|(_, [(t0, _)], t1)| {
1972 if *t0 == TargetIndex::from(3u16) {
1973 *t0 += 1
1974 };
1975 if *t1 == TargetIndex::from(3u16) {
1976 *t1 += 1
1977 };
1978 });
1979 assert_noop!(
1980 MultiPhase::feasibility_check(raw, COMPUTE),
1981 FeasibilityError::NposElection(sp_npos_elections::Error::SolutionInvalidIndex)
1982 );
1983 })
1984 }
1985
1986 #[test]
1987 fn voter_indices() {
1988 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
1990 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1991 assert!(CurrentPhase::<Runtime>::get().is_signed());
1992
1993 let mut solution = raw_solution();
1994 assert_eq!(Snapshot::<Runtime>::get().unwrap().voters.len(), 8);
1995 assert!(
1999 solution
2000 .solution
2001 .votes1
2002 .iter_mut()
2003 .filter(|(v, _)| *v == VoterIndex::from(7u32))
2004 .map(|(v, _)| *v = 8)
2005 .count() > 0
2006 );
2007 assert_noop!(
2008 MultiPhase::feasibility_check(solution, COMPUTE),
2009 FeasibilityError::NposElection(sp_npos_elections::Error::SolutionInvalidIndex),
2010 );
2011 })
2012 }
2013
2014 #[test]
2015 fn voter_votes() {
2016 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
2017 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
2018 assert!(CurrentPhase::<Runtime>::get().is_signed());
2019
2020 let mut solution = raw_solution();
2021 assert_eq!(Snapshot::<Runtime>::get().unwrap().voters.len(), 8);
2022 assert_eq!(
2027 solution
2028 .solution
2029 .votes1
2030 .iter_mut()
2031 .filter(|(v, t)| *v == 7 && *t == 3)
2032 .map(|(_, t)| *t = 2)
2033 .count(),
2034 1,
2035 );
2036 assert_noop!(
2037 MultiPhase::feasibility_check(solution, COMPUTE),
2038 FeasibilityError::InvalidVote,
2039 );
2040 })
2041 }
2042
2043 #[test]
2044 fn score() {
2045 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
2046 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
2047 assert!(CurrentPhase::<Runtime>::get().is_signed());
2048
2049 let mut solution = raw_solution();
2050 assert_eq!(Snapshot::<Runtime>::get().unwrap().voters.len(), 8);
2051
2052 solution.score.minimal_stake += 1;
2054
2055 assert_noop!(
2056 MultiPhase::feasibility_check(solution, COMPUTE),
2057 FeasibilityError::InvalidScore,
2058 );
2059 })
2060 }
2061}
2062
2063#[cfg(test)]
2064mod tests {
2065 use super::*;
2066 use crate::{
2067 mock::{
2068 multi_phase_events, raw_solution, roll_to, roll_to_signed, roll_to_unsigned,
2069 ElectionsBounds, ExtBuilder, MockWeightInfo, MockedWeightInfo, MultiPhase, Runtime,
2070 RuntimeOrigin, SignedMaxSubmissions, System, Voters,
2071 },
2072 Phase,
2073 };
2074 use frame_election_provider_support::bounds::ElectionBoundsBuilder;
2075 use frame_support::{assert_noop, assert_ok};
2076 use sp_npos_elections::{BalancingConfig, Support};
2077
2078 #[test]
2079 fn phase_rotation_works() {
2080 ExtBuilder::default().build_and_execute(|| {
2081 assert_eq!(System::block_number(), 0);
2086 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
2087 assert_eq!(Round::<Runtime>::get(), 1);
2088
2089 roll_to(4);
2090 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
2091 assert!(Snapshot::<Runtime>::get().is_none());
2092 assert_eq!(Round::<Runtime>::get(), 1);
2093
2094 roll_to_signed();
2095 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2096 assert_eq!(
2097 multi_phase_events(),
2098 vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }]
2099 );
2100 assert!(Snapshot::<Runtime>::get().is_some());
2101 assert_eq!(Round::<Runtime>::get(), 1);
2102
2103 roll_to(24);
2104 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2105 assert!(Snapshot::<Runtime>::get().is_some());
2106 assert_eq!(Round::<Runtime>::get(), 1);
2107
2108 roll_to_unsigned();
2109 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2110 assert_eq!(
2111 multi_phase_events(),
2112 vec![
2113 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2114 Event::PhaseTransitioned {
2115 from: Phase::Signed,
2116 to: Phase::Unsigned((true, 25)),
2117 round: 1
2118 },
2119 ],
2120 );
2121 assert!(Snapshot::<Runtime>::get().is_some());
2122
2123 roll_to(29);
2124 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2125 assert!(Snapshot::<Runtime>::get().is_some());
2126
2127 roll_to(30);
2128 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2129 assert!(Snapshot::<Runtime>::get().is_some());
2130
2131 roll_to(32);
2133 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2134 assert!(Snapshot::<Runtime>::get().is_some());
2135
2136 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2137
2138 assert!(CurrentPhase::<Runtime>::get().is_off());
2139 assert!(Snapshot::<Runtime>::get().is_none());
2140 assert_eq!(Round::<Runtime>::get(), 2);
2141
2142 roll_to(44);
2143 assert!(CurrentPhase::<Runtime>::get().is_off());
2144
2145 roll_to_signed();
2146 assert!(CurrentPhase::<Runtime>::get().is_signed());
2147
2148 roll_to(55);
2149 assert!(CurrentPhase::<Runtime>::get().is_unsigned_open_at(55));
2150
2151 assert_eq!(
2152 multi_phase_events(),
2153 vec![
2154 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2155 Event::PhaseTransitioned {
2156 from: Phase::Signed,
2157 to: Phase::Unsigned((true, 25)),
2158 round: 1
2159 },
2160 Event::ElectionFinalized {
2161 compute: ElectionCompute::Fallback,
2162 score: ElectionScore {
2163 minimal_stake: 0,
2164 sum_stake: 0,
2165 sum_stake_squared: 0
2166 }
2167 },
2168 Event::PhaseTransitioned {
2169 from: Phase::Unsigned((true, 25)),
2170 to: Phase::Off,
2171 round: 2
2172 },
2173 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 2 },
2174 Event::PhaseTransitioned {
2175 from: Phase::Signed,
2176 to: Phase::Unsigned((true, 55)),
2177 round: 2
2178 },
2179 ]
2180 );
2181 })
2182 }
2183
2184 #[test]
2185 fn signed_phase_void() {
2186 ExtBuilder::default().phases(0, 10).build_and_execute(|| {
2187 roll_to(15);
2188 assert!(CurrentPhase::<Runtime>::get().is_off());
2189
2190 roll_to(19);
2191 assert!(CurrentPhase::<Runtime>::get().is_off());
2192
2193 roll_to(20);
2194 assert!(CurrentPhase::<Runtime>::get().is_unsigned_open_at(20));
2195 assert!(Snapshot::<Runtime>::get().is_some());
2196
2197 roll_to(30);
2198 assert!(CurrentPhase::<Runtime>::get().is_unsigned_open_at(20));
2199
2200 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2201
2202 assert!(CurrentPhase::<Runtime>::get().is_off());
2203 assert!(Snapshot::<Runtime>::get().is_none());
2204
2205 assert_eq!(
2206 multi_phase_events(),
2207 vec![
2208 Event::PhaseTransitioned {
2209 from: Phase::Off,
2210 to: Phase::Unsigned((true, 20)),
2211 round: 1
2212 },
2213 Event::ElectionFinalized {
2214 compute: ElectionCompute::Fallback,
2215 score: ElectionScore {
2216 minimal_stake: 0,
2217 sum_stake: 0,
2218 sum_stake_squared: 0
2219 }
2220 },
2221 Event::PhaseTransitioned {
2222 from: Phase::Unsigned((true, 20)),
2223 to: Phase::Off,
2224 round: 2
2225 },
2226 ]
2227 );
2228 });
2229 }
2230
2231 #[test]
2232 fn unsigned_phase_void() {
2233 ExtBuilder::default().phases(10, 0).build_and_execute(|| {
2234 roll_to(15);
2235 assert!(CurrentPhase::<Runtime>::get().is_off());
2236
2237 roll_to(19);
2238 assert!(CurrentPhase::<Runtime>::get().is_off());
2239
2240 roll_to_signed();
2241 assert!(CurrentPhase::<Runtime>::get().is_signed());
2242 assert!(Snapshot::<Runtime>::get().is_some());
2243
2244 roll_to(30);
2245 assert!(CurrentPhase::<Runtime>::get().is_signed());
2246
2247 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2248
2249 assert!(CurrentPhase::<Runtime>::get().is_off());
2250 assert!(Snapshot::<Runtime>::get().is_none());
2251
2252 assert_eq!(
2253 multi_phase_events(),
2254 vec![
2255 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2256 Event::ElectionFinalized {
2257 compute: ElectionCompute::Fallback,
2258 score: ElectionScore {
2259 minimal_stake: 0,
2260 sum_stake: 0,
2261 sum_stake_squared: 0
2262 }
2263 },
2264 Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 },
2265 ]
2266 )
2267 });
2268 }
2269
2270 #[test]
2271 fn early_termination() {
2272 ExtBuilder::default().build_and_execute(|| {
2274 roll_to_signed();
2277 assert_eq!(
2278 multi_phase_events(),
2279 vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }]
2280 );
2281 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2282 assert_eq!(Round::<Runtime>::get(), 1);
2283
2284 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2286
2287 assert_eq!(
2289 multi_phase_events(),
2290 vec![
2291 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2292 Event::ElectionFinalized {
2293 compute: ElectionCompute::Fallback,
2294 score: Default::default()
2295 },
2296 Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 },
2297 ],
2298 );
2299 assert_eq!(Round::<Runtime>::get(), 2);
2301 assert!(Snapshot::<Runtime>::get().is_none());
2302 assert!(SnapshotMetadata::<Runtime>::get().is_none());
2303 assert!(DesiredTargets::<Runtime>::get().is_none());
2304 assert!(QueuedSolution::<Runtime>::get().is_none());
2305 assert!(MultiPhase::signed_submissions().is_empty());
2306 })
2307 }
2308
2309 #[test]
2310 fn early_termination_with_submissions() {
2311 ExtBuilder::default().build_and_execute(|| {
2313 roll_to_signed();
2316 assert_eq!(
2317 multi_phase_events(),
2318 vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }]
2319 );
2320 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2321 assert_eq!(Round::<Runtime>::get(), 1);
2322
2323 for s in 0..SignedMaxSubmissions::get() {
2325 let solution = RawSolution {
2326 score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
2327 ..Default::default()
2328 };
2329 assert_ok!(MultiPhase::submit(
2330 crate::mock::RuntimeOrigin::signed(99),
2331 Box::new(solution)
2332 ));
2333 }
2334
2335 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2337
2338 assert_eq!(Round::<Runtime>::get(), 2);
2340 assert!(Snapshot::<Runtime>::get().is_none());
2341 assert!(SnapshotMetadata::<Runtime>::get().is_none());
2342 assert!(DesiredTargets::<Runtime>::get().is_none());
2343 assert!(QueuedSolution::<Runtime>::get().is_none());
2344 assert!(MultiPhase::signed_submissions().is_empty());
2345
2346 assert_eq!(
2347 multi_phase_events(),
2348 vec![
2349 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2350 Event::SolutionStored {
2351 compute: ElectionCompute::Signed,
2352 origin: Some(99),
2353 prev_ejected: false
2354 },
2355 Event::SolutionStored {
2356 compute: ElectionCompute::Signed,
2357 origin: Some(99),
2358 prev_ejected: false
2359 },
2360 Event::SolutionStored {
2361 compute: ElectionCompute::Signed,
2362 origin: Some(99),
2363 prev_ejected: false
2364 },
2365 Event::SolutionStored {
2366 compute: ElectionCompute::Signed,
2367 origin: Some(99),
2368 prev_ejected: false
2369 },
2370 Event::SolutionStored {
2371 compute: ElectionCompute::Signed,
2372 origin: Some(99),
2373 prev_ejected: false
2374 },
2375 Event::Slashed { account: 99, value: 5 },
2376 Event::Slashed { account: 99, value: 5 },
2377 Event::Slashed { account: 99, value: 5 },
2378 Event::Slashed { account: 99, value: 5 },
2379 Event::Slashed { account: 99, value: 5 },
2380 Event::ElectionFinalized {
2381 compute: ElectionCompute::Fallback,
2382 score: ElectionScore {
2383 minimal_stake: 0,
2384 sum_stake: 0,
2385 sum_stake_squared: 0
2386 }
2387 },
2388 Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 },
2389 ]
2390 );
2391 })
2392 }
2393
2394 #[test]
2395 fn check_events_with_compute_signed() {
2396 ExtBuilder::default().build_and_execute(|| {
2397 roll_to_signed();
2398 assert!(CurrentPhase::<Runtime>::get().is_signed());
2399
2400 let solution = raw_solution();
2401 assert_ok!(MultiPhase::submit(
2402 crate::mock::RuntimeOrigin::signed(99),
2403 Box::new(solution)
2404 ));
2405
2406 roll_to(30);
2407 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2408
2409 assert_eq!(
2410 multi_phase_events(),
2411 vec![
2412 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2413 Event::SolutionStored {
2414 compute: ElectionCompute::Signed,
2415 origin: Some(99),
2416 prev_ejected: false
2417 },
2418 Event::Rewarded { account: 99, value: 7 },
2419 Event::PhaseTransitioned {
2420 from: Phase::Signed,
2421 to: Phase::Unsigned((true, 25)),
2422 round: 1
2423 },
2424 Event::ElectionFinalized {
2425 compute: ElectionCompute::Signed,
2426 score: ElectionScore {
2427 minimal_stake: 40,
2428 sum_stake: 100,
2429 sum_stake_squared: 5200
2430 }
2431 },
2432 Event::PhaseTransitioned {
2433 from: Phase::Unsigned((true, 25)),
2434 to: Phase::Off,
2435 round: 2
2436 },
2437 ],
2438 );
2439 })
2440 }
2441
2442 #[test]
2443 fn check_events_with_compute_unsigned() {
2444 ExtBuilder::default().build_and_execute(|| {
2445 roll_to_unsigned();
2446 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
2447
2448 assert!(Snapshot::<Runtime>::get().is_some());
2450 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 2);
2451
2452 let (solution, witness, _) = MultiPhase::mine_solution().unwrap();
2454
2455 assert!(QueuedSolution::<Runtime>::get().is_none());
2457 assert_ok!(MultiPhase::submit_unsigned(
2458 crate::mock::RuntimeOrigin::none(),
2459 Box::new(solution),
2460 witness
2461 ));
2462 assert!(QueuedSolution::<Runtime>::get().is_some());
2463
2464 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2465
2466 assert_eq!(
2467 multi_phase_events(),
2468 vec![
2469 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2470 Event::PhaseTransitioned {
2471 from: Phase::Signed,
2472 to: Phase::Unsigned((true, 25)),
2473 round: 1
2474 },
2475 Event::SolutionStored {
2476 compute: ElectionCompute::Unsigned,
2477 origin: None,
2478 prev_ejected: false
2479 },
2480 Event::ElectionFinalized {
2481 compute: ElectionCompute::Unsigned,
2482 score: ElectionScore {
2483 minimal_stake: 40,
2484 sum_stake: 100,
2485 sum_stake_squared: 5200
2486 }
2487 },
2488 Event::PhaseTransitioned {
2489 from: Phase::Unsigned((true, 25)),
2490 to: Phase::Off,
2491 round: 2
2492 },
2493 ],
2494 );
2495 })
2496 }
2497
2498 #[test]
2499 fn try_elect_multi_page_fails() {
2500 let prepare_election = || {
2501 roll_to_signed();
2502 assert!(Snapshot::<Runtime>::get().is_some());
2503
2504 let (solution, _, _) = MultiPhase::mine_solution().unwrap();
2506 assert_ok!(MultiPhase::submit(
2507 crate::mock::RuntimeOrigin::signed(99),
2508 Box::new(solution),
2509 ));
2510 roll_to(30);
2511 assert!(QueuedSolution::<Runtime>::get().is_some());
2512 };
2513
2514 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2515 prepare_election();
2516 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2518 });
2519
2520 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2521 prepare_election();
2522 assert_noop!(MultiPhase::elect(SINGLE_PAGE + 1), ElectionError::MultiPageNotSupported);
2524 })
2525 }
2526
2527 #[test]
2528 fn fallback_strategy_works() {
2529 ExtBuilder::default().onchain_fallback(true).build_and_execute(|| {
2530 roll_to_unsigned();
2531 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2532
2533 assert!(QueuedSolution::<Runtime>::get().is_none());
2535 let supports = MultiPhase::elect(SINGLE_PAGE).unwrap();
2536
2537 let expected_supports = vec![
2538 (30, Support { total: 40, voters: vec![(2, 5), (4, 5), (30, 30)] }),
2539 (40, Support { total: 60, voters: vec![(2, 5), (3, 10), (4, 5), (40, 40)] }),
2540 ]
2541 .try_into()
2542 .unwrap();
2543
2544 assert_eq!(supports, expected_supports);
2545
2546 assert_eq!(
2547 multi_phase_events(),
2548 vec![
2549 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2550 Event::PhaseTransitioned {
2551 from: Phase::Signed,
2552 to: Phase::Unsigned((true, 25)),
2553 round: 1
2554 },
2555 Event::ElectionFinalized {
2556 compute: ElectionCompute::Fallback,
2557 score: ElectionScore {
2558 minimal_stake: 0,
2559 sum_stake: 0,
2560 sum_stake_squared: 0
2561 }
2562 },
2563 Event::PhaseTransitioned {
2564 from: Phase::Unsigned((true, 25)),
2565 to: Phase::Off,
2566 round: 2
2567 },
2568 ]
2569 );
2570 });
2571
2572 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2573 roll_to_unsigned();
2574 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2575
2576 assert!(QueuedSolution::<Runtime>::get().is_none());
2578 assert_eq!(
2579 MultiPhase::elect(SINGLE_PAGE).unwrap_err(),
2580 ElectionError::Fallback("NoFallback.")
2581 );
2582 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Emergency);
2584 assert!(Snapshot::<Runtime>::get().is_some());
2586
2587 assert_eq!(
2588 multi_phase_events(),
2589 vec![
2590 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2591 Event::PhaseTransitioned {
2592 from: Phase::Signed,
2593 to: Phase::Unsigned((true, 25)),
2594 round: 1
2595 },
2596 Event::ElectionFailed,
2597 Event::PhaseTransitioned {
2598 from: Phase::Unsigned((true, 25)),
2599 to: Phase::Emergency,
2600 round: 1
2601 },
2602 ]
2603 );
2604 })
2605 }
2606
2607 #[test]
2608 fn governance_fallback_works() {
2609 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2610 roll_to_unsigned();
2611 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2612
2613 assert!(QueuedSolution::<Runtime>::get().is_none());
2615 assert_eq!(
2616 MultiPhase::elect(SINGLE_PAGE).unwrap_err(),
2617 ElectionError::Fallback("NoFallback.")
2618 );
2619
2620 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Emergency);
2622 assert!(QueuedSolution::<Runtime>::get().is_none());
2623 assert!(Snapshot::<Runtime>::get().is_some());
2624
2625 assert_noop!(
2627 MultiPhase::governance_fallback(RuntimeOrigin::signed(99)),
2628 DispatchError::BadOrigin
2629 );
2630
2631 assert_ok!(MultiPhase::governance_fallback(RuntimeOrigin::root()));
2633 assert!(QueuedSolution::<Runtime>::get().is_some());
2635 assert!(MultiPhase::elect(SINGLE_PAGE).is_ok());
2637 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
2638
2639 assert_eq!(
2640 multi_phase_events(),
2641 vec![
2642 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2643 Event::PhaseTransitioned {
2644 from: Phase::Signed,
2645 to: Phase::Unsigned((true, 25)),
2646 round: 1
2647 },
2648 Event::ElectionFailed,
2649 Event::PhaseTransitioned {
2650 from: Phase::Unsigned((true, 25)),
2651 to: Phase::Emergency,
2652 round: 1
2653 },
2654 Event::SolutionStored {
2655 compute: ElectionCompute::Fallback,
2656 origin: None,
2657 prev_ejected: false
2658 },
2659 Event::ElectionFinalized {
2660 compute: ElectionCompute::Fallback,
2661 score: Default::default()
2662 },
2663 Event::PhaseTransitioned { from: Phase::Emergency, to: Phase::Off, round: 2 },
2664 ]
2665 );
2666 })
2667 }
2668
2669 #[test]
2670 fn snapshot_too_big_truncate() {
2671 ExtBuilder::default().build_and_execute(|| {
2673 assert_eq!(Voters::get().len(), 8);
2675 let new_bounds = ElectionBoundsBuilder::default().voters_count(2.into()).build();
2677 ElectionsBounds::set(new_bounds);
2678
2679 roll_to_signed();
2681 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2682
2683 assert_eq!(
2684 SnapshotMetadata::<Runtime>::get().unwrap(),
2685 SolutionOrSnapshotSize { voters: 2, targets: 4 }
2686 );
2687 })
2688 }
2689
2690 #[test]
2691 fn untrusted_score_verification_is_respected() {
2692 ExtBuilder::default().build_and_execute(|| {
2693 roll_to_signed();
2694 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2695
2696 crate::mock::Balancing::set(Some(BalancingConfig { iterations: 2, tolerance: 0 }));
2698
2699 let (solution, _, _) = MultiPhase::mine_solution().unwrap();
2700 assert!(matches!(solution.score, ElectionScore { minimal_stake: 50, .. }));
2702
2703 MinimumUntrustedScore::<Runtime>::put(ElectionScore {
2704 minimal_stake: 49,
2705 ..Default::default()
2706 });
2707 assert_ok!(MultiPhase::feasibility_check(solution.clone(), ElectionCompute::Signed));
2708
2709 MinimumUntrustedScore::<Runtime>::put(ElectionScore {
2710 minimal_stake: 51,
2711 ..Default::default()
2712 });
2713 assert_noop!(
2714 MultiPhase::feasibility_check(solution, ElectionCompute::Signed),
2715 FeasibilityError::UntrustedScoreTooLow,
2716 );
2717 })
2718 }
2719
2720 #[test]
2721 fn number_of_voters_allowed_2sec_block() {
2722 assert_eq!(MockWeightInfo::get(), MockedWeightInfo::Real);
2724
2725 let all_voters: u32 = 10_000;
2726 let all_targets: u32 = 5_000;
2727 let desired: u32 = 1_000;
2728 let weight_with = |active| {
2729 <Runtime as Config>::WeightInfo::submit_unsigned(
2730 all_voters,
2731 all_targets,
2732 active,
2733 desired,
2734 )
2735 };
2736
2737 let mut active = 1;
2738 while weight_with(active)
2739 .all_lte(<Runtime as frame_system::Config>::BlockWeights::get().max_block) ||
2740 active == all_voters
2741 {
2742 active += 1;
2743 }
2744
2745 println!("can support {} voters to yield a weight of {}", active, weight_with(active));
2746 }
2747}