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
1768 fn try_state_phase_off() -> Result<(), TryRuntimeError> {
1771 match CurrentPhase::<T>::get().is_off() {
1772 false => Ok(()),
1773 true => {
1774 if Snapshot::<T>::get().is_some() {
1775 Err("Snapshot must be none when in Phase::Off".into())
1776 } else {
1777 Ok(())
1778 }
1779 },
1780 }
1781 }
1782}
1783
1784impl<T: Config> ElectionProvider for Pallet<T> {
1785 type AccountId = T::AccountId;
1786 type BlockNumber = BlockNumberFor<T>;
1787 type Error = ElectionError<T>;
1788 type MaxWinnersPerPage = T::MaxWinners;
1789 type MaxBackersPerWinner = T::MaxBackersPerWinner;
1790 type MaxBackersPerWinnerFinal = T::MaxBackersPerWinner;
1791 type Pages = sp_core::ConstU32<1>;
1792 type DataProvider = T::DataProvider;
1793
1794 fn elect(page: PageIndex) -> Result<BoundedSupportsOf<Self>, Self::Error> {
1795 ensure!(page == SINGLE_PAGE, ElectionError::<T>::MultiPageNotSupported);
1797
1798 let res = match Self::do_elect() {
1799 Ok(bounded_supports) => {
1800 Self::weigh_supports(&bounded_supports);
1802 Self::rotate_round();
1803 Ok(bounded_supports)
1804 },
1805 Err(why) => {
1806 log!(error, "Entering emergency mode: {:?}", why);
1807 Self::phase_transition(Phase::Emergency);
1808 Err(why)
1809 },
1810 };
1811
1812 log!(info, "ElectionProvider::elect({}) => {:?}", page, res.as_ref().map(|s| s.len()));
1813 res
1814 }
1815
1816 fn duration() -> Self::BlockNumber {
1817 let signed: BlockNumberFor<T> = T::SignedPhase::get().saturated_into();
1818 let unsigned: BlockNumberFor<T> = T::UnsignedPhase::get().saturated_into();
1819 signed + unsigned
1820 }
1821
1822 fn start() -> Result<(), Self::Error> {
1823 log!(
1824 warn,
1825 "we received signal, but this pallet works in the basis of legacy pull based election"
1826 );
1827 Ok(())
1828 }
1829
1830 fn status() -> Result<Option<Weight>, ()> {
1831 let has_queued = QueuedSolution::<T>::exists();
1832 let phase = CurrentPhase::<T>::get();
1833 match (phase, has_queued) {
1834 (Phase::Unsigned(_), true) => Ok(Some(Default::default())),
1836 (Phase::Off, _) => Err(()),
1837 _ => Ok(None),
1838 }
1839 }
1840
1841 #[cfg(feature = "runtime-benchmarks")]
1842 fn asap() {
1843 if !Snapshot::<T>::exists() {
1845 Self::create_snapshot()
1846 .inspect_err(|e| {
1847 crate::log!(error, "failed to create snapshot while asap-preparing: {:?}", e)
1848 })
1849 .unwrap()
1850 }
1851 }
1852}
1853
1854pub fn dispatch_error_to_invalid(error: DispatchError) -> InvalidTransaction {
1857 let error_number = match error {
1858 DispatchError::Module(ModuleError { error, .. }) => error[0],
1859 _ => 0,
1860 };
1861 InvalidTransaction::Custom(error_number)
1862}
1863
1864#[cfg(test)]
1865mod feasibility_check {
1866 use super::*;
1871 use crate::mock::{
1872 raw_solution, roll_to, EpochLength, ExtBuilder, MultiPhase, Runtime, SignedPhase,
1873 TargetIndex, UnsignedPhase, VoterIndex,
1874 };
1875 use frame_support::{assert_noop, assert_ok};
1876
1877 const COMPUTE: ElectionCompute = ElectionCompute::OnChain;
1878
1879 #[test]
1880 fn snapshot_is_there() {
1881 ExtBuilder::default().build_and_execute(|| {
1882 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1883 assert!(CurrentPhase::<Runtime>::get().is_signed());
1884 let solution = raw_solution();
1885
1886 SnapshotWrapper::<Runtime>::kill();
1889
1890 assert_noop!(
1891 MultiPhase::feasibility_check(solution, COMPUTE),
1892 FeasibilityError::SnapshotUnavailable
1893 );
1894 })
1895 }
1896
1897 #[test]
1898 fn round() {
1899 ExtBuilder::default().build_and_execute(|| {
1900 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1901 assert!(CurrentPhase::<Runtime>::get().is_signed());
1902
1903 let mut solution = raw_solution();
1904 solution.round += 1;
1905 assert_noop!(
1906 MultiPhase::feasibility_check(solution, COMPUTE),
1907 FeasibilityError::InvalidRound
1908 );
1909 })
1910 }
1911
1912 #[test]
1913 fn desired_targets_gets_capped() {
1914 ExtBuilder::default().desired_targets(8).build_and_execute(|| {
1915 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1916 assert!(CurrentPhase::<Runtime>::get().is_signed());
1917
1918 let raw = raw_solution();
1919
1920 assert_eq!(raw.solution.unique_targets().len(), 4);
1921 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 4);
1923
1924 assert_ok!(MultiPhase::feasibility_check(raw, COMPUTE));
1926 })
1927 }
1928
1929 #[test]
1930 fn less_than_desired_targets_fails() {
1931 ExtBuilder::default().desired_targets(8).build_and_execute(|| {
1932 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1933 assert!(CurrentPhase::<Runtime>::get().is_signed());
1934
1935 let mut raw = raw_solution();
1936
1937 assert_eq!(raw.solution.unique_targets().len(), 4);
1938 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 4);
1940
1941 raw.solution.votes1[0].1 = 4;
1943
1944 assert_noop!(
1946 MultiPhase::feasibility_check(raw, COMPUTE),
1947 FeasibilityError::WrongWinnerCount,
1948 );
1949 })
1950 }
1951
1952 #[test]
1953 fn winner_indices() {
1954 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
1955 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1956 assert!(CurrentPhase::<Runtime>::get().is_signed());
1957
1958 let mut raw = raw_solution();
1959 assert_eq!(Snapshot::<Runtime>::get().unwrap().targets.len(), 4);
1960 raw.solution
1966 .votes1
1967 .iter_mut()
1968 .filter(|(_, t)| *t == TargetIndex::from(3u16))
1969 .for_each(|(_, t)| *t += 1);
1970 raw.solution.votes2.iter_mut().for_each(|(_, [(t0, _)], t1)| {
1971 if *t0 == TargetIndex::from(3u16) {
1972 *t0 += 1
1973 };
1974 if *t1 == TargetIndex::from(3u16) {
1975 *t1 += 1
1976 };
1977 });
1978 assert_noop!(
1979 MultiPhase::feasibility_check(raw, COMPUTE),
1980 FeasibilityError::NposElection(sp_npos_elections::Error::SolutionInvalidIndex)
1981 );
1982 })
1983 }
1984
1985 #[test]
1986 fn voter_indices() {
1987 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
1989 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1990 assert!(CurrentPhase::<Runtime>::get().is_signed());
1991
1992 let mut solution = raw_solution();
1993 assert_eq!(Snapshot::<Runtime>::get().unwrap().voters.len(), 8);
1994 assert!(
1998 solution
1999 .solution
2000 .votes1
2001 .iter_mut()
2002 .filter(|(v, _)| *v == VoterIndex::from(7u32))
2003 .map(|(v, _)| *v = 8)
2004 .count() > 0
2005 );
2006 assert_noop!(
2007 MultiPhase::feasibility_check(solution, COMPUTE),
2008 FeasibilityError::NposElection(sp_npos_elections::Error::SolutionInvalidIndex),
2009 );
2010 })
2011 }
2012
2013 #[test]
2014 fn voter_votes() {
2015 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
2016 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
2017 assert!(CurrentPhase::<Runtime>::get().is_signed());
2018
2019 let mut solution = raw_solution();
2020 assert_eq!(Snapshot::<Runtime>::get().unwrap().voters.len(), 8);
2021 assert_eq!(
2026 solution
2027 .solution
2028 .votes1
2029 .iter_mut()
2030 .filter(|(v, t)| *v == 7 && *t == 3)
2031 .map(|(_, t)| *t = 2)
2032 .count(),
2033 1,
2034 );
2035 assert_noop!(
2036 MultiPhase::feasibility_check(solution, COMPUTE),
2037 FeasibilityError::InvalidVote,
2038 );
2039 })
2040 }
2041
2042 #[test]
2043 fn score() {
2044 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
2045 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
2046 assert!(CurrentPhase::<Runtime>::get().is_signed());
2047
2048 let mut solution = raw_solution();
2049 assert_eq!(Snapshot::<Runtime>::get().unwrap().voters.len(), 8);
2050
2051 solution.score.minimal_stake += 1;
2053
2054 assert_noop!(
2055 MultiPhase::feasibility_check(solution, COMPUTE),
2056 FeasibilityError::InvalidScore,
2057 );
2058 })
2059 }
2060}
2061
2062#[cfg(test)]
2063mod tests {
2064 use super::*;
2065 use crate::{
2066 mock::{
2067 multi_phase_events, raw_solution, roll_to, roll_to_signed, roll_to_unsigned,
2068 ElectionsBounds, ExtBuilder, MockWeightInfo, MockedWeightInfo, MultiPhase, Runtime,
2069 RuntimeOrigin, SignedMaxSubmissions, System, Voters,
2070 },
2071 Phase,
2072 };
2073 use frame_election_provider_support::bounds::ElectionBoundsBuilder;
2074 use frame_support::{assert_noop, assert_ok};
2075 use sp_npos_elections::{BalancingConfig, Support};
2076
2077 #[test]
2078 fn phase_rotation_works() {
2079 ExtBuilder::default().build_and_execute(|| {
2080 assert_eq!(System::block_number(), 0);
2085 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
2086 assert_eq!(Round::<Runtime>::get(), 1);
2087
2088 roll_to(4);
2089 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
2090 assert!(Snapshot::<Runtime>::get().is_none());
2091 assert_eq!(Round::<Runtime>::get(), 1);
2092
2093 roll_to_signed();
2094 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2095 assert_eq!(
2096 multi_phase_events(),
2097 vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }]
2098 );
2099 assert!(Snapshot::<Runtime>::get().is_some());
2100 assert_eq!(Round::<Runtime>::get(), 1);
2101
2102 roll_to(24);
2103 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2104 assert!(Snapshot::<Runtime>::get().is_some());
2105 assert_eq!(Round::<Runtime>::get(), 1);
2106
2107 roll_to_unsigned();
2108 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2109 assert_eq!(
2110 multi_phase_events(),
2111 vec![
2112 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2113 Event::PhaseTransitioned {
2114 from: Phase::Signed,
2115 to: Phase::Unsigned((true, 25)),
2116 round: 1
2117 },
2118 ],
2119 );
2120 assert!(Snapshot::<Runtime>::get().is_some());
2121
2122 roll_to(29);
2123 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2124 assert!(Snapshot::<Runtime>::get().is_some());
2125
2126 roll_to(30);
2127 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2128 assert!(Snapshot::<Runtime>::get().is_some());
2129
2130 roll_to(32);
2132 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2133 assert!(Snapshot::<Runtime>::get().is_some());
2134
2135 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2136
2137 assert!(CurrentPhase::<Runtime>::get().is_off());
2138 assert!(Snapshot::<Runtime>::get().is_none());
2139 assert_eq!(Round::<Runtime>::get(), 2);
2140
2141 roll_to(44);
2142 assert!(CurrentPhase::<Runtime>::get().is_off());
2143
2144 roll_to_signed();
2145 assert!(CurrentPhase::<Runtime>::get().is_signed());
2146
2147 roll_to(55);
2148 assert!(CurrentPhase::<Runtime>::get().is_unsigned_open_at(55));
2149
2150 assert_eq!(
2151 multi_phase_events(),
2152 vec![
2153 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2154 Event::PhaseTransitioned {
2155 from: Phase::Signed,
2156 to: Phase::Unsigned((true, 25)),
2157 round: 1
2158 },
2159 Event::ElectionFinalized {
2160 compute: ElectionCompute::Fallback,
2161 score: ElectionScore {
2162 minimal_stake: 0,
2163 sum_stake: 0,
2164 sum_stake_squared: 0
2165 }
2166 },
2167 Event::PhaseTransitioned {
2168 from: Phase::Unsigned((true, 25)),
2169 to: Phase::Off,
2170 round: 2
2171 },
2172 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 2 },
2173 Event::PhaseTransitioned {
2174 from: Phase::Signed,
2175 to: Phase::Unsigned((true, 55)),
2176 round: 2
2177 },
2178 ]
2179 );
2180 })
2181 }
2182
2183 #[test]
2184 fn signed_phase_void() {
2185 ExtBuilder::default().phases(0, 10).build_and_execute(|| {
2186 roll_to(15);
2187 assert!(CurrentPhase::<Runtime>::get().is_off());
2188
2189 roll_to(19);
2190 assert!(CurrentPhase::<Runtime>::get().is_off());
2191
2192 roll_to(20);
2193 assert!(CurrentPhase::<Runtime>::get().is_unsigned_open_at(20));
2194 assert!(Snapshot::<Runtime>::get().is_some());
2195
2196 roll_to(30);
2197 assert!(CurrentPhase::<Runtime>::get().is_unsigned_open_at(20));
2198
2199 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2200
2201 assert!(CurrentPhase::<Runtime>::get().is_off());
2202 assert!(Snapshot::<Runtime>::get().is_none());
2203
2204 assert_eq!(
2205 multi_phase_events(),
2206 vec![
2207 Event::PhaseTransitioned {
2208 from: Phase::Off,
2209 to: Phase::Unsigned((true, 20)),
2210 round: 1
2211 },
2212 Event::ElectionFinalized {
2213 compute: ElectionCompute::Fallback,
2214 score: ElectionScore {
2215 minimal_stake: 0,
2216 sum_stake: 0,
2217 sum_stake_squared: 0
2218 }
2219 },
2220 Event::PhaseTransitioned {
2221 from: Phase::Unsigned((true, 20)),
2222 to: Phase::Off,
2223 round: 2
2224 },
2225 ]
2226 );
2227 });
2228 }
2229
2230 #[test]
2231 fn unsigned_phase_void() {
2232 ExtBuilder::default().phases(10, 0).build_and_execute(|| {
2233 roll_to(15);
2234 assert!(CurrentPhase::<Runtime>::get().is_off());
2235
2236 roll_to(19);
2237 assert!(CurrentPhase::<Runtime>::get().is_off());
2238
2239 roll_to_signed();
2240 assert!(CurrentPhase::<Runtime>::get().is_signed());
2241 assert!(Snapshot::<Runtime>::get().is_some());
2242
2243 roll_to(30);
2244 assert!(CurrentPhase::<Runtime>::get().is_signed());
2245
2246 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2247
2248 assert!(CurrentPhase::<Runtime>::get().is_off());
2249 assert!(Snapshot::<Runtime>::get().is_none());
2250
2251 assert_eq!(
2252 multi_phase_events(),
2253 vec![
2254 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2255 Event::ElectionFinalized {
2256 compute: ElectionCompute::Fallback,
2257 score: ElectionScore {
2258 minimal_stake: 0,
2259 sum_stake: 0,
2260 sum_stake_squared: 0
2261 }
2262 },
2263 Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 },
2264 ]
2265 )
2266 });
2267 }
2268
2269 #[test]
2270 fn early_termination() {
2271 ExtBuilder::default().build_and_execute(|| {
2273 roll_to_signed();
2276 assert_eq!(
2277 multi_phase_events(),
2278 vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }]
2279 );
2280 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2281 assert_eq!(Round::<Runtime>::get(), 1);
2282
2283 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2285
2286 assert_eq!(
2288 multi_phase_events(),
2289 vec![
2290 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2291 Event::ElectionFinalized {
2292 compute: ElectionCompute::Fallback,
2293 score: Default::default()
2294 },
2295 Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 },
2296 ],
2297 );
2298 assert_eq!(Round::<Runtime>::get(), 2);
2300 assert!(Snapshot::<Runtime>::get().is_none());
2301 assert!(SnapshotMetadata::<Runtime>::get().is_none());
2302 assert!(DesiredTargets::<Runtime>::get().is_none());
2303 assert!(QueuedSolution::<Runtime>::get().is_none());
2304 assert!(MultiPhase::signed_submissions().is_empty());
2305 })
2306 }
2307
2308 #[test]
2309 fn early_termination_with_submissions() {
2310 ExtBuilder::default().build_and_execute(|| {
2312 roll_to_signed();
2315 assert_eq!(
2316 multi_phase_events(),
2317 vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }]
2318 );
2319 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2320 assert_eq!(Round::<Runtime>::get(), 1);
2321
2322 for s in 0..SignedMaxSubmissions::get() {
2324 let solution = RawSolution {
2325 score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
2326 ..Default::default()
2327 };
2328 assert_ok!(MultiPhase::submit(
2329 crate::mock::RuntimeOrigin::signed(99),
2330 Box::new(solution)
2331 ));
2332 }
2333
2334 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2336
2337 assert_eq!(Round::<Runtime>::get(), 2);
2339 assert!(Snapshot::<Runtime>::get().is_none());
2340 assert!(SnapshotMetadata::<Runtime>::get().is_none());
2341 assert!(DesiredTargets::<Runtime>::get().is_none());
2342 assert!(QueuedSolution::<Runtime>::get().is_none());
2343 assert!(MultiPhase::signed_submissions().is_empty());
2344
2345 assert_eq!(
2346 multi_phase_events(),
2347 vec![
2348 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2349 Event::SolutionStored {
2350 compute: ElectionCompute::Signed,
2351 origin: Some(99),
2352 prev_ejected: false
2353 },
2354 Event::SolutionStored {
2355 compute: ElectionCompute::Signed,
2356 origin: Some(99),
2357 prev_ejected: false
2358 },
2359 Event::SolutionStored {
2360 compute: ElectionCompute::Signed,
2361 origin: Some(99),
2362 prev_ejected: false
2363 },
2364 Event::SolutionStored {
2365 compute: ElectionCompute::Signed,
2366 origin: Some(99),
2367 prev_ejected: false
2368 },
2369 Event::SolutionStored {
2370 compute: ElectionCompute::Signed,
2371 origin: Some(99),
2372 prev_ejected: false
2373 },
2374 Event::Slashed { account: 99, value: 5 },
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::ElectionFinalized {
2380 compute: ElectionCompute::Fallback,
2381 score: ElectionScore {
2382 minimal_stake: 0,
2383 sum_stake: 0,
2384 sum_stake_squared: 0
2385 }
2386 },
2387 Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 },
2388 ]
2389 );
2390 })
2391 }
2392
2393 #[test]
2394 fn check_events_with_compute_signed() {
2395 ExtBuilder::default().build_and_execute(|| {
2396 roll_to_signed();
2397 assert!(CurrentPhase::<Runtime>::get().is_signed());
2398
2399 let solution = raw_solution();
2400 assert_ok!(MultiPhase::submit(
2401 crate::mock::RuntimeOrigin::signed(99),
2402 Box::new(solution)
2403 ));
2404
2405 roll_to(30);
2406 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2407
2408 assert_eq!(
2409 multi_phase_events(),
2410 vec![
2411 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2412 Event::SolutionStored {
2413 compute: ElectionCompute::Signed,
2414 origin: Some(99),
2415 prev_ejected: false
2416 },
2417 Event::Rewarded { account: 99, value: 7 },
2418 Event::PhaseTransitioned {
2419 from: Phase::Signed,
2420 to: Phase::Unsigned((true, 25)),
2421 round: 1
2422 },
2423 Event::ElectionFinalized {
2424 compute: ElectionCompute::Signed,
2425 score: ElectionScore {
2426 minimal_stake: 40,
2427 sum_stake: 100,
2428 sum_stake_squared: 5200
2429 }
2430 },
2431 Event::PhaseTransitioned {
2432 from: Phase::Unsigned((true, 25)),
2433 to: Phase::Off,
2434 round: 2
2435 },
2436 ],
2437 );
2438 })
2439 }
2440
2441 #[test]
2442 fn check_events_with_compute_unsigned() {
2443 ExtBuilder::default().build_and_execute(|| {
2444 roll_to_unsigned();
2445 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
2446
2447 assert!(Snapshot::<Runtime>::get().is_some());
2449 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 2);
2450
2451 let (solution, witness, _) = MultiPhase::mine_solution().unwrap();
2453
2454 assert!(QueuedSolution::<Runtime>::get().is_none());
2456 assert_ok!(MultiPhase::submit_unsigned(
2457 crate::mock::RuntimeOrigin::none(),
2458 Box::new(solution),
2459 witness
2460 ));
2461 assert!(QueuedSolution::<Runtime>::get().is_some());
2462
2463 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2464
2465 assert_eq!(
2466 multi_phase_events(),
2467 vec![
2468 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2469 Event::PhaseTransitioned {
2470 from: Phase::Signed,
2471 to: Phase::Unsigned((true, 25)),
2472 round: 1
2473 },
2474 Event::SolutionStored {
2475 compute: ElectionCompute::Unsigned,
2476 origin: None,
2477 prev_ejected: false
2478 },
2479 Event::ElectionFinalized {
2480 compute: ElectionCompute::Unsigned,
2481 score: ElectionScore {
2482 minimal_stake: 40,
2483 sum_stake: 100,
2484 sum_stake_squared: 5200
2485 }
2486 },
2487 Event::PhaseTransitioned {
2488 from: Phase::Unsigned((true, 25)),
2489 to: Phase::Off,
2490 round: 2
2491 },
2492 ],
2493 );
2494 })
2495 }
2496
2497 #[test]
2498 fn try_elect_multi_page_fails() {
2499 let prepare_election = || {
2500 roll_to_signed();
2501 assert!(Snapshot::<Runtime>::get().is_some());
2502
2503 let (solution, _, _) = MultiPhase::mine_solution().unwrap();
2505 assert_ok!(MultiPhase::submit(
2506 crate::mock::RuntimeOrigin::signed(99),
2507 Box::new(solution),
2508 ));
2509 roll_to(30);
2510 assert!(QueuedSolution::<Runtime>::get().is_some());
2511 };
2512
2513 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2514 prepare_election();
2515 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2517 });
2518
2519 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2520 prepare_election();
2521 assert_noop!(MultiPhase::elect(SINGLE_PAGE + 1), ElectionError::MultiPageNotSupported);
2523 })
2524 }
2525
2526 #[test]
2527 fn fallback_strategy_works() {
2528 ExtBuilder::default().onchain_fallback(true).build_and_execute(|| {
2529 roll_to_unsigned();
2530 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2531
2532 assert!(QueuedSolution::<Runtime>::get().is_none());
2534 let supports = MultiPhase::elect(SINGLE_PAGE).unwrap();
2535
2536 let expected_supports = vec![
2537 (30, Support { total: 40, voters: vec![(2, 5), (4, 5), (30, 30)] }),
2538 (40, Support { total: 60, voters: vec![(2, 5), (3, 10), (4, 5), (40, 40)] }),
2539 ]
2540 .try_into()
2541 .unwrap();
2542
2543 assert_eq!(supports, expected_supports);
2544
2545 assert_eq!(
2546 multi_phase_events(),
2547 vec![
2548 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2549 Event::PhaseTransitioned {
2550 from: Phase::Signed,
2551 to: Phase::Unsigned((true, 25)),
2552 round: 1
2553 },
2554 Event::ElectionFinalized {
2555 compute: ElectionCompute::Fallback,
2556 score: ElectionScore {
2557 minimal_stake: 0,
2558 sum_stake: 0,
2559 sum_stake_squared: 0
2560 }
2561 },
2562 Event::PhaseTransitioned {
2563 from: Phase::Unsigned((true, 25)),
2564 to: Phase::Off,
2565 round: 2
2566 },
2567 ]
2568 );
2569 });
2570
2571 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2572 roll_to_unsigned();
2573 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2574
2575 assert!(QueuedSolution::<Runtime>::get().is_none());
2577 assert_eq!(
2578 MultiPhase::elect(SINGLE_PAGE).unwrap_err(),
2579 ElectionError::Fallback("NoFallback.")
2580 );
2581 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Emergency);
2583 assert!(Snapshot::<Runtime>::get().is_some());
2585
2586 assert_eq!(
2587 multi_phase_events(),
2588 vec![
2589 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2590 Event::PhaseTransitioned {
2591 from: Phase::Signed,
2592 to: Phase::Unsigned((true, 25)),
2593 round: 1
2594 },
2595 Event::ElectionFailed,
2596 Event::PhaseTransitioned {
2597 from: Phase::Unsigned((true, 25)),
2598 to: Phase::Emergency,
2599 round: 1
2600 },
2601 ]
2602 );
2603 })
2604 }
2605
2606 #[test]
2607 fn governance_fallback_works() {
2608 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2609 roll_to_unsigned();
2610 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2611
2612 assert!(QueuedSolution::<Runtime>::get().is_none());
2614 assert_eq!(
2615 MultiPhase::elect(SINGLE_PAGE).unwrap_err(),
2616 ElectionError::Fallback("NoFallback.")
2617 );
2618
2619 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Emergency);
2621 assert!(QueuedSolution::<Runtime>::get().is_none());
2622 assert!(Snapshot::<Runtime>::get().is_some());
2623
2624 assert_noop!(
2626 MultiPhase::governance_fallback(RuntimeOrigin::signed(99)),
2627 DispatchError::BadOrigin
2628 );
2629
2630 assert_ok!(MultiPhase::governance_fallback(RuntimeOrigin::root()));
2632 assert!(QueuedSolution::<Runtime>::get().is_some());
2634 assert!(MultiPhase::elect(SINGLE_PAGE).is_ok());
2636 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
2637
2638 assert_eq!(
2639 multi_phase_events(),
2640 vec![
2641 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2642 Event::PhaseTransitioned {
2643 from: Phase::Signed,
2644 to: Phase::Unsigned((true, 25)),
2645 round: 1
2646 },
2647 Event::ElectionFailed,
2648 Event::PhaseTransitioned {
2649 from: Phase::Unsigned((true, 25)),
2650 to: Phase::Emergency,
2651 round: 1
2652 },
2653 Event::SolutionStored {
2654 compute: ElectionCompute::Fallback,
2655 origin: None,
2656 prev_ejected: false
2657 },
2658 Event::ElectionFinalized {
2659 compute: ElectionCompute::Fallback,
2660 score: Default::default()
2661 },
2662 Event::PhaseTransitioned { from: Phase::Emergency, to: Phase::Off, round: 2 },
2663 ]
2664 );
2665 })
2666 }
2667
2668 #[test]
2669 fn snapshot_too_big_truncate() {
2670 ExtBuilder::default().build_and_execute(|| {
2672 assert_eq!(Voters::get().len(), 8);
2674 let new_bounds = ElectionBoundsBuilder::default().voters_count(2.into()).build();
2676 ElectionsBounds::set(new_bounds);
2677
2678 roll_to_signed();
2680 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2681
2682 assert_eq!(
2683 SnapshotMetadata::<Runtime>::get().unwrap(),
2684 SolutionOrSnapshotSize { voters: 2, targets: 4 }
2685 );
2686 })
2687 }
2688
2689 #[test]
2690 fn untrusted_score_verification_is_respected() {
2691 ExtBuilder::default().build_and_execute(|| {
2692 roll_to_signed();
2693 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2694
2695 crate::mock::Balancing::set(Some(BalancingConfig { iterations: 2, tolerance: 0 }));
2697
2698 let (solution, _, _) = MultiPhase::mine_solution().unwrap();
2699 assert!(matches!(solution.score, ElectionScore { minimal_stake: 50, .. }));
2701
2702 MinimumUntrustedScore::<Runtime>::put(ElectionScore {
2703 minimal_stake: 49,
2704 ..Default::default()
2705 });
2706 assert_ok!(MultiPhase::feasibility_check(solution.clone(), ElectionCompute::Signed));
2707
2708 MinimumUntrustedScore::<Runtime>::put(ElectionScore {
2709 minimal_stake: 51,
2710 ..Default::default()
2711 });
2712 assert_noop!(
2713 MultiPhase::feasibility_check(solution, ElectionCompute::Signed),
2714 FeasibilityError::UntrustedScoreTooLow,
2715 );
2716 })
2717 }
2718
2719 #[test]
2720 fn number_of_voters_allowed_2sec_block() {
2721 assert_eq!(MockWeightInfo::get(), MockedWeightInfo::Real);
2723
2724 let all_voters: u32 = 10_000;
2725 let all_targets: u32 = 5_000;
2726 let desired: u32 = 1_000;
2727 let weight_with = |active| {
2728 <Runtime as Config>::WeightInfo::submit_unsigned(
2729 all_voters,
2730 all_targets,
2731 active,
2732 desired,
2733 )
2734 };
2735
2736 let mut active = 1;
2737 while weight_with(active)
2738 .all_lte(<Runtime as frame_system::Config>::BlockWeights::get().max_block) ||
2739 active == all_voters
2740 {
2741 active += 1;
2742 }
2743
2744 println!("can support {} voters to yield a weight of {}", active, weight_with(active));
2745 }
2746}