Bound Election and Staking by MaxActiveValidators (#12436)

* bounding election provider with kian

* multi phase implement bounded election provider

* election provider blanket implementation

* staking compiles

* fix test for election provider support

* fmt

* fixing epmp tests, does not compile yet

* fix epmp tests

* fix staking tests

* fmt

* fix runtime tests

* fmt

* remove outdated wip tags

* add enum error

* sort and truncate supports

* comment

* error when unsupported number of election winners

* compiling wip after kian's suggestions

* fix TODOs

* remove,fix tags

* ensure validator count does not exceed maxwinners

* clean up

* some more clean up and todos

* handle too many winners

* rename parameter for mock

* todo

* add sort and truncate rule if there are too many winners

* fmt

* fail, not swallow emergency result bound not met

* remove too many winners resolution as it can be guaranteed to be bounded

* fix benchmark

* give MaxWinners more contextual name

* make ready solution generic over T

* kian feedback

* fix stuff

* Kian's way of solvign this

* comment fix

* fix compile

* remove use of BoundedExecution

* fmt

* comment out failing integrity test

* cap validator count increment to max winners

* dont panic

* add test for bad data provider

* Update frame/staking/src/pallet/impls.rs

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* fix namespace conflict and add test for onchain max winners less than desired targets

* defensive unwrap

* early convert to bounded vec

* fix syntax

* fmt

* fix doc

* fix rustdoc

* fmt

* fix maxwinner count for benchmarking

* add instant election for noelection

* fmt

* fix compile

* pr feedbacks

* always error at validator count exceeding max winners

* add useful error message

* pr comments

* import fix

* add checked_desired_targets

* fmt

* fmt

* fix rust doc

Co-authored-by: parity-processbot <>
Co-authored-by: kianenigma <kian@parity.io>
Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
This commit is contained in:
Ankan
2022-11-09 10:11:51 +01:00
committed by GitHub
parent 535c6f2e94
commit 657d99202c
21 changed files with 544 additions and 318 deletions
@@ -114,8 +114,8 @@
//! 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 [`pallet::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. [`NoFallback`] does nothing and enables [`Phase::Emergency`], which
//! is a more *fail-safe* approach.
//! reduction post-processing. If [`pallet::Config::Fallback`] fails, the next phase
//! [`Phase::Emergency`] is enabled, which is a more *fail-safe* approach.
//!
//! ### Emergency Phase
//!
@@ -231,23 +231,25 @@
use codec::{Decode, Encode};
use frame_election_provider_support::{
ElectionDataProvider, ElectionProvider, ElectionProviderBase, InstantElectionProvider,
NposSolution,
BoundedSupportsOf, ElectionDataProvider, ElectionProvider, ElectionProviderBase,
InstantElectionProvider, NposSolution,
};
use frame_support::{
dispatch::DispatchClass,
ensure,
traits::{Currency, Get, OnUnbalanced, ReservableCurrency},
weights::Weight,
DefaultNoBound, EqNoBound, PartialEqNoBound,
};
use frame_system::{ensure_none, offchain::SendTransactionTypes};
use scale_info::TypeInfo;
use sp_arithmetic::{
traits::{Bounded, CheckedAdd, Zero},
traits::{CheckedAdd, Zero},
UpperOf,
};
use sp_npos_elections::{
assignment_ratio_to_staked_normalized, ElectionScore, EvaluateSupport, Supports, VoteWeight,
assignment_ratio_to_staked_normalized, BoundedSupports, ElectionScore, EvaluateSupport,
Supports, VoteWeight,
};
use sp_runtime::{
transaction_validity::{
@@ -311,33 +313,6 @@ pub trait BenchmarkingConfig {
const MAXIMUM_TARGETS: u32;
}
/// A fallback implementation that transitions the pallet to the emergency phase.
pub struct NoFallback<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> ElectionProviderBase for NoFallback<T> {
type AccountId = T::AccountId;
type BlockNumber = T::BlockNumber;
type DataProvider = T::DataProvider;
type Error = &'static str;
fn ongoing() -> bool {
false
}
}
impl<T: Config> ElectionProvider for NoFallback<T> {
fn elect() -> Result<Supports<T::AccountId>, Self::Error> {
// Do nothing, this will enable the emergency phase.
Err("NoFallback.")
}
}
impl<T: Config> InstantElectionProvider for NoFallback<T> {
fn elect_with_bounds(_: usize, _: usize) -> Result<Supports<T::AccountId>, Self::Error> {
Err("NoFallback.")
}
}
/// Current phase of the pallet.
#[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, Debug, TypeInfo)]
pub enum Phase<Bn> {
@@ -445,13 +420,23 @@ impl<C: Default> Default for RawSolution<C> {
}
/// A checked solution, ready to be enacted.
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, Default, TypeInfo)]
pub struct ReadySolution<A> {
#[derive(
PartialEqNoBound,
EqNoBound,
Clone,
Encode,
Decode,
RuntimeDebug,
DefaultNoBound,
scale_info::TypeInfo,
)]
#[scale_info(skip_type_params(T))]
pub struct ReadySolution<T: Config> {
/// The final supports of the solution.
///
/// This is target-major vector, storing each winners, total backing, and each individual
/// backer.
pub supports: Supports<A>,
pub supports: BoundedSupports<T::AccountId, T::MaxWinners>,
/// The score of the solution.
///
/// This is needed to potentially challenge the solution.
@@ -465,7 +450,6 @@ pub struct ReadySolution<A> {
///
/// These are stored together because they are often accessed together.
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, Default, TypeInfo)]
#[codec(mel_bound())]
#[scale_info(skip_type_params(T))]
pub struct RoundSnapshot<T: Config> {
/// All of the voters.
@@ -561,6 +545,8 @@ pub enum FeasibilityError {
InvalidRound,
/// Comparison against `MinimumUntrustedScore` failed.
UntrustedScoreTooLow,
/// Data Provider returned too many desired targets
TooManyDesiredTargets,
}
impl From<sp_npos_elections::Error> for FeasibilityError {
@@ -574,7 +560,10 @@ pub use pallet::*;
pub mod pallet {
use super::*;
use frame_election_provider_support::{InstantElectionProvider, NposSolver};
use frame_support::{pallet_prelude::*, traits::EstimateCallFee};
use frame_support::{
pallet_prelude::*,
traits::{DefensiveResult, EstimateCallFee},
};
use frame_system::pallet_prelude::*;
#[pallet::config]
@@ -674,6 +663,13 @@ pub mod pallet {
#[pallet::constant]
type MaxElectableTargets: Get<SolutionTargetIndexOf<Self::MinerConfig>>;
/// The maximum number of winners that can be elected by this `ElectionProvider`
/// implementation.
///
/// Note: This must always be greater or equal to `T::DataProvider::desired_targets()`.
#[pallet::constant]
type MaxWinners: Get<u32>;
/// Handler for the slashed deposits.
type SlashHandler: OnUnbalanced<NegativeImbalanceOf<Self>>;
@@ -691,6 +687,7 @@ pub mod pallet {
AccountId = Self::AccountId,
BlockNumber = Self::BlockNumber,
DataProvider = Self::DataProvider,
MaxWinners = Self::MaxWinners,
>;
/// Configuration of the governance-only fallback.
@@ -701,6 +698,7 @@ pub mod pallet {
AccountId = Self::AccountId,
BlockNumber = Self::BlockNumber,
DataProvider = Self::DataProvider,
MaxWinners = Self::MaxWinners,
>;
/// OCW election solution miner algorithm implementation.
@@ -968,9 +966,11 @@ pub mod pallet {
T::ForceOrigin::ensure_origin(origin)?;
ensure!(Self::current_phase().is_emergency(), <Error<T>>::CallNotAllowed);
// bound supports with T::MaxWinners
let supports = supports.try_into().map_err(|_| Error::<T>::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(),
@@ -1073,17 +1073,20 @@ pub mod pallet {
T::ForceOrigin::ensure_origin(origin)?;
ensure!(Self::current_phase().is_emergency(), <Error<T>>::CallNotAllowed);
let maybe_max_voters = maybe_max_voters.map(|x| x as usize);
let maybe_max_targets = maybe_max_targets.map(|x| x as usize);
let supports =
T::GovernanceFallback::instant_elect(maybe_max_voters, maybe_max_targets).map_err(
|e| {
log!(error, "GovernanceFallback failed: {:?}", e);
Error::<T>::FallbackFailed
},
)?;
let supports = T::GovernanceFallback::elect_with_bounds(
maybe_max_voters.unwrap_or(Bounded::max_value()),
maybe_max_targets.unwrap_or(Bounded::max_value()),
)
.map_err(|e| {
log!(error, "GovernanceFallback failed: {:?}", e);
Error::<T>::FallbackFailed
})?;
// transform BoundedVec<_, T::GovernanceFallback::MaxWinners> into
// `BoundedVec<_, T::MaxWinners>`
let supports: BoundedVec<_, T::MaxWinners> = supports
.into_inner()
.try_into()
.defensive_map_err(|_| Error::<T>::BoundNotMet)?;
let solution = ReadySolution {
supports,
@@ -1154,6 +1157,10 @@ pub mod pallet {
CallNotAllowed,
/// The fallback failed
FallbackFailed,
/// Some bound not met
BoundNotMet,
/// Submitted solution has too many winners
TooManyWinners,
}
#[pallet::validate_unsigned]
@@ -1227,7 +1234,7 @@ pub mod pallet {
/// Current best solution, signed or unsigned, queued to be returned upon `elect`.
#[pallet::storage]
#[pallet::getter(fn queued_solution)]
pub type QueuedSolution<T: Config> = StorageValue<_, ReadySolution<T::AccountId>>;
pub type QueuedSolution<T: Config> = StorageValue<_, ReadySolution<T>>;
/// Snapshot data of the round.
///
@@ -1393,31 +1400,28 @@ impl<T: Config> Pallet<T> {
let targets = T::DataProvider::electable_targets(Some(target_limit))
.map_err(ElectionError::DataProvider)?;
let voters = T::DataProvider::electing_voters(Some(voter_limit))
.map_err(ElectionError::DataProvider)?;
let mut desired_targets =
T::DataProvider::desired_targets().map_err(ElectionError::DataProvider)?;
// Defensive-only.
if targets.len() > target_limit || voters.len() > voter_limit {
debug_assert!(false, "Snapshot limit has not been respected.");
return Err(ElectionError::DataProvider("Snapshot too big for submission."))
}
// If `desired_targets` > `targets.len()`, cap `desired_targets` to that level and emit a
// warning
let max_len = targets
.len()
.try_into()
.map_err(|_| ElectionError::DataProvider("Failed to convert usize"))?;
if desired_targets > max_len {
let mut desired_targets =
T::DataProvider::desired_targets().map_err(ElectionError::DataProvider)?;
// If `desired_targets` > `targets.len()`, cap `desired_targets` to that
// level and emit a warning
let max_desired_targets: u32 = (targets.len() as u32).min(T::MaxWinners::get());
if desired_targets > max_desired_targets {
log!(
warn,
"desired_targets: {} > targets.len(): {}, capping desired_targets",
desired_targets,
max_len
max_desired_targets
);
desired_targets = max_len;
desired_targets = max_desired_targets;
}
Ok((targets, voters, desired_targets))
@@ -1466,7 +1470,7 @@ impl<T: Config> Pallet<T> {
pub fn feasibility_check(
raw_solution: RawSolution<SolutionOf<T::MinerConfig>>,
compute: ElectionCompute,
) -> Result<ReadySolution<T::AccountId>, FeasibilityError> {
) -> Result<ReadySolution<T>, FeasibilityError> {
let RawSolution { solution, score, round } = raw_solution;
// First, check round.
@@ -1479,6 +1483,11 @@ impl<T: Config> Pallet<T> {
Self::desired_targets().ok_or(FeasibilityError::SnapshotUnavailable)?;
ensure!(winners.len() as u32 == desired_targets, FeasibilityError::WrongWinnerCount);
// Fail early if targets requested by data provider exceed maximum winners supported.
ensure!(
desired_targets <= <T as pallet::Config>::MaxWinners::get(),
FeasibilityError::TooManyDesiredTargets
);
// Ensure that the solution's score can pass absolute min-score.
let submitted_score = raw_solution.score;
@@ -1539,6 +1548,8 @@ impl<T: Config> Pallet<T> {
let known_score = supports.evaluate();
ensure!(known_score == score, FeasibilityError::InvalidScore);
// Size of winners in miner solution is equal to `desired_targets` <= `MaxWinners`.
let supports = supports.try_into().expect("checked desired_targets <= MaxWinners; qed");
Ok(ReadySolution { supports, compute, score })
}
@@ -1558,7 +1569,7 @@ impl<T: Config> Pallet<T> {
Self::kill_snapshot();
}
fn do_elect() -> Result<Supports<T::AccountId>, ElectionError<T>> {
fn do_elect() -> Result<BoundedSupportsOf<Self>, ElectionError<T>> {
// We have to unconditionally try finalizing the signed phase here. There are only two
// possibilities:
//
@@ -1570,13 +1581,15 @@ impl<T: Config> Pallet<T> {
<QueuedSolution<T>>::take()
.ok_or(ElectionError::<T>::NothingQueued)
.or_else(|_| {
<T::Fallback as ElectionProvider>::elect()
.map(|supports| ReadySolution {
supports,
score: Default::default(),
compute: ElectionCompute::Fallback,
})
T::Fallback::instant_elect(None, None)
.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 });
@@ -1609,18 +1622,19 @@ impl<T: Config> ElectionProviderBase for Pallet<T> {
type AccountId = T::AccountId;
type BlockNumber = T::BlockNumber;
type Error = ElectionError<T>;
type MaxWinners = T::MaxWinners;
type DataProvider = T::DataProvider;
}
impl<T: Config> ElectionProvider for Pallet<T> {
fn ongoing() -> bool {
match Self::current_phase() {
Phase::Off => false,
_ => true,
}
}
}
impl<T: Config> ElectionProvider for Pallet<T> {
fn elect() -> Result<Supports<T::AccountId>, Self::Error> {
fn elect() -> Result<BoundedSupportsOf<Self>, Self::Error> {
match Self::do_elect() {
Ok(supports) => {
// All went okay, record the weight, put sign to be Off, clean snapshot, etc.
@@ -1636,6 +1650,7 @@ impl<T: Config> ElectionProvider for Pallet<T> {
}
}
}
/// convert a DispatchError to a custom InvalidTransaction with the inner code being the error
/// number.
pub fn dispatch_error_to_invalid(error: DispatchError) -> InvalidTransaction {
@@ -1854,7 +1869,6 @@ mod tests {
},
Phase,
};
use frame_election_provider_support::ElectionProvider;
use frame_support::{assert_noop, assert_ok};
use sp_npos_elections::{BalancingConfig, Support};