diff --git a/prdoc/pr_8987.prdoc b/prdoc/pr_8987.prdoc new file mode 100644 index 0000000000000..46fd6a3be3c98 --- /dev/null +++ b/prdoc/pr_8987.prdoc @@ -0,0 +1,10 @@ +title: 'EPMB/unsigned: fixed multi-page winner computation' +doc: +- audience: Runtime User + description: |- + Change the calculation of `MaxWinnersPerPage` in `FullSupportsOfMiner` to `Pages * MaxWinnersPerPage` (instead of the overall maximum number of winners across pages) + to prevent the computed solution from having a low overall total of winners, which could result in a `WrongWinnerCount` error. + +crates: +- name: pallet-election-provider-multi-block + bump: minor diff --git a/substrate/frame/election-provider-multi-block/src/mock/mod.rs b/substrate/frame/election-provider-multi-block/src/mock/mod.rs index 1c5581a4a8e60..120d76f9d183f 100644 --- a/substrate/frame/election-provider-multi-block/src/mock/mod.rs +++ b/substrate/frame/election-provider-multi-block/src/mock/mod.rs @@ -409,6 +409,12 @@ impl ExtBuilder { SignedMaxSubmissions::set(s); self } + + pub(crate) fn max_winners_per_page(self, w: u32) -> Self { + MaxWinnersPerPage::set(w); + self + } + #[allow(unused)] pub(crate) fn add_voter(self, who: AccountId, stake: Balance, targets: Vec) -> Self { staking::VOTERS.with(|v| v.borrow_mut().push((who, stake, targets.try_into().unwrap()))); diff --git a/substrate/frame/election-provider-multi-block/src/unsigned/miner.rs b/substrate/frame/election-provider-multi-block/src/unsigned/miner.rs index 8bf94765ae15c..6f425711e9ec0 100644 --- a/substrate/frame/election-provider-multi-block/src/unsigned/miner.rs +++ b/substrate/frame/election-provider-multi-block/src/unsigned/miner.rs @@ -221,6 +221,15 @@ pub type PageSupportsOfMiner = frame_election_provider_support::BoundedSuppor ::MaxBackersPerWinner, >; +/// Helper type that computes the maximum total winners across all pages. +pub struct MaxWinnersFinal(core::marker::PhantomData); + +impl frame_support::traits::Get for MaxWinnersFinal { + fn get() -> u32 { + T::Pages::get().saturating_mul(T::MaxWinnersPerPage::get()) + } +} + /// The full version of [`PageSupportsOfMiner`]. /// /// This should be used on a support instance that is encapsulating the full solution. @@ -228,7 +237,7 @@ pub type PageSupportsOfMiner = frame_election_provider_support::BoundedSuppor /// Another way to look at it, this is never wrapped in a `Vec<_>` pub type FullSupportsOfMiner = frame_election_provider_support::BoundedSupports< ::AccountId, - ::MaxWinnersPerPage, + MaxWinnersFinal, ::MaxBackersPerWinnerFinal, >; @@ -1335,6 +1344,52 @@ mod trimming { ); }) } + + #[test] + fn aggressive_backer_trimming_maintains_winner_count() { + // Test the scenario where aggressive backer trimming is applied but the solution + // should still maintain the correct winner count to avoid WrongWinnerCount errors. + ExtBuilder::unsigned() + .desired_targets(3) + .max_winners_per_page(2) + .pages(2) + .max_backers_per_winner_final(1) // aggressive final trimming + .max_backers_per_winner(1) // aggressive per-page trimming + .build_and_execute(|| { + // Use default 4 targets to stay within TargetSnapshotPerBlock limit + + // Adjust the voters a bit, such that they are all different backings + let mut current_voters = Voters::get(); + current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who); + Voters::set(current_voters); + + roll_to_snapshot_created(); + + let solution = mine_full_solution().unwrap(); + + // The solution should still be valid despite aggressive trimming + assert!(solution.solution_pages.len() > 0); + + let winner_count = solution + .solution_pages + .iter() + .flat_map(|page| page.unique_targets()) + .collect::>() + .len(); + + // We should get 3 winners. + // This demonstrates that FullSupportsOfMiner can accommodate winners from multiple + // pages and can hold more winners than MaxWinnersPerPage. + assert_eq!(winner_count, 3); + + // Load and verify the solution passes all checks without WrongWinnerCount error + load_mock_signed_and_start(solution); + let _supports = roll_to_full_verification(); + + // A solution should be successfully queued + assert!(VerifierPallet::queued_score().is_some()); + }) + } } #[cfg(test)]