Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 60 additions & 32 deletions frame/election-provider-multi-phase/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
//!
Expand All @@ -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:
//!
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down