Skip to content

Commit 0f9a800

Browse files
niklasad1ukint-vs
authored andcommitted
frame epm: expose feasibility_check in MinerConfig (paritytech#13555)
* frame epm: expose feasibity_check in miner The goal with this commit is to expose the `feasibity_check` such that anyone that implements the `MinerConfig trait` can utilize it * cleanup * fix tests
1 parent 14a76ef commit 0f9a800

File tree

5 files changed

+134
-97
lines changed

5 files changed

+134
-97
lines changed

bin/node/runtime/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,7 @@ impl pallet_election_provider_multi_phase::MinerConfig for Runtime {
702702
type Solution = NposSolution16;
703703
type MaxVotesPerVoter =
704704
<<Self as pallet_election_provider_multi_phase::Config>::DataProvider as ElectionDataProvider>::MaxVotesPerVoter;
705+
type MaxWinners = MaxActiveValidators;
705706

706707
// The unsigned submissions have to respect the weight of the submit_unsigned call, thus their
707708
// weight estimate function is wired to this call's weight.

frame/election-provider-multi-phase/src/lib.rs

Lines changed: 34 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,7 @@ use sp_arithmetic::{
247247
traits::{CheckedAdd, Zero},
248248
UpperOf,
249249
};
250-
use sp_npos_elections::{
251-
assignment_ratio_to_staked_normalized, BoundedSupports, ElectionScore, EvaluateSupport,
252-
Supports, VoteWeight,
253-
};
250+
use sp_npos_elections::{BoundedSupports, ElectionScore, IdentifierT, Supports, VoteWeight};
254251
use sp_runtime::{
255252
transaction_validity::{
256253
InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
@@ -430,13 +427,17 @@ impl<C: Default> Default for RawSolution<C> {
430427
DefaultNoBound,
431428
scale_info::TypeInfo,
432429
)]
433-
#[scale_info(skip_type_params(T))]
434-
pub struct ReadySolution<T: Config> {
430+
#[scale_info(skip_type_params(AccountId, MaxWinners))]
431+
pub struct ReadySolution<AccountId, MaxWinners>
432+
where
433+
AccountId: IdentifierT,
434+
MaxWinners: Get<u32>,
435+
{
435436
/// The final supports of the solution.
436437
///
437438
/// This is target-major vector, storing each winners, total backing, and each individual
438439
/// backer.
439-
pub supports: BoundedSupports<T::AccountId, T::MaxWinners>,
440+
pub supports: BoundedSupports<AccountId, MaxWinners>,
440441
/// The score of the solution.
441442
///
442443
/// This is needed to potentially challenge the solution.
@@ -451,11 +452,11 @@ pub struct ReadySolution<T: Config> {
451452
/// These are stored together because they are often accessed together.
452453
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, Default, TypeInfo)]
453454
#[scale_info(skip_type_params(T))]
454-
pub struct RoundSnapshot<T: Config> {
455+
pub struct RoundSnapshot<AccountId, DataProvider> {
455456
/// All of the voters.
456-
pub voters: Vec<VoterOf<T>>,
457+
pub voters: Vec<DataProvider>,
457458
/// All of the targets.
458-
pub targets: Vec<T::AccountId>,
459+
pub targets: Vec<AccountId>,
459460
}
460461

461462
/// Encodes the length of a solution or a snapshot.
@@ -614,6 +615,7 @@ pub mod pallet {
614615
type MinerConfig: crate::unsigned::MinerConfig<
615616
AccountId = Self::AccountId,
616617
MaxVotesPerVoter = <Self::DataProvider as ElectionDataProvider>::MaxVotesPerVoter,
618+
MaxWinners = Self::MaxWinners,
617619
>;
618620

619621
/// Maximum number of signed submissions that can be queued.
@@ -733,6 +735,11 @@ pub mod pallet {
733735
fn max_votes_per_voter() -> u32 {
734736
<T::MinerConfig as MinerConfig>::MaxVotesPerVoter::get()
735737
}
738+
739+
#[pallet::constant_name(MinerMaxWinners)]
740+
fn max_winners() -> u32 {
741+
<T::MinerConfig as MinerConfig>::MaxWinners::get()
742+
}
736743
}
737744

738745
#[pallet::hooks]
@@ -1247,14 +1254,15 @@ pub mod pallet {
12471254
/// Current best solution, signed or unsigned, queued to be returned upon `elect`.
12481255
#[pallet::storage]
12491256
#[pallet::getter(fn queued_solution)]
1250-
pub type QueuedSolution<T: Config> = StorageValue<_, ReadySolution<T>>;
1257+
pub type QueuedSolution<T: Config> =
1258+
StorageValue<_, ReadySolution<T::AccountId, T::MaxWinners>>;
12511259

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

12591267
/// Desired number of targets to elect for this round.
12601268
///
@@ -1385,7 +1393,7 @@ impl<T: Config> Pallet<T> {
13851393
// instead of using storage APIs, we do a manual encoding into a fixed-size buffer.
13861394
// `encoded_size` encodes it without storing it anywhere, this should not cause any
13871395
// allocation.
1388-
let snapshot = RoundSnapshot::<T> { voters, targets };
1396+
let snapshot = RoundSnapshot::<T::AccountId, VoterOf<T>> { voters, targets };
13891397
let size = snapshot.encoded_size();
13901398
log!(debug, "snapshot pre-calculated size {:?}", size);
13911399
let mut buffer = Vec::with_capacity(size);
@@ -1479,89 +1487,22 @@ impl<T: Config> Pallet<T> {
14791487
pub fn feasibility_check(
14801488
raw_solution: RawSolution<SolutionOf<T::MinerConfig>>,
14811489
compute: ElectionCompute,
1482-
) -> Result<ReadySolution<T>, FeasibilityError> {
1483-
let RawSolution { solution, score, round } = raw_solution;
1484-
1485-
// First, check round.
1486-
ensure!(Self::round() == round, FeasibilityError::InvalidRound);
1487-
1488-
// Winners are not directly encoded in the solution.
1489-
let winners = solution.unique_targets();
1490-
1490+
) -> Result<ReadySolution<T::AccountId, T::MaxWinners>, FeasibilityError> {
14911491
let desired_targets =
14921492
Self::desired_targets().ok_or(FeasibilityError::SnapshotUnavailable)?;
14931493

1494-
ensure!(winners.len() as u32 == desired_targets, FeasibilityError::WrongWinnerCount);
1495-
// Fail early if targets requested by data provider exceed maximum winners supported.
1496-
ensure!(
1497-
desired_targets <= <T as pallet::Config>::MaxWinners::get(),
1498-
FeasibilityError::TooManyDesiredTargets
1499-
);
1500-
1501-
// Ensure that the solution's score can pass absolute min-score.
1502-
let submitted_score = raw_solution.score;
1503-
ensure!(
1504-
Self::minimum_untrusted_score().map_or(true, |min_score| {
1505-
submitted_score.strict_threshold_better(min_score, Perbill::zero())
1506-
}),
1507-
FeasibilityError::UntrustedScoreTooLow
1508-
);
1509-
1510-
// Read the entire snapshot.
1511-
let RoundSnapshot { voters: snapshot_voters, targets: snapshot_targets } =
1512-
Self::snapshot().ok_or(FeasibilityError::SnapshotUnavailable)?;
1513-
1514-
// ----- Start building. First, we need some closures.
1515-
let cache = helpers::generate_voter_cache::<T::MinerConfig>(&snapshot_voters);
1516-
let voter_at = helpers::voter_at_fn::<T::MinerConfig>(&snapshot_voters);
1517-
let target_at = helpers::target_at_fn::<T::MinerConfig>(&snapshot_targets);
1518-
let voter_index = helpers::voter_index_fn_usize::<T::MinerConfig>(&cache);
1519-
1520-
// Then convert solution -> assignment. This will fail if any of the indices are gibberish,
1521-
// namely any of the voters or targets.
1522-
let assignments = solution
1523-
.into_assignment(voter_at, target_at)
1524-
.map_err::<FeasibilityError, _>(Into::into)?;
1525-
1526-
// Ensure that assignments is correct.
1527-
let _ = assignments.iter().try_for_each(|assignment| {
1528-
// Check that assignment.who is actually a voter (defensive-only).
1529-
// NOTE: while using the index map from `voter_index` is better than a blind linear
1530-
// search, this *still* has room for optimization. Note that we had the index when
1531-
// we did `solution -> assignment` and we lost it. Ideal is to keep the index
1532-
// around.
1533-
1534-
// Defensive-only: must exist in the snapshot.
1535-
let snapshot_index =
1536-
voter_index(&assignment.who).ok_or(FeasibilityError::InvalidVoter)?;
1537-
// Defensive-only: index comes from the snapshot, must exist.
1538-
let (_voter, _stake, targets) =
1539-
snapshot_voters.get(snapshot_index).ok_or(FeasibilityError::InvalidVoter)?;
1540-
1541-
// Check that all of the targets are valid based on the snapshot.
1542-
if assignment.distribution.iter().any(|(d, _)| !targets.contains(d)) {
1543-
return Err(FeasibilityError::InvalidVote)
1544-
}
1545-
Ok(())
1546-
})?;
1547-
1548-
// ----- Start building support. First, we need one more closure.
1549-
let stake_of = helpers::stake_of_fn::<T::MinerConfig>(&snapshot_voters, &cache);
1550-
1551-
// This might fail if the normalization fails. Very unlikely. See `integrity_test`.
1552-
let staked_assignments = assignment_ratio_to_staked_normalized(assignments, stake_of)
1553-
.map_err::<FeasibilityError, _>(Into::into)?;
1554-
let supports = sp_npos_elections::to_supports(&staked_assignments);
1555-
1556-
// Finally, check that the claimed score was indeed correct.
1557-
let known_score = supports.evaluate();
1558-
ensure!(known_score == score, FeasibilityError::InvalidScore);
1559-
1560-
// Size of winners in miner solution is equal to `desired_targets` <= `MaxWinners`.
1561-
let supports = supports
1562-
.try_into()
1563-
.defensive_map_err(|_| FeasibilityError::BoundedConversionFailed)?;
1564-
Ok(ReadySolution { supports, compute, score })
1494+
let snapshot = Self::snapshot().ok_or(FeasibilityError::SnapshotUnavailable)?;
1495+
let round = Self::round();
1496+
let minimum_untrusted_score = Self::minimum_untrusted_score();
1497+
1498+
Miner::<T::MinerConfig>::feasibility_check(
1499+
raw_solution,
1500+
compute,
1501+
desired_targets,
1502+
snapshot,
1503+
round,
1504+
minimum_untrusted_score,
1505+
)
15651506
}
15661507

15671508
/// Perform the tasks to be done after a new `elect` has been triggered:

frame/election-provider-multi-phase/src/mock.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ parameter_types! {
297297
pub static MockWeightInfo: MockedWeightInfo = MockedWeightInfo::Real;
298298
pub static MaxElectingVoters: VoterIndex = u32::max_value();
299299
pub static MaxElectableTargets: TargetIndex = TargetIndex::max_value();
300+
301+
#[derive(Debug)]
300302
pub static MaxWinners: u32 = 200;
301303

302304
pub static EpochLength: u64 = 30;
@@ -359,6 +361,7 @@ impl MinerConfig for Runtime {
359361
type MaxLength = MinerMaxLength;
360362
type MaxWeight = MinerMaxWeight;
361363
type MaxVotesPerVoter = <StakingMock as ElectionDataProvider>::MaxVotesPerVoter;
364+
type MaxWinners = MaxWinners;
362365
type Solution = TestNposSolution;
363366

364367
fn solution_weight(v: u32, t: u32, a: u32, d: u32) -> Weight {

frame/election-provider-multi-phase/src/signed.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ impl<T: Config> Pallet<T> {
462462
///
463463
/// Infallible
464464
pub fn finalize_signed_phase_accept_solution(
465-
ready_solution: ReadySolution<T>,
465+
ready_solution: ReadySolution<T::AccountId, T::MaxWinners>,
466466
who: &T::AccountId,
467467
deposit: BalanceOf<T>,
468468
call_fee: BalanceOf<T>,

frame/election-provider-multi-phase/src/unsigned.rs

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,17 @@ use crate::{
2323
};
2424
use codec::Encode;
2525
use frame_election_provider_support::{NposSolution, NposSolver, PerThing128, VoteWeight};
26-
use frame_support::{dispatch::DispatchResult, ensure, traits::Get, BoundedVec};
26+
use frame_support::{
27+
dispatch::DispatchResult,
28+
ensure,
29+
traits::{DefensiveResult, Get},
30+
BoundedVec,
31+
};
2732
use frame_system::offchain::SubmitTransaction;
2833
use scale_info::TypeInfo;
2934
use sp_npos_elections::{
3035
assignment_ratio_to_staked_normalized, assignment_staked_to_ratio_normalized, ElectionResult,
31-
ElectionScore,
36+
ElectionScore, EvaluateSupport,
3237
};
3338
use sp_runtime::{
3439
offchain::storage::{MutateStorageError, StorageValueRef},
@@ -351,7 +356,7 @@ impl<T: Config> Pallet<T> {
351356

352357
// ensure score is being improved. Panic henceforth.
353358
ensure!(
354-
Self::queued_solution().map_or(true, |q: ReadySolution<_>| raw_solution
359+
Self::queued_solution().map_or(true, |q: ReadySolution<_, _>| raw_solution
355360
.score
356361
.strict_threshold_better(q.score, T::BetterUnsignedThreshold::get())),
357362
Error::<T>::PreDispatchWeakSubmission,
@@ -387,6 +392,8 @@ pub trait MinerConfig {
387392
///
388393
/// The weight is computed using `solution_weight`.
389394
type MaxWeight: Get<Weight>;
395+
/// The maximum number of winners that can be elected.
396+
type MaxWinners: Get<u32>;
390397
/// Something that can compute the weight of a solution.
391398
///
392399
/// This weight estimate is then used to trim the solution, based on [`MinerConfig::MaxWeight`].
@@ -689,6 +696,91 @@ impl<T: MinerConfig> Miner<T> {
689696
);
690697
final_decision
691698
}
699+
700+
/// Checks the feasibility of a solution.
701+
pub fn feasibility_check(
702+
raw_solution: RawSolution<SolutionOf<T>>,
703+
compute: ElectionCompute,
704+
desired_targets: u32,
705+
snapshot: RoundSnapshot<T::AccountId, MinerVoterOf<T>>,
706+
current_round: u32,
707+
minimum_untrusted_score: Option<ElectionScore>,
708+
) -> Result<ReadySolution<T::AccountId, T::MaxWinners>, FeasibilityError> {
709+
let RawSolution { solution, score, round } = raw_solution;
710+
let RoundSnapshot { voters: snapshot_voters, targets: snapshot_targets } = snapshot;
711+
712+
// First, check round.
713+
ensure!(current_round == round, FeasibilityError::InvalidRound);
714+
715+
// Winners are not directly encoded in the solution.
716+
let winners = solution.unique_targets();
717+
718+
ensure!(winners.len() as u32 == desired_targets, FeasibilityError::WrongWinnerCount);
719+
// Fail early if targets requested by data provider exceed maximum winners supported.
720+
ensure!(desired_targets <= T::MaxWinners::get(), FeasibilityError::TooManyDesiredTargets);
721+
722+
// Ensure that the solution's score can pass absolute min-score.
723+
let submitted_score = raw_solution.score;
724+
ensure!(
725+
minimum_untrusted_score.map_or(true, |min_score| {
726+
submitted_score.strict_threshold_better(min_score, sp_runtime::Perbill::zero())
727+
}),
728+
FeasibilityError::UntrustedScoreTooLow
729+
);
730+
731+
// ----- Start building. First, we need some closures.
732+
let cache = helpers::generate_voter_cache::<T>(&snapshot_voters);
733+
let voter_at = helpers::voter_at_fn::<T>(&snapshot_voters);
734+
let target_at = helpers::target_at_fn::<T>(&snapshot_targets);
735+
let voter_index = helpers::voter_index_fn_usize::<T>(&cache);
736+
737+
// Then convert solution -> assignment. This will fail if any of the indices are gibberish,
738+
// namely any of the voters or targets.
739+
let assignments = solution
740+
.into_assignment(voter_at, target_at)
741+
.map_err::<FeasibilityError, _>(Into::into)?;
742+
743+
// Ensure that assignments is correct.
744+
let _ = assignments.iter().try_for_each(|assignment| {
745+
// Check that assignment.who is actually a voter (defensive-only).
746+
// NOTE: while using the index map from `voter_index` is better than a blind linear
747+
// search, this *still* has room for optimization. Note that we had the index when
748+
// we did `solution -> assignment` and we lost it. Ideal is to keep the index
749+
// around.
750+
751+
// Defensive-only: must exist in the snapshot.
752+
let snapshot_index =
753+
voter_index(&assignment.who).ok_or(FeasibilityError::InvalidVoter)?;
754+
// Defensive-only: index comes from the snapshot, must exist.
755+
let (_voter, _stake, targets) =
756+
snapshot_voters.get(snapshot_index).ok_or(FeasibilityError::InvalidVoter)?;
757+
758+
// Check that all of the targets are valid based on the snapshot.
759+
if assignment.distribution.iter().any(|(d, _)| !targets.contains(d)) {
760+
return Err(FeasibilityError::InvalidVote)
761+
}
762+
Ok(())
763+
})?;
764+
765+
// ----- Start building support. First, we need one more closure.
766+
let stake_of = helpers::stake_of_fn::<T>(&snapshot_voters, &cache);
767+
768+
// This might fail if the normalization fails. Very unlikely. See `integrity_test`.
769+
let staked_assignments = assignment_ratio_to_staked_normalized(assignments, stake_of)
770+
.map_err::<FeasibilityError, _>(Into::into)?;
771+
let supports = sp_npos_elections::to_supports(&staked_assignments);
772+
773+
// Finally, check that the claimed score was indeed correct.
774+
let known_score = supports.evaluate();
775+
ensure!(known_score == score, FeasibilityError::InvalidScore);
776+
777+
// Size of winners in miner solution is equal to `desired_targets` <= `MaxWinners`.
778+
let supports = supports
779+
.try_into()
780+
.defensive_map_err(|_| FeasibilityError::BoundedConversionFailed)?;
781+
782+
Ok(ReadySolution { supports, compute, score })
783+
}
692784
}
693785

694786
#[cfg(test)]

0 commit comments

Comments
 (0)