1#![cfg_attr(not(feature = "std"), no_std)]
244
245extern crate alloc;
246
247use alloc::{boxed::Box, vec::Vec};
248use codec::{Decode, DecodeWithMemTracking, Encode};
249use pezframe_election_provider_support::{
250 bounds::{CountBound, ElectionBounds, SizeBound},
251 BoundedSupports, BoundedSupportsOf, ElectionDataProvider, ElectionProvider,
252 InstantElectionProvider, NposSolution, PageIndex,
253};
254use pezframe_support::{
255 dispatch::DispatchClass,
256 ensure,
257 traits::{Currency, Get, OnUnbalanced, ReservableCurrency},
258 weights::Weight,
259 DefaultNoBound, EqNoBound, PartialEqNoBound,
260};
261use pezframe_system::{ensure_none, offchain::CreateBare, pezpallet_prelude::BlockNumberFor};
262use pezsp_arithmetic::{
263 traits::{CheckedAdd, Zero},
264 UpperOf,
265};
266use pezsp_npos_elections::{ElectionScore, IdentifierT, Supports, VoteWeight};
267use pezsp_runtime::{
268 transaction_validity::{
269 InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
270 TransactionValidityError, ValidTransaction,
271 },
272 DispatchError, ModuleError, PerThing, Perbill, RuntimeDebug, SaturatedConversion,
273};
274use scale_info::TypeInfo;
275
276#[cfg(feature = "try-runtime")]
277use pezsp_runtime::TryRuntimeError;
278
279#[cfg(feature = "runtime-benchmarks")]
280mod benchmarking;
281#[cfg(test)]
282mod mock;
283#[cfg(all(test, feature = "remote-mining"))]
284mod remote_mining;
285#[macro_use]
286pub mod helpers;
287
288pub(crate) const SINGLE_PAGE: u32 = 0;
290const LOG_TARGET: &str = "runtime::election-provider";
291
292pub mod migrations;
293pub mod signed;
294pub mod unsigned;
295pub mod weights;
296
297pub use signed::{
298 BalanceOf, GeometricDepositBase, NegativeImbalanceOf, PositiveImbalanceOf, SignedSubmission,
299 SignedSubmissionOf, SignedSubmissions, SubmissionIndicesOf,
300};
301use unsigned::VoterOf;
302pub use unsigned::{Miner, MinerConfig};
303pub use weights::WeightInfo;
304
305pub type SolutionOf<T> = <T as MinerConfig>::Solution;
307pub type SolutionVoterIndexOf<T> = <SolutionOf<T> as NposSolution>::VoterIndex;
309pub type SolutionTargetIndexOf<T> = <SolutionOf<T> as NposSolution>::TargetIndex;
311pub type SolutionAccuracyOf<T> =
313 <SolutionOf<<T as crate::Config>::MinerConfig> as NposSolution>::Accuracy;
314pub type ReadySolutionOf<T> = ReadySolution<
316 <T as MinerConfig>::AccountId,
317 <T as MinerConfig>::MaxWinners,
318 <T as MinerConfig>::MaxBackersPerWinner,
319>;
320pub type FallbackErrorOf<T> = <<T as crate::Config>::Fallback as ElectionProvider>::Error;
322
323pub trait BenchmarkingConfig {
325 const VOTERS: [u32; 2];
327 const TARGETS: [u32; 2];
329 const ACTIVE_VOTERS: [u32; 2];
331 const DESIRED_TARGETS: [u32; 2];
333 const SNAPSHOT_MAXIMUM_VOTERS: u32;
335 const MINER_MAXIMUM_VOTERS: u32;
337 const MAXIMUM_TARGETS: u32;
339}
340
341#[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, DecodeWithMemTracking, Debug, TypeInfo)]
343pub enum Phase<Bn> {
344 Off,
346 Signed,
348 Unsigned((bool, Bn)),
359 Emergency,
363}
364
365impl<Bn> Default for Phase<Bn> {
366 fn default() -> Self {
367 Phase::Off
368 }
369}
370
371impl<Bn: PartialEq + Eq> Phase<Bn> {
372 pub fn is_emergency(&self) -> bool {
374 matches!(self, Phase::Emergency)
375 }
376
377 pub fn is_signed(&self) -> bool {
379 matches!(self, Phase::Signed)
380 }
381
382 pub fn is_unsigned(&self) -> bool {
384 matches!(self, Phase::Unsigned(_))
385 }
386
387 pub fn is_unsigned_open_at(&self, at: Bn) -> bool {
389 matches!(self, Phase::Unsigned((true, real)) if *real == at)
390 }
391
392 pub fn is_unsigned_open(&self) -> bool {
394 matches!(self, Phase::Unsigned((true, _)))
395 }
396
397 pub fn is_off(&self) -> bool {
399 matches!(self, Phase::Off)
400 }
401}
402
403#[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, DecodeWithMemTracking, Debug, TypeInfo)]
405pub enum ElectionCompute {
406 OnChain,
408 Signed,
410 Unsigned,
412 Fallback,
414 Emergency,
416}
417
418impl Default for ElectionCompute {
419 fn default() -> Self {
420 ElectionCompute::OnChain
421 }
422}
423
424#[derive(
431 PartialEq,
432 Eq,
433 Clone,
434 Encode,
435 Decode,
436 DecodeWithMemTracking,
437 RuntimeDebug,
438 PartialOrd,
439 Ord,
440 TypeInfo,
441)]
442pub struct RawSolution<S> {
443 pub solution: S,
445 pub score: ElectionScore,
447 pub round: u32,
449}
450
451impl<C: Default> Default for RawSolution<C> {
452 fn default() -> Self {
453 Self { round: 1, solution: Default::default(), score: Default::default() }
455 }
456}
457
458#[derive(
460 PartialEqNoBound,
461 EqNoBound,
462 Clone,
463 Encode,
464 Decode,
465 RuntimeDebug,
466 DefaultNoBound,
467 scale_info::TypeInfo,
468)]
469#[scale_info(skip_type_params(AccountId, MaxWinners, MaxBackersPerWinner))]
470pub struct ReadySolution<AccountId, MaxWinners, MaxBackersPerWinner>
471where
472 AccountId: IdentifierT,
473 MaxWinners: Get<u32>,
474 MaxBackersPerWinner: Get<u32>,
475{
476 pub supports: BoundedSupports<AccountId, MaxWinners, MaxBackersPerWinner>,
481 pub score: ElectionScore,
485 pub compute: ElectionCompute,
487}
488
489#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, Default, TypeInfo)]
494#[scale_info(skip_type_params(T))]
495pub struct RoundSnapshot<AccountId, VoterType> {
496 pub voters: Vec<VoterType>,
498 pub targets: Vec<AccountId>,
500}
501
502#[derive(
508 PartialEq, Eq, Clone, Copy, Encode, Decode, DecodeWithMemTracking, Debug, Default, TypeInfo,
509)]
510pub struct SolutionOrSnapshotSize {
511 #[codec(compact)]
513 pub voters: u32,
514 #[codec(compact)]
516 pub targets: u32,
517}
518
519#[derive(pezframe_support::DebugNoBound)]
523#[cfg_attr(feature = "runtime-benchmarks", derive(strum::IntoStaticStr))]
524pub enum ElectionError<T: Config> {
525 Feasibility(FeasibilityError),
527 Miner(unsigned::MinerError),
529 DataProvider(&'static str),
531 Fallback(FallbackErrorOf<T>),
533 MultiPageNotSupported,
536 NothingQueued,
538}
539
540impl<T: Config> PartialEq for ElectionError<T>
543where
544 FallbackErrorOf<T>: PartialEq,
545{
546 fn eq(&self, other: &Self) -> bool {
547 use ElectionError::*;
548 match (self, other) {
549 (Feasibility(x), Feasibility(y)) if x == y => true,
550 (Miner(x), Miner(y)) if x == y => true,
551 (DataProvider(x), DataProvider(y)) if x == y => true,
552 (Fallback(x), Fallback(y)) if x == y => true,
553 (MultiPageNotSupported, MultiPageNotSupported) => true,
554 (NothingQueued, NothingQueued) => true,
555 _ => false,
556 }
557 }
558}
559
560impl<T: Config> From<FeasibilityError> for ElectionError<T> {
561 fn from(e: FeasibilityError) -> Self {
562 ElectionError::Feasibility(e)
563 }
564}
565
566impl<T: Config> From<unsigned::MinerError> for ElectionError<T> {
567 fn from(e: unsigned::MinerError) -> Self {
568 ElectionError::Miner(e)
569 }
570}
571
572#[derive(Debug, Eq, PartialEq)]
574#[cfg_attr(feature = "runtime-benchmarks", derive(strum::IntoStaticStr))]
575pub enum FeasibilityError {
576 WrongWinnerCount,
578 SnapshotUnavailable,
583 NposElection(pezsp_npos_elections::Error),
585 InvalidVote,
587 InvalidVoter,
589 InvalidScore,
591 InvalidRound,
593 UntrustedScoreTooLow,
595 TooManyDesiredTargets,
597 BoundedConversionFailed,
601}
602
603impl From<pezsp_npos_elections::Error> for FeasibilityError {
604 fn from(e: pezsp_npos_elections::Error) -> Self {
605 FeasibilityError::NposElection(e)
606 }
607}
608
609pub use pezpallet::*;
610#[pezframe_support::pezpallet]
611pub mod pezpallet {
612 use super::*;
613 use pezframe_election_provider_support::{InstantElectionProvider, NposSolver};
614 use pezframe_support::{pezpallet_prelude::*, traits::EstimateCallFee};
615 use pezframe_system::pezpallet_prelude::*;
616 use pezsp_runtime::traits::Convert;
617
618 #[pezpallet::config]
619 pub trait Config: pezframe_system::Config + CreateBare<Call<Self>> {
620 #[allow(deprecated)]
621 type RuntimeEvent: From<Event<Self>>
622 + IsType<<Self as pezframe_system::Config>::RuntimeEvent>
623 + TryInto<Event<Self>>;
624
625 type Currency: ReservableCurrency<Self::AccountId> + Currency<Self::AccountId>;
627
628 type EstimateCallFee: EstimateCallFee<Call<Self>, BalanceOf<Self>>;
630
631 type UnsignedPhase: Get<BlockNumberFor<Self>>;
633 type SignedPhase: Get<BlockNumberFor<Self>>;
635
636 #[pezpallet::constant]
639 type BetterSignedThreshold: Get<Perbill>;
640
641 #[pezpallet::constant]
646 type OffchainRepeat: Get<BlockNumberFor<Self>>;
647
648 #[pezpallet::constant]
650 type MinerTxPriority: Get<TransactionPriority>;
651
652 type MinerConfig: crate::unsigned::MinerConfig<
657 AccountId = Self::AccountId,
658 MaxVotesPerVoter = <Self::DataProvider as ElectionDataProvider>::MaxVotesPerVoter,
659 MaxWinners = Self::MaxWinners,
660 MaxBackersPerWinner = Self::MaxBackersPerWinner,
661 >;
662
663 #[pezpallet::constant]
671 type SignedMaxSubmissions: Get<u32>;
672
673 #[pezpallet::constant]
679 type SignedMaxWeight: Get<Weight>;
680
681 #[pezpallet::constant]
683 type SignedMaxRefunds: Get<u32>;
684
685 #[pezpallet::constant]
687 type SignedRewardBase: Get<BalanceOf<Self>>;
688
689 #[pezpallet::constant]
691 type SignedDepositByte: Get<BalanceOf<Self>>;
692
693 #[pezpallet::constant]
695 type SignedDepositWeight: Get<BalanceOf<Self>>;
696
697 #[pezpallet::constant]
701 type MaxWinners: Get<u32>;
702
703 #[pezpallet::constant]
707 type MaxBackersPerWinner: Get<u32>;
708
709 type SignedDepositBase: Convert<usize, BalanceOf<Self>>;
712
713 type ElectionBounds: Get<ElectionBounds>;
715
716 type SlashHandler: OnUnbalanced<NegativeImbalanceOf<Self>>;
718
719 type RewardHandler: OnUnbalanced<PositiveImbalanceOf<Self>>;
721
722 type DataProvider: ElectionDataProvider<
724 AccountId = Self::AccountId,
725 BlockNumber = BlockNumberFor<Self>,
726 >;
727
728 type Fallback: InstantElectionProvider<
730 AccountId = Self::AccountId,
731 BlockNumber = BlockNumberFor<Self>,
732 DataProvider = Self::DataProvider,
733 MaxBackersPerWinner = Self::MaxBackersPerWinner,
734 MaxWinnersPerPage = Self::MaxWinners,
735 >;
736
737 type GovernanceFallback: InstantElectionProvider<
742 AccountId = Self::AccountId,
743 BlockNumber = BlockNumberFor<Self>,
744 DataProvider = Self::DataProvider,
745 MaxWinnersPerPage = Self::MaxWinners,
746 MaxBackersPerWinner = Self::MaxBackersPerWinner,
747 >;
748
749 type Solver: NposSolver<AccountId = Self::AccountId>;
751
752 type ForceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
755
756 type BenchmarkingConfig: BenchmarkingConfig;
758
759 type WeightInfo: WeightInfo;
761 }
762
763 #[pezpallet::extra_constants]
765 impl<T: Config> Pezpallet<T> {
766 #[pezpallet::constant_name(MinerMaxLength)]
767 fn max_length() -> u32 {
768 <T::MinerConfig as MinerConfig>::MaxLength::get()
769 }
770
771 #[pezpallet::constant_name(MinerMaxWeight)]
772 fn max_weight() -> Weight {
773 <T::MinerConfig as MinerConfig>::MaxWeight::get()
774 }
775
776 #[pezpallet::constant_name(MinerMaxVotesPerVoter)]
777 fn max_votes_per_voter() -> u32 {
778 <T::MinerConfig as MinerConfig>::MaxVotesPerVoter::get()
779 }
780
781 #[pezpallet::constant_name(MinerMaxWinners)]
782 fn max_winners() -> u32 {
783 <T::MinerConfig as MinerConfig>::MaxWinners::get()
784 }
785 }
786
787 #[pezpallet::hooks]
788 impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T> {
789 fn on_initialize(now: BlockNumberFor<T>) -> Weight {
790 let next_election = T::DataProvider::next_election_prediction(now).max(now);
791
792 let signed_deadline = T::SignedPhase::get() + T::UnsignedPhase::get();
793 let unsigned_deadline = T::UnsignedPhase::get();
794
795 let remaining = next_election - now;
796 let current_phase = CurrentPhase::<T>::get();
797
798 log!(
799 trace,
800 "current phase {:?}, next election {:?}, queued? {:?}, metadata: {:?}",
801 current_phase,
802 next_election,
803 QueuedSolution::<T>::get().map(|rs| (rs.supports.len(), rs.compute, rs.score)),
804 SnapshotMetadata::<T>::get()
805 );
806 match current_phase {
807 Phase::Off if remaining <= signed_deadline && remaining > unsigned_deadline => {
808 match Self::create_snapshot() {
810 Ok(_) => {
811 Self::phase_transition(Phase::Signed);
812 T::WeightInfo::on_initialize_open_signed()
813 },
814 Err(why) => {
815 log!(warn, "failed to open signed phase due to {:?}", why);
817 T::WeightInfo::on_initialize_nothing()
818 },
819 }
820 },
821 Phase::Signed | Phase::Off
822 if remaining <= unsigned_deadline && remaining > Zero::zero() =>
823 {
824 let (need_snapshot, enabled) = if current_phase == Phase::Signed {
827 let _ = Self::finalize_signed_phase();
837 (false, true)
841 } else {
842 (true, true)
845 };
846
847 if need_snapshot {
848 match Self::create_snapshot() {
849 Ok(_) => {
850 Self::phase_transition(Phase::Unsigned((enabled, now)));
851 T::WeightInfo::on_initialize_open_unsigned()
852 },
853 Err(why) => {
854 log!(warn, "failed to open unsigned phase due to {:?}", why);
855 T::WeightInfo::on_initialize_nothing()
856 },
857 }
858 } else {
859 Self::phase_transition(Phase::Unsigned((enabled, now)));
860 T::WeightInfo::on_initialize_open_unsigned()
861 }
862 },
863 _ => T::WeightInfo::on_initialize_nothing(),
864 }
865 }
866
867 fn offchain_worker(now: BlockNumberFor<T>) {
868 use pezsp_runtime::offchain::storage_lock::{BlockAndTime, StorageLock};
869
870 let mut lock =
874 StorageLock::<BlockAndTime<pezframe_system::Pezpallet<T>>>::with_block_deadline(
875 unsigned::OFFCHAIN_LOCK,
876 T::UnsignedPhase::get().saturated_into(),
877 );
878
879 match lock.try_lock() {
880 Ok(_guard) => {
881 Self::do_synchronized_offchain_worker(now);
882 },
883 Err(deadline) => {
884 log!(debug, "offchain worker lock not released, deadline is {:?}", deadline);
885 },
886 };
887 }
888
889 fn integrity_test() {
890 use core::mem::size_of;
891 assert!(size_of::<SolutionVoterIndexOf<T::MinerConfig>>() <= size_of::<usize>());
894 assert!(size_of::<SolutionTargetIndexOf<T::MinerConfig>>() <= size_of::<usize>());
895
896 let max_vote: usize = <SolutionOf<T::MinerConfig> as NposSolution>::LIMIT;
899
900 let maximum_chain_accuracy: Vec<UpperOf<SolutionAccuracyOf<T>>> = (0..max_vote)
902 .map(|_| {
903 <UpperOf<SolutionAccuracyOf<T>>>::from(
904 SolutionAccuracyOf::<T>::one().deconstruct(),
905 )
906 })
907 .collect();
908 let _: UpperOf<SolutionAccuracyOf<T>> = maximum_chain_accuracy
909 .iter()
910 .fold(Zero::zero(), |acc, x| acc.checked_add(x).unwrap());
911
912 assert_eq!(
918 <T::DataProvider as ElectionDataProvider>::MaxVotesPerVoter::get(),
919 <SolutionOf<T::MinerConfig> as NposSolution>::LIMIT as u32,
920 );
921
922 assert!(T::SignedMaxSubmissions::get() >= T::SignedMaxRefunds::get());
926 }
927
928 #[cfg(feature = "try-runtime")]
929 fn try_state(_n: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
930 Self::do_try_state()
931 }
932 }
933
934 #[pezpallet::call]
935 impl<T: Config> Pezpallet<T> {
936 #[pezpallet::call_index(0)]
951 #[pezpallet::weight((
952 T::WeightInfo::submit_unsigned(
953 witness.voters,
954 witness.targets,
955 raw_solution.solution.voter_count() as u32,
956 raw_solution.solution.unique_targets().len() as u32
957 ),
958 DispatchClass::Operational,
959 ))]
960 pub fn submit_unsigned(
961 origin: OriginFor<T>,
962 raw_solution: Box<RawSolution<SolutionOf<T::MinerConfig>>>,
963 witness: SolutionOrSnapshotSize,
964 ) -> DispatchResult {
965 ensure_none(origin)?;
966 let error_message = "Invalid unsigned submission must produce invalid block and \
967 deprive validator from their authoring reward.";
968
969 Self::unsigned_pre_dispatch_checks(&raw_solution).expect(error_message);
971
972 let SolutionOrSnapshotSize { voters, targets } =
974 SnapshotMetadata::<T>::get().expect(error_message);
975
976 assert!(voters as u32 == witness.voters, "{}", error_message);
978 assert!(targets as u32 == witness.targets, "{}", error_message);
979
980 let ready = Self::feasibility_check(*raw_solution, ElectionCompute::Unsigned)
981 .expect(error_message);
982
983 log!(debug, "queued unsigned solution with score {:?}", ready.score);
985 let ejected_a_solution = QueuedSolution::<T>::exists();
986 QueuedSolution::<T>::put(ready);
987 Self::deposit_event(Event::SolutionStored {
988 compute: ElectionCompute::Unsigned,
989 origin: None,
990 prev_ejected: ejected_a_solution,
991 });
992
993 Ok(())
994 }
995
996 #[pezpallet::call_index(1)]
1002 #[pezpallet::weight(T::DbWeight::get().writes(1))]
1003 pub fn set_minimum_untrusted_score(
1004 origin: OriginFor<T>,
1005 maybe_next_score: Option<ElectionScore>,
1006 ) -> DispatchResult {
1007 T::ForceOrigin::ensure_origin(origin)?;
1008 MinimumUntrustedScore::<T>::set(maybe_next_score);
1009 Ok(())
1010 }
1011
1012 #[pezpallet::call_index(2)]
1021 #[pezpallet::weight(T::DbWeight::get().reads_writes(1, 1))]
1022 pub fn set_emergency_election_result(
1023 origin: OriginFor<T>,
1024 supports: Supports<T::AccountId>,
1025 ) -> DispatchResult {
1026 T::ForceOrigin::ensure_origin(origin)?;
1027 ensure!(CurrentPhase::<T>::get().is_emergency(), Error::<T>::CallNotAllowed);
1028
1029 let supports: BoundedSupportsOf<Self> =
1031 supports.try_into().map_err(|_| Error::<T>::TooManyWinners)?;
1032
1033 let solution = ReadySolution {
1036 supports,
1037 score: Default::default(),
1038 compute: ElectionCompute::Emergency,
1039 };
1040
1041 Self::deposit_event(Event::SolutionStored {
1042 compute: ElectionCompute::Emergency,
1043 origin: None,
1044 prev_ejected: QueuedSolution::<T>::exists(),
1045 });
1046
1047 QueuedSolution::<T>::put(solution);
1048 Ok(())
1049 }
1050
1051 #[pezpallet::call_index(3)]
1061 #[pezpallet::weight(T::WeightInfo::submit())]
1062 pub fn submit(
1063 origin: OriginFor<T>,
1064 raw_solution: Box<RawSolution<SolutionOf<T::MinerConfig>>>,
1065 ) -> DispatchResult {
1066 let who = ensure_signed(origin)?;
1067
1068 ensure!(CurrentPhase::<T>::get().is_signed(), Error::<T>::PreDispatchEarlySubmission);
1070 ensure!(raw_solution.round == Round::<T>::get(), Error::<T>::PreDispatchDifferentRound);
1071
1072 let size = SnapshotMetadata::<T>::get().ok_or(Error::<T>::MissingSnapshotMetadata)?;
1078
1079 ensure!(
1080 Self::solution_weight_of(&raw_solution, size).all_lt(T::SignedMaxWeight::get()),
1081 Error::<T>::SignedTooMuchWeight,
1082 );
1083
1084 let deposit = Self::deposit_for(&raw_solution, size);
1086 let call_fee = {
1087 let call = Call::submit { raw_solution: raw_solution.clone() };
1088 T::EstimateCallFee::estimate_call_fee(&call, None::<Weight>.into())
1089 };
1090
1091 let submission = SignedSubmission {
1092 who: who.clone(),
1093 deposit,
1094 raw_solution: *raw_solution,
1095 call_fee,
1096 };
1097
1098 let mut signed_submissions = Self::signed_submissions();
1101 let maybe_removed = match signed_submissions.insert(submission) {
1102 signed::InsertResult::NotInserted => return Err(Error::<T>::SignedQueueFull.into()),
1105 signed::InsertResult::Inserted => None,
1106 signed::InsertResult::InsertedEjecting(weakest) => Some(weakest),
1107 };
1108
1109 T::Currency::reserve(&who, deposit).map_err(|_| Error::<T>::SignedCannotPayDeposit)?;
1111
1112 let ejected_a_solution = maybe_removed.is_some();
1113 if let Some(removed) = maybe_removed {
1115 let _remainder = T::Currency::unreserve(&removed.who, removed.deposit);
1116 debug_assert!(_remainder.is_zero());
1117 }
1118
1119 signed_submissions.put();
1120 Self::deposit_event(Event::SolutionStored {
1121 compute: ElectionCompute::Signed,
1122 origin: Some(who),
1123 prev_ejected: ejected_a_solution,
1124 });
1125 Ok(())
1126 }
1127
1128 #[pezpallet::call_index(4)]
1133 #[pezpallet::weight(T::DbWeight::get().reads_writes(1, 1))]
1134 pub fn governance_fallback(origin: OriginFor<T>) -> DispatchResult {
1135 T::ForceOrigin::ensure_origin(origin)?;
1136 ensure!(CurrentPhase::<T>::get().is_emergency(), Error::<T>::CallNotAllowed);
1137
1138 let RoundSnapshot { voters, targets } =
1139 Snapshot::<T>::get().ok_or(Error::<T>::MissingSnapshotMetadata)?;
1140 let desired_targets =
1141 DesiredTargets::<T>::get().ok_or(Error::<T>::MissingSnapshotMetadata)?;
1142
1143 let supports = T::GovernanceFallback::instant_elect(voters, targets, desired_targets)
1144 .map_err(|e| {
1145 log!(error, "GovernanceFallback failed: {:?}", e);
1146 Error::<T>::FallbackFailed
1147 })?;
1148
1149 let solution = ReadySolution {
1150 supports,
1151 score: Default::default(),
1152 compute: ElectionCompute::Fallback,
1153 };
1154
1155 Self::deposit_event(Event::SolutionStored {
1156 compute: ElectionCompute::Fallback,
1157 origin: None,
1158 prev_ejected: QueuedSolution::<T>::exists(),
1159 });
1160
1161 QueuedSolution::<T>::put(solution);
1162 Ok(())
1163 }
1164 }
1165
1166 #[pezpallet::event]
1167 #[pezpallet::generate_deposit(pub(super) fn deposit_event)]
1168 pub enum Event<T: Config> {
1169 SolutionStored {
1177 compute: ElectionCompute,
1178 origin: Option<T::AccountId>,
1179 prev_ejected: bool,
1180 },
1181 ElectionFinalized { compute: ElectionCompute, score: ElectionScore },
1183 ElectionFailed,
1187 Rewarded { account: <T as pezframe_system::Config>::AccountId, value: BalanceOf<T> },
1189 Slashed { account: <T as pezframe_system::Config>::AccountId, value: BalanceOf<T> },
1191 PhaseTransitioned {
1193 from: Phase<BlockNumberFor<T>>,
1194 to: Phase<BlockNumberFor<T>>,
1195 round: u32,
1196 },
1197 }
1198
1199 #[pezpallet::error]
1201 pub enum Error<T> {
1202 PreDispatchEarlySubmission,
1204 PreDispatchWrongWinnerCount,
1206 PreDispatchWeakSubmission,
1208 SignedQueueFull,
1210 SignedCannotPayDeposit,
1212 SignedInvalidWitness,
1214 SignedTooMuchWeight,
1216 OcwCallWrongEra,
1218 MissingSnapshotMetadata,
1220 InvalidSubmissionIndex,
1222 CallNotAllowed,
1224 FallbackFailed,
1226 BoundNotMet,
1228 TooManyWinners,
1230 PreDispatchDifferentRound,
1232 }
1233
1234 #[pezpallet::validate_unsigned]
1235 impl<T: Config> ValidateUnsigned for Pezpallet<T> {
1236 type Call = Call<T>;
1237 fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
1238 if let Call::submit_unsigned { raw_solution, .. } = call {
1239 match source {
1241 TransactionSource::Local | TransactionSource::InBlock => { },
1242 _ => return InvalidTransaction::Call.into(),
1243 }
1244
1245 Self::unsigned_pre_dispatch_checks(raw_solution)
1246 .inspect_err(|err| {
1247 log!(debug, "unsigned transaction validation failed due to {:?}", err);
1248 })
1249 .map_err(dispatch_error_to_invalid)?;
1250
1251 ValidTransaction::with_tag_prefix("OffchainElection")
1252 .priority(
1254 T::MinerTxPriority::get()
1255 .saturating_add(raw_solution.score.minimal_stake.saturated_into()),
1256 )
1257 .and_provides(raw_solution.round)
1260 .longevity(T::UnsignedPhase::get().saturated_into::<u64>())
1262 .propagate(false)
1264 .build()
1265 } else {
1266 InvalidTransaction::Call.into()
1267 }
1268 }
1269
1270 fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
1271 if let Call::submit_unsigned { raw_solution, .. } = call {
1272 Self::unsigned_pre_dispatch_checks(raw_solution)
1273 .map_err(dispatch_error_to_invalid)
1274 .map_err(Into::into)
1275 } else {
1276 Err(InvalidTransaction::Call.into())
1277 }
1278 }
1279 }
1280
1281 #[pezpallet::type_value]
1282 pub fn DefaultForRound() -> u32 {
1283 1
1284 }
1285
1286 #[pezpallet::storage]
1293 pub type Round<T: Config> = StorageValue<_, u32, ValueQuery, DefaultForRound>;
1294
1295 #[pezpallet::storage]
1297 pub type CurrentPhase<T: Config> = StorageValue<_, Phase<BlockNumberFor<T>>, ValueQuery>;
1298
1299 #[pezpallet::storage]
1303 pub type QueuedSolution<T: Config> = StorageValue<_, ReadySolutionOf<T::MinerConfig>>;
1304
1305 #[pezpallet::storage]
1310 pub type Snapshot<T: Config> = StorageValue<_, RoundSnapshot<T::AccountId, VoterOf<T>>>;
1311
1312 #[pezpallet::storage]
1317 pub type DesiredTargets<T> = StorageValue<_, u32>;
1318
1319 #[pezpallet::storage]
1324 pub type SnapshotMetadata<T: Config> = StorageValue<_, SolutionOrSnapshotSize>;
1325
1326 #[pezpallet::storage]
1340 pub type SignedSubmissionNextIndex<T: Config> = StorageValue<_, u32, ValueQuery>;
1341
1342 #[pezpallet::storage]
1349 pub type SignedSubmissionIndices<T: Config> =
1350 StorageValue<_, SubmissionIndicesOf<T>, ValueQuery>;
1351
1352 #[pezpallet::storage]
1360 pub type SignedSubmissionsMap<T: Config> =
1361 StorageMap<_, Twox64Concat, u32, SignedSubmissionOf<T>, OptionQuery>;
1362
1363 #[pezpallet::storage]
1370 pub type MinimumUntrustedScore<T: Config> = StorageValue<_, ElectionScore>;
1371
1372 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
1376
1377 #[pezpallet::pezpallet]
1378 #[pezpallet::without_storage_info]
1379 #[pezpallet::storage_version(STORAGE_VERSION)]
1380 pub struct Pezpallet<T>(_);
1381}
1382
1383pub struct SnapshotWrapper<T>(core::marker::PhantomData<T>);
1386
1387impl<T: Config> SnapshotWrapper<T> {
1388 pub fn kill() {
1390 Snapshot::<T>::kill();
1391 SnapshotMetadata::<T>::kill();
1392 DesiredTargets::<T>::kill();
1393 }
1394 pub fn set(metadata: SolutionOrSnapshotSize, desired_targets: u32, buffer: &[u8]) {
1396 SnapshotMetadata::<T>::put(metadata);
1397 DesiredTargets::<T>::put(desired_targets);
1398 pezsp_io::storage::set(&Snapshot::<T>::hashed_key(), &buffer);
1399 }
1400
1401 #[cfg(feature = "try-runtime")]
1404 pub fn is_consistent() -> bool {
1405 let snapshots = [
1406 Snapshot::<T>::exists(),
1407 SnapshotMetadata::<T>::exists(),
1408 DesiredTargets::<T>::exists(),
1409 ];
1410
1411 snapshots.iter().skip(1).all(|v| snapshots[0] == *v)
1413 }
1414}
1415
1416impl<T: Config> Pezpallet<T> {
1417 pub fn round() -> u32 {
1424 Round::<T>::get()
1425 }
1426
1427 pub fn current_phase() -> Phase<BlockNumberFor<T>> {
1429 CurrentPhase::<T>::get()
1430 }
1431
1432 pub fn queued_solution() -> Option<ReadySolutionOf<T::MinerConfig>> {
1436 QueuedSolution::<T>::get()
1437 }
1438
1439 pub fn snapshot() -> Option<RoundSnapshot<T::AccountId, VoterOf<T>>> {
1444 Snapshot::<T>::get()
1445 }
1446
1447 pub fn desired_targets() -> Option<u32> {
1452 DesiredTargets::<T>::get()
1453 }
1454
1455 pub fn snapshot_metadata() -> Option<SolutionOrSnapshotSize> {
1460 SnapshotMetadata::<T>::get()
1461 }
1462
1463 pub fn minimum_untrusted_score() -> Option<ElectionScore> {
1468 MinimumUntrustedScore::<T>::get()
1469 }
1470
1471 fn do_synchronized_offchain_worker(now: BlockNumberFor<T>) {
1474 let current_phase = CurrentPhase::<T>::get();
1475 log!(trace, "lock for offchain worker acquired. Phase = {:?}", current_phase);
1476 match current_phase {
1477 Phase::Unsigned((true, opened)) if opened == now => {
1478 let initial_output = Self::ensure_offchain_repeat_frequency(now).and_then(|_| {
1480 unsigned::kill_ocw_solution::<T>();
1483 Self::mine_check_save_submit()
1484 });
1485 log!(debug, "initial offchain thread output: {:?}", initial_output);
1486 },
1487 Phase::Unsigned((true, opened)) if opened < now => {
1488 let resubmit_output = Self::ensure_offchain_repeat_frequency(now)
1491 .and_then(|_| Self::restore_or_compute_then_maybe_submit());
1492 log!(debug, "resubmit offchain thread output: {:?}", resubmit_output);
1493 },
1494 _ => {},
1495 }
1496 }
1497
1498 pub(crate) fn phase_transition(to: Phase<BlockNumberFor<T>>) {
1500 log!(info, "Starting phase {:?}, round {}.", to, Round::<T>::get());
1501 Self::deposit_event(Event::PhaseTransitioned {
1502 from: CurrentPhase::<T>::get(),
1503 to,
1504 round: Round::<T>::get(),
1505 });
1506 CurrentPhase::<T>::put(to);
1507 }
1508
1509 fn create_snapshot_internal(
1513 targets: Vec<T::AccountId>,
1514 voters: Vec<VoterOf<T>>,
1515 desired_targets: u32,
1516 ) {
1517 let metadata =
1518 SolutionOrSnapshotSize { voters: voters.len() as u32, targets: targets.len() as u32 };
1519 log!(info, "creating a snapshot with metadata {:?}", metadata);
1520
1521 let snapshot = RoundSnapshot::<T::AccountId, VoterOf<T>> { voters, targets };
1525 let size = snapshot.encoded_size();
1526 log!(debug, "snapshot pre-calculated size {:?}", size);
1527 let mut buffer = Vec::with_capacity(size);
1528 snapshot.encode_to(&mut buffer);
1529
1530 debug_assert_eq!(buffer, snapshot.encode());
1532 debug_assert!(buffer.len() == size && size == buffer.capacity());
1534
1535 SnapshotWrapper::<T>::set(metadata, desired_targets, &buffer);
1536 }
1537
1538 fn create_snapshot_external(
1544 ) -> Result<(Vec<T::AccountId>, Vec<VoterOf<T>>, u32), ElectionError<T>> {
1545 let election_bounds = T::ElectionBounds::get();
1546 let targets = T::DataProvider::electable_targets_stateless(election_bounds.targets)
1547 .and_then(|t| {
1548 election_bounds.ensure_targets_limits(
1549 CountBound(t.len() as u32),
1550 SizeBound(t.encoded_size() as u32),
1551 )?;
1552 Ok(t)
1553 })
1554 .map_err(ElectionError::DataProvider)?;
1555
1556 let voters = T::DataProvider::electing_voters_stateless(election_bounds.voters)
1557 .and_then(|v| {
1558 election_bounds.ensure_voters_limits(
1559 CountBound(v.len() as u32),
1560 SizeBound(v.encoded_size() as u32),
1561 )?;
1562 Ok(v)
1563 })
1564 .map_err(ElectionError::DataProvider)?;
1565
1566 let mut desired_targets = <Pezpallet<T> as ElectionProvider>::desired_targets_checked()
1567 .map_err(|e| ElectionError::DataProvider(e))?;
1568
1569 let max_desired_targets: u32 = targets.len() as u32;
1572 if desired_targets > max_desired_targets {
1573 log!(
1574 warn,
1575 "desired_targets: {} > targets.len(): {}, capping desired_targets",
1576 desired_targets,
1577 max_desired_targets
1578 );
1579 desired_targets = max_desired_targets;
1580 }
1581
1582 Ok((targets, voters, desired_targets))
1583 }
1584
1585 pub fn create_snapshot() -> Result<(), ElectionError<T>> {
1596 let (targets, voters, desired_targets) = Self::create_snapshot_external()?;
1598
1599 let internal_weight =
1601 T::WeightInfo::create_snapshot_internal(voters.len() as u32, targets.len() as u32);
1602 Self::create_snapshot_internal(targets, voters, desired_targets);
1603 Self::register_weight(internal_weight);
1604 Ok(())
1605 }
1606
1607 fn register_weight(weight: Weight) {
1611 pezframe_system::Pezpallet::<T>::register_extra_weight_unchecked(
1612 weight,
1613 DispatchClass::Mandatory,
1614 );
1615 }
1616
1617 pub fn feasibility_check(
1619 raw_solution: RawSolution<SolutionOf<T::MinerConfig>>,
1620 compute: ElectionCompute,
1621 ) -> Result<ReadySolutionOf<T::MinerConfig>, FeasibilityError> {
1622 let desired_targets =
1623 DesiredTargets::<T>::get().ok_or(FeasibilityError::SnapshotUnavailable)?;
1624
1625 let snapshot = Snapshot::<T>::get().ok_or(FeasibilityError::SnapshotUnavailable)?;
1626 let round = Round::<T>::get();
1627 let minimum_untrusted_score = MinimumUntrustedScore::<T>::get();
1628
1629 Miner::<T::MinerConfig>::feasibility_check(
1630 raw_solution,
1631 compute,
1632 desired_targets,
1633 snapshot,
1634 round,
1635 minimum_untrusted_score,
1636 )
1637 }
1638
1639 fn rotate_round() {
1645 Round::<T>::mutate(|r| *r += 1);
1647
1648 Self::phase_transition(Phase::Off);
1650
1651 SnapshotWrapper::<T>::kill();
1653 }
1654
1655 fn do_elect() -> Result<BoundedSupportsOf<Self>, ElectionError<T>> {
1656 let _ = Self::finalize_signed_phase();
1664
1665 QueuedSolution::<T>::take()
1666 .ok_or(ElectionError::<T>::NothingQueued)
1667 .or_else(|_| {
1668 log!(warn, "No solution queued, falling back to instant fallback.",);
1669
1670 #[cfg(feature = "runtime-benchmarks")]
1671 Self::asap();
1672
1673 let (voters, targets, desired_targets) = if T::Fallback::bother() {
1674 let RoundSnapshot { voters, targets } = Snapshot::<T>::get().ok_or(
1675 ElectionError::<T>::Feasibility(FeasibilityError::SnapshotUnavailable),
1676 )?;
1677 let desired_targets = DesiredTargets::<T>::get().ok_or(
1678 ElectionError::<T>::Feasibility(FeasibilityError::SnapshotUnavailable),
1679 )?;
1680 (voters, targets, desired_targets)
1681 } else {
1682 (Default::default(), Default::default(), Default::default())
1683 };
1684 T::Fallback::instant_elect(voters, targets, desired_targets)
1685 .map_err(|fe| ElectionError::Fallback(fe))
1686 .and_then(|supports| {
1687 Ok(ReadySolution {
1688 supports,
1689 score: Default::default(),
1690 compute: ElectionCompute::Fallback,
1691 })
1692 })
1693 })
1694 .map(|ReadySolution { compute, score, supports }| {
1695 Self::deposit_event(Event::ElectionFinalized { compute, score });
1696 log!(info, "Finalized election round with compute {:?}.", compute);
1697 supports
1698 })
1699 .map_err(|err| {
1700 Self::deposit_event(Event::ElectionFailed);
1701 log!(warn, "Failed to finalize election round. reason {:?}", err);
1702 err
1703 })
1704 }
1705
1706 fn weigh_supports(supports: &BoundedSupportsOf<Self>) {
1708 let active_voters = supports
1709 .iter()
1710 .map(|(_, x)| x)
1711 .fold(Zero::zero(), |acc, next| acc + next.voters.len() as u32);
1712 let desired_targets = supports.len() as u32;
1713 Self::register_weight(T::WeightInfo::elect_queued(active_voters, desired_targets));
1714 }
1715}
1716
1717#[cfg(feature = "try-runtime")]
1718impl<T: Config> Pezpallet<T> {
1719 fn do_try_state() -> Result<(), TryRuntimeError> {
1720 Self::try_state_snapshot()?;
1721 Self::try_state_signed_submissions_map()?;
1722 Self::try_state_phase_off()
1723 }
1724
1725 fn try_state_snapshot() -> Result<(), TryRuntimeError> {
1729 if SnapshotWrapper::<T>::is_consistent() {
1730 Ok(())
1731 } else {
1732 Err("If snapshot exists, metadata and desired targets should be set too. Otherwise, none should be set.".into())
1733 }
1734 }
1735
1736 fn try_state_signed_submissions_map() -> Result<(), TryRuntimeError> {
1741 let mut last_score: ElectionScore = Default::default();
1742 let indices = SignedSubmissionIndices::<T>::get();
1743
1744 for (i, indice) in indices.iter().enumerate() {
1745 let submission = SignedSubmissionsMap::<T>::get(indice.2);
1746 if submission.is_none() {
1747 return Err(
1748 "All signed submissions indices must be part of the submissions map".into()
1749 );
1750 }
1751
1752 if i == 0 {
1753 last_score = indice.0
1754 } else {
1755 if last_score.strict_better(indice.0) {
1756 return Err(
1757 "Signed submission indices vector must be ordered by election score".into(),
1758 );
1759 }
1760 last_score = indice.0;
1761 }
1762 }
1763
1764 if SignedSubmissionsMap::<T>::iter().nth(indices.len()).is_some() {
1765 return Err(
1766 "Signed submissions map length should be the same as the indices vec length".into(),
1767 );
1768 }
1769
1770 match SignedSubmissionNextIndex::<T>::get() {
1771 0 => Ok(()),
1772 next => {
1773 if SignedSubmissionsMap::<T>::get(next).is_some() {
1774 return Err(
1775 "The next submissions index should not be in the submissions maps already"
1776 .into(),
1777 );
1778 } else {
1779 Ok(())
1780 }
1781 },
1782 }
1783 }
1784
1785 fn try_state_phase_off() -> Result<(), TryRuntimeError> {
1788 match CurrentPhase::<T>::get().is_off() {
1789 false => Ok(()),
1790 true => {
1791 if Snapshot::<T>::get().is_some() {
1792 Err("Snapshot must be none when in Phase::Off".into())
1793 } else {
1794 Ok(())
1795 }
1796 },
1797 }
1798 }
1799}
1800
1801impl<T: Config> ElectionProvider for Pezpallet<T> {
1802 type AccountId = T::AccountId;
1803 type BlockNumber = BlockNumberFor<T>;
1804 type Error = ElectionError<T>;
1805 type MaxWinnersPerPage = T::MaxWinners;
1806 type MaxBackersPerWinner = T::MaxBackersPerWinner;
1807 type MaxBackersPerWinnerFinal = T::MaxBackersPerWinner;
1808 type Pages = pezsp_core::ConstU32<1>;
1809 type DataProvider = T::DataProvider;
1810
1811 fn elect(page: PageIndex) -> Result<BoundedSupportsOf<Self>, Self::Error> {
1812 ensure!(page == SINGLE_PAGE, ElectionError::<T>::MultiPageNotSupported);
1814
1815 let res = match Self::do_elect() {
1816 Ok(bounded_supports) => {
1817 Self::weigh_supports(&bounded_supports);
1819 Self::rotate_round();
1820 Ok(bounded_supports)
1821 },
1822 Err(why) => {
1823 log!(error, "Entering emergency mode: {:?}", why);
1824 Self::phase_transition(Phase::Emergency);
1825 Err(why)
1826 },
1827 };
1828
1829 log!(info, "ElectionProvider::elect({}) => {:?}", page, res.as_ref().map(|s| s.len()));
1830 res
1831 }
1832
1833 fn duration() -> Self::BlockNumber {
1834 let signed: BlockNumberFor<T> = T::SignedPhase::get().saturated_into();
1835 let unsigned: BlockNumberFor<T> = T::UnsignedPhase::get().saturated_into();
1836 signed + unsigned
1837 }
1838
1839 fn start() -> Result<(), Self::Error> {
1840 log!(
1841 warn,
1842 "we received signal, but this pezpallet works in the basis of legacy pull based election"
1843 );
1844 Ok(())
1845 }
1846
1847 fn status() -> Result<bool, ()> {
1848 let has_queued = QueuedSolution::<T>::exists();
1849 let phase = CurrentPhase::<T>::get();
1850 match (phase, has_queued) {
1851 (Phase::Unsigned(_), true) => Ok(true),
1852 (Phase::Off, _) => Err(()),
1853 _ => Ok(false),
1854 }
1855 }
1856
1857 #[cfg(feature = "runtime-benchmarks")]
1858 fn asap() {
1859 if !Snapshot::<T>::exists() {
1861 Self::create_snapshot()
1862 .inspect_err(|e| {
1863 crate::log!(error, "failed to create snapshot while asap-preparing: {:?}", e)
1864 })
1865 .unwrap()
1866 }
1867 }
1868}
1869
1870pub fn dispatch_error_to_invalid(error: DispatchError) -> InvalidTransaction {
1873 let error_number = match error {
1874 DispatchError::Module(ModuleError { error, .. }) => error[0],
1875 _ => 0,
1876 };
1877 InvalidTransaction::Custom(error_number)
1878}
1879
1880#[cfg(test)]
1881mod feasibility_check {
1882 use super::*;
1887 use crate::mock::{
1888 raw_solution, roll_to, EpochLength, ExtBuilder, MultiPhase, Runtime, SignedPhase,
1889 TargetIndex, UnsignedPhase, VoterIndex,
1890 };
1891 use pezframe_support::{assert_noop, assert_ok};
1892
1893 const COMPUTE: ElectionCompute = ElectionCompute::OnChain;
1894
1895 #[test]
1896 fn snapshot_is_there() {
1897 ExtBuilder::default().build_and_execute(|| {
1898 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1899 assert!(CurrentPhase::<Runtime>::get().is_signed());
1900 let solution = raw_solution();
1901
1902 SnapshotWrapper::<Runtime>::kill();
1905
1906 assert_noop!(
1907 MultiPhase::feasibility_check(solution, COMPUTE),
1908 FeasibilityError::SnapshotUnavailable
1909 );
1910 })
1911 }
1912
1913 #[test]
1914 fn round() {
1915 ExtBuilder::default().build_and_execute(|| {
1916 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1917 assert!(CurrentPhase::<Runtime>::get().is_signed());
1918
1919 let mut solution = raw_solution();
1920 solution.round += 1;
1921 assert_noop!(
1922 MultiPhase::feasibility_check(solution, COMPUTE),
1923 FeasibilityError::InvalidRound
1924 );
1925 })
1926 }
1927
1928 #[test]
1929 fn desired_targets_gets_capped() {
1930 ExtBuilder::default().desired_targets(8).build_and_execute(|| {
1931 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1932 assert!(CurrentPhase::<Runtime>::get().is_signed());
1933
1934 let raw = raw_solution();
1935
1936 assert_eq!(raw.solution.unique_targets().len(), 4);
1937 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 4);
1939
1940 assert_ok!(MultiPhase::feasibility_check(raw, COMPUTE));
1942 })
1943 }
1944
1945 #[test]
1946 fn less_than_desired_targets_fails() {
1947 ExtBuilder::default().desired_targets(8).build_and_execute(|| {
1948 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1949 assert!(CurrentPhase::<Runtime>::get().is_signed());
1950
1951 let mut raw = raw_solution();
1952
1953 assert_eq!(raw.solution.unique_targets().len(), 4);
1954 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 4);
1956
1957 raw.solution.votes1[0].1 = 4;
1959
1960 assert_noop!(
1962 MultiPhase::feasibility_check(raw, COMPUTE),
1963 FeasibilityError::WrongWinnerCount,
1964 );
1965 })
1966 }
1967
1968 #[test]
1969 fn winner_indices() {
1970 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
1971 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
1972 assert!(CurrentPhase::<Runtime>::get().is_signed());
1973
1974 let mut raw = raw_solution();
1975 assert_eq!(Snapshot::<Runtime>::get().unwrap().targets.len(), 4);
1976 raw.solution
1982 .votes1
1983 .iter_mut()
1984 .filter(|(_, t)| *t == TargetIndex::from(3u16))
1985 .for_each(|(_, t)| *t += 1);
1986 raw.solution.votes2.iter_mut().for_each(|(_, [(t0, _)], t1)| {
1987 if *t0 == TargetIndex::from(3u16) {
1988 *t0 += 1
1989 };
1990 if *t1 == TargetIndex::from(3u16) {
1991 *t1 += 1
1992 };
1993 });
1994 assert_noop!(
1995 MultiPhase::feasibility_check(raw, COMPUTE),
1996 FeasibilityError::NposElection(pezsp_npos_elections::Error::SolutionInvalidIndex)
1997 );
1998 })
1999 }
2000
2001 #[test]
2002 fn voter_indices() {
2003 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
2005 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
2006 assert!(CurrentPhase::<Runtime>::get().is_signed());
2007
2008 let mut solution = raw_solution();
2009 assert_eq!(Snapshot::<Runtime>::get().unwrap().voters.len(), 8);
2010 assert!(
2014 solution
2015 .solution
2016 .votes1
2017 .iter_mut()
2018 .filter(|(v, _)| *v == VoterIndex::from(7u32))
2019 .map(|(v, _)| *v = 8)
2020 .count() > 0
2021 );
2022 assert_noop!(
2023 MultiPhase::feasibility_check(solution, COMPUTE),
2024 FeasibilityError::NposElection(pezsp_npos_elections::Error::SolutionInvalidIndex),
2025 );
2026 })
2027 }
2028
2029 #[test]
2030 fn voter_votes() {
2031 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
2032 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
2033 assert!(CurrentPhase::<Runtime>::get().is_signed());
2034
2035 let mut solution = raw_solution();
2036 assert_eq!(Snapshot::<Runtime>::get().unwrap().voters.len(), 8);
2037 assert_eq!(
2042 solution
2043 .solution
2044 .votes1
2045 .iter_mut()
2046 .filter(|(v, t)| *v == 7 && *t == 3)
2047 .map(|(_, t)| *t = 2)
2048 .count(),
2049 1,
2050 );
2051 assert_noop!(
2052 MultiPhase::feasibility_check(solution, COMPUTE),
2053 FeasibilityError::InvalidVote,
2054 );
2055 })
2056 }
2057
2058 #[test]
2059 fn score() {
2060 ExtBuilder::default().desired_targets(2).build_and_execute(|| {
2061 roll_to(<EpochLength>::get() - <SignedPhase>::get() - <UnsignedPhase>::get());
2062 assert!(CurrentPhase::<Runtime>::get().is_signed());
2063
2064 let mut solution = raw_solution();
2065 assert_eq!(Snapshot::<Runtime>::get().unwrap().voters.len(), 8);
2066
2067 solution.score.minimal_stake += 1;
2069
2070 assert_noop!(
2071 MultiPhase::feasibility_check(solution, COMPUTE),
2072 FeasibilityError::InvalidScore,
2073 );
2074 })
2075 }
2076}
2077
2078#[cfg(test)]
2079mod tests {
2080 use super::*;
2081 use crate::{
2082 mock::{
2083 multi_phase_events, raw_solution, roll_to, roll_to_signed, roll_to_unsigned,
2084 ElectionsBounds, ExtBuilder, MockWeightInfo, MockedWeightInfo, MultiPhase, Runtime,
2085 RuntimeOrigin, SignedMaxSubmissions, System, Voters,
2086 },
2087 Phase,
2088 };
2089 use pezframe_election_provider_support::bounds::ElectionBoundsBuilder;
2090 use pezframe_support::{assert_noop, assert_ok};
2091 use pezsp_npos_elections::{BalancingConfig, Support};
2092
2093 #[test]
2094 fn phase_rotation_works() {
2095 ExtBuilder::default().build_and_execute(|| {
2096 assert_eq!(System::block_number(), 0);
2101 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
2102 assert_eq!(Round::<Runtime>::get(), 1);
2103
2104 roll_to(4);
2105 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
2106 assert!(Snapshot::<Runtime>::get().is_none());
2107 assert_eq!(Round::<Runtime>::get(), 1);
2108
2109 roll_to_signed();
2110 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2111 assert_eq!(
2112 multi_phase_events(),
2113 vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }]
2114 );
2115 assert!(Snapshot::<Runtime>::get().is_some());
2116 assert_eq!(Round::<Runtime>::get(), 1);
2117
2118 roll_to(24);
2119 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2120 assert!(Snapshot::<Runtime>::get().is_some());
2121 assert_eq!(Round::<Runtime>::get(), 1);
2122
2123 roll_to_unsigned();
2124 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2125 assert_eq!(
2126 multi_phase_events(),
2127 vec![
2128 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2129 Event::PhaseTransitioned {
2130 from: Phase::Signed,
2131 to: Phase::Unsigned((true, 25)),
2132 round: 1
2133 },
2134 ],
2135 );
2136 assert!(Snapshot::<Runtime>::get().is_some());
2137
2138 roll_to(29);
2139 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2140 assert!(Snapshot::<Runtime>::get().is_some());
2141
2142 roll_to(30);
2143 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2144 assert!(Snapshot::<Runtime>::get().is_some());
2145
2146 roll_to(32);
2148 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2149 assert!(Snapshot::<Runtime>::get().is_some());
2150
2151 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2152
2153 assert!(CurrentPhase::<Runtime>::get().is_off());
2154 assert!(Snapshot::<Runtime>::get().is_none());
2155 assert_eq!(Round::<Runtime>::get(), 2);
2156
2157 roll_to(44);
2158 assert!(CurrentPhase::<Runtime>::get().is_off());
2159
2160 roll_to_signed();
2161 assert!(CurrentPhase::<Runtime>::get().is_signed());
2162
2163 roll_to(55);
2164 assert!(CurrentPhase::<Runtime>::get().is_unsigned_open_at(55));
2165
2166 assert_eq!(
2167 multi_phase_events(),
2168 vec![
2169 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2170 Event::PhaseTransitioned {
2171 from: Phase::Signed,
2172 to: Phase::Unsigned((true, 25)),
2173 round: 1
2174 },
2175 Event::ElectionFinalized {
2176 compute: ElectionCompute::Fallback,
2177 score: ElectionScore {
2178 minimal_stake: 0,
2179 sum_stake: 0,
2180 sum_stake_squared: 0
2181 }
2182 },
2183 Event::PhaseTransitioned {
2184 from: Phase::Unsigned((true, 25)),
2185 to: Phase::Off,
2186 round: 2
2187 },
2188 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 2 },
2189 Event::PhaseTransitioned {
2190 from: Phase::Signed,
2191 to: Phase::Unsigned((true, 55)),
2192 round: 2
2193 },
2194 ]
2195 );
2196 })
2197 }
2198
2199 #[test]
2200 fn signed_phase_void() {
2201 ExtBuilder::default().phases(0, 10).build_and_execute(|| {
2202 roll_to(15);
2203 assert!(CurrentPhase::<Runtime>::get().is_off());
2204
2205 roll_to(19);
2206 assert!(CurrentPhase::<Runtime>::get().is_off());
2207
2208 roll_to(20);
2209 assert!(CurrentPhase::<Runtime>::get().is_unsigned_open_at(20));
2210 assert!(Snapshot::<Runtime>::get().is_some());
2211
2212 roll_to(30);
2213 assert!(CurrentPhase::<Runtime>::get().is_unsigned_open_at(20));
2214
2215 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2216
2217 assert!(CurrentPhase::<Runtime>::get().is_off());
2218 assert!(Snapshot::<Runtime>::get().is_none());
2219
2220 assert_eq!(
2221 multi_phase_events(),
2222 vec![
2223 Event::PhaseTransitioned {
2224 from: Phase::Off,
2225 to: Phase::Unsigned((true, 20)),
2226 round: 1
2227 },
2228 Event::ElectionFinalized {
2229 compute: ElectionCompute::Fallback,
2230 score: ElectionScore {
2231 minimal_stake: 0,
2232 sum_stake: 0,
2233 sum_stake_squared: 0
2234 }
2235 },
2236 Event::PhaseTransitioned {
2237 from: Phase::Unsigned((true, 20)),
2238 to: Phase::Off,
2239 round: 2
2240 },
2241 ]
2242 );
2243 });
2244 }
2245
2246 #[test]
2247 fn unsigned_phase_void() {
2248 ExtBuilder::default().phases(10, 0).build_and_execute(|| {
2249 roll_to(15);
2250 assert!(CurrentPhase::<Runtime>::get().is_off());
2251
2252 roll_to(19);
2253 assert!(CurrentPhase::<Runtime>::get().is_off());
2254
2255 roll_to_signed();
2256 assert!(CurrentPhase::<Runtime>::get().is_signed());
2257 assert!(Snapshot::<Runtime>::get().is_some());
2258
2259 roll_to(30);
2260 assert!(CurrentPhase::<Runtime>::get().is_signed());
2261
2262 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2263
2264 assert!(CurrentPhase::<Runtime>::get().is_off());
2265 assert!(Snapshot::<Runtime>::get().is_none());
2266
2267 assert_eq!(
2268 multi_phase_events(),
2269 vec![
2270 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2271 Event::ElectionFinalized {
2272 compute: ElectionCompute::Fallback,
2273 score: ElectionScore {
2274 minimal_stake: 0,
2275 sum_stake: 0,
2276 sum_stake_squared: 0
2277 }
2278 },
2279 Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 },
2280 ]
2281 )
2282 });
2283 }
2284
2285 #[test]
2286 fn early_termination() {
2287 ExtBuilder::default().build_and_execute(|| {
2289 roll_to_signed();
2292 assert_eq!(
2293 multi_phase_events(),
2294 vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }]
2295 );
2296 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2297 assert_eq!(Round::<Runtime>::get(), 1);
2298
2299 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2301
2302 assert_eq!(
2304 multi_phase_events(),
2305 vec![
2306 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2307 Event::ElectionFinalized {
2308 compute: ElectionCompute::Fallback,
2309 score: Default::default()
2310 },
2311 Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 },
2312 ],
2313 );
2314 assert_eq!(Round::<Runtime>::get(), 2);
2316 assert!(Snapshot::<Runtime>::get().is_none());
2317 assert!(SnapshotMetadata::<Runtime>::get().is_none());
2318 assert!(DesiredTargets::<Runtime>::get().is_none());
2319 assert!(QueuedSolution::<Runtime>::get().is_none());
2320 assert!(MultiPhase::signed_submissions().is_empty());
2321 })
2322 }
2323
2324 #[test]
2325 fn early_termination_with_submissions() {
2326 ExtBuilder::default().build_and_execute(|| {
2328 roll_to_signed();
2331 assert_eq!(
2332 multi_phase_events(),
2333 vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }]
2334 );
2335 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2336 assert_eq!(Round::<Runtime>::get(), 1);
2337
2338 for s in 0..SignedMaxSubmissions::get() {
2340 let solution = RawSolution {
2341 score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
2342 ..Default::default()
2343 };
2344 assert_ok!(MultiPhase::submit(
2345 crate::mock::RuntimeOrigin::signed(99),
2346 Box::new(solution)
2347 ));
2348 }
2349
2350 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2352
2353 assert_eq!(Round::<Runtime>::get(), 2);
2355 assert!(Snapshot::<Runtime>::get().is_none());
2356 assert!(SnapshotMetadata::<Runtime>::get().is_none());
2357 assert!(DesiredTargets::<Runtime>::get().is_none());
2358 assert!(QueuedSolution::<Runtime>::get().is_none());
2359 assert!(MultiPhase::signed_submissions().is_empty());
2360
2361 assert_eq!(
2362 multi_phase_events(),
2363 vec![
2364 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
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::SolutionStored {
2376 compute: ElectionCompute::Signed,
2377 origin: Some(99),
2378 prev_ejected: false
2379 },
2380 Event::SolutionStored {
2381 compute: ElectionCompute::Signed,
2382 origin: Some(99),
2383 prev_ejected: false
2384 },
2385 Event::SolutionStored {
2386 compute: ElectionCompute::Signed,
2387 origin: Some(99),
2388 prev_ejected: false
2389 },
2390 Event::Slashed { account: 99, value: 5 },
2391 Event::Slashed { account: 99, value: 5 },
2392 Event::Slashed { account: 99, value: 5 },
2393 Event::Slashed { account: 99, value: 5 },
2394 Event::Slashed { account: 99, value: 5 },
2395 Event::ElectionFinalized {
2396 compute: ElectionCompute::Fallback,
2397 score: ElectionScore {
2398 minimal_stake: 0,
2399 sum_stake: 0,
2400 sum_stake_squared: 0
2401 }
2402 },
2403 Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 },
2404 ]
2405 );
2406 })
2407 }
2408
2409 #[test]
2410 fn check_events_with_compute_signed() {
2411 ExtBuilder::default().build_and_execute(|| {
2412 roll_to_signed();
2413 assert!(CurrentPhase::<Runtime>::get().is_signed());
2414
2415 let solution = raw_solution();
2416 assert_ok!(MultiPhase::submit(
2417 crate::mock::RuntimeOrigin::signed(99),
2418 Box::new(solution)
2419 ));
2420
2421 roll_to(30);
2422 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2423
2424 assert_eq!(
2425 multi_phase_events(),
2426 vec![
2427 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2428 Event::SolutionStored {
2429 compute: ElectionCompute::Signed,
2430 origin: Some(99),
2431 prev_ejected: false
2432 },
2433 Event::Rewarded { account: 99, value: 7 },
2434 Event::PhaseTransitioned {
2435 from: Phase::Signed,
2436 to: Phase::Unsigned((true, 25)),
2437 round: 1
2438 },
2439 Event::ElectionFinalized {
2440 compute: ElectionCompute::Signed,
2441 score: ElectionScore {
2442 minimal_stake: 40,
2443 sum_stake: 100,
2444 sum_stake_squared: 5200
2445 }
2446 },
2447 Event::PhaseTransitioned {
2448 from: Phase::Unsigned((true, 25)),
2449 to: Phase::Off,
2450 round: 2
2451 },
2452 ],
2453 );
2454 })
2455 }
2456
2457 #[test]
2458 fn check_events_with_compute_unsigned() {
2459 ExtBuilder::default().build_and_execute(|| {
2460 roll_to_unsigned();
2461 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
2462
2463 assert!(Snapshot::<Runtime>::get().is_some());
2465 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 2);
2466
2467 let (solution, witness, _) = MultiPhase::mine_solution().unwrap();
2469
2470 assert!(QueuedSolution::<Runtime>::get().is_none());
2472 assert_ok!(MultiPhase::submit_unsigned(
2473 crate::mock::RuntimeOrigin::none(),
2474 Box::new(solution),
2475 witness
2476 ));
2477 assert!(QueuedSolution::<Runtime>::get().is_some());
2478
2479 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2480
2481 assert_eq!(
2482 multi_phase_events(),
2483 vec![
2484 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2485 Event::PhaseTransitioned {
2486 from: Phase::Signed,
2487 to: Phase::Unsigned((true, 25)),
2488 round: 1
2489 },
2490 Event::SolutionStored {
2491 compute: ElectionCompute::Unsigned,
2492 origin: None,
2493 prev_ejected: false
2494 },
2495 Event::ElectionFinalized {
2496 compute: ElectionCompute::Unsigned,
2497 score: ElectionScore {
2498 minimal_stake: 40,
2499 sum_stake: 100,
2500 sum_stake_squared: 5200
2501 }
2502 },
2503 Event::PhaseTransitioned {
2504 from: Phase::Unsigned((true, 25)),
2505 to: Phase::Off,
2506 round: 2
2507 },
2508 ],
2509 );
2510 })
2511 }
2512
2513 #[test]
2514 fn try_elect_multi_page_fails() {
2515 let prepare_election = || {
2516 roll_to_signed();
2517 assert!(Snapshot::<Runtime>::get().is_some());
2518
2519 let (solution, _, _) = MultiPhase::mine_solution().unwrap();
2521 assert_ok!(MultiPhase::submit(
2522 crate::mock::RuntimeOrigin::signed(99),
2523 Box::new(solution),
2524 ));
2525 roll_to(30);
2526 assert!(QueuedSolution::<Runtime>::get().is_some());
2527 };
2528
2529 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2530 prepare_election();
2531 assert_ok!(MultiPhase::elect(SINGLE_PAGE));
2533 });
2534
2535 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2536 prepare_election();
2537 assert_noop!(MultiPhase::elect(SINGLE_PAGE + 1), ElectionError::MultiPageNotSupported);
2539 })
2540 }
2541
2542 #[test]
2543 fn fallback_strategy_works() {
2544 ExtBuilder::default().onchain_fallback(true).build_and_execute(|| {
2545 roll_to_unsigned();
2546 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2547
2548 assert!(QueuedSolution::<Runtime>::get().is_none());
2550 let supports = MultiPhase::elect(SINGLE_PAGE).unwrap();
2551
2552 let expected_supports = vec![
2553 (30, Support { total: 40, voters: vec![(2, 5), (4, 5), (30, 30)] }),
2554 (40, Support { total: 60, voters: vec![(2, 5), (3, 10), (4, 5), (40, 40)] }),
2555 ]
2556 .try_into()
2557 .unwrap();
2558
2559 assert_eq!(supports, expected_supports);
2560
2561 assert_eq!(
2562 multi_phase_events(),
2563 vec![
2564 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2565 Event::PhaseTransitioned {
2566 from: Phase::Signed,
2567 to: Phase::Unsigned((true, 25)),
2568 round: 1
2569 },
2570 Event::ElectionFinalized {
2571 compute: ElectionCompute::Fallback,
2572 score: ElectionScore {
2573 minimal_stake: 0,
2574 sum_stake: 0,
2575 sum_stake_squared: 0
2576 }
2577 },
2578 Event::PhaseTransitioned {
2579 from: Phase::Unsigned((true, 25)),
2580 to: Phase::Off,
2581 round: 2
2582 },
2583 ]
2584 );
2585 });
2586
2587 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2588 roll_to_unsigned();
2589 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2590
2591 assert!(QueuedSolution::<Runtime>::get().is_none());
2593 assert_eq!(
2594 MultiPhase::elect(SINGLE_PAGE).unwrap_err(),
2595 ElectionError::Fallback("NoFallback.")
2596 );
2597 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Emergency);
2599 assert!(Snapshot::<Runtime>::get().is_some());
2601
2602 assert_eq!(
2603 multi_phase_events(),
2604 vec![
2605 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2606 Event::PhaseTransitioned {
2607 from: Phase::Signed,
2608 to: Phase::Unsigned((true, 25)),
2609 round: 1
2610 },
2611 Event::ElectionFailed,
2612 Event::PhaseTransitioned {
2613 from: Phase::Unsigned((true, 25)),
2614 to: Phase::Emergency,
2615 round: 1
2616 },
2617 ]
2618 );
2619 })
2620 }
2621
2622 #[test]
2623 fn governance_fallback_works() {
2624 ExtBuilder::default().onchain_fallback(false).build_and_execute(|| {
2625 roll_to_unsigned();
2626 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
2627
2628 assert!(QueuedSolution::<Runtime>::get().is_none());
2630 assert_eq!(
2631 MultiPhase::elect(SINGLE_PAGE).unwrap_err(),
2632 ElectionError::Fallback("NoFallback.")
2633 );
2634
2635 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Emergency);
2637 assert!(QueuedSolution::<Runtime>::get().is_none());
2638 assert!(Snapshot::<Runtime>::get().is_some());
2639
2640 assert_noop!(
2642 MultiPhase::governance_fallback(RuntimeOrigin::signed(99)),
2643 DispatchError::BadOrigin
2644 );
2645
2646 assert_ok!(MultiPhase::governance_fallback(RuntimeOrigin::root()));
2648 assert!(QueuedSolution::<Runtime>::get().is_some());
2650 assert!(MultiPhase::elect(SINGLE_PAGE).is_ok());
2652 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
2653
2654 assert_eq!(
2655 multi_phase_events(),
2656 vec![
2657 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
2658 Event::PhaseTransitioned {
2659 from: Phase::Signed,
2660 to: Phase::Unsigned((true, 25)),
2661 round: 1
2662 },
2663 Event::ElectionFailed,
2664 Event::PhaseTransitioned {
2665 from: Phase::Unsigned((true, 25)),
2666 to: Phase::Emergency,
2667 round: 1
2668 },
2669 Event::SolutionStored {
2670 compute: ElectionCompute::Fallback,
2671 origin: None,
2672 prev_ejected: false
2673 },
2674 Event::ElectionFinalized {
2675 compute: ElectionCompute::Fallback,
2676 score: Default::default()
2677 },
2678 Event::PhaseTransitioned { from: Phase::Emergency, to: Phase::Off, round: 2 },
2679 ]
2680 );
2681 })
2682 }
2683
2684 #[test]
2685 fn snapshot_too_big_truncate() {
2686 ExtBuilder::default().build_and_execute(|| {
2688 assert_eq!(Voters::get().len(), 8);
2690 let new_bounds = ElectionBoundsBuilder::default().voters_count(2.into()).build();
2692 ElectionsBounds::set(new_bounds);
2693
2694 roll_to_signed();
2696 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2697
2698 assert_eq!(
2699 SnapshotMetadata::<Runtime>::get().unwrap(),
2700 SolutionOrSnapshotSize { voters: 2, targets: 4 }
2701 );
2702 })
2703 }
2704
2705 #[test]
2706 fn untrusted_score_verification_is_respected() {
2707 ExtBuilder::default().build_and_execute(|| {
2708 roll_to_signed();
2709 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
2710
2711 crate::mock::Balancing::set(Some(BalancingConfig { iterations: 2, tolerance: 0 }));
2713
2714 let (solution, _, _) = MultiPhase::mine_solution().unwrap();
2715 assert!(matches!(solution.score, ElectionScore { minimal_stake: 50, .. }));
2717
2718 MinimumUntrustedScore::<Runtime>::put(ElectionScore {
2719 minimal_stake: 49,
2720 ..Default::default()
2721 });
2722 assert_ok!(MultiPhase::feasibility_check(solution.clone(), ElectionCompute::Signed));
2723
2724 MinimumUntrustedScore::<Runtime>::put(ElectionScore {
2725 minimal_stake: 51,
2726 ..Default::default()
2727 });
2728 assert_noop!(
2729 MultiPhase::feasibility_check(solution, ElectionCompute::Signed),
2730 FeasibilityError::UntrustedScoreTooLow,
2731 );
2732 })
2733 }
2734
2735 #[test]
2736 fn number_of_voters_allowed_2sec_block() {
2737 assert_eq!(MockWeightInfo::get(), MockedWeightInfo::Real);
2739
2740 let all_voters: u32 = 10_000;
2741 let all_targets: u32 = 5_000;
2742 let desired: u32 = 1_000;
2743 let weight_with = |active| {
2744 <Runtime as Config>::WeightInfo::submit_unsigned(
2745 all_voters,
2746 all_targets,
2747 active,
2748 desired,
2749 )
2750 };
2751
2752 let mut active = 1;
2753 while weight_with(active)
2754 .all_lte(<Runtime as pezframe_system::Config>::BlockWeights::get().max_block)
2755 || active == all_voters
2756 {
2757 active += 1;
2758 }
2759
2760 println!("can support {} voters to yield a weight of {}", active, weight_with(active));
2761 }
2762}