Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

frame epm: expose feasibility_check in MinerConfig #13555

Merged
merged 3 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,7 @@ impl pallet_election_provider_multi_phase::MinerConfig for Runtime {
type Solution = NposSolution16;
type MaxVotesPerVoter =
<<Self as pallet_election_provider_multi_phase::Config>::DataProvider as ElectionDataProvider>::MaxVotesPerVoter;
type MaxWinners = MaxActiveValidators;

// The unsigned submissions have to respect the weight of the submit_unsigned call, thus their
// weight estimate function is wired to this call's weight.
Expand Down
127 changes: 34 additions & 93 deletions frame/election-provider-multi-phase/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,7 @@ use sp_arithmetic::{
traits::{CheckedAdd, Zero},
UpperOf,
};
use sp_npos_elections::{
assignment_ratio_to_staked_normalized, BoundedSupports, ElectionScore, EvaluateSupport,
Supports, VoteWeight,
};
use sp_npos_elections::{BoundedSupports, ElectionScore, IdentifierT, Supports, VoteWeight};
use sp_runtime::{
transaction_validity::{
InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
Expand Down Expand Up @@ -430,13 +427,17 @@ impl<C: Default> Default for RawSolution<C> {
DefaultNoBound,
scale_info::TypeInfo,
)]
#[scale_info(skip_type_params(T))]
pub struct ReadySolution<T: Config> {
#[scale_info(skip_type_params(AccountId, MaxWinners))]
pub struct ReadySolution<AccountId, MaxWinners>
where
AccountId: IdentifierT,
MaxWinners: Get<u32>,
{
/// The final supports of the solution.
///
/// This is target-major vector, storing each winners, total backing, and each individual
/// backer.
pub supports: BoundedSupports<T::AccountId, T::MaxWinners>,
pub supports: BoundedSupports<AccountId, MaxWinners>,
/// The score of the solution.
///
/// This is needed to potentially challenge the solution.
Expand All @@ -451,11 +452,11 @@ pub struct ReadySolution<T: Config> {
/// 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<T: Config> {
pub struct RoundSnapshot<AccountId, DataProvider> {
/// All of the voters.
pub voters: Vec<VoterOf<T>>,
pub voters: Vec<DataProvider>,
/// All of the targets.
pub targets: Vec<T::AccountId>,
pub targets: Vec<AccountId>,
}

/// Encodes the length of a solution or a snapshot.
Expand Down Expand Up @@ -614,6 +615,7 @@ pub mod pallet {
type MinerConfig: crate::unsigned::MinerConfig<
AccountId = Self::AccountId,
MaxVotesPerVoter = <Self::DataProvider as ElectionDataProvider>::MaxVotesPerVoter,
MaxWinners = Self::MaxWinners,
>;

/// Maximum number of signed submissions that can be queued.
Expand Down Expand Up @@ -733,6 +735,11 @@ pub mod pallet {
fn max_votes_per_voter() -> u32 {
<T::MinerConfig as MinerConfig>::MaxVotesPerVoter::get()
}

#[pallet::constant_name(MinerMaxWinners)]
fn max_winners() -> u32 {
<T::MinerConfig as MinerConfig>::MaxWinners::get()
}
}

#[pallet::hooks]
Expand Down Expand Up @@ -1247,14 +1254,15 @@ 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>>;
pub type QueuedSolution<T: Config> =
StorageValue<_, ReadySolution<T::AccountId, T::MaxWinners>>;

/// Snapshot data of the round.
///
/// This is created at the beginning of the signed phase and cleared upon calling `elect`.
#[pallet::storage]
#[pallet::getter(fn snapshot)]
pub type Snapshot<T: Config> = StorageValue<_, RoundSnapshot<T>>;
pub type Snapshot<T: Config> = StorageValue<_, RoundSnapshot<T::AccountId, VoterOf<T>>>;

/// Desired number of targets to elect for this round.
///
Expand Down Expand Up @@ -1385,7 +1393,7 @@ impl<T: Config> Pallet<T> {
// 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::<T> { voters, targets };
let snapshot = RoundSnapshot::<T::AccountId, VoterOf<T>> { voters, targets };
let size = snapshot.encoded_size();
log!(debug, "snapshot pre-calculated size {:?}", size);
let mut buffer = Vec::with_capacity(size);
Expand Down Expand Up @@ -1479,89 +1487,22 @@ impl<T: Config> Pallet<T> {
pub fn feasibility_check(
raw_solution: RawSolution<SolutionOf<T::MinerConfig>>,
compute: ElectionCompute,
) -> Result<ReadySolution<T>, FeasibilityError> {
let RawSolution { solution, score, round } = raw_solution;

// First, check round.
ensure!(Self::round() == round, FeasibilityError::InvalidRound);

// Winners are not directly encoded in the solution.
let winners = solution.unique_targets();

) -> Result<ReadySolution<T::AccountId, T::MaxWinners>, FeasibilityError> {
let desired_targets =
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;
ensure!(
Self::minimum_untrusted_score().map_or(true, |min_score| {
submitted_score.strict_threshold_better(min_score, Perbill::zero())
}),
FeasibilityError::UntrustedScoreTooLow
);

// Read the entire snapshot.
let RoundSnapshot { voters: snapshot_voters, targets: snapshot_targets } =
Self::snapshot().ok_or(FeasibilityError::SnapshotUnavailable)?;

// ----- Start building. First, we need some closures.
let cache = helpers::generate_voter_cache::<T::MinerConfig>(&snapshot_voters);
let voter_at = helpers::voter_at_fn::<T::MinerConfig>(&snapshot_voters);
let target_at = helpers::target_at_fn::<T::MinerConfig>(&snapshot_targets);
let voter_index = helpers::voter_index_fn_usize::<T::MinerConfig>(&cache);

// Then convert solution -> assignment. This will fail if any of the indices are gibberish,
// namely any of the voters or targets.
let assignments = solution
.into_assignment(voter_at, target_at)
.map_err::<FeasibilityError, _>(Into::into)?;

// Ensure that assignments is correct.
let _ = assignments.iter().try_for_each(|assignment| {
// Check that assignment.who is actually a voter (defensive-only).
// NOTE: while using the index map from `voter_index` is better than a blind linear
// search, this *still* has room for optimization. Note that we had the index when
// we did `solution -> assignment` and we lost it. Ideal is to keep the index
// around.

// Defensive-only: must exist in the snapshot.
let snapshot_index =
voter_index(&assignment.who).ok_or(FeasibilityError::InvalidVoter)?;
// Defensive-only: index comes from the snapshot, must exist.
let (_voter, _stake, targets) =
snapshot_voters.get(snapshot_index).ok_or(FeasibilityError::InvalidVoter)?;

// Check that all of the targets are valid based on the snapshot.
if assignment.distribution.iter().any(|(d, _)| !targets.contains(d)) {
return Err(FeasibilityError::InvalidVote)
}
Ok(())
})?;

// ----- Start building support. First, we need one more closure.
let stake_of = helpers::stake_of_fn::<T::MinerConfig>(&snapshot_voters, &cache);

// This might fail if the normalization fails. Very unlikely. See `integrity_test`.
let staked_assignments = assignment_ratio_to_staked_normalized(assignments, stake_of)
.map_err::<FeasibilityError, _>(Into::into)?;
let supports = sp_npos_elections::to_supports(&staked_assignments);

// Finally, check that the claimed score was indeed correct.
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()
.defensive_map_err(|_| FeasibilityError::BoundedConversionFailed)?;
Ok(ReadySolution { supports, compute, score })
let snapshot = Self::snapshot().ok_or(FeasibilityError::SnapshotUnavailable)?;
let round = Self::round();
let minimum_untrusted_score = Self::minimum_untrusted_score();

Miner::<T::MinerConfig>::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:
Expand Down
3 changes: 3 additions & 0 deletions frame/election-provider-multi-phase/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ parameter_types! {
pub static MockWeightInfo: MockedWeightInfo = MockedWeightInfo::Real;
pub static MaxElectingVoters: VoterIndex = u32::max_value();
pub static MaxElectableTargets: TargetIndex = TargetIndex::max_value();

#[derive(Debug)]
Copy link
Member Author

@niklasad1 niklasad1 Mar 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was forced to add this Debug bound I suspect it's because rustc resolved it before as this was part of the Config trait.

Only relevant for tests anyway so should be a big deal

pub static MaxWinners: u32 = 200;

pub static EpochLength: u64 = 30;
Expand Down Expand Up @@ -359,6 +361,7 @@ impl MinerConfig for Runtime {
type MaxLength = MinerMaxLength;
type MaxWeight = MinerMaxWeight;
type MaxVotesPerVoter = <StakingMock as ElectionDataProvider>::MaxVotesPerVoter;
type MaxWinners = MaxWinners;
type Solution = TestNposSolution;

fn solution_weight(v: u32, t: u32, a: u32, d: u32) -> Weight {
Expand Down
2 changes: 1 addition & 1 deletion frame/election-provider-multi-phase/src/signed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ impl<T: Config> Pallet<T> {
///
/// Infallible
pub fn finalize_signed_phase_accept_solution(
ready_solution: ReadySolution<T>,
ready_solution: ReadySolution<T::AccountId, T::MaxWinners>,
who: &T::AccountId,
deposit: BalanceOf<T>,
call_fee: BalanceOf<T>,
Expand Down
98 changes: 95 additions & 3 deletions frame/election-provider-multi-phase/src/unsigned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,17 @@ use crate::{
};
use codec::Encode;
use frame_election_provider_support::{NposSolution, NposSolver, PerThing128, VoteWeight};
use frame_support::{dispatch::DispatchResult, ensure, traits::Get, BoundedVec};
use frame_support::{
dispatch::DispatchResult,
ensure,
traits::{DefensiveResult, Get},
BoundedVec,
};
use frame_system::offchain::SubmitTransaction;
use scale_info::TypeInfo;
use sp_npos_elections::{
assignment_ratio_to_staked_normalized, assignment_staked_to_ratio_normalized, ElectionResult,
ElectionScore,
ElectionScore, EvaluateSupport,
};
use sp_runtime::{
offchain::storage::{MutateStorageError, StorageValueRef},
Expand Down Expand Up @@ -351,7 +356,7 @@ impl<T: Config> Pallet<T> {

// ensure score is being improved. Panic henceforth.
ensure!(
Self::queued_solution().map_or(true, |q: ReadySolution<_>| raw_solution
Self::queued_solution().map_or(true, |q: ReadySolution<_, _>| raw_solution
.score
.strict_threshold_better(q.score, T::BetterUnsignedThreshold::get())),
Error::<T>::PreDispatchWeakSubmission,
Expand Down Expand Up @@ -387,6 +392,8 @@ pub trait MinerConfig {
///
/// The weight is computed using `solution_weight`.
type MaxWeight: Get<Weight>;
/// The maximum number of winners that can be elected.
type MaxWinners: Get<u32>;
/// Something that can compute the weight of a solution.
///
/// This weight estimate is then used to trim the solution, based on [`MinerConfig::MaxWeight`].
Expand Down Expand Up @@ -689,6 +696,91 @@ impl<T: MinerConfig> Miner<T> {
);
final_decision
}

/// Checks the feasibility of a solution.
pub fn feasibility_check(
raw_solution: RawSolution<SolutionOf<T>>,
compute: ElectionCompute,
desired_targets: u32,
snapshot: RoundSnapshot<T::AccountId, MinerVoterOf<T>>,
current_round: u32,
minimum_untrusted_score: Option<ElectionScore>,
) -> Result<ReadySolution<T::AccountId, T::MaxWinners>, FeasibilityError> {
let RawSolution { solution, score, round } = raw_solution;
let RoundSnapshot { voters: snapshot_voters, targets: snapshot_targets } = snapshot;

// First, check round.
ensure!(current_round == round, FeasibilityError::InvalidRound);

// Winners are not directly encoded in the solution.
let winners = solution.unique_targets();

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::MaxWinners::get(), FeasibilityError::TooManyDesiredTargets);

// Ensure that the solution's score can pass absolute min-score.
let submitted_score = raw_solution.score;
ensure!(
minimum_untrusted_score.map_or(true, |min_score| {
submitted_score.strict_threshold_better(min_score, sp_runtime::Perbill::zero())
}),
FeasibilityError::UntrustedScoreTooLow
);

// ----- Start building. First, we need some closures.
let cache = helpers::generate_voter_cache::<T>(&snapshot_voters);
let voter_at = helpers::voter_at_fn::<T>(&snapshot_voters);
let target_at = helpers::target_at_fn::<T>(&snapshot_targets);
let voter_index = helpers::voter_index_fn_usize::<T>(&cache);

// Then convert solution -> assignment. This will fail if any of the indices are gibberish,
// namely any of the voters or targets.
let assignments = solution
.into_assignment(voter_at, target_at)
.map_err::<FeasibilityError, _>(Into::into)?;

// Ensure that assignments is correct.
let _ = assignments.iter().try_for_each(|assignment| {
// Check that assignment.who is actually a voter (defensive-only).
// NOTE: while using the index map from `voter_index` is better than a blind linear
// search, this *still* has room for optimization. Note that we had the index when
// we did `solution -> assignment` and we lost it. Ideal is to keep the index
// around.

// Defensive-only: must exist in the snapshot.
let snapshot_index =
voter_index(&assignment.who).ok_or(FeasibilityError::InvalidVoter)?;
// Defensive-only: index comes from the snapshot, must exist.
let (_voter, _stake, targets) =
snapshot_voters.get(snapshot_index).ok_or(FeasibilityError::InvalidVoter)?;

// Check that all of the targets are valid based on the snapshot.
if assignment.distribution.iter().any(|(d, _)| !targets.contains(d)) {
return Err(FeasibilityError::InvalidVote)
}
Ok(())
})?;

// ----- Start building support. First, we need one more closure.
let stake_of = helpers::stake_of_fn::<T>(&snapshot_voters, &cache);

// This might fail if the normalization fails. Very unlikely. See `integrity_test`.
let staked_assignments = assignment_ratio_to_staked_normalized(assignments, stake_of)
.map_err::<FeasibilityError, _>(Into::into)?;
let supports = sp_npos_elections::to_supports(&staked_assignments);

// Finally, check that the claimed score was indeed correct.
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()
.defensive_map_err(|_| FeasibilityError::BoundedConversionFailed)?;

Ok(ReadySolution { supports, compute, score })
}
}

#[cfg(test)]
Expand Down