// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! # Multi phase, offchain election provider pezpallet. //! //! Currently, this election-provider has two distinct phases (see [`Phase`]), **signed** and //! **unsigned**. //! //! ## Phases //! //! The timeline of pezpallet is as follows. At each block, //! [`pezframe_election_provider_support::ElectionDataProvider::next_election_prediction`] is used //! to estimate the time remaining to the next call to //! [`pezframe_election_provider_support::ElectionProvider::elect`]. Based on this, a phase is //! chosen. The timeline is as follows. //! //! ```ignore //! elect() //! + <--T::SignedPhase--> + <--T::UnsignedPhase--> + //! +-------------------------------------------------------------------+ //! Phase::Off + Phase::Signed + Phase::Unsigned + //! ``` //! //! Note that the unsigned phase starts [`pezpallet::Config::UnsignedPhase`] blocks before the //! `next_election_prediction`, but only ends when a call to [`ElectionProvider::elect`] happens. If //! no `elect` happens, the signed phase is extended. //! //! > Given this, it is rather important for the user of this pezpallet to ensure it always //! > terminates //! election via `elect` before requesting a new one. //! //! Each of the phases can be disabled by essentially setting their length to zero. If both phases //! have length zero, then the pezpallet essentially runs only the fallback strategy, denoted by //! [`Config::Fallback`]. //! //! ### Signed Phase //! //! In the signed phase, solutions (of type [`RawSolution`]) are submitted and queued on chain. A //! deposit is reserved, based on the size of the solution, for the cost of keeping this solution //! on-chain for a number of blocks, and the potential weight of the solution upon being checked. A //! maximum of `pezpallet::Config::SignedMaxSubmissions` solutions are stored. The queue is always //! sorted based on score (worse to best). //! //! Upon arrival of a new solution: //! //! 1. If the queue is not full, it is stored in the appropriate sorted index. //! 2. If the queue is full but the submitted solution is better than one of the queued ones, the //! worse solution is discarded, the bond of the outgoing solution is returned, and the new //! solution is stored in the correct index. //! 3. If the queue is full and the solution is not an improvement compared to any of the queued //! ones, it is instantly rejected and no additional bond is reserved. //! //! A signed solution cannot be reversed, taken back, updated, or retracted. In other words, the //! origin can not bail out in any way, if their solution is queued. //! //! Upon the end of the signed phase, the solutions are examined from best to worse (i.e. `pop()`ed //! until drained). Each solution undergoes an expensive `Pezpallet::feasibility_check`, which //! ensures the score claimed by this score was correct, and it is valid based on the election data //! (i.e. votes and targets). At each step, if the current best solution passes the feasibility //! check, it is considered to be the best one. The sender of the origin is rewarded, and the rest //! of the queued solutions get their deposit back and are discarded, without being checked. //! //! The following example covers all of the cases at the end of the signed phase: //! //! ```ignore //! Queue //! +-------------------------------+ //! |Solution(score=20, valid=false)| +--> Slashed //! +-------------------------------+ //! |Solution(score=15, valid=true )| +--> Rewarded, Saved //! +-------------------------------+ //! |Solution(score=10, valid=true )| +--> Discarded //! +-------------------------------+ //! |Solution(score=05, valid=false)| +--> Discarded //! +-------------------------------+ //! | None | //! +-------------------------------+ //! ``` //! //! Note that both of the bottom solutions end up being discarded and get their deposit back, //! despite one of them being *invalid*. //! //! ## Unsigned Phase //! //! The unsigned phase will always follow the signed phase, with the specified duration. In this //! phase, only validator nodes can submit solutions. A validator node who has offchain workers //! enabled will start to mine a solution in this phase and submits it back to the chain as an //! unsigned transaction, thus the name _unsigned_ phase. This unsigned transaction can never be //! valid if propagated, and it acts similar to an inherent. //! //! Validators will only submit solutions if the one that they have computed is strictly better than //! the best queued one and will limit the weight of the solution to [`MinerConfig::MaxWeight`]. //! //! The unsigned phase can be made passive depending on how the previous signed phase went, by //! setting the first inner value of [`Phase`] to `false`. For now, the signed phase is always //! active. //! //! ### Fallback //! //! If we reach the end of both phases (i.e. call to [`ElectionProvider::elect`] happens) and no //! good solution is queued, then the fallback strategy [`pezpallet::Config::Fallback`] is used to //! determine what needs to be done. The on-chain election is slow, and contains no balancing or //! reduction post-processing. If [`pezpallet::Config::Fallback`] fails, the next phase //! [`Phase::Emergency`] is enabled, which is a more *fail-safe* approach. //! //! ### Emergency Phase //! //! If, for any of the below reasons: //! //! 1. No **signed** or **unsigned** solution submitted, and no successful [`Config::Fallback`] is //! provided //! 2. Any other unforeseen internal error //! //! A call to `T::ElectionProvider::elect` is made, and `Ok(_)` cannot be returned, then the //! pezpallet proceeds to the [`Phase::Emergency`]. During this phase, any solution can be submitted //! from [`Config::ForceOrigin`], without any checking, via //! [`Pezpallet::set_emergency_election_result`] transaction. Hence, `[`Config::ForceOrigin`]` //! should only be set to a trusted origin, such as the council or root. Once submitted, the forced //! solution is kept in [`QueuedSolution`] until the next call to `T::ElectionProvider::elect`, //! where it is returned and [`Phase`] goes back to `Off`. //! //! This implies that the user of this pezpallet (i.e. a staking pezpallet) should re-try calling //! `T::ElectionProvider::elect` in case of error, until `OK(_)` is returned. //! //! To generate an emergency solution, one must only provide one argument: [`Supports`]. This is //! essentially a collection of elected winners for the election, and voters who support them. The //! supports can be generated by any means. In the simplest case, it could be manual. For example, //! in the case of massive network failure or misbehavior, [`Config::ForceOrigin`] might decide to //! select only a small number of emergency winners (which would greatly restrict the next validator //! set, if this pezpallet is used with `pezpallet-staking`). If the failure is for other technical //! reasons, then a simple and safe way to generate supports is using the staking-miner binary //! provided in the Pezkuwi repository. This binary has a subcommand named `emergency-solution` //! which is capable of connecting to a live network, and generating appropriate `supports` using a //! standard algorithm, and outputting the `supports` in hex format, ready for submission. Note that //! while this binary lives in the Pezkuwi repository, this particular subcommand of it can work //! against any bizinikiwi-based chain. //! //! See the [`staking-miner`](https://github.com/paritytech/staking-miner-v2) docs for more //! information. //! //! ## Feasible Solution (correct solution) //! //! All submissions must undergo a feasibility check. Signed solutions are checked one by one at the //! end of the signed phase, and the unsigned solutions are checked on the spot. A feasible solution //! is as follows: //! //! 0. **all** of the used indices must be correct. //! 1. present *exactly* correct number of winners. //! 2. any assignment is checked to match with [`RoundSnapshot::voters`]. //! 3. the claimed score is valid, based on the fixed point arithmetic accuracy. //! //! ## Accuracy //! //! The accuracy of the election is configured via [`SolutionAccuracyOf`] which is the accuracy that //! the submitted solutions must adhere to. //! //! Note that the accuracy is of great importance. The offchain solution should be as small as //! possible, reducing solutions size/weight. //! //! ## Error types //! //! This pezpallet provides a verbose error system to ease future debugging and debugging. The //! overall hierarchy of errors is as follows: //! //! 1. [`pezpallet::Error`]: These are the errors that can be returned in the dispatchables of the //! pezpallet, either signed or unsigned. Since decomposition with nested enums is not possible //! here, they are prefixed with the logical sub-system to which they belong. //! 2. [`ElectionError`]: These are the errors that can be generated while the pezpallet is doing //! something in automatic scenarios, such as `offchain_worker` or `on_initialize`. These errors //! are helpful for logging and are thus nested as: //! - [`ElectionError::Miner`]: wraps a [`unsigned::MinerError`]. //! - [`ElectionError::Feasibility`]: wraps a [`FeasibilityError`]. //! - [`ElectionError::Fallback`]: wraps a fallback error. //! - [`ElectionError::DataProvider`]: wraps a static str. //! //! Note that there could be an overlap between these sub-errors. For example, A //! `SnapshotUnavailable` can happen in both miner and feasibility check phase. //! //! ## Multi-page election support //! //! The [`pezframe_election_provider_support::ElectionDataProvider`] and //! [`pezframe_election_provider_support::ElectionProvider`] traits used by this pezpallet can //! support a multi-page election. //! //! However, this pezpallet only supports single-page election and data //! provider and all the relevant trait implementation and configurations reflect that assumption. //! //! If external callers request the election of a page index higher than 0, the election will fail //! with [`ElectionError::MultiPageNotSupported`]. //! //! ## Future Plans //! //! **Emergency-phase recovery script**: This script should be taken out of staking-miner in //! pezkuwi and ideally live in `bizinikiwi/utils/pezframe/elections`. //! //! **Challenge Phase**. We plan on adding a third phase to the pezpallet, called the challenge //! phase. This is a phase in which no further solutions are processed, and the current best //! solution might be challenged by anyone (signed or unsigned). The main plan here is to enforce //! the solution to be PJR. Checking PJR on-chain is quite expensive, yet proving that a solution is //! **not** PJR is rather cheap. If a queued solution is successfully proven bad: //! //! 1. We must surely slash whoever submitted that solution (might be a challenge for unsigned //! solutions). //! 2. We will fallback to the emergency strategy (likely extending the current era). //! //! **Bailing out**. The functionality of bailing out of a queued solution is nice. A miner can //! submit a solution as soon as they _think_ it is high probability feasible, and do the checks //! afterwards, and remove their solution (for a small cost of probably just transaction fees, or a //! portion of the bond). //! //! **Conditionally open unsigned phase**: Currently, the unsigned phase is always opened. This is //! useful because an honest validator will run bizinikiwi OCW code, which should be good enough to //! trump a mediocre or malicious signed submission (assuming in the absence of honest signed bots). //! If there are signed submissions, they can be checked against an absolute measure (e.g. PJR), //! then we can only open the unsigned phase in extreme conditions (i.e. "no good signed solution //! received") to spare some work for the active validators. //! //! **Allow smaller solutions and build up**: For now we only allow solutions that are exactly //! [`DesiredTargets`], no more, no less. Over time, we can change this to a [min, max] where any //! solution within this range is acceptable, where bigger solutions are prioritized. //! //! **Score based on (byte) size**: We should always prioritize small solutions over bigger ones, if //! there is a tie. Even more harsh should be to enforce the bound of the `reduce` algorithm. //! //! **Take into account the encode/decode weight in benchmarks.** Currently, we only take into //! account the weight of encode/decode in the `submit_unsigned` given its priority. Nonetheless, //! all operations on the solution and the snapshot are worthy of taking this into account. #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; use alloc::{boxed::Box, vec::Vec}; use codec::{Decode, DecodeWithMemTracking, Encode}; use pezframe_election_provider_support::{ bounds::{CountBound, ElectionBounds, SizeBound}, BoundedSupports, BoundedSupportsOf, ElectionDataProvider, ElectionProvider, InstantElectionProvider, NposSolution, PageIndex, }; use pezframe_support::{ dispatch::DispatchClass, ensure, traits::{Currency, Get, OnUnbalanced, ReservableCurrency}, weights::Weight, DefaultNoBound, EqNoBound, PartialEqNoBound, }; use pezframe_system::{ensure_none, offchain::CreateBare, pezpallet_prelude::BlockNumberFor}; use pezsp_arithmetic::{ traits::{CheckedAdd, Zero}, UpperOf, }; use pezsp_npos_elections::{ElectionScore, IdentifierT, Supports, VoteWeight}; use pezsp_runtime::{ transaction_validity::{ InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, TransactionValidityError, ValidTransaction, }, DispatchError, ModuleError, PerThing, Perbill, RuntimeDebug, SaturatedConversion, }; use scale_info::TypeInfo; #[cfg(feature = "try-runtime")] use pezsp_runtime::TryRuntimeError; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; #[cfg(test)] mod mock; #[cfg(all(test, feature = "remote-mining"))] mod remote_mining; #[macro_use] pub mod helpers; /// This pezpallet only supports a single page election flow. pub(crate) const SINGLE_PAGE: u32 = 0; const LOG_TARGET: &str = "runtime::election-provider"; pub mod migrations; pub mod signed; pub mod unsigned; pub mod weights; pub use signed::{ BalanceOf, GeometricDepositBase, NegativeImbalanceOf, PositiveImbalanceOf, SignedSubmission, SignedSubmissionOf, SignedSubmissions, SubmissionIndicesOf, }; use unsigned::VoterOf; pub use unsigned::{Miner, MinerConfig}; pub use weights::WeightInfo; /// The solution type used by this crate. pub type SolutionOf = ::Solution; /// The voter index. Derived from [`SolutionOf`]. pub type SolutionVoterIndexOf = as NposSolution>::VoterIndex; /// The target index. Derived from [`SolutionOf`]. pub type SolutionTargetIndexOf = as NposSolution>::TargetIndex; /// The accuracy of the election, when submitted from offchain. Derived from [`SolutionOf`]. pub type SolutionAccuracyOf = ::MinerConfig> as NposSolution>::Accuracy; /// A ready solution parameterized with this pezpallet's miner config. pub type ReadySolutionOf = ReadySolution< ::AccountId, ::MaxWinners, ::MaxBackersPerWinner, >; /// The fallback election type. pub type FallbackErrorOf = <::Fallback as ElectionProvider>::Error; /// Configuration for the benchmarks of the pezpallet. pub trait BenchmarkingConfig { /// Range of voters. const VOTERS: [u32; 2]; /// Range of targets. const TARGETS: [u32; 2]; /// Range of active voters. const ACTIVE_VOTERS: [u32; 2]; /// Range of desired targets. const DESIRED_TARGETS: [u32; 2]; /// Maximum number of voters expected. This is used only for memory-benchmarking of snapshot. const SNAPSHOT_MAXIMUM_VOTERS: u32; /// Maximum number of voters expected. This is used only for memory-benchmarking of miner. const MINER_MAXIMUM_VOTERS: u32; /// Maximum number of targets expected. This is used only for memory-benchmarking. const MAXIMUM_TARGETS: u32; } /// Current phase of the pezpallet. #[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, DecodeWithMemTracking, Debug, TypeInfo)] pub enum Phase { /// Nothing, the election is not happening. Off, /// Signed phase is open. Signed, /// Unsigned phase. First element is whether it is active or not, second the starting block /// number. /// /// We do not yet check whether the unsigned phase is active or passive. The intent is for the /// blockchain to be able to declare: "I believe that there exists an adequate signed /// solution," advising validators not to bother running the unsigned offchain worker. /// /// As validator nodes are free to edit their OCW code, they could simply ignore this advisory /// and always compute their own solution. However, by default, when the unsigned phase is /// passive, the offchain workers will not bother running. Unsigned((bool, Bn)), /// The emergency phase. This is enabled upon a failing call to `T::ElectionProvider::elect`. /// After that, the only way to leave this phase is through a successful /// `T::ElectionProvider::elect`. Emergency, } impl Default for Phase { fn default() -> Self { Phase::Off } } impl Phase { /// Whether the phase is emergency or not. pub fn is_emergency(&self) -> bool { matches!(self, Phase::Emergency) } /// Whether the phase is signed or not. pub fn is_signed(&self) -> bool { matches!(self, Phase::Signed) } /// Whether the phase is unsigned or not. pub fn is_unsigned(&self) -> bool { matches!(self, Phase::Unsigned(_)) } /// Whether the phase is unsigned and open or not, with specific start. pub fn is_unsigned_open_at(&self, at: Bn) -> bool { matches!(self, Phase::Unsigned((true, real)) if *real == at) } /// Whether the phase is unsigned and open or not. pub fn is_unsigned_open(&self) -> bool { matches!(self, Phase::Unsigned((true, _))) } /// Whether the phase is off or not. pub fn is_off(&self) -> bool { matches!(self, Phase::Off) } } /// The type of `Computation` that provided this election data. #[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, DecodeWithMemTracking, Debug, TypeInfo)] pub enum ElectionCompute { /// Election was computed on-chain. OnChain, /// Election was computed with a signed submission. Signed, /// Election was computed with an unsigned submission. Unsigned, /// Election was computed using the fallback Fallback, /// Election was computed with emergency status. Emergency, } impl Default for ElectionCompute { fn default() -> Self { ElectionCompute::OnChain } } /// A raw, unchecked solution. /// /// This is what will get submitted to the chain. /// /// Such a solution should never become effective in anyway before being checked by the /// `Pezpallet::feasibility_check`. #[derive( PartialEq, Eq, Clone, Encode, Decode, DecodeWithMemTracking, RuntimeDebug, PartialOrd, Ord, TypeInfo, )] pub struct RawSolution { /// the solution itself. pub solution: S, /// The _claimed_ score of the solution. pub score: ElectionScore, /// The round at which this solution should be submitted. pub round: u32, } impl Default for RawSolution { fn default() -> Self { // Round 0 is always invalid, only set this to 1. Self { round: 1, solution: Default::default(), score: Default::default() } } } /// A checked solution, ready to be enacted. #[derive( PartialEqNoBound, EqNoBound, Clone, Encode, Decode, RuntimeDebug, DefaultNoBound, scale_info::TypeInfo, )] #[scale_info(skip_type_params(AccountId, MaxWinners, MaxBackersPerWinner))] pub struct ReadySolution where AccountId: IdentifierT, MaxWinners: Get, MaxBackersPerWinner: Get, { /// The final supports of the solution. /// /// This is target-major vector, storing each winners, total backing, and each individual /// backer. pub supports: BoundedSupports, /// The score of the solution. /// /// This is needed to potentially challenge the solution. pub score: ElectionScore, /// How this election was computed. pub compute: ElectionCompute, } /// A snapshot of all the data that is needed for en entire round. They are provided by /// [`ElectionDataProvider`] and are kept around until the round is finished. /// /// These are stored together because they are often accessed together. #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, Default, TypeInfo)] #[scale_info(skip_type_params(T))] pub struct RoundSnapshot { /// All of the voters. pub voters: Vec, /// All of the targets. pub targets: Vec, } /// Encodes the length of a solution or a snapshot. /// /// This is stored automatically on-chain, and it contains the **size of the entire snapshot**. /// This is also used in dispatchables as weight witness data and should **only contain the size of /// the presented solution**, not the entire snapshot. #[derive( PartialEq, Eq, Clone, Copy, Encode, Decode, DecodeWithMemTracking, Debug, Default, TypeInfo, )] pub struct SolutionOrSnapshotSize { /// The length of voters. #[codec(compact)] pub voters: u32, /// The length of targets. #[codec(compact)] pub targets: u32, } /// Internal errors of the pezpallet. /// /// Note that this is different from [`pezpallet::Error`]. #[derive(pezframe_support::DebugNoBound)] #[cfg_attr(feature = "runtime-benchmarks", derive(strum::IntoStaticStr))] pub enum ElectionError { /// An error happened in the feasibility check sub-system. Feasibility(FeasibilityError), /// An error in the miner (offchain) sub-system. Miner(unsigned::MinerError), /// An error happened in the data provider. DataProvider(&'static str), /// An error nested in the fallback. Fallback(FallbackErrorOf), /// An error occurred when requesting an election result. The caller expects a multi-paged /// election, which this pezpallet does not support. MultiPageNotSupported, /// No solution has been queued. NothingQueued, } // NOTE: we have to do this manually because of the additional where clause needed on // `FallbackErrorOf`. impl PartialEq for ElectionError where FallbackErrorOf: PartialEq, { fn eq(&self, other: &Self) -> bool { use ElectionError::*; match (self, other) { (Feasibility(x), Feasibility(y)) if x == y => true, (Miner(x), Miner(y)) if x == y => true, (DataProvider(x), DataProvider(y)) if x == y => true, (Fallback(x), Fallback(y)) if x == y => true, (MultiPageNotSupported, MultiPageNotSupported) => true, (NothingQueued, NothingQueued) => true, _ => false, } } } impl From for ElectionError { fn from(e: FeasibilityError) -> Self { ElectionError::Feasibility(e) } } impl From for ElectionError { fn from(e: unsigned::MinerError) -> Self { ElectionError::Miner(e) } } /// Errors that can happen in the feasibility check. #[derive(Debug, Eq, PartialEq)] #[cfg_attr(feature = "runtime-benchmarks", derive(strum::IntoStaticStr))] pub enum FeasibilityError { /// Wrong number of winners presented. WrongWinnerCount, /// The snapshot is not available. /// /// Kinda defensive: The pezpallet should technically never attempt to do a feasibility check /// when no snapshot is present. SnapshotUnavailable, /// Internal error from the election crate. NposElection(pezsp_npos_elections::Error), /// A vote is invalid. InvalidVote, /// A voter is invalid. InvalidVoter, /// The given score was invalid. InvalidScore, /// The provided round is incorrect. InvalidRound, /// Comparison against `MinimumUntrustedScore` failed. UntrustedScoreTooLow, /// Data Provider returned too many desired targets TooManyDesiredTargets, /// Conversion into bounded types failed. /// /// Should never happen under correct configurations. BoundedConversionFailed, } impl From for FeasibilityError { fn from(e: pezsp_npos_elections::Error) -> Self { FeasibilityError::NposElection(e) } } pub use pezpallet::*; #[pezframe_support::pezpallet] pub mod pezpallet { use super::*; use pezframe_election_provider_support::{InstantElectionProvider, NposSolver}; use pezframe_support::{pezpallet_prelude::*, traits::EstimateCallFee}; use pezframe_system::pezpallet_prelude::*; use pezsp_runtime::traits::Convert; #[pezpallet::config] pub trait Config: pezframe_system::Config + CreateBare> { #[allow(deprecated)] type RuntimeEvent: From> + IsType<::RuntimeEvent> + TryInto>; /// Currency type. type Currency: ReservableCurrency + Currency; /// Something that can predict the fee of a call. Used to sensibly distribute rewards. type EstimateCallFee: EstimateCallFee, BalanceOf>; /// Duration of the unsigned phase. type UnsignedPhase: Get>; /// Duration of the signed phase. type SignedPhase: Get>; /// The minimum amount of improvement to the solution score that defines a solution as /// "better" in the Signed phase. #[pezpallet::constant] type BetterSignedThreshold: Get; /// The repeat threshold of the offchain worker. /// /// For example, if it is 5, that means that at least 5 blocks will elapse between attempts /// to submit the worker's solution. #[pezpallet::constant] type OffchainRepeat: Get>; /// The priority of the unsigned transaction submitted in the unsigned-phase #[pezpallet::constant] type MinerTxPriority: Get; /// Configurations of the embedded miner. /// /// Any external software implementing this can use the [`unsigned::Miner`] type provided, /// which can mine new solutions and trim them accordingly. type MinerConfig: crate::unsigned::MinerConfig< AccountId = Self::AccountId, MaxVotesPerVoter = ::MaxVotesPerVoter, MaxWinners = Self::MaxWinners, MaxBackersPerWinner = Self::MaxBackersPerWinner, >; /// Maximum number of signed submissions that can be queued. /// /// It is best to avoid adjusting this during an election, as it impacts downstream data /// structures. In particular, `SignedSubmissionIndices` is bounded on this value. If you /// update this value during an election, you _must_ ensure that /// `SignedSubmissionIndices.len()` is less than or equal to the new value. Otherwise, /// attempts to submit new solutions may cause a runtime panic. #[pezpallet::constant] type SignedMaxSubmissions: Get; /// Maximum weight of a signed solution. /// /// If [`Config::MinerConfig`] is being implemented to submit signed solutions (outside of /// this pezpallet), then [`MinerConfig::solution_weight`] is used to compare against /// this value. #[pezpallet::constant] type SignedMaxWeight: Get; /// The maximum amount of unchecked solutions to refund the call fee for. #[pezpallet::constant] type SignedMaxRefunds: Get; /// Base reward for a signed solution #[pezpallet::constant] type SignedRewardBase: Get>; /// Per-byte deposit for a signed solution. #[pezpallet::constant] type SignedDepositByte: Get>; /// Per-weight deposit for a signed solution. #[pezpallet::constant] type SignedDepositWeight: Get>; /// Maximum number of winners that an election supports. /// /// Note: This must always be greater or equal to `T::DataProvider::desired_targets()`. #[pezpallet::constant] type MaxWinners: Get; /// Maximum number of voters that can support a winner in an election solution. /// /// This is needed to ensure election computation is bounded. #[pezpallet::constant] type MaxBackersPerWinner: Get; /// Something that calculates the signed deposit base based on the signed submissions queue /// size. type SignedDepositBase: Convert>; /// The maximum number of electing voters and electable targets to put in the snapshot. type ElectionBounds: Get; /// Handler for the slashed deposits. type SlashHandler: OnUnbalanced>; /// Handler for the rewards. type RewardHandler: OnUnbalanced>; /// Something that will provide the election data. type DataProvider: ElectionDataProvider< AccountId = Self::AccountId, BlockNumber = BlockNumberFor, >; /// Configuration for the fallback. type Fallback: InstantElectionProvider< AccountId = Self::AccountId, BlockNumber = BlockNumberFor, DataProvider = Self::DataProvider, MaxBackersPerWinner = Self::MaxBackersPerWinner, MaxWinnersPerPage = Self::MaxWinners, >; /// Configuration of the governance-only fallback. /// /// As a side-note, it is recommend for test-nets to use `type ElectionProvider = /// BoundedExecution<_>` if the test-net is not expected to have thousands of nominators. type GovernanceFallback: InstantElectionProvider< AccountId = Self::AccountId, BlockNumber = BlockNumberFor, DataProvider = Self::DataProvider, MaxWinnersPerPage = Self::MaxWinners, MaxBackersPerWinner = Self::MaxBackersPerWinner, >; /// OCW election solution miner algorithm implementation. type Solver: NposSolver; /// Origin that can control this pezpallet. Note that any action taken by this origin (such) /// as providing an emergency solution is not checked. Thus, it must be a trusted origin. type ForceOrigin: EnsureOrigin; /// The configuration of benchmarking. type BenchmarkingConfig: BenchmarkingConfig; /// The weight of the pezpallet. type WeightInfo: WeightInfo; } // Expose miner configs over the metadata such that they can be re-implemented. #[pezpallet::extra_constants] impl Pezpallet { #[pezpallet::constant_name(MinerMaxLength)] fn max_length() -> u32 { ::MaxLength::get() } #[pezpallet::constant_name(MinerMaxWeight)] fn max_weight() -> Weight { ::MaxWeight::get() } #[pezpallet::constant_name(MinerMaxVotesPerVoter)] fn max_votes_per_voter() -> u32 { ::MaxVotesPerVoter::get() } #[pezpallet::constant_name(MinerMaxWinners)] fn max_winners() -> u32 { ::MaxWinners::get() } } #[pezpallet::hooks] impl Hooks> for Pezpallet { fn on_initialize(now: BlockNumberFor) -> Weight { let next_election = T::DataProvider::next_election_prediction(now).max(now); let signed_deadline = T::SignedPhase::get() + T::UnsignedPhase::get(); let unsigned_deadline = T::UnsignedPhase::get(); let remaining = next_election - now; let current_phase = CurrentPhase::::get(); log!( trace, "current phase {:?}, next election {:?}, queued? {:?}, metadata: {:?}", current_phase, next_election, QueuedSolution::::get().map(|rs| (rs.supports.len(), rs.compute, rs.score)), SnapshotMetadata::::get() ); match current_phase { Phase::Off if remaining <= signed_deadline && remaining > unsigned_deadline => { // NOTE: if signed-phase length is zero, second part of the if-condition fails. match Self::create_snapshot() { Ok(_) => { Self::phase_transition(Phase::Signed); T::WeightInfo::on_initialize_open_signed() }, Err(why) => { // Not much we can do about this at this point. log!(warn, "failed to open signed phase due to {:?}", why); T::WeightInfo::on_initialize_nothing() }, } }, Phase::Signed | Phase::Off if remaining <= unsigned_deadline && remaining > Zero::zero() => { // our needs vary according to whether or not the unsigned phase follows a // signed phase let (need_snapshot, enabled) = if current_phase == Phase::Signed { // there was previously a signed phase: close the signed phase, no need for // snapshot. // // Notes: // // - `Self::finalize_signed_phase()` also appears in `fn do_elect`. This // is a guard against the case that `elect` is called prematurely. This // adds a small amount of overhead, but that is unfortunately // unavoidable. let _ = Self::finalize_signed_phase(); // In the future we can consider disabling the unsigned phase if the signed // phase completes successfully, but for now we're enabling it // unconditionally as a defensive measure. (false, true) } else { // No signed phase: create a new snapshot, definitely `enable` the unsigned // phase. (true, true) }; if need_snapshot { match Self::create_snapshot() { Ok(_) => { Self::phase_transition(Phase::Unsigned((enabled, now))); T::WeightInfo::on_initialize_open_unsigned() }, Err(why) => { log!(warn, "failed to open unsigned phase due to {:?}", why); T::WeightInfo::on_initialize_nothing() }, } } else { Self::phase_transition(Phase::Unsigned((enabled, now))); T::WeightInfo::on_initialize_open_unsigned() } }, _ => T::WeightInfo::on_initialize_nothing(), } } fn offchain_worker(now: BlockNumberFor) { use pezsp_runtime::offchain::storage_lock::{BlockAndTime, StorageLock}; // Create a lock with the maximum deadline of number of blocks in the unsigned phase. // This should only come useful in an **abrupt** termination of execution, otherwise the // guard will be dropped upon successful execution. let mut lock = StorageLock::>>::with_block_deadline( unsigned::OFFCHAIN_LOCK, T::UnsignedPhase::get().saturated_into(), ); match lock.try_lock() { Ok(_guard) => { Self::do_synchronized_offchain_worker(now); }, Err(deadline) => { log!(debug, "offchain worker lock not released, deadline is {:?}", deadline); }, }; } fn integrity_test() { use core::mem::size_of; // The index type of both voters and targets need to be smaller than that of usize (very // unlikely to be the case, but anyhow).. assert!(size_of::>() <= size_of::()); assert!(size_of::>() <= size_of::()); // ---------------------------- // Based on the requirements of [`pezsp_npos_elections::Assignment::try_normalize`]. let max_vote: usize = as NposSolution>::LIMIT; // 2. Maximum sum of [SolutionAccuracy; 16] must fit into `UpperOf`. let maximum_chain_accuracy: Vec>> = (0..max_vote) .map(|_| { >>::from( SolutionAccuracyOf::::one().deconstruct(), ) }) .collect(); let _: UpperOf> = maximum_chain_accuracy .iter() .fold(Zero::zero(), |acc, x| acc.checked_add(x).unwrap()); // We only accept data provider who's maximum votes per voter matches our // `T::Solution`'s `LIMIT`. // // NOTE that this pezpallet does not really need to enforce this in runtime. The // solution cannot represent any voters more than `LIMIT` anyhow. assert_eq!( ::MaxVotesPerVoter::get(), as NposSolution>::LIMIT as u32, ); // While it won't cause any failures, setting `SignedMaxRefunds` gt // `SignedMaxSubmissions` is a red flag that the developer does not understand how to // configure this pezpallet. assert!(T::SignedMaxSubmissions::get() >= T::SignedMaxRefunds::get()); } #[cfg(feature = "try-runtime")] fn try_state(_n: BlockNumberFor) -> Result<(), TryRuntimeError> { Self::do_try_state() } } #[pezpallet::call] impl Pezpallet { /// Submit a solution for the unsigned phase. /// /// The dispatch origin fo this call must be __none__. /// /// This submission is checked on the fly. Moreover, this unsigned solution is only /// validated when submitted to the pool from the **local** node. Effectively, this means /// that only active validators can submit this transaction when authoring a block (similar /// to an inherent). /// /// To prevent any incorrect solution (and thus wasted time/weight), this transaction will /// panic if the solution submitted by the validator is invalid in any way, effectively /// putting their authoring reward at risk. /// /// No deposit or reward is associated with this submission. #[pezpallet::call_index(0)] #[pezpallet::weight(( T::WeightInfo::submit_unsigned( witness.voters, witness.targets, raw_solution.solution.voter_count() as u32, raw_solution.solution.unique_targets().len() as u32 ), DispatchClass::Operational, ))] pub fn submit_unsigned( origin: OriginFor, raw_solution: Box>>, witness: SolutionOrSnapshotSize, ) -> DispatchResult { ensure_none(origin)?; let error_message = "Invalid unsigned submission must produce invalid block and \ deprive validator from their authoring reward."; // Check score being an improvement, phase, and desired targets. Self::unsigned_pre_dispatch_checks(&raw_solution).expect(error_message); // Ensure witness was correct. let SolutionOrSnapshotSize { voters, targets } = SnapshotMetadata::::get().expect(error_message); // NOTE: we are asserting, not `ensure`ing -- we want to panic here. assert!(voters as u32 == witness.voters, "{}", error_message); assert!(targets as u32 == witness.targets, "{}", error_message); let ready = Self::feasibility_check(*raw_solution, ElectionCompute::Unsigned) .expect(error_message); // Store the newly received solution. log!(debug, "queued unsigned solution with score {:?}", ready.score); let ejected_a_solution = QueuedSolution::::exists(); QueuedSolution::::put(ready); Self::deposit_event(Event::SolutionStored { compute: ElectionCompute::Unsigned, origin: None, prev_ejected: ejected_a_solution, }); Ok(()) } /// Set a new value for `MinimumUntrustedScore`. /// /// Dispatch origin must be aligned with `T::ForceOrigin`. /// /// This check can be turned off by setting the value to `None`. #[pezpallet::call_index(1)] #[pezpallet::weight(T::DbWeight::get().writes(1))] pub fn set_minimum_untrusted_score( origin: OriginFor, maybe_next_score: Option, ) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; MinimumUntrustedScore::::set(maybe_next_score); Ok(()) } /// Set a solution in the queue, to be handed out to the client of this pezpallet in the /// next call to `ElectionProvider::elect`. /// /// This can only be set by `T::ForceOrigin`, and only when the phase is `Emergency`. /// /// The solution is not checked for any feasibility and is assumed to be trustworthy, as any /// feasibility check itself can in principle cause the election process to fail (due to /// memory/weight constrains). #[pezpallet::call_index(2)] #[pezpallet::weight(T::DbWeight::get().reads_writes(1, 1))] pub fn set_emergency_election_result( origin: OriginFor, supports: Supports, ) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; ensure!(CurrentPhase::::get().is_emergency(), Error::::CallNotAllowed); // bound supports with T::MaxWinners. let supports: BoundedSupportsOf = supports.try_into().map_err(|_| Error::::TooManyWinners)?; // Note: we don't `rotate_round` at this point; the next call to // `ElectionProvider::elect` will succeed and take care of that. let solution = ReadySolution { supports, score: Default::default(), compute: ElectionCompute::Emergency, }; Self::deposit_event(Event::SolutionStored { compute: ElectionCompute::Emergency, origin: None, prev_ejected: QueuedSolution::::exists(), }); QueuedSolution::::put(solution); Ok(()) } /// Submit a solution for the signed phase. /// /// The dispatch origin fo this call must be __signed__. /// /// The solution is potentially queued, based on the claimed score and processed at the end /// of the signed phase. /// /// A deposit is reserved and recorded for the solution. Based on the outcome, the solution /// might be rewarded, slashed, or get all or a part of the deposit back. #[pezpallet::call_index(3)] #[pezpallet::weight(T::WeightInfo::submit())] pub fn submit( origin: OriginFor, raw_solution: Box>>, ) -> DispatchResult { let who = ensure_signed(origin)?; // ensure solution is timely. ensure!(CurrentPhase::::get().is_signed(), Error::::PreDispatchEarlySubmission); ensure!(raw_solution.round == Round::::get(), Error::::PreDispatchDifferentRound); // NOTE: this is the only case where having separate snapshot would have been better // because could do just decode_len. But we can create abstractions to do this. // build size. Note: this is not needed for weight calc, thus not input. // unlikely to ever return an error: if phase is signed, snapshot will exist. let size = SnapshotMetadata::::get().ok_or(Error::::MissingSnapshotMetadata)?; ensure!( Self::solution_weight_of(&raw_solution, size).all_lt(T::SignedMaxWeight::get()), Error::::SignedTooMuchWeight, ); // create the submission let deposit = Self::deposit_for(&raw_solution, size); let call_fee = { let call = Call::submit { raw_solution: raw_solution.clone() }; T::EstimateCallFee::estimate_call_fee(&call, None::.into()) }; let submission = SignedSubmission { who: who.clone(), deposit, raw_solution: *raw_solution, call_fee, }; // insert the submission if the queue has space or it's better than the weakest // eject the weakest if the queue was full let mut signed_submissions = Self::signed_submissions(); let maybe_removed = match signed_submissions.insert(submission) { // it's an error if we failed to insert a submission: this indicates the queue was // full but our solution had insufficient score to eject any solution signed::InsertResult::NotInserted => return Err(Error::::SignedQueueFull.into()), signed::InsertResult::Inserted => None, signed::InsertResult::InsertedEjecting(weakest) => Some(weakest), }; // collect deposit. Thereafter, the function cannot fail. T::Currency::reserve(&who, deposit).map_err(|_| Error::::SignedCannotPayDeposit)?; let ejected_a_solution = maybe_removed.is_some(); // if we had to remove the weakest solution, unreserve its deposit if let Some(removed) = maybe_removed { let _remainder = T::Currency::unreserve(&removed.who, removed.deposit); debug_assert!(_remainder.is_zero()); } signed_submissions.put(); Self::deposit_event(Event::SolutionStored { compute: ElectionCompute::Signed, origin: Some(who), prev_ejected: ejected_a_solution, }); Ok(()) } /// Trigger the governance fallback. /// /// This can only be called when [`Phase::Emergency`] is enabled, as an alternative to /// calling [`Call::set_emergency_election_result`]. #[pezpallet::call_index(4)] #[pezpallet::weight(T::DbWeight::get().reads_writes(1, 1))] pub fn governance_fallback(origin: OriginFor) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; ensure!(CurrentPhase::::get().is_emergency(), Error::::CallNotAllowed); let RoundSnapshot { voters, targets } = Snapshot::::get().ok_or(Error::::MissingSnapshotMetadata)?; let desired_targets = DesiredTargets::::get().ok_or(Error::::MissingSnapshotMetadata)?; let supports = T::GovernanceFallback::instant_elect(voters, targets, desired_targets) .map_err(|e| { log!(error, "GovernanceFallback failed: {:?}", e); Error::::FallbackFailed })?; let solution = ReadySolution { supports, score: Default::default(), compute: ElectionCompute::Fallback, }; Self::deposit_event(Event::SolutionStored { compute: ElectionCompute::Fallback, origin: None, prev_ejected: QueuedSolution::::exists(), }); QueuedSolution::::put(solution); Ok(()) } } #[pezpallet::event] #[pezpallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// A solution was stored with the given compute. /// /// The `origin` indicates the origin of the solution. If `origin` is `Some(AccountId)`, /// the stored solution was submitted in the signed phase by a miner with the `AccountId`. /// Otherwise, the solution was stored either during the unsigned phase or by /// `T::ForceOrigin`. The `bool` is `true` when a previous solution was ejected to make /// room for this one. SolutionStored { compute: ElectionCompute, origin: Option, prev_ejected: bool, }, /// The election has been finalized, with the given computation and score. ElectionFinalized { compute: ElectionCompute, score: ElectionScore }, /// An election failed. /// /// Not much can be said about which computes failed in the process. ElectionFailed, /// An account has been rewarded for their signed submission being finalized. Rewarded { account: ::AccountId, value: BalanceOf }, /// An account has been slashed for submitting an invalid signed submission. Slashed { account: ::AccountId, value: BalanceOf }, /// There was a phase transition in a given round. PhaseTransitioned { from: Phase>, to: Phase>, round: u32, }, } /// Error of the pezpallet that can be returned in response to dispatches. #[pezpallet::error] pub enum Error { /// Submission was too early. PreDispatchEarlySubmission, /// Wrong number of winners presented. PreDispatchWrongWinnerCount, /// Submission was too weak, score-wise. PreDispatchWeakSubmission, /// The queue was full, and the solution was not better than any of the existing ones. SignedQueueFull, /// The origin failed to pay the deposit. SignedCannotPayDeposit, /// Witness data to dispatchable is invalid. SignedInvalidWitness, /// The signed submission consumes too much weight SignedTooMuchWeight, /// OCW submitted solution for wrong round OcwCallWrongEra, /// Snapshot metadata should exist but didn't. MissingSnapshotMetadata, /// `Self::insert_submission` returned an invalid index. InvalidSubmissionIndex, /// The call is not allowed at this point. CallNotAllowed, /// The fallback failed FallbackFailed, /// Some bound not met BoundNotMet, /// Submitted solution has too many winners TooManyWinners, /// Submission was prepared for a different round. PreDispatchDifferentRound, } #[pezpallet::validate_unsigned] impl ValidateUnsigned for Pezpallet { type Call = Call; fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity { if let Call::submit_unsigned { raw_solution, .. } = call { // Discard solution not coming from the local OCW. match source { TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ }, _ => return InvalidTransaction::Call.into(), } Self::unsigned_pre_dispatch_checks(raw_solution) .inspect_err(|err| { log!(debug, "unsigned transaction validation failed due to {:?}", err); }) .map_err(dispatch_error_to_invalid)?; ValidTransaction::with_tag_prefix("OffchainElection") // The higher the score.minimal_stake, the better a solution is. .priority( T::MinerTxPriority::get() .saturating_add(raw_solution.score.minimal_stake.saturated_into()), ) // Used to deduplicate unsigned solutions: each validator should produce one // solution per round at most, and solutions are not propagate. .and_provides(raw_solution.round) // Transaction should stay in the pool for the duration of the unsigned phase. .longevity(T::UnsignedPhase::get().saturated_into::()) // We don't propagate this. This can never be validated at a remote node. .propagate(false) .build() } else { InvalidTransaction::Call.into() } } fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> { if let Call::submit_unsigned { raw_solution, .. } = call { Self::unsigned_pre_dispatch_checks(raw_solution) .map_err(dispatch_error_to_invalid) .map_err(Into::into) } else { Err(InvalidTransaction::Call.into()) } } } #[pezpallet::type_value] pub fn DefaultForRound() -> u32 { 1 } /// Internal counter for the number of rounds. /// /// This is useful for de-duplication of transactions submitted to the pool, and general /// diagnostics of the pezpallet. /// /// This is merely incremented once per every time that an upstream `elect` is called. #[pezpallet::storage] pub type Round = StorageValue<_, u32, ValueQuery, DefaultForRound>; /// Current phase. #[pezpallet::storage] pub type CurrentPhase = StorageValue<_, Phase>, ValueQuery>; /// Current best solution, signed or unsigned, queued to be returned upon `elect`. /// /// Always sorted by score. #[pezpallet::storage] pub type QueuedSolution = StorageValue<_, ReadySolutionOf>; /// Snapshot data of the round. /// /// This is created at the beginning of the signed phase and cleared upon calling `elect`. /// Note: This storage type must only be mutated through [`SnapshotWrapper`]. #[pezpallet::storage] pub type Snapshot = StorageValue<_, RoundSnapshot>>; /// Desired number of targets to elect for this round. /// /// Only exists when [`Snapshot`] is present. /// Note: This storage type must only be mutated through [`SnapshotWrapper`]. #[pezpallet::storage] pub type DesiredTargets = StorageValue<_, u32>; /// The metadata of the [`RoundSnapshot`] /// /// Only exists when [`Snapshot`] is present. /// Note: This storage type must only be mutated through [`SnapshotWrapper`]. #[pezpallet::storage] pub type SnapshotMetadata = StorageValue<_, SolutionOrSnapshotSize>; // The following storage items collectively comprise `SignedSubmissions`, and should never be // accessed independently. Instead, get `Self::signed_submissions()`, modify it as desired, and // then do `signed_submissions.put()` when you're done with it. /// The next index to be assigned to an incoming signed submission. /// /// Every accepted submission is assigned a unique index; that index is bound to that particular /// submission for the duration of the election. On election finalization, the next index is /// reset to 0. /// /// We can't just use `SignedSubmissionIndices.len()`, because that's a bounded set; past its /// capacity, it will simply saturate. We can't just iterate over `SignedSubmissionsMap`, /// because iteration is slow. Instead, we store the value here. #[pezpallet::storage] pub type SignedSubmissionNextIndex = StorageValue<_, u32, ValueQuery>; /// A sorted, bounded vector of `(score, block_number, index)`, where each `index` points to a /// value in `SignedSubmissions`. /// /// We never need to process more than a single signed submission at a time. Signed submissions /// can be quite large, so we're willing to pay the cost of multiple database accesses to access /// them one at a time instead of reading and decoding all of them at once. #[pezpallet::storage] pub type SignedSubmissionIndices = StorageValue<_, SubmissionIndicesOf, ValueQuery>; /// Unchecked, signed solutions. /// /// Together with `SubmissionIndices`, this stores a bounded set of `SignedSubmissions` while /// allowing us to keep only a single one in memory at a time. /// /// Twox note: the key of the map is an auto-incrementing index which users cannot inspect or /// affect; we shouldn't need a cryptographically secure hasher. #[pezpallet::storage] pub type SignedSubmissionsMap = StorageMap<_, Twox64Concat, u32, SignedSubmissionOf, OptionQuery>; // `SignedSubmissions` items end here. /// The minimum score that each 'untrusted' solution must attain in order to be considered /// feasible. /// /// Can be set via `set_minimum_untrusted_score`. #[pezpallet::storage] pub type MinimumUntrustedScore = StorageValue<_, ElectionScore>; /// The in-code storage version. /// /// v1: https://github.com/pezkuwichain/kurdistan-sdk/issues/38/ const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); #[pezpallet::pezpallet] #[pezpallet::without_storage_info] #[pezpallet::storage_version(STORAGE_VERSION)] pub struct Pezpallet(_); } /// This wrapper is created for handling the synchronization of [`Snapshot`], [`SnapshotMetadata`] /// and [`DesiredTargets`] storage items. pub struct SnapshotWrapper(core::marker::PhantomData); impl SnapshotWrapper { /// Kill all snapshot related storage items at the same time. pub fn kill() { Snapshot::::kill(); SnapshotMetadata::::kill(); DesiredTargets::::kill(); } /// Set all snapshot related storage items at the same time. pub fn set(metadata: SolutionOrSnapshotSize, desired_targets: u32, buffer: &[u8]) { SnapshotMetadata::::put(metadata); DesiredTargets::::put(desired_targets); pezsp_io::storage::set(&Snapshot::::hashed_key(), &buffer); } /// Check if all of the storage items exist at the same time or all of the storage items do not /// exist. #[cfg(feature = "try-runtime")] pub fn is_consistent() -> bool { let snapshots = [ Snapshot::::exists(), SnapshotMetadata::::exists(), DesiredTargets::::exists(), ]; // All should either exist or not exist snapshots.iter().skip(1).all(|v| snapshots[0] == *v) } } impl Pezpallet { /// Internal counter for the number of rounds. /// /// This is useful for de-duplication of transactions submitted to the pool, and general /// diagnostics of the pezpallet. /// /// This is merely incremented once per every time that an upstream `elect` is called. pub fn round() -> u32 { Round::::get() } /// Current phase. pub fn current_phase() -> Phase> { CurrentPhase::::get() } /// Current best solution, signed or unsigned, queued to be returned upon `elect`. /// /// Always sorted by score. pub fn queued_solution() -> Option> { QueuedSolution::::get() } /// Snapshot data of the round. /// /// This is created at the beginning of the signed phase and cleared upon calling `elect`. /// Note: This storage type must only be mutated through [`SnapshotWrapper`]. pub fn snapshot() -> Option>> { Snapshot::::get() } /// Desired number of targets to elect for this round. /// /// Only exists when [`Snapshot`] is present. /// Note: This storage type must only be mutated through [`SnapshotWrapper`]. pub fn desired_targets() -> Option { DesiredTargets::::get() } /// The metadata of the [`RoundSnapshot`] /// /// Only exists when [`Snapshot`] is present. /// Note: This storage type must only be mutated through [`SnapshotWrapper`]. pub fn snapshot_metadata() -> Option { SnapshotMetadata::::get() } /// The minimum score that each 'untrusted' solution must attain in order to be considered /// feasible. /// /// Can be set via `set_minimum_untrusted_score`. pub fn minimum_untrusted_score() -> Option { MinimumUntrustedScore::::get() } /// Internal logic of the offchain worker, to be executed only when the offchain lock is /// acquired with success. fn do_synchronized_offchain_worker(now: BlockNumberFor) { let current_phase = CurrentPhase::::get(); log!(trace, "lock for offchain worker acquired. Phase = {:?}", current_phase); match current_phase { Phase::Unsigned((true, opened)) if opened == now => { // Mine a new solution, cache it, and attempt to submit it let initial_output = Self::ensure_offchain_repeat_frequency(now).and_then(|_| { // This is executed at the beginning of each round. Any cache is now invalid. // Clear it. unsigned::kill_ocw_solution::(); Self::mine_check_save_submit() }); log!(debug, "initial offchain thread output: {:?}", initial_output); }, Phase::Unsigned((true, opened)) if opened < now => { // Try and resubmit the cached solution, and recompute ONLY if it is not // feasible. let resubmit_output = Self::ensure_offchain_repeat_frequency(now) .and_then(|_| Self::restore_or_compute_then_maybe_submit()); log!(debug, "resubmit offchain thread output: {:?}", resubmit_output); }, _ => {}, } } /// Phase transition helper. pub(crate) fn phase_transition(to: Phase>) { log!(info, "Starting phase {:?}, round {}.", to, Round::::get()); Self::deposit_event(Event::PhaseTransitioned { from: CurrentPhase::::get(), to, round: Round::::get(), }); CurrentPhase::::put(to); } /// Parts of [`create_snapshot`] that happen inside of this pezpallet. /// /// Extracted for easier weight calculation. fn create_snapshot_internal( targets: Vec, voters: Vec>, desired_targets: u32, ) { let metadata = SolutionOrSnapshotSize { voters: voters.len() as u32, targets: targets.len() as u32 }; log!(info, "creating a snapshot with metadata {:?}", metadata); // instead of using storage APIs, we do a manual encoding into a fixed-size buffer. // `encoded_size` encodes it without storing it anywhere, this should not cause any // allocation. let snapshot = RoundSnapshot::> { voters, targets }; let size = snapshot.encoded_size(); log!(debug, "snapshot pre-calculated size {:?}", size); let mut buffer = Vec::with_capacity(size); snapshot.encode_to(&mut buffer); // do some checks. debug_assert_eq!(buffer, snapshot.encode()); // buffer should have not re-allocated since. debug_assert!(buffer.len() == size && size == buffer.capacity()); SnapshotWrapper::::set(metadata, desired_targets, &buffer); } /// Parts of [`create_snapshot`] that happen outside of this pezpallet. /// /// Extracted for easier weight calculation. /// /// Note: this pezpallet only supports one page of voter and target snapshots. fn create_snapshot_external( ) -> Result<(Vec, Vec>, u32), ElectionError> { let election_bounds = T::ElectionBounds::get(); let targets = T::DataProvider::electable_targets_stateless(election_bounds.targets) .and_then(|t| { election_bounds.ensure_targets_limits( CountBound(t.len() as u32), SizeBound(t.encoded_size() as u32), )?; Ok(t) }) .map_err(ElectionError::DataProvider)?; let voters = T::DataProvider::electing_voters_stateless(election_bounds.voters) .and_then(|v| { election_bounds.ensure_voters_limits( CountBound(v.len() as u32), SizeBound(v.encoded_size() as u32), )?; Ok(v) }) .map_err(ElectionError::DataProvider)?; let mut desired_targets = as ElectionProvider>::desired_targets_checked() .map_err(|e| ElectionError::DataProvider(e))?; // If `desired_targets` > `targets.len()`, cap `desired_targets` to that level and emit a // warning let max_desired_targets: u32 = targets.len() as u32; if desired_targets > max_desired_targets { log!( warn, "desired_targets: {} > targets.len(): {}, capping desired_targets", desired_targets, max_desired_targets ); desired_targets = max_desired_targets; } Ok((targets, voters, desired_targets)) } /// Creates the snapshot. Writes new data to: /// /// 1. [`SnapshotMetadata`] /// 2. [`RoundSnapshot`] /// 3. [`DesiredTargets`] /// /// Returns `Ok(())` if operation is okay. /// /// This is a *self-weighing* function, it will register its own extra weight as /// [`DispatchClass::Mandatory`] with the system pezpallet. pub fn create_snapshot() -> Result<(), ElectionError> { // this is self-weighing itself.. let (targets, voters, desired_targets) = Self::create_snapshot_external()?; // ..therefore we only measure the weight of this and add it. let internal_weight = T::WeightInfo::create_snapshot_internal(voters.len() as u32, targets.len() as u32); Self::create_snapshot_internal(targets, voters, desired_targets); Self::register_weight(internal_weight); Ok(()) } /// Register some amount of weight directly with the system pezpallet. /// /// This is always mandatory weight. fn register_weight(weight: Weight) { pezframe_system::Pezpallet::::register_extra_weight_unchecked( weight, DispatchClass::Mandatory, ); } /// Checks the feasibility of a solution. pub fn feasibility_check( raw_solution: RawSolution>, compute: ElectionCompute, ) -> Result, FeasibilityError> { let desired_targets = DesiredTargets::::get().ok_or(FeasibilityError::SnapshotUnavailable)?; let snapshot = Snapshot::::get().ok_or(FeasibilityError::SnapshotUnavailable)?; let round = Round::::get(); let minimum_untrusted_score = MinimumUntrustedScore::::get(); Miner::::feasibility_check( raw_solution, compute, desired_targets, snapshot, round, minimum_untrusted_score, ) } /// Perform the tasks to be done after a new `elect` has been triggered: /// /// 1. Increment round. /// 2. Change phase to [`Phase::Off`] /// 3. Clear all snapshot data. fn rotate_round() { // Inc round. Round::::mutate(|r| *r += 1); // Phase is off now. Self::phase_transition(Phase::Off); // Kill snapshot and relevant metadata (everything created by [`SnapshotMetadata::set`]). SnapshotWrapper::::kill(); } fn do_elect() -> Result, ElectionError> { // We have to unconditionally try finalizing the signed phase here. There are only two // possibilities: // // - signed phase was open, in which case this is essential for correct functioning of the // system // - signed phase was complete or not started, in which case finalization is idempotent and // inexpensive (1 read of an empty vector). let _ = Self::finalize_signed_phase(); QueuedSolution::::take() .ok_or(ElectionError::::NothingQueued) .or_else(|_| { log!(warn, "No solution queued, falling back to instant fallback.",); #[cfg(feature = "runtime-benchmarks")] Self::asap(); let (voters, targets, desired_targets) = if T::Fallback::bother() { let RoundSnapshot { voters, targets } = Snapshot::::get().ok_or( ElectionError::::Feasibility(FeasibilityError::SnapshotUnavailable), )?; let desired_targets = DesiredTargets::::get().ok_or( ElectionError::::Feasibility(FeasibilityError::SnapshotUnavailable), )?; (voters, targets, desired_targets) } else { (Default::default(), Default::default(), Default::default()) }; T::Fallback::instant_elect(voters, targets, desired_targets) .map_err(|fe| ElectionError::Fallback(fe)) .and_then(|supports| { Ok(ReadySolution { supports, score: Default::default(), compute: ElectionCompute::Fallback, }) }) }) .map(|ReadySolution { compute, score, supports }| { Self::deposit_event(Event::ElectionFinalized { compute, score }); log!(info, "Finalized election round with compute {:?}.", compute); supports }) .map_err(|err| { Self::deposit_event(Event::ElectionFailed); log!(warn, "Failed to finalize election round. reason {:?}", err); err }) } /// record the weight of the given `supports`. fn weigh_supports(supports: &BoundedSupportsOf) { let active_voters = supports .iter() .map(|(_, x)| x) .fold(Zero::zero(), |acc, next| acc + next.voters.len() as u32); let desired_targets = supports.len() as u32; Self::register_weight(T::WeightInfo::elect_queued(active_voters, desired_targets)); } } #[cfg(feature = "try-runtime")] impl Pezpallet { fn do_try_state() -> Result<(), TryRuntimeError> { Self::try_state_snapshot()?; Self::try_state_signed_submissions_map()?; Self::try_state_phase_off() } // [`Snapshot`] state check. Invariants: // - [`DesiredTargets`] exists if and only if [`Snapshot`] is present. // - [`SnapshotMetadata`] exist if and only if [`Snapshot`] is present. fn try_state_snapshot() -> Result<(), TryRuntimeError> { if SnapshotWrapper::::is_consistent() { Ok(()) } else { Err("If snapshot exists, metadata and desired targets should be set too. Otherwise, none should be set.".into()) } } // [`SignedSubmissionsMap`] state check. Invariants: // - All [`SignedSubmissionIndices`] are present in [`SignedSubmissionsMap`], and no more; // - [`SignedSubmissionNextIndex`] is not present in [`SignedSubmissionsMap`]; // - [`SignedSubmissionIndices`] is sorted by election score. fn try_state_signed_submissions_map() -> Result<(), TryRuntimeError> { let mut last_score: ElectionScore = Default::default(); let indices = SignedSubmissionIndices::::get(); for (i, indice) in indices.iter().enumerate() { let submission = SignedSubmissionsMap::::get(indice.2); if submission.is_none() { return Err( "All signed submissions indices must be part of the submissions map".into() ); } if i == 0 { last_score = indice.0 } else { if last_score.strict_better(indice.0) { return Err( "Signed submission indices vector must be ordered by election score".into(), ); } last_score = indice.0; } } if SignedSubmissionsMap::::iter().nth(indices.len()).is_some() { return Err( "Signed submissions map length should be the same as the indices vec length".into(), ); } match SignedSubmissionNextIndex::::get() { 0 => Ok(()), next => if SignedSubmissionsMap::::get(next).is_some() { return Err( "The next submissions index should not be in the submissions maps already" .into(), ); } else { Ok(()) }, } } // [`Phase::Off`] state check. Invariants: // - If phase is `Phase::Off`, [`Snapshot`] must be none. fn try_state_phase_off() -> Result<(), TryRuntimeError> { match CurrentPhase::::get().is_off() { false => Ok(()), true => if Snapshot::::get().is_some() { Err("Snapshot must be none when in Phase::Off".into()) } else { Ok(()) }, } } } impl ElectionProvider for Pezpallet { type AccountId = T::AccountId; type BlockNumber = BlockNumberFor; type Error = ElectionError; type MaxWinnersPerPage = T::MaxWinners; type MaxBackersPerWinner = T::MaxBackersPerWinner; type MaxBackersPerWinnerFinal = T::MaxBackersPerWinner; type Pages = pezsp_core::ConstU32<1>; type DataProvider = T::DataProvider; fn elect(page: PageIndex) -> Result, Self::Error> { // Note: this pezpallet **MUST** only by used in the single-page mode. ensure!(page == SINGLE_PAGE, ElectionError::::MultiPageNotSupported); let res = match Self::do_elect() { Ok(bounded_supports) => { // All went okay, record the weight, put sign to be Off, clean snapshot, etc. Self::weigh_supports(&bounded_supports); Self::rotate_round(); Ok(bounded_supports) }, Err(why) => { log!(error, "Entering emergency mode: {:?}", why); Self::phase_transition(Phase::Emergency); Err(why) }, }; log!(info, "ElectionProvider::elect({}) => {:?}", page, res.as_ref().map(|s| s.len())); res } fn duration() -> Self::BlockNumber { let signed: BlockNumberFor = T::SignedPhase::get().saturated_into(); let unsigned: BlockNumberFor = T::UnsignedPhase::get().saturated_into(); signed + unsigned } fn start() -> Result<(), Self::Error> { log!( warn, "we received signal, but this pezpallet works in the basis of legacy pull based election" ); Ok(()) } fn status() -> Result { let has_queued = QueuedSolution::::exists(); let phase = CurrentPhase::::get(); match (phase, has_queued) { (Phase::Unsigned(_), true) => Ok(true), (Phase::Off, _) => Err(()), _ => Ok(false), } } #[cfg(feature = "runtime-benchmarks")] fn asap() { // prepare our snapshot so we can "hopefully" run a fallback. if !Snapshot::::exists() { Self::create_snapshot() .inspect_err(|e| { crate::log!(error, "failed to create snapshot while asap-preparing: {:?}", e) }) .unwrap() } } } /// convert a DispatchError to a custom InvalidTransaction with the inner code being the error /// number. pub fn dispatch_error_to_invalid(error: DispatchError) -> InvalidTransaction { let error_number = match error { DispatchError::Module(ModuleError { error, .. }) => error[0], _ => 0, }; InvalidTransaction::Custom(error_number) } #[cfg(test)] mod feasibility_check { //! All of the tests here should be dedicated to only testing the feasibility check and nothing //! more. The best way to audit and review these tests is to try and come up with a solution //! that is invalid, but gets through the system as valid. use super::*; use crate::mock::{ raw_solution, roll_to, EpochLength, ExtBuilder, MultiPhase, Runtime, SignedPhase, TargetIndex, UnsignedPhase, VoterIndex, }; use pezframe_support::{assert_noop, assert_ok}; const COMPUTE: ElectionCompute = ElectionCompute::OnChain; #[test] fn snapshot_is_there() { ExtBuilder::default().build_and_execute(|| { roll_to(::get() - ::get() - ::get()); assert!(CurrentPhase::::get().is_signed()); let solution = raw_solution(); // kill `Snapshot`, `SnapshotMetadata` and `DesiredTargets` for the storage state to // be consistent, by using the `SnapshotWrapper` for the try_state checks to pass. SnapshotWrapper::::kill(); assert_noop!( MultiPhase::feasibility_check(solution, COMPUTE), FeasibilityError::SnapshotUnavailable ); }) } #[test] fn round() { ExtBuilder::default().build_and_execute(|| { roll_to(::get() - ::get() - ::get()); assert!(CurrentPhase::::get().is_signed()); let mut solution = raw_solution(); solution.round += 1; assert_noop!( MultiPhase::feasibility_check(solution, COMPUTE), FeasibilityError::InvalidRound ); }) } #[test] fn desired_targets_gets_capped() { ExtBuilder::default().desired_targets(8).build_and_execute(|| { roll_to(::get() - ::get() - ::get()); assert!(CurrentPhase::::get().is_signed()); let raw = raw_solution(); assert_eq!(raw.solution.unique_targets().len(), 4); // desired_targets is capped to the number of targets which is 4 assert_eq!(DesiredTargets::::get().unwrap(), 4); // It should succeed assert_ok!(MultiPhase::feasibility_check(raw, COMPUTE)); }) } #[test] fn less_than_desired_targets_fails() { ExtBuilder::default().desired_targets(8).build_and_execute(|| { roll_to(::get() - ::get() - ::get()); assert!(CurrentPhase::::get().is_signed()); let mut raw = raw_solution(); assert_eq!(raw.solution.unique_targets().len(), 4); // desired_targets is capped to the number of targets which is 4 assert_eq!(DesiredTargets::::get().unwrap(), 4); // Force the number of winners to be bigger to fail raw.solution.votes1[0].1 = 4; // It should succeed assert_noop!( MultiPhase::feasibility_check(raw, COMPUTE), FeasibilityError::WrongWinnerCount, ); }) } #[test] fn winner_indices() { ExtBuilder::default().desired_targets(2).build_and_execute(|| { roll_to(::get() - ::get() - ::get()); assert!(CurrentPhase::::get().is_signed()); let mut raw = raw_solution(); assert_eq!(Snapshot::::get().unwrap().targets.len(), 4); // ----------------------------------------------------^^ valid range is [0..3]. // Swap all votes from 3 to 4. This will ensure that the number of unique winners will // still be 4, but one of the indices will be gibberish. Requirement is to make sure 3 a // winner, which we don't do here. raw.solution .votes1 .iter_mut() .filter(|(_, t)| *t == TargetIndex::from(3u16)) .for_each(|(_, t)| *t += 1); raw.solution.votes2.iter_mut().for_each(|(_, [(t0, _)], t1)| { if *t0 == TargetIndex::from(3u16) { *t0 += 1 }; if *t1 == TargetIndex::from(3u16) { *t1 += 1 }; }); assert_noop!( MultiPhase::feasibility_check(raw, COMPUTE), FeasibilityError::NposElection(pezsp_npos_elections::Error::SolutionInvalidIndex) ); }) } #[test] fn voter_indices() { // Should be caught in `solution.into_assignment`. ExtBuilder::default().desired_targets(2).build_and_execute(|| { roll_to(::get() - ::get() - ::get()); assert!(CurrentPhase::::get().is_signed()); let mut solution = raw_solution(); assert_eq!(Snapshot::::get().unwrap().voters.len(), 8); // ----------------------------------------------------^^ valid range is [0..7]. // Check that there is an index 7 in votes1, and flip to 8. assert!( solution .solution .votes1 .iter_mut() .filter(|(v, _)| *v == VoterIndex::from(7u32)) .map(|(v, _)| *v = 8) .count() > 0 ); assert_noop!( MultiPhase::feasibility_check(solution, COMPUTE), FeasibilityError::NposElection(pezsp_npos_elections::Error::SolutionInvalidIndex), ); }) } #[test] fn voter_votes() { ExtBuilder::default().desired_targets(2).build_and_execute(|| { roll_to(::get() - ::get() - ::get()); assert!(CurrentPhase::::get().is_signed()); let mut solution = raw_solution(); assert_eq!(Snapshot::::get().unwrap().voters.len(), 8); // ----------------------------------------------------^^ valid range is [0..7]. // First, check that voter at index 7 (40) actually voted for 3 (40) -- this is self // vote. Then, change the vote to 2 (30). assert_eq!( solution .solution .votes1 .iter_mut() .filter(|(v, t)| *v == 7 && *t == 3) .map(|(_, t)| *t = 2) .count(), 1, ); assert_noop!( MultiPhase::feasibility_check(solution, COMPUTE), FeasibilityError::InvalidVote, ); }) } #[test] fn score() { ExtBuilder::default().desired_targets(2).build_and_execute(|| { roll_to(::get() - ::get() - ::get()); assert!(CurrentPhase::::get().is_signed()); let mut solution = raw_solution(); assert_eq!(Snapshot::::get().unwrap().voters.len(), 8); // Simply faff with the score. solution.score.minimal_stake += 1; assert_noop!( MultiPhase::feasibility_check(solution, COMPUTE), FeasibilityError::InvalidScore, ); }) } } #[cfg(test)] mod tests { use super::*; use crate::{ mock::{ multi_phase_events, raw_solution, roll_to, roll_to_signed, roll_to_unsigned, ElectionsBounds, ExtBuilder, MockWeightInfo, MockedWeightInfo, MultiPhase, Runtime, RuntimeOrigin, SignedMaxSubmissions, System, Voters, }, Phase, }; use pezframe_election_provider_support::bounds::ElectionBoundsBuilder; use pezframe_support::{assert_noop, assert_ok}; use pezsp_npos_elections::{BalancingConfig, Support}; #[test] fn phase_rotation_works() { ExtBuilder::default().build_and_execute(|| { // 0 ------- 15 ------- 25 ------- 30 ------- ------- 45 ------- 55 ------- 60 // | | | | | | // Signed Unsigned Elect Signed Unsigned Elect assert_eq!(System::block_number(), 0); assert_eq!(CurrentPhase::::get(), Phase::Off); assert_eq!(Round::::get(), 1); roll_to(4); assert_eq!(CurrentPhase::::get(), Phase::Off); assert!(Snapshot::::get().is_none()); assert_eq!(Round::::get(), 1); roll_to_signed(); assert_eq!(CurrentPhase::::get(), Phase::Signed); assert_eq!( multi_phase_events(), vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }] ); assert!(Snapshot::::get().is_some()); assert_eq!(Round::::get(), 1); roll_to(24); assert_eq!(CurrentPhase::::get(), Phase::Signed); assert!(Snapshot::::get().is_some()); assert_eq!(Round::::get(), 1); roll_to_unsigned(); assert_eq!(CurrentPhase::::get(), Phase::Unsigned((true, 25))); assert_eq!( multi_phase_events(), vec![ Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }, Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Unsigned((true, 25)), round: 1 }, ], ); assert!(Snapshot::::get().is_some()); roll_to(29); assert_eq!(CurrentPhase::::get(), Phase::Unsigned((true, 25))); assert!(Snapshot::::get().is_some()); roll_to(30); assert_eq!(CurrentPhase::::get(), Phase::Unsigned((true, 25))); assert!(Snapshot::::get().is_some()); // We close when upstream tells us to elect. roll_to(32); assert_eq!(CurrentPhase::::get(), Phase::Unsigned((true, 25))); assert!(Snapshot::::get().is_some()); assert_ok!(MultiPhase::elect(SINGLE_PAGE)); assert!(CurrentPhase::::get().is_off()); assert!(Snapshot::::get().is_none()); assert_eq!(Round::::get(), 2); roll_to(44); assert!(CurrentPhase::::get().is_off()); roll_to_signed(); assert!(CurrentPhase::::get().is_signed()); roll_to(55); assert!(CurrentPhase::::get().is_unsigned_open_at(55)); assert_eq!( multi_phase_events(), vec![ Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }, Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Unsigned((true, 25)), round: 1 }, Event::ElectionFinalized { compute: ElectionCompute::Fallback, score: ElectionScore { minimal_stake: 0, sum_stake: 0, sum_stake_squared: 0 } }, Event::PhaseTransitioned { from: Phase::Unsigned((true, 25)), to: Phase::Off, round: 2 }, Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 2 }, Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Unsigned((true, 55)), round: 2 }, ] ); }) } #[test] fn signed_phase_void() { ExtBuilder::default().phases(0, 10).build_and_execute(|| { roll_to(15); assert!(CurrentPhase::::get().is_off()); roll_to(19); assert!(CurrentPhase::::get().is_off()); roll_to(20); assert!(CurrentPhase::::get().is_unsigned_open_at(20)); assert!(Snapshot::::get().is_some()); roll_to(30); assert!(CurrentPhase::::get().is_unsigned_open_at(20)); assert_ok!(MultiPhase::elect(SINGLE_PAGE)); assert!(CurrentPhase::::get().is_off()); assert!(Snapshot::::get().is_none()); assert_eq!( multi_phase_events(), vec![ Event::PhaseTransitioned { from: Phase::Off, to: Phase::Unsigned((true, 20)), round: 1 }, Event::ElectionFinalized { compute: ElectionCompute::Fallback, score: ElectionScore { minimal_stake: 0, sum_stake: 0, sum_stake_squared: 0 } }, Event::PhaseTransitioned { from: Phase::Unsigned((true, 20)), to: Phase::Off, round: 2 }, ] ); }); } #[test] fn unsigned_phase_void() { ExtBuilder::default().phases(10, 0).build_and_execute(|| { roll_to(15); assert!(CurrentPhase::::get().is_off()); roll_to(19); assert!(CurrentPhase::::get().is_off()); roll_to_signed(); assert!(CurrentPhase::::get().is_signed()); assert!(Snapshot::::get().is_some()); roll_to(30); assert!(CurrentPhase::::get().is_signed()); assert_ok!(MultiPhase::elect(SINGLE_PAGE)); assert!(CurrentPhase::::get().is_off()); assert!(Snapshot::::get().is_none()); assert_eq!( multi_phase_events(), vec![ Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }, Event::ElectionFinalized { compute: ElectionCompute::Fallback, score: ElectionScore { minimal_stake: 0, sum_stake: 0, sum_stake_squared: 0 } }, Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 }, ] ) }); } #[test] fn early_termination() { // An early termination in the signed phase, with no queued solution. ExtBuilder::default().build_and_execute(|| { // Signed phase started at block 15 and will end at 25. roll_to_signed(); assert_eq!( multi_phase_events(), vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }] ); assert_eq!(CurrentPhase::::get(), Phase::Signed); assert_eq!(Round::::get(), 1); // An unexpected call to elect. assert_ok!(MultiPhase::elect(SINGLE_PAGE)); // We surely can't have any feasible solutions. This will cause an on-chain election. assert_eq!( multi_phase_events(), vec![ Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }, Event::ElectionFinalized { compute: ElectionCompute::Fallback, score: Default::default() }, Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 }, ], ); // All storage items must be cleared. assert_eq!(Round::::get(), 2); assert!(Snapshot::::get().is_none()); assert!(SnapshotMetadata::::get().is_none()); assert!(DesiredTargets::::get().is_none()); assert!(QueuedSolution::::get().is_none()); assert!(MultiPhase::signed_submissions().is_empty()); }) } #[test] fn early_termination_with_submissions() { // an early termination in the signed phase, with no queued solution. ExtBuilder::default().build_and_execute(|| { // signed phase started at block 15 and will end at 25. roll_to_signed(); assert_eq!( multi_phase_events(), vec![Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }] ); assert_eq!(CurrentPhase::::get(), Phase::Signed); assert_eq!(Round::::get(), 1); // fill the queue with signed submissions for s in 0..SignedMaxSubmissions::get() { let solution = RawSolution { score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() }, ..Default::default() }; assert_ok!(MultiPhase::submit( crate::mock::RuntimeOrigin::signed(99), Box::new(solution) )); } // an unexpected call to elect. assert_ok!(MultiPhase::elect(SINGLE_PAGE)); // all storage items must be cleared. assert_eq!(Round::::get(), 2); assert!(Snapshot::::get().is_none()); assert!(SnapshotMetadata::::get().is_none()); assert!(DesiredTargets::::get().is_none()); assert!(QueuedSolution::::get().is_none()); assert!(MultiPhase::signed_submissions().is_empty()); assert_eq!( multi_phase_events(), vec![ Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }, Event::SolutionStored { compute: ElectionCompute::Signed, origin: Some(99), prev_ejected: false }, Event::SolutionStored { compute: ElectionCompute::Signed, origin: Some(99), prev_ejected: false }, Event::SolutionStored { compute: ElectionCompute::Signed, origin: Some(99), prev_ejected: false }, Event::SolutionStored { compute: ElectionCompute::Signed, origin: Some(99), prev_ejected: false }, Event::SolutionStored { compute: ElectionCompute::Signed, origin: Some(99), prev_ejected: false }, Event::Slashed { account: 99, value: 5 }, Event::Slashed { account: 99, value: 5 }, Event::Slashed { account: 99, value: 5 }, Event::Slashed { account: 99, value: 5 }, Event::Slashed { account: 99, value: 5 }, Event::ElectionFinalized { compute: ElectionCompute::Fallback, score: ElectionScore { minimal_stake: 0, sum_stake: 0, sum_stake_squared: 0 } }, Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Off, round: 2 }, ] ); }) } #[test] fn check_events_with_compute_signed() { ExtBuilder::default().build_and_execute(|| { roll_to_signed(); assert!(CurrentPhase::::get().is_signed()); let solution = raw_solution(); assert_ok!(MultiPhase::submit( crate::mock::RuntimeOrigin::signed(99), Box::new(solution) )); roll_to(30); assert_ok!(MultiPhase::elect(SINGLE_PAGE)); assert_eq!( multi_phase_events(), vec![ Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }, Event::SolutionStored { compute: ElectionCompute::Signed, origin: Some(99), prev_ejected: false }, Event::Rewarded { account: 99, value: 7 }, Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Unsigned((true, 25)), round: 1 }, Event::ElectionFinalized { compute: ElectionCompute::Signed, score: ElectionScore { minimal_stake: 40, sum_stake: 100, sum_stake_squared: 5200 } }, Event::PhaseTransitioned { from: Phase::Unsigned((true, 25)), to: Phase::Off, round: 2 }, ], ); }) } #[test] fn check_events_with_compute_unsigned() { ExtBuilder::default().build_and_execute(|| { roll_to_unsigned(); assert!(CurrentPhase::::get().is_unsigned()); // ensure we have snapshots in place. assert!(Snapshot::::get().is_some()); assert_eq!(DesiredTargets::::get().unwrap(), 2); // mine seq_phragmen solution with 2 iters. let (solution, witness, _) = MultiPhase::mine_solution().unwrap(); // ensure this solution is valid. assert!(QueuedSolution::::get().is_none()); assert_ok!(MultiPhase::submit_unsigned( crate::mock::RuntimeOrigin::none(), Box::new(solution), witness )); assert!(QueuedSolution::::get().is_some()); assert_ok!(MultiPhase::elect(SINGLE_PAGE)); assert_eq!( multi_phase_events(), vec![ Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }, Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Unsigned((true, 25)), round: 1 }, Event::SolutionStored { compute: ElectionCompute::Unsigned, origin: None, prev_ejected: false }, Event::ElectionFinalized { compute: ElectionCompute::Unsigned, score: ElectionScore { minimal_stake: 40, sum_stake: 100, sum_stake_squared: 5200 } }, Event::PhaseTransitioned { from: Phase::Unsigned((true, 25)), to: Phase::Off, round: 2 }, ], ); }) } #[test] fn try_elect_multi_page_fails() { let prepare_election = || { roll_to_signed(); assert!(Snapshot::::get().is_some()); // submit solution and assert it is queued and ready for elect to be called. let (solution, _, _) = MultiPhase::mine_solution().unwrap(); assert_ok!(MultiPhase::submit( crate::mock::RuntimeOrigin::signed(99), Box::new(solution), )); roll_to(30); assert!(QueuedSolution::::get().is_some()); }; ExtBuilder::default().onchain_fallback(false).build_and_execute(|| { prepare_election(); // single page elect call works as expected. assert_ok!(MultiPhase::elect(SINGLE_PAGE)); }); ExtBuilder::default().onchain_fallback(false).build_and_execute(|| { prepare_election(); // multi page calls will fail with multi-page not supported error. assert_noop!(MultiPhase::elect(SINGLE_PAGE + 1), ElectionError::MultiPageNotSupported); }) } #[test] fn fallback_strategy_works() { ExtBuilder::default().onchain_fallback(true).build_and_execute(|| { roll_to_unsigned(); assert_eq!(CurrentPhase::::get(), Phase::Unsigned((true, 25))); // Zilch solutions thus far, but we get a result. assert!(QueuedSolution::::get().is_none()); let supports = MultiPhase::elect(SINGLE_PAGE).unwrap(); let expected_supports = vec![ (30, Support { total: 40, voters: vec![(2, 5), (4, 5), (30, 30)] }), (40, Support { total: 60, voters: vec![(2, 5), (3, 10), (4, 5), (40, 40)] }), ] .try_into() .unwrap(); assert_eq!(supports, expected_supports); assert_eq!( multi_phase_events(), vec![ Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }, Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Unsigned((true, 25)), round: 1 }, Event::ElectionFinalized { compute: ElectionCompute::Fallback, score: ElectionScore { minimal_stake: 0, sum_stake: 0, sum_stake_squared: 0 } }, Event::PhaseTransitioned { from: Phase::Unsigned((true, 25)), to: Phase::Off, round: 2 }, ] ); }); ExtBuilder::default().onchain_fallback(false).build_and_execute(|| { roll_to_unsigned(); assert_eq!(CurrentPhase::::get(), Phase::Unsigned((true, 25))); // Zilch solutions thus far. assert!(QueuedSolution::::get().is_none()); assert_eq!( MultiPhase::elect(SINGLE_PAGE).unwrap_err(), ElectionError::Fallback("NoFallback.") ); // phase is now emergency. assert_eq!(CurrentPhase::::get(), Phase::Emergency); // snapshot is still there until election finalizes. assert!(Snapshot::::get().is_some()); assert_eq!( multi_phase_events(), vec![ Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }, Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Unsigned((true, 25)), round: 1 }, Event::ElectionFailed, Event::PhaseTransitioned { from: Phase::Unsigned((true, 25)), to: Phase::Emergency, round: 1 }, ] ); }) } #[test] fn governance_fallback_works() { ExtBuilder::default().onchain_fallback(false).build_and_execute(|| { roll_to_unsigned(); assert_eq!(CurrentPhase::::get(), Phase::Unsigned((true, 25))); // Zilch solutions thus far. assert!(QueuedSolution::::get().is_none()); assert_eq!( MultiPhase::elect(SINGLE_PAGE).unwrap_err(), ElectionError::Fallback("NoFallback.") ); // phase is now emergency. assert_eq!(CurrentPhase::::get(), Phase::Emergency); assert!(QueuedSolution::::get().is_none()); assert!(Snapshot::::get().is_some()); // no single account can trigger this assert_noop!( MultiPhase::governance_fallback(RuntimeOrigin::signed(99)), DispatchError::BadOrigin ); // only root can assert_ok!(MultiPhase::governance_fallback(RuntimeOrigin::root())); // something is queued now assert!(QueuedSolution::::get().is_some()); // next election call with fix everything.; assert!(MultiPhase::elect(SINGLE_PAGE).is_ok()); assert_eq!(CurrentPhase::::get(), Phase::Off); assert_eq!( multi_phase_events(), vec![ Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 }, Event::PhaseTransitioned { from: Phase::Signed, to: Phase::Unsigned((true, 25)), round: 1 }, Event::ElectionFailed, Event::PhaseTransitioned { from: Phase::Unsigned((true, 25)), to: Phase::Emergency, round: 1 }, Event::SolutionStored { compute: ElectionCompute::Fallback, origin: None, prev_ejected: false }, Event::ElectionFinalized { compute: ElectionCompute::Fallback, score: Default::default() }, Event::PhaseTransitioned { from: Phase::Emergency, to: Phase::Off, round: 2 }, ] ); }) } #[test] fn snapshot_too_big_truncate() { // but if there are too many voters, we simply truncate them. ExtBuilder::default().build_and_execute(|| { // we have 8 voters in total. assert_eq!(Voters::get().len(), 8); // but we want to take 2. let new_bounds = ElectionBoundsBuilder::default().voters_count(2.into()).build(); ElectionsBounds::set(new_bounds); // Signed phase opens just fine. roll_to_signed(); assert_eq!(CurrentPhase::::get(), Phase::Signed); assert_eq!( SnapshotMetadata::::get().unwrap(), SolutionOrSnapshotSize { voters: 2, targets: 4 } ); }) } #[test] fn untrusted_score_verification_is_respected() { ExtBuilder::default().build_and_execute(|| { roll_to_signed(); assert_eq!(CurrentPhase::::get(), Phase::Signed); // set the solution balancing to get the desired score. crate::mock::Balancing::set(Some(BalancingConfig { iterations: 2, tolerance: 0 })); let (solution, _, _) = MultiPhase::mine_solution().unwrap(); // Default solution's score. assert!(matches!(solution.score, ElectionScore { minimal_stake: 50, .. })); MinimumUntrustedScore::::put(ElectionScore { minimal_stake: 49, ..Default::default() }); assert_ok!(MultiPhase::feasibility_check(solution.clone(), ElectionCompute::Signed)); MinimumUntrustedScore::::put(ElectionScore { minimal_stake: 51, ..Default::default() }); assert_noop!( MultiPhase::feasibility_check(solution, ElectionCompute::Signed), FeasibilityError::UntrustedScoreTooLow, ); }) } #[test] fn number_of_voters_allowed_2sec_block() { // Just a rough estimate with the bizinikiwi weights. assert_eq!(MockWeightInfo::get(), MockedWeightInfo::Real); let all_voters: u32 = 10_000; let all_targets: u32 = 5_000; let desired: u32 = 1_000; let weight_with = |active| { ::WeightInfo::submit_unsigned( all_voters, all_targets, active, desired, ) }; let mut active = 1; while weight_with(active) .all_lte(::BlockWeights::get().max_block) || active == all_voters { active += 1; } println!("can support {} voters to yield a weight of {}", active, weight_with(active)); } }