diff --git a/frame/election-provider-multi-phase/src/lib.rs b/frame/election-provider-multi-phase/src/lib.rs index c4a5e0fa6936a..e460e39025300 100644 --- a/frame/election-provider-multi-phase/src/lib.rs +++ b/frame/election-provider-multi-phase/src/lib.rs @@ -28,29 +28,30 @@ //! on this, a phase is chosen. The timeline is as follows. //! //! ```ignore -//! elect() -//! + <--T::SignedPhase--> + <--T::UnsignedPhase--> + -//! +-------------------------------------------------------------------+ -//! Phase::Off + Phase::Signed + Phase::Unsigned + +//! elect() +//! + <--T::SignedPhase--> + <--T::UnsignedPhase--> + <--T::ChallengePhase--> + +//! +-------------------------------------------------------------------------------------------------+ +//! Phase::Off + Phase::Signed + Phase::Unsigned + Phase::Challenge + //! ``` //! -//! Note that the unsigned phase starts [`pallet::Config::UnsignedPhase`] blocks before the +//! Note that the challenge phase starts [`pallet::Config::ChallengePhase`] 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. +//! no `elect` happens, the challenge phase is extended. //! -//! > Given this, it is rather important for the user of this pallet to ensure it always terminates -//! election via `elect` before requesting a new one. +//! > It is important for the user of this pallet to ensure `elect` is called before a new election +//! > phase begins. //! -//! Each of the phases can be disabled by essentially setting their length to zero. If both phases -//! have length zero, then the pallet essentially runs only the fallback strategy, denoted by +//! Each of the phases can be disabled by setting their length to zero. If all phases have length +//! zero, then the pallet essentially runs only the fallback strategy, configured as //! [`Config::FallbackStrategy`]. +//! //! ### 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 [`pallet::Config::MaxSignedSubmissions`] solutions are stored. The queue is always -//! sorted based on score (worse to best). +//! sorted based on score (worst to best). //! //! Upon arrival of a new solution: //! @@ -64,12 +65,15 @@ //! 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 [`Pallet::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 candidates). 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. +//! At the end of the signed phase, the solutions are examined from best to worst. For each +//! solution: +//! +//! - Compute a (relatively expensive) on-chain [`Pallet::feasibility_check`]. This ensures the +//! score claimed by this score was correct, and it is valid based on the election data (i.e. +//! votes and candidates). +//! - 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: //! @@ -91,7 +95,10 @@ //! Note that both of the bottom solutions end up being discarded and get their deposit back, //! despite one of them being *invalid*. //! -//! ## Unsigned Phase +//! At the end of this phase, the only item remaining in storage is the best feasible solution, if +//! one was computed. Otherwise, there is nothing. +//! +//! ### 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 @@ -101,12 +108,43 @@ //! //! Validators will only submit solutions if the one that they have computed is sufficiently better //! than the best queued one (see [`pallet::Config::SolutionImprovementThreshold`]) and will limit -//! the weigh of the solution to [`pallet::Config::MinerMaxWeight`]. +//! the weight of the solution to [`pallet::Config::MinerMaxWeight`]. //! -//! The unsigned phase can be made passive depending on how the previous signed phase went, by +//! The unsigned phase can be skipped 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. //! +//! At the end of this phase, the only item remaining in storage is the best feasible solution, if +//! one was computed. If the validator mined a better solution than was provided in the signed phase, +//! then the signed solution is replaced. +//! +//! ### Challenge Phase +//! +//! The challenge phase is designed to ensure that only solutions which exceed an absolute quality +//! floor are accepted. During this phase, miners can download the best feasible solution and +//! subject it to a (very expensive) offchain PJR check. In the event that this check fails, they +//! will discover a counterexample. The counterexample is a small piece of data which can be used to +//! cheaply prove that the solution does not satisfy PJR. They can then submit a transaction +//! containing this counterexample. One of a few things can happen then: +//! +//! - The counterexample correctly disproves the solution: solution author slashed, solution +//! discarded, miner rewarded. We must then fall back to an on-chain election. +//! - The counterexample correctly disproves the solution but with a severity below some threshold: +//! miner receives minor reward, but solution is retained +//! - The counterexample fails to disprove the solution: miner slashed +//! +//! It's worth noting how PJR severity is defined. The PJR property is defined in terms of a +//! threshold. This threshold is an arbitrary positive number; the PJR property guarantees only that +//! the property is stronger when the threshold is smaller. There exists a standard threshold, which +//! can be computed from the voting solution. +//! +//! For a counterexample to cause the solution author to be slashed, it must cause the PJR check to +//! fail at the standard threshold. For a counterexample to give the challenge miner a reward but +//! cause no slash, it must cause the cause the PJR check to fail at threshold _t'_. The exact ratio +//! is set by governance, but by default, `t' = t * 1.1`. +//! +//! > TODO: link to PJR docs +//! //! ### Fallback //! //! If we reach the end of both phases (i.e. call to [`ElectionProvider::elect`] happens) and no @@ -138,8 +176,8 @@ //! //! ## Error types //! -//! This pallet provides a verbose error system to ease future debugging and debugging. The -//! overall hierarchy of errors is as follows: +//! This pallet provides a verbose error system to ease future debugging and debugging. The overall +//! hierarchy of errors is as follows: //! //! 1. [`pallet::Error`]: These are the errors that can be returned in the dispatchables of the //! pallet, either signed or unsigned. Since decomposition with nested enums is not possible @@ -156,16 +194,6 @@ //! //! ## Future Plans //! -//! **Challenge Phase**. We plan adding a third phase to the pallet, called the challenge phase. -//! This is 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 challenged: -//! -//! 1. We must surely slash whoever submitted that solution (might be a challenge for unsigned -//! solutions). -//! 2. It is probably fine to fallback to the on-chain election, as we expect this to happen rarely. -//! //! **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