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
@@ -220,11 +220,7 @@ frame_benchmarking::benchmarks! {
let receiver = account("receiver", 0, SEED);
let initial_balance = T::Currency::minimum_balance() * 10u32.into();
T::Currency::make_free_balance_be(&receiver, initial_balance);
let ready = ReadySolution {
supports: vec![],
score: Default::default(),
compute: Default::default()
};
let ready = Default::default();
let deposit: BalanceOf<T> = 10u32.into();
let reward: BalanceOf<T> = T::SignedRewardBase::get();
@@ -403,7 +399,7 @@ frame_benchmarking::benchmarks! {
assert_eq!(raw_solution.solution.voter_count() as u32, a);
assert_eq!(raw_solution.solution.unique_targets().len() as u32, d);
}: {
assert_ok!(<MultiPhase<T>>::feasibility_check(raw_solution, ElectionCompute::Unsigned));
assert!(<MultiPhase<T>>::feasibility_check(raw_solution, ElectionCompute::Unsigned).is_ok());
}
// NOTE: this weight is not used anywhere, but the fact that it should succeed when execution in
@@ -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};
@@ -19,7 +19,7 @@ use super::*;
use crate::{self as multi_phase, unsigned::MinerConfig};
use frame_election_provider_support::{
data_provider,
onchain::{self, UnboundedExecution},
onchain::{self},
ElectionDataProvider, NposSolution, SequentialPhragmen,
};
pub use frame_support::{assert_noop, assert_ok, pallet_prelude::GetDefault};
@@ -297,6 +297,7 @@ parameter_types! {
pub static MockWeightInfo: MockedWeightInfo = MockedWeightInfo::Real;
pub static MaxElectingVoters: VoterIndex = u32::max_value();
pub static MaxElectableTargets: TargetIndex = TargetIndex::max_value();
pub static MaxWinners: u32 = 200;
pub static EpochLength: u64 = 30;
pub static OnChainFallback: bool = true;
@@ -308,6 +309,9 @@ impl onchain::Config for OnChainSeqPhragmen {
type Solver = SequentialPhragmen<AccountId, SolutionAccuracyOf<Runtime>, Balancing>;
type DataProvider = StakingMock;
type WeightInfo = ();
type MaxWinners = MaxWinners;
type VotersBound = ConstU32<{ u32::MAX }>;
type TargetsBound = ConstU32<{ u32::MAX }>;
}
pub struct MockFallback;
@@ -316,30 +320,19 @@ impl ElectionProviderBase for MockFallback {
type BlockNumber = u64;
type Error = &'static str;
type DataProvider = StakingMock;
fn ongoing() -> bool {
false
}
}
impl ElectionProvider for MockFallback {
fn elect() -> Result<Supports<AccountId>, Self::Error> {
Self::elect_with_bounds(Bounded::max_value(), Bounded::max_value())
}
type MaxWinners = MaxWinners;
}
impl InstantElectionProvider for MockFallback {
fn elect_with_bounds(
max_voters: usize,
max_targets: usize,
) -> Result<Supports<Self::AccountId>, Self::Error> {
fn instant_elect(
max_voters: Option<u32>,
max_targets: Option<u32>,
) -> Result<BoundedSupportsOf<Self>, Self::Error> {
if OnChainFallback::get() {
onchain::UnboundedExecution::<OnChainSeqPhragmen>::elect_with_bounds(
max_voters,
max_targets,
)
.map_err(|_| "onchain::UnboundedExecution failed.")
onchain::OnChainExecution::<OnChainSeqPhragmen>::instant_elect(max_voters, max_targets)
.map_err(|_| "onchain::OnChainExecution failed.")
} else {
super::NoFallback::<Runtime>::elect_with_bounds(max_voters, max_targets)
Err("NoFallback.")
}
}
}
@@ -404,10 +397,12 @@ impl crate::Config for Runtime {
type WeightInfo = ();
type BenchmarkingConfig = TestBenchmarkingConfig;
type Fallback = MockFallback;
type GovernanceFallback = UnboundedExecution<OnChainSeqPhragmen>;
type GovernanceFallback =
frame_election_provider_support::onchain::OnChainExecution<OnChainSeqPhragmen>;
type ForceOrigin = frame_system::EnsureRoot<AccountId>;
type MaxElectingVoters = MaxElectingVoters;
type MaxElectableTargets = MaxElectableTargets;
type MaxWinners = MaxWinners;
type MinerConfig = Self;
type Solver = SequentialPhragmen<AccountId, SolutionAccuracyOf<Runtime>, Balancing>;
}
@@ -424,6 +419,8 @@ pub type Extrinsic = sp_runtime::testing::TestXt<RuntimeCall, ()>;
parameter_types! {
pub MaxNominations: u32 = <TestNposSolution as NposSolution>::LIMIT as u32;
// only used in testing to manipulate mock behaviour
pub static DataProviderAllowBadData: bool = false;
}
#[derive(Default)]
@@ -438,7 +435,9 @@ impl ElectionDataProvider for StakingMock {
fn electable_targets(maybe_max_len: Option<usize>) -> data_provider::Result<Vec<AccountId>> {
let targets = Targets::get();
if maybe_max_len.map_or(false, |max_len| targets.len() > max_len) {
if !DataProviderAllowBadData::get() &&
maybe_max_len.map_or(false, |max_len| targets.len() > max_len)
{
return Err("Targets too big")
}
@@ -449,8 +448,10 @@ impl ElectionDataProvider for StakingMock {
maybe_max_len: Option<usize>,
) -> data_provider::Result<Vec<VoterOf<Runtime>>> {
let mut voters = Voters::get();
if let Some(max_len) = maybe_max_len {
voters.truncate(max_len)
if !DataProviderAllowBadData::get() {
if let Some(max_len) = maybe_max_len {
voters.truncate(max_len)
}
}
Ok(voters)
@@ -462,7 +462,7 @@ impl<T: Config> Pallet<T> {
///
/// Infallible
pub fn finalize_signed_phase_accept_solution(
ready_solution: ReadySolution<T::AccountId>,
ready_solution: ReadySolution<T>,
who: &T::AccountId,
deposit: BalanceOf<T>,
call_fee: BalanceOf<T>,
@@ -537,7 +537,7 @@ impl<T: Config> Pallet<T> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{mock::*, ElectionCompute, Error, Event, Perbill, Phase};
use crate::{mock::*, ElectionCompute, ElectionError, Error, Event, Perbill, Phase};
use frame_support::{assert_noop, assert_ok, assert_storage_noop};
#[test]
@@ -557,6 +557,50 @@ mod tests {
})
}
#[test]
fn data_provider_should_respect_target_limits() {
ExtBuilder::default().build_and_execute(|| {
// given a reduced expectation of maximum electable targets
MaxElectableTargets::set(2);
// and a data provider that does not respect limits
DataProviderAllowBadData::set(true);
assert_noop!(
MultiPhase::create_snapshot(),
ElectionError::DataProvider("Snapshot too big for submission."),
);
})
}
#[test]
fn data_provider_should_respect_voter_limits() {
ExtBuilder::default().build_and_execute(|| {
// given a reduced expectation of maximum electing voters
MaxElectingVoters::set(2);
// and a data provider that does not respect limits
DataProviderAllowBadData::set(true);
assert_noop!(
MultiPhase::create_snapshot(),
ElectionError::DataProvider("Snapshot too big for submission."),
);
})
}
#[test]
fn desired_targets_greater_than_max_winners() {
ExtBuilder::default().build_and_execute(|| {
// given desired_targets bigger than MaxWinners
DesiredTargets::set(4);
MaxWinners::set(3);
let (_, _, actual_desired_targets) = MultiPhase::create_snapshot_external().unwrap();
// snapshot is created with min of desired_targets and MaxWinners
assert_eq!(actual_desired_targets, 3);
})
}
#[test]
fn should_pay_deposit() {
ExtBuilder::default().build_and_execute(|| {