referrerpolicy=no-referrer-when-downgrade

pallet_election_provider_multi_block/unsigned/
miner.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! The miner code for the EPMB pallet.
19//!
20//! It is broadly consisted of two main types:
21//!
22//! * [`crate::unsigned::miner::BaseMiner`], which is more generic, needs parameterization via
23//!   [`crate::unsigned::miner::MinerConfig`], and can be used by an external implementation.
24//! * [`crate::unsigned::miner::OffchainWorkerMiner`], which is more opinionated, and is used by
25//!   this pallet via the `offchain_worker` hook to also mine solutions during the
26//!   `Phase::Unsigned`.
27
28use super::{Call, Config, Pallet};
29use crate::{
30	helpers,
31	types::{PadSolutionPages, *},
32	verifier::{self},
33	CommonError,
34};
35use codec::Encode;
36use frame_election_provider_support::{ExtendedBalance, NposSolver, Support, VoteWeight};
37use frame_support::{traits::Get, BoundedVec};
38use frame_system::pallet_prelude::*;
39use scale_info::TypeInfo;
40use sp_npos_elections::EvaluateSupport;
41use sp_runtime::{
42	offchain::storage::{MutateStorageError, StorageValueRef},
43	traits::{SaturatedConversion, Saturating, Zero},
44};
45use sp_std::{collections::btree_map::BTreeMap, prelude::*};
46
47// TODO: we should have a fuzzer for miner that ensures no matter the parameters, it generates a
48// valid solution. Esp. for the trimming.
49
50/// The type of the snapshot.
51///
52/// Used to express errors.
53#[derive(Debug, Eq, PartialEq)]
54pub enum SnapshotType {
55	/// Voters at the given page missing.
56	Voters(PageIndex),
57	/// Targets missing.
58	Targets,
59	/// Metadata missing.
60	Metadata,
61	/// Desired targets missing.
62	DesiredTargets,
63}
64
65pub(crate) type MinerSolverErrorOf<T> = <<T as MinerConfig>::Solver as NposSolver>::Error;
66
67/// The errors related to the [`BaseMiner`].
68#[derive(
69	frame_support::DebugNoBound, frame_support::EqNoBound, frame_support::PartialEqNoBound,
70)]
71pub enum MinerError<T: MinerConfig> {
72	/// An internal error in the NPoS elections crate.
73	NposElections(sp_npos_elections::Error),
74	/// An internal error in the generic solver.
75	Solver(MinerSolverErrorOf<T>),
76	/// Snapshot data was unavailable unexpectedly.
77	SnapshotUnAvailable(SnapshotType),
78	/// The base, common errors from the pallet.
79	Common(CommonError),
80	/// The solution generated from the miner is not feasible.
81	Feasibility(verifier::FeasibilityError),
82	/// Some page index has been invalid.
83	InvalidPage,
84	/// Too many winners were removed during trimming.
85	TooManyWinnersRemoved,
86	/// A defensive error has occurred.
87	Defensive(&'static str),
88}
89
90impl<T: MinerConfig> From<sp_npos_elections::Error> for MinerError<T> {
91	fn from(e: sp_npos_elections::Error) -> Self {
92		MinerError::NposElections(e)
93	}
94}
95
96impl<T: MinerConfig> From<verifier::FeasibilityError> for MinerError<T> {
97	fn from(e: verifier::FeasibilityError) -> Self {
98		MinerError::Feasibility(e)
99	}
100}
101
102impl<T: MinerConfig> From<CommonError> for MinerError<T> {
103	fn from(e: CommonError) -> Self {
104		MinerError::Common(e)
105	}
106}
107
108/// The errors related to the `OffchainWorkerMiner`.
109#[derive(
110	frame_support::DebugNoBound, frame_support::EqNoBound, frame_support::PartialEqNoBound,
111)]
112pub enum OffchainMinerError<T: Config> {
113	/// An error in the base miner.
114	BaseMiner(MinerError<T::MinerConfig>),
115	/// The base, common errors from the pallet.
116	Common(CommonError),
117	/// Something went wrong fetching the lock.
118	Lock(&'static str),
119	/// Submitting a transaction to the pool failed.
120	PoolSubmissionFailed,
121	/// Cannot restore a solution that was not stored.
122	NoStoredSolution,
123	/// Cached solution is not a `submit_unsigned` call.
124	SolutionCallInvalid,
125	/// Failed to store a solution.
126	FailedToStoreSolution,
127	/// Cannot mine a solution with zero pages.
128	ZeroPages,
129}
130
131impl<T: Config> From<MinerError<T::MinerConfig>> for OffchainMinerError<T> {
132	fn from(e: MinerError<T::MinerConfig>) -> Self {
133		OffchainMinerError::BaseMiner(e)
134	}
135}
136
137impl<T: Config> From<CommonError> for OffchainMinerError<T> {
138	fn from(e: CommonError) -> Self {
139		OffchainMinerError::Common(e)
140	}
141}
142
143/// Configurations for the miner.
144///
145/// This is extracted from the main crate's config so that an offchain miner can readily use the
146/// [`BaseMiner`] without needing to deal with the rest of the pallet's configuration.
147pub trait MinerConfig {
148	/// The account id type.
149	type AccountId: Ord + Clone + codec::Codec + core::fmt::Debug;
150	/// The solution that the miner is mining.
151	/// The solution type.
152	type Solution: codec::FullCodec
153		+ Default
154		+ PartialEq
155		+ Eq
156		+ Clone
157		+ sp_std::fmt::Debug
158		+ Ord
159		+ NposSolution
160		+ TypeInfo
161		+ codec::MaxEncodedLen;
162	/// The solver type.
163	type Solver: NposSolver<AccountId = Self::AccountId>;
164	/// The maximum length that the miner should use for a solution, per page.
165	///
166	/// This value is not set in stone, and it is up to an individual miner to configure. A good
167	/// value is something like 75% of the total block length, which can be fetched from the system
168	/// pallet.
169	type MaxLength: Get<u32>;
170	/// Maximum number of votes per voter.
171	///
172	/// Must be the same as configured in the [`crate::Config::DataProvider`].
173	///
174	/// For simplicity, this is 16 in Polkadot and 24 in Kusama.
175	type MaxVotesPerVoter: Get<u32>;
176	/// Maximum number of winners to select per page.
177	///
178	/// The miner should respect this, it is used for trimming, and bounded data types.
179	///
180	/// Should equal to the onchain value set in `Verifier::Config`.
181	type MaxWinnersPerPage: Get<u32>;
182	/// Maximum number of backers per winner, per page.
183	///
184	/// The miner should respect this, it is used for trimming, and bounded data types.
185	///
186	/// Should equal to the onchain value set in `Verifier::Config`.
187	type MaxBackersPerWinner: Get<u32>;
188	/// Maximum number of backers, per winner, across all pages.
189	///
190	/// The miner should respect this, it is used for trimming, and bounded data types.
191	///
192	/// Should equal to the onchain value set in `Verifier::Config`.
193	type MaxBackersPerWinnerFinal: Get<u32>;
194	/// **Maximum** number of pages that we may compute.
195	///
196	/// Must be the same as configured in the [`crate::Config`].
197	type Pages: Get<u32>;
198	/// Maximum number of voters per snapshot page.
199	///
200	/// Must be the same as configured in the [`crate::Config`].
201	type VoterSnapshotPerBlock: Get<u32>;
202	/// Maximum number of targets per snapshot page.
203	///
204	/// Must be the same as configured in the [`crate::Config`].
205	type TargetSnapshotPerBlock: Get<u32>;
206	/// The hash type of the runtime.
207	type Hash: Eq + PartialEq;
208}
209
210/// A base miner that is only capable of mining a new solution and checking it against the state of
211/// this pallet for feasibility, and trimming its length/weight.
212pub struct BaseMiner<T: MinerConfig>(sp_std::marker::PhantomData<T>);
213
214/// Parameterized `BoundedSupports` for the miner.
215///
216/// The bounds of this are set such to only encapsulate a single page of a snapshot. The other
217/// counterpart is [`FullSupportsOfMiner`].
218pub type PageSupportsOfMiner<T> = frame_election_provider_support::BoundedSupports<
219	<T as MinerConfig>::AccountId,
220	<T as MinerConfig>::MaxWinnersPerPage,
221	<T as MinerConfig>::MaxBackersPerWinner,
222>;
223
224/// Helper type that computes the maximum total winners across all pages.
225pub struct MaxWinnersFinal<T: MinerConfig>(core::marker::PhantomData<T>);
226
227impl<T: MinerConfig> frame_support::traits::Get<u32> for MaxWinnersFinal<T> {
228	fn get() -> u32 {
229		T::Pages::get().saturating_mul(T::MaxWinnersPerPage::get())
230	}
231}
232
233/// The full version of [`PageSupportsOfMiner`].
234///
235/// This should be used on a support instance that is encapsulating the full solution.
236///
237/// Another way to look at it, this is never wrapped in a `Vec<_>`
238pub type FullSupportsOfMiner<T> = frame_election_provider_support::BoundedSupports<
239	<T as MinerConfig>::AccountId,
240	MaxWinnersFinal<T>,
241	<T as MinerConfig>::MaxBackersPerWinnerFinal,
242>;
243
244/// Aggregator for inputs to [`BaseMiner`].
245pub struct MineInput<T: MinerConfig> {
246	/// Number of winners to pick.
247	pub desired_targets: u32,
248	/// All of the targets.
249	pub all_targets: BoundedVec<T::AccountId, T::TargetSnapshotPerBlock>,
250	/// Paginated list of voters.
251	///
252	/// Note for staking-miners: How this is calculated is rather delicate, and the order of the
253	/// nested vectors matter. See carefully how `OffchainWorkerMiner::mine_solution` is doing
254	/// this.
255	pub voter_pages: AllVoterPagesOf<T>,
256	/// Number of pages to mind.
257	///
258	/// Note for staking-miner: Always use [`MinerConfig::Pages`] unless explicitly wanted
259	/// otherwise.
260	pub pages: PageIndex,
261	/// Whether to reduce the solution. Almost always``
262	pub do_reduce: bool,
263	/// The current round for which the solution is being calculated.
264	pub round: u32,
265}
266
267impl<T: MinerConfig> BaseMiner<T> {
268	/// Mine a new npos solution, with the given number of pages.
269	///
270	/// This miner is only capable of mining a solution that either uses all of the pages of the
271	/// snapshot, or the top `pages` thereof.
272	///
273	/// This always trims the solution to match a few parameters:
274	///
275	/// [`MinerConfig::MaxWinnersPerPage`], [`MinerConfig::MaxBackersPerWinner`],
276	/// [`MinerConfig::MaxBackersPerWinnerFinal`] and [`MinerConfig::MaxLength`].
277	///
278	/// The order of pages returned is aligned with the snapshot. For example, the index 0 of the
279	/// returning solution pages corresponds to the page 0 of the snapshot.
280	///
281	/// The only difference is, if the solution is partial, then [`Pagify`] must be used to properly
282	/// pad the results.
283	pub fn mine_solution(
284		MineInput { desired_targets, all_targets, voter_pages, mut pages, do_reduce, round }: MineInput<
285			T,
286		>,
287	) -> Result<PagedRawSolution<T>, MinerError<T>> {
288		pages = pages.min(T::Pages::get());
289
290		// we also build this closure early, so we can let `targets` be consumed.
291		let voter_page_fn = helpers::generate_voter_page_fn::<T>(&voter_pages);
292		let target_index_fn = helpers::target_index_fn::<T>(&all_targets);
293
294		// now flatten the voters, ready to be used as if pagination did not existed.
295		let all_voters: AllVoterPagesFlattenedOf<T> = voter_pages
296			.iter()
297			.cloned()
298			.flatten()
299			.collect::<Vec<_>>()
300			.try_into()
301			.expect("Flattening the voters into `AllVoterPagesFlattenedOf` cannot fail; qed");
302
303		let ElectionResult { winners: _, assignments } = T::Solver::solve(
304			desired_targets as usize,
305			all_targets.clone().to_vec(),
306			all_voters.clone().into_inner(),
307		)
308		.map_err(|e| MinerError::Solver(e))?;
309
310		// reduce and trim supports. We don't trim length and weight here, since those are dependent
311		// on the final form of the solution ([`PagedRawSolution`]), thus we do it later.
312		let trimmed_assignments = {
313			// Implementation note: the overall code path is as follows: election_results ->
314			// assignments -> staked assignments -> reduce -> supports -> trim supports -> staked
315			// assignments -> final assignments
316			// This is by no means the most performant, but is the clear and correct.
317			use sp_npos_elections::{
318				assignment_ratio_to_staked_normalized, assignment_staked_to_ratio_normalized,
319				reduce, supports_to_staked_assignment, to_supports, EvaluateSupport,
320			};
321
322			// These closures are of no use in the rest of these code, since they only deal with the
323			// overall list of voters.
324			let cache = helpers::generate_voter_cache::<T, _>(&all_voters);
325			let stake_of = helpers::stake_of_fn::<T, _>(&all_voters, &cache);
326
327			// 1. convert to staked and reduce
328			let (reduced_count, staked) = {
329				let mut staked = assignment_ratio_to_staked_normalized(assignments, &stake_of)
330					.map_err::<MinerError<T>, _>(Into::into)?;
331
332				// first, reduce the solution if requested. This will already remove a lot of
333				// "redundant" and reduce the chance for the need of any further trimming.
334				let count = if do_reduce { reduce(&mut staked) } else { 0 };
335				(count, staked)
336			};
337
338			// 2. trim the supports by FINAL backing.
339			let (_pre_score, final_trimmed_assignments, winners_removed, backers_removed) = {
340				// these supports could very well be invalid for SCORE purposes. The reason is that
341				// you might trim out half of an account's stake, but we don't look for this
342				// account's other votes to fix it.
343				let supports_invalid_score = to_supports(&staked);
344
345				let pre_score = (&supports_invalid_score).evaluate();
346				let (bounded_invalid_score, winners_removed, backers_removed) =
347					FullSupportsOfMiner::<T>::sorted_truncate_from(supports_invalid_score);
348
349				// now recreated the staked assignments
350				let staked = supports_to_staked_assignment(bounded_invalid_score.into());
351				let assignments = assignment_staked_to_ratio_normalized(staked)
352					.map_err::<MinerError<T>, _>(Into::into)?;
353				(pre_score, assignments, winners_removed, backers_removed)
354			};
355
356			miner_log!(
357				debug,
358				"initial score = {:?}, reduced {} edges, trimmed {} winners and {} backers due to global support limits",
359				_pre_score,
360				reduced_count,
361				winners_removed,
362				backers_removed,
363			);
364
365			final_trimmed_assignments
366		};
367
368		// split the assignments into different pages.
369		let mut paged_assignments: BoundedVec<Vec<AssignmentOf<T>>, T::Pages> =
370			BoundedVec::with_bounded_capacity(pages as usize);
371		paged_assignments.bounded_resize(pages as usize, Default::default());
372
373		for assignment in trimmed_assignments {
374			// NOTE: this `page` index is LOCAL. It does not correspond to the actual page index of
375			// the snapshot map, but rather the index in the `voter_pages`.
376			let page = voter_page_fn(&assignment.who).ok_or(MinerError::InvalidPage)?;
377			let assignment_page =
378				paged_assignments.get_mut(page as usize).ok_or(MinerError::InvalidPage)?;
379			assignment_page.push(assignment);
380		}
381
382		// convert each page to a compact struct -- no more change allowed.
383		let mut solution_pages: Vec<SolutionOf<T>> = paged_assignments
384			.into_iter()
385			.enumerate()
386			.map(|(page_index, assignment_page)| {
387				// get the page of the snapshot that corresponds to this page of the assignments.
388				let page: PageIndex = page_index.saturated_into();
389				let voter_snapshot_page = voter_pages
390					.get(page as usize)
391					.ok_or(MinerError::SnapshotUnAvailable(SnapshotType::Voters(page)))?;
392
393				// one last trimming -- `MaxBackersPerWinner`, the per-page variant.
394				let trimmed_assignment_page = Self::trim_supports_max_backers_per_winner_per_page(
395					assignment_page,
396					voter_snapshot_page,
397					page_index as u32,
398				)?;
399
400				let voter_index_fn = {
401					let cache = helpers::generate_voter_cache::<T, _>(&voter_snapshot_page);
402					helpers::voter_index_fn_owned::<T>(cache)
403				};
404
405				<SolutionOf<T>>::from_assignment(
406					&trimmed_assignment_page,
407					&voter_index_fn,
408					&target_index_fn,
409				)
410				.map_err::<MinerError<T>, _>(Into::into)
411			})
412			.collect::<Result<Vec<_>, _>>()?;
413
414		// now do the length trim.
415		let _trim_length_weight =
416			Self::maybe_trim_weight_and_len(&mut solution_pages, &voter_pages)?;
417		miner_log!(debug, "trimmed {} voters due to length restriction.", _trim_length_weight);
418
419		// finally, wrap everything up. Assign a fake score here, since we might need to re-compute
420		// it.
421		let mut paged = PagedRawSolution { round, solution_pages, score: Default::default() };
422
423		// OPTIMIZATION: we do feasibility_check inside `compute_score`, and once later
424		// pre_dispatch. I think it is fine, but maybe we can improve it.
425		let score = Self::compute_score(&paged, &voter_pages, &all_targets, desired_targets)
426			.map_err::<MinerError<T>, _>(Into::into)?;
427		paged.score = score;
428
429		miner_log!(
430			debug,
431			"mined a solution with {} pages, score {:?}, {} winners, {} voters, {} edges, and {} bytes",
432			pages,
433			score,
434			paged.winner_count_single_page_target_snapshot(),
435			paged.voter_count(),
436			paged.edge_count(),
437			paged.using_encoded(|b| b.len())
438		);
439
440		Ok(paged)
441	}
442
443	/// perform the feasibility check on all pages of a solution, returning `Ok(())` if all good and
444	/// the corresponding error otherwise.
445	pub fn check_feasibility(
446		paged_solution: &PagedRawSolution<T>,
447		paged_voters: &AllVoterPagesOf<T>,
448		snapshot_targets: &BoundedVec<T::AccountId, T::TargetSnapshotPerBlock>,
449		desired_targets: u32,
450	) -> Result<Vec<PageSupportsOfMiner<T>>, MinerError<T>> {
451		// check every solution page for feasibility.
452		let padded_voters = paged_voters.clone().pad_solution_pages(T::Pages::get());
453		paged_solution
454			.solution_pages
455			.pagify(T::Pages::get())
456			.map(|(page_index, page_solution)| {
457				match verifier::feasibility_check_page_inner_with_snapshot::<T>(
458					page_solution.clone(),
459					&padded_voters[page_index as usize],
460					snapshot_targets,
461					desired_targets,
462				) {
463					Ok(x) => {
464						miner_log!(debug, "feasibility check of page {:?} was okay", page_index,);
465						Ok(x)
466					},
467					Err(e) => {
468						miner_log!(
469							warn,
470							"feasibility check of page {:?} {:?} failed for solution because: {:?}",
471							page_index,
472							page_solution,
473							e,
474						);
475						Err(e)
476					},
477				}
478			})
479			.collect::<Result<Vec<_>, _>>()
480			.map_err(|err| MinerError::from(err))
481			.and_then(|supports| {
482				// If we someday want to check `MaxBackersPerWinnerFinal`, it would be here.
483				Ok(supports)
484			})
485	}
486
487	/// Take the given raw paged solution and compute its score. This will replicate what the chain
488	/// would do as closely as possible, and expects all the corresponding snapshot data to be
489	/// available.
490	fn compute_score(
491		paged_solution: &PagedRawSolution<T>,
492		paged_voters: &AllVoterPagesOf<T>,
493		all_targets: &BoundedVec<T::AccountId, T::TargetSnapshotPerBlock>,
494		desired_targets: u32,
495	) -> Result<ElectionScore, MinerError<T>> {
496		let all_supports =
497			Self::check_feasibility(paged_solution, paged_voters, all_targets, desired_targets)?;
498		let mut total_backings: BTreeMap<T::AccountId, ExtendedBalance> = BTreeMap::new();
499		all_supports.into_iter().flat_map(|x| x.0).for_each(|(who, support)| {
500			let backing = total_backings.entry(who).or_default();
501			*backing = backing.saturating_add(support.total);
502		});
503
504		let all_supports = total_backings
505			.into_iter()
506			.map(|(who, total)| (who, Support { total, ..Default::default() }))
507			.collect::<Vec<_>>();
508
509		Ok((&all_supports).evaluate())
510	}
511
512	fn trim_supports_max_backers_per_winner_per_page(
513		untrimmed_assignments: Vec<AssignmentOf<T>>,
514		page_voters: &VoterPageOf<T>,
515		page: PageIndex,
516	) -> Result<Vec<AssignmentOf<T>>, MinerError<T>> {
517		use sp_npos_elections::{
518			assignment_ratio_to_staked_normalized, assignment_staked_to_ratio_normalized,
519			supports_to_staked_assignment, to_supports,
520		};
521		// convert to staked
522		let cache = helpers::generate_voter_cache::<T, _>(page_voters);
523		let stake_of = helpers::stake_of_fn::<T, _>(&page_voters, &cache);
524		let untrimmed_staked_assignments =
525			assignment_ratio_to_staked_normalized(untrimmed_assignments, &stake_of)?;
526
527		// convert to supports
528		let supports = to_supports(&untrimmed_staked_assignments);
529		drop(untrimmed_staked_assignments);
530
531		// Convert it to our desired bounds, which will truncate the smallest backers if need
532		// be.
533		let (bounded, winners_removed, backers_removed) =
534			PageSupportsOfMiner::<T>::sorted_truncate_from(supports);
535
536		miner_log!(
537			debug,
538			"trimmed {} winners and {} backers from page {} due to per-page limits",
539			winners_removed,
540			backers_removed,
541			page
542		);
543
544		// convert back to staked
545		let trimmed_staked_assignments = supports_to_staked_assignment(bounded.into());
546		// and then ratio assignments
547		let trimmed_assignments =
548			assignment_staked_to_ratio_normalized(trimmed_staked_assignments)?;
549
550		Ok(trimmed_assignments)
551	}
552
553	/// Maybe tim the weight and length of the given multi-page solution.
554	///
555	/// Returns the number of voters removed.
556	///
557	/// If either of the bounds are not met, the trimming strategy is as follows:
558	///
559	/// Start from the least significant page. Assume only this page is going to be trimmed. call
560	/// `page.sort()` on this page. This will make sure in each field (`votes1`, `votes2`, etc.) of
561	/// that page, the voters are sorted by descending stake. Then, we compare the last item of each
562	/// field. This is the process of removing the single least staked voter.
563	///
564	/// We repeat this until satisfied, for both weight and length. If a full page is removed, but
565	/// the bound is not satisfied, we need to make sure that we sort the next least valuable page,
566	/// and repeat the same process.
567	///
568	/// NOTE: this is a public function to be used by the `OffchainWorkerMiner` or any similar one,
569	/// based on the submission strategy. The length and weight bounds of a call are dependent on
570	/// the number of pages being submitted, the number of blocks over which we submit, and the type
571	/// of the transaction and its weight (e.g. signed or unsigned).
572	///
573	/// NOTE: It could be that this function removes too many voters, and the solution becomes
574	/// invalid. This is not yet handled and only a warning is emitted.
575	pub fn maybe_trim_weight_and_len(
576		solution_pages: &mut Vec<SolutionOf<T>>,
577		paged_voters: &AllVoterPagesOf<T>,
578	) -> Result<u32, MinerError<T>> {
579		debug_assert_eq!(solution_pages.len(), paged_voters.len());
580		let size_limit = T::MaxLength::get();
581
582		let needs_any_trim = |solution_pages: &mut Vec<SolutionOf<T>>| {
583			let size = solution_pages.encoded_size() as u32;
584			let needs_len_trim = size > size_limit;
585			// a reminder that we used to have weight trimming here, but not more!
586			let needs_weight_trim = false;
587			needs_weight_trim || needs_len_trim
588		};
589
590		// Note the solution might be partial. In either case, this is its least significant page.
591		let mut current_trimming_page = 0;
592		let current_trimming_page_stake_of = |current_trimming_page: usize| {
593			Box::new(move |voter_index: &SolutionVoterIndexOf<T>| -> VoteWeight {
594				paged_voters
595					.get(current_trimming_page)
596					.and_then(|page_voters| {
597						page_voters
598							.get((*voter_index).saturated_into::<usize>())
599							.map(|(_, s, _)| *s)
600					})
601					.unwrap_or_default()
602			})
603		};
604
605		let sort_current_trimming_page =
606			|current_trimming_page: usize, solution_pages: &mut Vec<SolutionOf<T>>| {
607				solution_pages.get_mut(current_trimming_page).map(|solution_page| {
608					let stake_of_fn = current_trimming_page_stake_of(current_trimming_page);
609					solution_page.sort(stake_of_fn)
610				});
611			};
612
613		let is_empty = |solution_pages: &Vec<SolutionOf<T>>| {
614			solution_pages.iter().all(|page| page.voter_count().is_zero())
615		};
616
617		if needs_any_trim(solution_pages) {
618			sort_current_trimming_page(current_trimming_page, solution_pages)
619		}
620
621		// Implementation note: we want `solution_pages` and `paged_voters` to remain in sync, so
622		// while one of the pages of `solution_pages` might become "empty" we prefer not removing
623		// it. This has a slight downside that even an empty pages consumes a few dozens of bytes,
624		// which we accept for code simplicity.
625
626		let mut removed = 0;
627		while needs_any_trim(solution_pages) && !is_empty(solution_pages) {
628			if let Some(removed_idx) =
629				solution_pages.get_mut(current_trimming_page).and_then(|page| {
630					let stake_of_fn = current_trimming_page_stake_of(current_trimming_page);
631					page.remove_weakest_sorted(&stake_of_fn)
632				}) {
633				miner_log!(
634					trace,
635					"removed voter at index {:?} of (un-pagified) page {} as the weakest due to weight/length limits.",
636					removed_idx,
637					current_trimming_page
638				);
639				// we removed one person, continue.
640				removed.saturating_inc();
641			} else {
642				// this page cannot support remove anymore. Try and go to the next page.
643				miner_log!(
644					debug,
645					"page {} seems to be fully empty now, moving to the next one",
646					current_trimming_page
647				);
648				let next_page = current_trimming_page.saturating_add(1);
649				if paged_voters.len() > next_page {
650					current_trimming_page = next_page;
651					sort_current_trimming_page(current_trimming_page, solution_pages);
652				} else {
653					miner_log!(
654						warn,
655						"no more pages to trim from at page {}, already trimmed",
656						current_trimming_page
657					);
658					break;
659				}
660			}
661		}
662
663		Ok(removed)
664	}
665}
666
667/// A miner that is suited to work inside offchain worker environment.
668///
669/// This is parameterized by [`Config`], rather than [`MinerConfig`].
670pub struct OffchainWorkerMiner<T: Config>(sp_std::marker::PhantomData<T>);
671
672impl<T: Config> OffchainWorkerMiner<T> {
673	/// Storage key used to store the offchain worker running status.
674	pub(crate) const OFFCHAIN_LOCK: &'static [u8] = b"parity/multi-block-unsigned-election/lock";
675	/// Storage key used to store the last block number at which offchain worker ran.
676	const OFFCHAIN_LAST_BLOCK: &'static [u8] = b"parity/multi-block-unsigned-election";
677	/// Storage key used to cache the solution `call` and its snapshot fingerprint.
678	const OFFCHAIN_CACHED_CALL: &'static [u8] = b"parity/multi-block-unsigned-election/call";
679
680	pub(crate) fn fetch_snapshot(
681		pages: PageIndex,
682	) -> Result<
683		(AllVoterPagesOf<T::MinerConfig>, BoundedVec<T::AccountId, T::TargetSnapshotPerBlock>, u32),
684		OffchainMinerError<T>,
685	> {
686		// read the appropriate snapshot pages.
687		let desired_targets = crate::Snapshot::<T>::desired_targets()
688			.ok_or(MinerError::SnapshotUnAvailable(SnapshotType::DesiredTargets))?;
689		let all_targets = crate::Snapshot::<T>::targets()
690			.ok_or(MinerError::SnapshotUnAvailable(SnapshotType::Targets))?;
691
692		// This is the range of voters that we are interested in.
693		let voter_pages_range = crate::Pallet::<T>::msp_range_for(pages as usize);
694
695		sublog!(
696			debug,
697			"unsigned::base-miner",
698			"mining a solution with {} pages, voter snapshot range will be: {:?}",
699			pages,
700			voter_pages_range
701		);
702
703		// NOTE: if `pages (2) < T::Pages (3)`, at this point this vector will have length 2,
704		// with a layout of `[snapshot(1), snapshot(2)]`, namely the two most significant pages
705		//  of the snapshot.
706		let voter_pages: BoundedVec<_, T::Pages> = voter_pages_range
707			.into_iter()
708			.map(|p| {
709				crate::Snapshot::<T>::voters(p)
710					.ok_or(MinerError::SnapshotUnAvailable(SnapshotType::Voters(p)))
711			})
712			.collect::<Result<Vec<_>, _>>()?
713			.try_into()
714			.expect(
715				"`voter_pages_range` has `.take(pages)`; it must have length less than pages; it
716				must convert to `BoundedVec`; qed",
717			);
718
719		Ok((voter_pages, all_targets, desired_targets))
720	}
721
722	pub fn mine_solution(
723		pages: PageIndex,
724		do_reduce: bool,
725	) -> Result<PagedRawSolution<T::MinerConfig>, OffchainMinerError<T>> {
726		if pages.is_zero() {
727			return Err(OffchainMinerError::<T>::ZeroPages);
728		}
729		let (voter_pages, all_targets, desired_targets) = Self::fetch_snapshot(pages)?;
730		let round = crate::Pallet::<T>::round();
731		BaseMiner::<T::MinerConfig>::mine_solution(MineInput {
732			desired_targets,
733			all_targets,
734			voter_pages,
735			pages,
736			do_reduce,
737			round,
738		})
739		.map_err(Into::into)
740	}
741
742	/// Get a checked solution from the base miner, ensure unsigned-specific checks also pass, then
743	/// return an submittable call.
744	fn mine_checked_call() -> Result<Call<T>, OffchainMinerError<T>> {
745		// we always do reduce in the offchain worker miner.
746		let reduce = true;
747
748		// NOTE: we don't run any checks in the base miner, and run all of them via
749		// `Self::full_checks`.
750		let paged_solution = Self::mine_solution(T::MinerPages::get(), reduce)
751			.map_err::<OffchainMinerError<T>, _>(Into::into)?;
752		// check the call fully, no fingerprinting.
753		let _ = Self::check_solution(&paged_solution, None, true)?;
754
755		let call: Call<T> =
756			Call::<T>::submit_unsigned { paged_solution: Box::new(paged_solution) }.into();
757
758		Ok(call)
759	}
760
761	/// Mine a new checked solution, maybe cache it, and submit it back to the chain as an unsigned
762	/// transaction.
763	pub(crate) fn mine_check_maybe_save_submit(save: bool) -> Result<(), OffchainMinerError<T>> {
764		sublog!(debug, "unsigned::ocw-miner", "miner attempting to compute an unsigned solution.");
765		let call = Self::mine_checked_call()?;
766		if save {
767			Self::save_solution(&call, crate::Snapshot::<T>::fingerprint())?;
768		}
769		Self::submit_call(call)
770	}
771
772	/// Check the solution, from the perspective of the offchain-worker miner:
773	///
774	/// 1. unsigned-specific checks.
775	/// 2. full-checks of the base miner
776	/// 	1. optionally feasibility check.
777	/// 	2. snapshot-independent checks.
778	/// 		1. optionally, snapshot fingerprint.
779	pub(crate) fn check_solution(
780		paged_solution: &PagedRawSolution<T::MinerConfig>,
781		maybe_snapshot_fingerprint: Option<T::Hash>,
782		do_feasibility: bool,
783	) -> Result<(), OffchainMinerError<T>> {
784		// NOTE: we prefer cheap checks first, so first run unsigned checks.
785		Pallet::<T>::unsigned_specific_checks(paged_solution)?;
786		Self::base_check_solution(paged_solution, maybe_snapshot_fingerprint, do_feasibility)
787	}
788
789	fn submit_call(call: Call<T>) -> Result<(), OffchainMinerError<T>> {
790		let xt = T::create_bare(call.into());
791		frame_system::offchain::SubmitTransaction::<T, Call<T>>::submit_transaction(xt)
792			.map(|_| {
793				sublog!(
794					debug,
795					"unsigned::ocw-miner",
796					"miner submitted a solution as an unsigned transaction",
797				);
798			})
799			.map_err(|_| OffchainMinerError::PoolSubmissionFailed)
800	}
801
802	/// Check the solution, from the perspective of the base miner:
803	///
804	/// 1. snapshot-independent checks.
805	/// 	- with the fingerprint check being an optional step fo that.
806	/// 2. optionally, feasibility check.
807	///
808	/// In most cases, you should always use this either with `do_feasibility = true` or
809	/// `maybe_snapshot_fingerprint.is_some()`. Doing both could be an overkill. The snapshot
810	/// staying constant (which can be checked via the hash) is a string guarantee that the
811	/// feasibility still holds.
812	///
813	/// The difference between this and [`Self::check_solution`] is that this does not run unsigned
814	/// specific checks.
815	pub(crate) fn base_check_solution(
816		paged_solution: &PagedRawSolution<T::MinerConfig>,
817		maybe_snapshot_fingerprint: Option<T::Hash>,
818		do_feasibility: bool,
819	) -> Result<(), OffchainMinerError<T>> {
820		let _ = crate::Pallet::<T>::snapshot_independent_checks(
821			paged_solution,
822			maybe_snapshot_fingerprint,
823		)?;
824
825		if do_feasibility {
826			let (voter_pages, all_targets, desired_targets) =
827				Self::fetch_snapshot(paged_solution.solution_pages.len() as PageIndex)?;
828			let _ = BaseMiner::<T::MinerConfig>::check_feasibility(
829				&paged_solution,
830				&voter_pages,
831				&all_targets,
832				desired_targets,
833			)?;
834		}
835
836		Ok(())
837	}
838
839	/// Attempt to restore a solution from cache. Otherwise, compute it fresh. Either way,
840	/// submit if our call's score is greater than that of the cached solution.
841	pub(crate) fn restore_or_compute_then_maybe_submit() -> Result<(), OffchainMinerError<T>> {
842		sublog!(
843			debug,
844			"unsigned::ocw-miner",
845			"miner attempting to restore or compute an unsigned solution."
846		);
847
848		let call = Self::restore_solution()
849			.and_then(|(call, snapshot_fingerprint)| {
850				// ensure the cached call is still current before submitting
851				if let Call::submit_unsigned { paged_solution, .. } = &call {
852					// we check the snapshot fingerprint instead of doing a full feasibility.
853					OffchainWorkerMiner::<T>::check_solution(
854						paged_solution,
855						Some(snapshot_fingerprint),
856						false,
857					).map_err::<OffchainMinerError<T>, _>(Into::into)?;
858					Ok(call)
859				} else {
860					Err(OffchainMinerError::SolutionCallInvalid)
861				}
862			})
863			.or_else::<OffchainMinerError<T>, _>(|error| {
864				use OffchainMinerError as OE;
865				use MinerError as ME;
866				use CommonError as CE;
867				match error {
868					OE::NoStoredSolution => {
869						// IFF, not present regenerate.
870						let call = Self::mine_checked_call()?;
871						Self::save_solution(&call, crate::Snapshot::<T>::fingerprint())?;
872						Ok(call)
873					},
874					OE::Common(ref e) => {
875						sublog!(
876							error,
877							"unsigned::ocw-miner",
878							"unsigned specific checks failed ({:?}) while restoring solution. This should never happen. clearing cache.",
879							e,
880						);
881						Self::clear_offchain_solution_cache();
882						Err(error)
883					},
884					OE::BaseMiner(ME::Feasibility(_))
885						| OE::BaseMiner(ME::Common(CE::WrongRound))
886						| OE::BaseMiner(ME::Common(CE::WrongFingerprint))
887					=> {
888						// note that failing `Feasibility` can only mean that the solution was
889						// computed over a snapshot that has changed due to a fork.
890						sublog!(warn, "unsigned::ocw-miner", "wiping infeasible solution ({:?}).", error);
891						// kill the "bad" solution.
892						Self::clear_offchain_solution_cache();
893
894						// .. then return the error as-is.
895						Err(error)
896					},
897					_ => {
898						sublog!(debug, "unsigned::ocw-miner", "unhandled error in restoring offchain solution {:?}", error);
899						// nothing to do. Return the error as-is.
900						Err(error)
901					},
902				}
903			})?;
904
905		Self::submit_call(call)
906	}
907
908	/// Checks if an execution of the offchain worker is permitted at the given block number, or
909	/// not.
910	///
911	/// This makes sure that
912	/// 1. we don't run on previous blocks in case of a re-org
913	/// 2. we don't run twice within a window of length `T::OffchainRepeat`.
914	///
915	/// Returns `Ok(())` if offchain worker limit is respected, `Err(reason)` otherwise. If
916	/// `Ok()` is returned, `now` is written in storage and will be used in further calls as the
917	/// baseline.
918	pub fn ensure_offchain_repeat_frequency(
919		now: BlockNumberFor<T>,
920	) -> Result<(), OffchainMinerError<T>> {
921		let threshold = T::OffchainRepeat::get();
922		let last_block = StorageValueRef::persistent(&Self::OFFCHAIN_LAST_BLOCK);
923
924		let mutate_stat = last_block.mutate::<_, &'static str, _>(
925			|maybe_head: Result<Option<BlockNumberFor<T>>, _>| {
926				match maybe_head {
927					Ok(Some(head)) if now < head => Err("fork."),
928					Ok(Some(head)) if now >= head && now <= head + threshold => {
929						Err("recently executed.")
930					},
931					Ok(Some(head)) if now > head + threshold => {
932						// we can run again now. Write the new head.
933						Ok(now)
934					},
935					_ => {
936						// value doesn't exists. Probably this node just booted up. Write, and
937						// run
938						Ok(now)
939					},
940				}
941			},
942		);
943
944		match mutate_stat {
945			// all good
946			Ok(_) => Ok(()),
947			// failed to write.
948			Err(MutateStorageError::ConcurrentModification(_)) => Err(OffchainMinerError::Lock(
949				"failed to write to offchain db (concurrent modification).",
950			)),
951			// fork etc.
952			Err(MutateStorageError::ValueFunctionFailed(why)) => Err(OffchainMinerError::Lock(why)),
953		}
954	}
955
956	/// Save a given call into OCW storage.
957	fn save_solution(
958		call: &Call<T>,
959		snapshot_fingerprint: T::Hash,
960	) -> Result<(), OffchainMinerError<T>> {
961		sublog!(debug, "unsigned::ocw-miner", "saving a call to the offchain storage.");
962		let storage = StorageValueRef::persistent(&Self::OFFCHAIN_CACHED_CALL);
963		match storage.mutate::<_, (), _>(|_| Ok((call.clone(), snapshot_fingerprint))) {
964			Ok(_) => Ok(()),
965			Err(MutateStorageError::ConcurrentModification(_)) => {
966				Err(OffchainMinerError::FailedToStoreSolution)
967			},
968			Err(MutateStorageError::ValueFunctionFailed(_)) => {
969				// this branch should be unreachable according to the definition of
970				// `StorageValueRef::mutate`: that function should only ever `Err` if the closure we
971				// pass it returns an error. however, for safety in case the definition changes, we
972				// do not optimize the branch away or panic.
973				Err(OffchainMinerError::FailedToStoreSolution)
974			},
975		}
976	}
977
978	/// Get a saved solution from OCW storage if it exists.
979	fn restore_solution() -> Result<(Call<T>, T::Hash), OffchainMinerError<T>> {
980		StorageValueRef::persistent(&Self::OFFCHAIN_CACHED_CALL)
981			.get()
982			.ok()
983			.flatten()
984			.ok_or(OffchainMinerError::NoStoredSolution)
985	}
986
987	/// Clear a saved solution from OCW storage.
988	fn clear_offchain_solution_cache() {
989		sublog!(debug, "unsigned::ocw-miner", "clearing offchain call cache storage.");
990		let mut storage = StorageValueRef::persistent(&Self::OFFCHAIN_CACHED_CALL);
991		storage.clear();
992	}
993
994	#[cfg(test)]
995	fn cached_solution() -> Option<Call<T>> {
996		StorageValueRef::persistent(&Self::OFFCHAIN_CACHED_CALL)
997			.get::<Call<T>>()
998			.unwrap()
999	}
1000}
1001
1002// This will only focus on testing the internals of `maybe_trim_weight_and_len_works`.
1003#[cfg(test)]
1004mod trimming {
1005	use super::*;
1006	use crate::{mock::*, verifier::Verifier};
1007	use frame_election_provider_support::TryFromUnboundedPagedSupports;
1008	use sp_npos_elections::Support;
1009
1010	#[test]
1011	fn solution_without_any_trimming() {
1012		ExtBuilder::mock_signed().build_and_execute(|| {
1013			// adjust the voters a bit, such that they are all different backings
1014			let mut current_voters = Voters::get();
1015			current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1016			Voters::set(current_voters);
1017
1018			roll_to_snapshot_created();
1019
1020			// now we let the miner mine something for us..
1021			let solution = mine_full_solution().unwrap();
1022			assert_eq!(
1023				solution.solution_pages.iter().map(|page| page.voter_count()).sum::<usize>(),
1024				8
1025			);
1026
1027			assert_eq!(solution.solution_pages.encoded_size(), 105);
1028			load_mock_signed_and_start(solution);
1029			let supports = roll_to_full_verification();
1030
1031			// a solution is queued.
1032			assert!(VerifierPallet::queued_score().is_some());
1033
1034			assert_eq!(
1035				supports,
1036				vec![
1037					vec![
1038						(30, Support { total: 30, voters: vec![(30, 30)] }),
1039						(40, Support { total: 40, voters: vec![(40, 40)] })
1040					],
1041					vec![
1042						(30, Support { total: 11, voters: vec![(5, 2), (6, 2), (7, 7)] }),
1043						(40, Support { total: 7, voters: vec![(5, 3), (6, 4)] })
1044					],
1045					vec![(40, Support { total: 9, voters: vec![(2, 2), (3, 3), (4, 4)] })]
1046				]
1047				.try_from_unbounded_paged()
1048				.unwrap()
1049			);
1050		})
1051	}
1052
1053	#[test]
1054	fn trim_length() {
1055		ExtBuilder::mock_signed().miner_max_length(104).build_and_execute(|| {
1056			// adjust the voters a bit, such that they are all different backings
1057			let mut current_voters = Voters::get();
1058			current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1059			Voters::set(current_voters);
1060
1061			roll_to_snapshot_created();
1062			ensure_voters(3, 12);
1063
1064			let solution = mine_full_solution().unwrap();
1065
1066			assert_eq!(
1067				solution.solution_pages.iter().map(|page| page.voter_count()).sum::<usize>(),
1068				7
1069			);
1070
1071			assert_eq!(solution.solution_pages.encoded_size(), 99);
1072
1073			load_mock_signed_and_start(solution);
1074			let supports = roll_to_full_verification();
1075
1076			// a solution is queued.
1077			assert!(VerifierPallet::queued_score().is_some());
1078
1079			assert_eq!(
1080				supports,
1081				vec![
1082					// 30 is gone! Note that length trimming starts from lsp, so we trim from this
1083					// page only.
1084					vec![(40, Support { total: 40, voters: vec![(40, 40)] })],
1085					vec![
1086						(30, Support { total: 11, voters: vec![(5, 2), (6, 2), (7, 7)] }),
1087						(40, Support { total: 7, voters: vec![(5, 3), (6, 4)] })
1088					],
1089					vec![(40, Support { total: 9, voters: vec![(2, 2), (3, 3), (4, 4)] })]
1090				]
1091				.try_from_unbounded_paged()
1092				.unwrap()
1093			);
1094		});
1095	}
1096
1097	#[test]
1098	fn trim_length_2() {
1099		ExtBuilder::mock_signed().miner_max_length(98).build_and_execute(|| {
1100			// adjust the voters a bit, such that they are all different backings
1101			let mut current_voters = Voters::get();
1102			current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1103			Voters::set(current_voters);
1104
1105			roll_to_snapshot_created();
1106			ensure_voters(3, 12);
1107
1108			let solution = mine_full_solution().unwrap();
1109
1110			assert_eq!(
1111				solution.solution_pages.iter().map(|page| page.voter_count()).sum::<usize>(),
1112				6
1113			);
1114
1115			assert_eq!(solution.solution_pages.encoded_size(), 93);
1116
1117			load_mock_signed_and_start(solution);
1118			let supports = roll_to_full_verification();
1119
1120			// a solution is queued.
1121			assert!(VerifierPallet::queued_score().is_some());
1122
1123			assert_eq!(
1124				supports,
1125				vec![
1126					vec![],
1127					vec![
1128						(30, Support { total: 11, voters: vec![(5, 2), (6, 2), (7, 7)] }),
1129						(40, Support { total: 7, voters: vec![(5, 3), (6, 4)] })
1130					],
1131					vec![(40, Support { total: 9, voters: vec![(2, 2), (3, 3), (4, 4)] })]
1132				]
1133				.try_from_unbounded_paged()
1134				.unwrap()
1135			);
1136		});
1137	}
1138
1139	#[test]
1140	fn trim_length_3() {
1141		ExtBuilder::mock_signed().miner_max_length(92).build_and_execute(|| {
1142			// adjust the voters a bit, such that they are all different backings
1143			let mut current_voters = Voters::get();
1144			current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1145			Voters::set(current_voters);
1146
1147			roll_to_snapshot_created();
1148			ensure_voters(3, 12);
1149
1150			let solution = mine_full_solution().unwrap();
1151
1152			assert_eq!(
1153				solution.solution_pages.iter().map(|page| page.voter_count()).sum::<usize>(),
1154				5
1155			);
1156
1157			assert_eq!(solution.solution_pages.encoded_size(), 83);
1158
1159			load_mock_signed_and_start(solution);
1160			let supports = roll_to_full_verification();
1161
1162			// a solution is queued.
1163			assert!(VerifierPallet::queued_score().is_some());
1164
1165			assert_eq!(
1166				supports,
1167				vec![
1168					vec![],
1169					vec![
1170						(30, Support { total: 9, voters: vec![(6, 2), (7, 7)] }),
1171						(40, Support { total: 4, voters: vec![(6, 4)] })
1172					],
1173					vec![(40, Support { total: 9, voters: vec![(2, 2), (3, 3), (4, 4)] })]
1174				]
1175				.try_from_unbounded_paged()
1176				.unwrap()
1177			);
1178		});
1179	}
1180
1181	#[test]
1182	fn trim_backers_per_page_works() {
1183		ExtBuilder::mock_signed().max_backers_per_winner(2).build_and_execute(|| {
1184			// adjust the voters a bit, such that they are all different backings
1185			let mut current_voters = Voters::get();
1186			current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1187			Voters::set(current_voters);
1188
1189			roll_to_snapshot_created();
1190			ensure_voters(3, 12);
1191
1192			let solution = mine_full_solution().unwrap();
1193
1194			load_mock_signed_and_start(solution);
1195			let supports = roll_to_full_verification();
1196
1197			// a solution is queued.
1198			assert!(VerifierPallet::queued_score().is_some());
1199
1200			// each page is trimmed individually, based on `solution_without_any_trimming`.
1201			assert_eq!(
1202				supports,
1203				vec![
1204					vec![
1205						(30, Support { total: 30, voters: vec![(30, 30)] }),
1206						(40, Support { total: 40, voters: vec![(40, 40)] })
1207					],
1208					vec![
1209						(30, Support { total: 9, voters: vec![(6, 2), (7, 7)] }),
1210						(40, Support { total: 9, voters: vec![(5, 5), (6, 4)] }) /* notice how
1211						                                                          * 5's stake is
1212						                                                          * re-distributed
1213						                                                          * all here ^^ */
1214					],
1215					vec![(40, Support { total: 7, voters: vec![(3, 3), (4, 4)] })]
1216				]
1217				.try_from_unbounded_paged()
1218				.unwrap()
1219			);
1220		})
1221	}
1222
1223	#[test]
1224	fn trim_backers_per_page_works_2() {
1225		// This one is more interesting, as it also shows that as we trim backers, we re-distribute
1226		// their weight elsewhere.
1227		ExtBuilder::mock_signed().max_backers_per_winner(1).build_and_execute(|| {
1228			// adjust the voters a bit, such that they are all different backings
1229			let mut current_voters = Voters::get();
1230			current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1231			Voters::set(current_voters);
1232
1233			roll_to_snapshot_created();
1234			ensure_voters(3, 12);
1235
1236			let solution = mine_full_solution().unwrap();
1237
1238			load_mock_signed_and_start(solution);
1239			let supports = roll_to_full_verification();
1240
1241			// a solution is queued.
1242			assert!(VerifierPallet::queued_score().is_some());
1243
1244			// each page is trimmed individually, based on `solution_without_any_trimming`.
1245			assert_eq!(
1246				supports,
1247				vec![
1248					vec![
1249						(30, Support { total: 30, voters: vec![(30, 30)] }),
1250						(40, Support { total: 40, voters: vec![(40, 40)] })
1251					],
1252					vec![
1253						(30, Support { total: 7, voters: vec![(7, 7)] }),
1254						(40, Support { total: 6, voters: vec![(6, 6)] })
1255					],
1256					vec![(40, Support { total: 4, voters: vec![(4, 4)] })]
1257				]
1258				.try_from_unbounded_paged()
1259				.unwrap()
1260			);
1261		})
1262	}
1263
1264	#[test]
1265	fn trim_backers_final_works() {
1266		ExtBuilder::mock_signed()
1267			.max_backers_per_winner(4)
1268			.max_backers_per_winner_final(4)
1269			.build_and_execute(|| {
1270				// adjust the voters a bit, such that they are all different backings
1271				let mut current_voters = Voters::get();
1272				current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1273				Voters::set(current_voters);
1274
1275				roll_to_snapshot_created();
1276				ensure_voters(3, 12);
1277
1278				let solution = mine_full_solution().unwrap();
1279
1280				load_mock_signed_and_start(solution);
1281				let supports = roll_to_full_verification();
1282
1283				// a solution is queued.
1284				assert!(VerifierPallet::queued_score().is_some());
1285
1286				// 30 has 1 + 3 = 4 backers -- all good
1287				// 40 has 1 + 2 + 3 = 6 backers -- needs to lose 2
1288				assert_eq!(
1289					supports,
1290					vec![
1291						vec![
1292							(30, Support { total: 30, voters: vec![(30, 30)] }),
1293							(40, Support { total: 40, voters: vec![(40, 40)] })
1294						],
1295						vec![
1296							(30, Support { total: 14, voters: vec![(5, 5), (6, 2), (7, 7)] }),
1297							(40, Support { total: 4, voters: vec![(6, 4)] })
1298						],
1299						vec![(40, Support { total: 7, voters: vec![(3, 3), (4, 4)] })]
1300					]
1301					.try_from_unbounded_paged()
1302					.unwrap()
1303				);
1304			})
1305	}
1306
1307	#[test]
1308	fn trim_backers_per_page_and_final_works() {
1309		ExtBuilder::mock_signed()
1310			.max_backers_per_winner_final(4)
1311			.max_backers_per_winner(2)
1312			.build_and_execute(|| {
1313				// adjust the voters a bit, such that they are all different backings
1314				let mut current_voters = Voters::get();
1315				current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1316				Voters::set(current_voters);
1317
1318				roll_to_snapshot_created();
1319				ensure_voters(3, 12);
1320
1321				let solution = mine_full_solution().unwrap();
1322
1323				load_mock_signed_and_start(solution);
1324				let supports = roll_to_full_verification();
1325
1326				// a solution is queued.
1327				assert!(VerifierPallet::queued_score().is_some());
1328
1329				// each page is trimmed individually, based on `solution_without_any_trimming`.
1330				assert_eq!(
1331					supports,
1332					vec![
1333						vec![
1334							(30, Support { total: 30, voters: vec![(30, 30)] }),
1335							(40, Support { total: 40, voters: vec![(40, 40)] })
1336						],
1337						vec![
1338							(30, Support { total: 12, voters: vec![(5, 5), (7, 7)] }),
1339							(40, Support { total: 6, voters: vec![(6, 6)] })
1340						],
1341						vec![(40, Support { total: 7, voters: vec![(3, 3), (4, 4)] })]
1342					]
1343					.try_from_unbounded_paged()
1344					.unwrap()
1345				);
1346			})
1347	}
1348
1349	#[test]
1350	fn aggressive_backer_trimming_maintains_winner_count() {
1351		// Test the scenario where aggressive backer trimming is applied but the solution
1352		// should still maintain the correct winner count to avoid WrongWinnerCount errors.
1353		ExtBuilder::mock_signed()
1354			.desired_targets(3)
1355			.max_winners_per_page(2)
1356			.pages(2)
1357			.max_backers_per_winner_final(1) // aggressive final trimming
1358			.max_backers_per_winner(1) // aggressive per-page trimming
1359			.build_and_execute(|| {
1360				// Use default 4 targets to stay within TargetSnapshotPerBlock limit
1361
1362				// Adjust the voters a bit, such that they are all different backings
1363				let mut current_voters = Voters::get();
1364				current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1365				Voters::set(current_voters);
1366
1367				roll_to_snapshot_created();
1368
1369				let solution = mine_full_solution().unwrap();
1370
1371				// The solution should still be valid despite aggressive trimming
1372				assert!(solution.solution_pages.len() > 0);
1373
1374				let winner_count = solution
1375					.solution_pages
1376					.iter()
1377					.flat_map(|page| page.unique_targets())
1378					.collect::<std::collections::HashSet<_>>()
1379					.len();
1380
1381				// We should get 3 winners.
1382				// This demonstrates that FullSupportsOfMiner can accommodate winners from multiple
1383				// pages and can hold more winners than MaxWinnersPerPage.
1384				assert_eq!(winner_count, 3);
1385
1386				// Load and verify the solution passes all checks without WrongWinnerCount error
1387				load_mock_signed_and_start(solution);
1388				let _supports = roll_to_full_verification();
1389
1390				// A solution should be successfully queued
1391				assert!(VerifierPallet::queued_score().is_some());
1392			})
1393	}
1394}
1395
1396#[cfg(test)]
1397mod base_miner {
1398	use std::vec;
1399
1400	use super::*;
1401	use crate::{mock::*, Snapshot};
1402	use frame_election_provider_support::TryFromUnboundedPagedSupports;
1403	use sp_npos_elections::Support;
1404	use sp_runtime::PerU16;
1405
1406	#[test]
1407	fn pagination_does_not_affect_score() {
1408		let score_1 = ExtBuilder::mock_signed()
1409			.pages(1)
1410			.voter_per_page(12)
1411			.build_unchecked()
1412			.execute_with(|| {
1413				roll_to_snapshot_created();
1414				mine_full_solution().unwrap().score
1415			});
1416		let score_2 = ExtBuilder::mock_signed()
1417			.pages(2)
1418			.voter_per_page(6)
1419			.build_unchecked()
1420			.execute_with(|| {
1421				roll_to_snapshot_created();
1422				mine_full_solution().unwrap().score
1423			});
1424		let score_3 = ExtBuilder::mock_signed()
1425			.pages(3)
1426			.voter_per_page(4)
1427			.build_unchecked()
1428			.execute_with(|| {
1429				roll_to_snapshot_created();
1430				mine_full_solution().unwrap().score
1431			});
1432
1433		assert_eq!(score_1, score_2);
1434		assert_eq!(score_2, score_3);
1435	}
1436
1437	#[test]
1438	fn mine_solution_single_page_works() {
1439		ExtBuilder::mock_signed().pages(1).voter_per_page(8).build_and_execute(|| {
1440			roll_to_snapshot_created();
1441
1442			ensure_voters(1, 8);
1443			ensure_targets(1, 4);
1444
1445			assert_eq!(
1446				Snapshot::<Runtime>::voters(0)
1447					.unwrap()
1448					.into_iter()
1449					.map(|(x, _, _)| x)
1450					.collect::<Vec<_>>(),
1451				vec![1, 2, 3, 4, 5, 6, 7, 8]
1452			);
1453
1454			let paged = mine_full_solution().unwrap();
1455			assert_eq!(paged.solution_pages.len(), 1);
1456
1457			// this solution must be feasible and submittable.
1458			OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1459
1460			// now do a realistic full verification
1461			load_mock_signed_and_start(paged.clone());
1462			let supports = roll_to_full_verification();
1463
1464			assert_eq!(
1465				supports,
1466				vec![vec![
1467					(10, Support { total: 30, voters: vec![(1, 10), (4, 5), (5, 5), (8, 10)] }),
1468					(
1469						40,
1470						Support {
1471							total: 40,
1472							voters: vec![(2, 10), (3, 10), (4, 5), (5, 5), (6, 10)]
1473						}
1474					)
1475				]]
1476				.try_from_unbounded_paged()
1477				.unwrap()
1478			);
1479
1480			// NOTE: this is the same as the score of any other test that contains the first 8
1481			// voters, we already test for this in `pagination_does_not_affect_score`.
1482			assert_eq!(
1483				paged.score,
1484				ElectionScore { minimal_stake: 30, sum_stake: 70, sum_stake_squared: 2500 }
1485			);
1486		})
1487	}
1488
1489	#[test]
1490	fn mine_solution_double_page_works() {
1491		ExtBuilder::mock_signed().pages(2).voter_per_page(4).build_and_execute(|| {
1492			roll_to_snapshot_created();
1493
1494			// 2 pages of 8 voters
1495			ensure_voters(2, 8);
1496			// 1 page of 4 targets
1497			ensure_targets(1, 4);
1498
1499			// voters in pages. note the reverse page index.
1500			assert_eq!(
1501				Snapshot::<Runtime>::voters(0)
1502					.unwrap()
1503					.into_iter()
1504					.map(|(x, _, _)| x)
1505					.collect::<Vec<_>>(),
1506				vec![5, 6, 7, 8]
1507			);
1508			assert_eq!(
1509				Snapshot::<Runtime>::voters(1)
1510					.unwrap()
1511					.into_iter()
1512					.map(|(x, _, _)| x)
1513					.collect::<Vec<_>>(),
1514				vec![1, 2, 3, 4]
1515			);
1516			// targets in pages.
1517			assert_eq!(Snapshot::<Runtime>::targets().unwrap(), vec![10, 20, 30, 40]);
1518			let paged = mine_full_solution().unwrap();
1519
1520			assert_eq!(
1521				paged.solution_pages,
1522				vec![
1523					TestNposSolution {
1524						// voter 6 (index 1) is backing 40 (index 3).
1525						// voter 8 (index 3) is backing 10 (index 0)
1526						votes1: vec![(1, 3), (3, 0)],
1527						// voter 5 (index 0) is backing 40 (index 3) and 10 (index 0)
1528						votes2: vec![(0, [(0, PerU16::from_parts(32768))], 3)],
1529						..Default::default()
1530					},
1531					TestNposSolution {
1532						// voter 1 (index 0) is backing 10 (index 0)
1533						// voter 2 (index 1) is backing 40 (index 3)
1534						// voter 3 (index 2) is backing 40 (index 3)
1535						votes1: vec![(0, 0), (1, 3), (2, 3)],
1536						// voter 4 (index 3) is backing 40 (index 10) and 10 (index 0)
1537						votes2: vec![(3, [(0, PerU16::from_parts(32768))], 3)],
1538						..Default::default()
1539					},
1540				]
1541			);
1542
1543			// this solution must be feasible and submittable.
1544			OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, false).unwrap();
1545
1546			// it must also be verified in the verifier
1547			load_mock_signed_and_start(paged.clone());
1548			let supports = roll_to_full_verification();
1549
1550			assert_eq!(
1551				supports,
1552				vec![
1553					// page0, supports from voters 5, 6, 7, 8
1554					vec![
1555						(10, Support { total: 15, voters: vec![(5, 5), (8, 10)] }),
1556						(40, Support { total: 15, voters: vec![(5, 5), (6, 10)] })
1557					],
1558					// page1 supports from voters 1, 2, 3, 4
1559					vec![
1560						(10, Support { total: 15, voters: vec![(1, 10), (4, 5)] }),
1561						(40, Support { total: 25, voters: vec![(2, 10), (3, 10), (4, 5)] })
1562					]
1563				]
1564				.try_from_unbounded_paged()
1565				.unwrap()
1566			);
1567
1568			assert_eq!(
1569				paged.score,
1570				ElectionScore { minimal_stake: 30, sum_stake: 70, sum_stake_squared: 2500 }
1571			);
1572		})
1573	}
1574
1575	#[test]
1576	fn mine_solution_triple_page_works() {
1577		ExtBuilder::mock_signed().pages(3).voter_per_page(4).build_and_execute(|| {
1578			roll_to_snapshot_created();
1579
1580			ensure_voters(3, 12);
1581			ensure_targets(1, 4);
1582
1583			// voters in pages. note the reverse page index.
1584			assert_eq!(
1585				Snapshot::<Runtime>::voters(2)
1586					.unwrap()
1587					.into_iter()
1588					.map(|(x, _, _)| x)
1589					.collect::<Vec<_>>(),
1590				vec![1, 2, 3, 4]
1591			);
1592			assert_eq!(
1593				Snapshot::<Runtime>::voters(1)
1594					.unwrap()
1595					.into_iter()
1596					.map(|(x, _, _)| x)
1597					.collect::<Vec<_>>(),
1598				vec![5, 6, 7, 8]
1599			);
1600			assert_eq!(
1601				Snapshot::<Runtime>::voters(0)
1602					.unwrap()
1603					.into_iter()
1604					.map(|(x, _, _)| x)
1605					.collect::<Vec<_>>(),
1606				vec![10, 20, 30, 40]
1607			);
1608
1609			let paged = mine_full_solution().unwrap();
1610			assert_eq!(
1611				paged.solution_pages,
1612				vec![
1613					TestNposSolution { votes1: vec![(2, 2), (3, 3)], ..Default::default() },
1614					TestNposSolution {
1615						votes1: vec![(2, 2)],
1616						votes2: vec![
1617							(0, [(2, PerU16::from_parts(32768))], 3),
1618							(1, [(2, PerU16::from_parts(32768))], 3)
1619						],
1620						..Default::default()
1621					},
1622					TestNposSolution {
1623						votes1: vec![(2, 3), (3, 3)],
1624						votes2: vec![(1, [(2, PerU16::from_parts(32768))], 3)],
1625						..Default::default()
1626					},
1627				]
1628			);
1629
1630			// this solution must be feasible and submittable.
1631			OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1632			// now do a realistic full verification
1633			load_mock_signed_and_start(paged.clone());
1634			let supports = roll_to_full_verification();
1635
1636			assert_eq!(
1637				supports,
1638				vec![
1639					// page 0: self-votes.
1640					vec![
1641						(30, Support { total: 30, voters: vec![(30, 30)] }),
1642						(40, Support { total: 40, voters: vec![(40, 40)] })
1643					],
1644					// page 1: 5, 6, 7, 8
1645					vec![
1646						(30, Support { total: 20, voters: vec![(5, 5), (6, 5), (7, 10)] }),
1647						(40, Support { total: 10, voters: vec![(5, 5), (6, 5)] })
1648					],
1649					// page 2: 1, 2, 3, 4
1650					vec![
1651						(30, Support { total: 5, voters: vec![(2, 5)] }),
1652						(40, Support { total: 25, voters: vec![(2, 5), (3, 10), (4, 10)] })
1653					]
1654				]
1655				.try_from_unbounded_paged()
1656				.unwrap()
1657			);
1658
1659			assert_eq!(
1660				paged.score,
1661				ElectionScore { minimal_stake: 55, sum_stake: 130, sum_stake_squared: 8650 }
1662			);
1663		})
1664	}
1665
1666	#[test]
1667	fn mine_solution_choses_most_significant_pages() {
1668		ExtBuilder::mock_signed().pages(2).voter_per_page(4).build_and_execute(|| {
1669			roll_to_snapshot_created();
1670
1671			ensure_voters(2, 8);
1672			ensure_targets(1, 4);
1673
1674			// these folks should be ignored safely.
1675			assert_eq!(
1676				Snapshot::<Runtime>::voters(0)
1677					.unwrap()
1678					.into_iter()
1679					.map(|(x, _, _)| x)
1680					.collect::<Vec<_>>(),
1681				vec![5, 6, 7, 8]
1682			);
1683			// voters in pages 1, this is the most significant page.
1684			assert_eq!(
1685				Snapshot::<Runtime>::voters(1)
1686					.unwrap()
1687					.into_iter()
1688					.map(|(x, _, _)| x)
1689					.collect::<Vec<_>>(),
1690				vec![1, 2, 3, 4]
1691			);
1692
1693			// now we ask for just 1 page of solution.
1694			let paged = mine_solution(1).unwrap();
1695
1696			assert_eq!(
1697				paged.solution_pages,
1698				vec![TestNposSolution {
1699					// voter 1 (index 0) is backing 10 (index 0)
1700					// voter 2 (index 1) is backing 40 (index 3)
1701					// voter 3 (index 2) is backing 40 (index 3)
1702					votes1: vec![(0, 0), (1, 3), (2, 3)],
1703					// voter 4 (index 3) is backing 40 (index 10) and 10 (index 0)
1704					votes2: vec![(3, [(0, PerU16::from_parts(32768))], 3)],
1705					..Default::default()
1706				}]
1707			);
1708
1709			// this solution must be feasible and submittable.
1710			OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1711			// now do a realistic full verification.
1712			load_mock_signed_and_start(paged.clone());
1713			let supports = roll_to_full_verification();
1714
1715			assert_eq!(
1716				supports,
1717				vec![
1718					// page0: non existent.
1719					vec![],
1720					// page1 supports from voters 1, 2, 3, 4
1721					vec![
1722						(10, Support { total: 15, voters: vec![(1, 10), (4, 5)] }),
1723						(40, Support { total: 25, voters: vec![(2, 10), (3, 10), (4, 5)] })
1724					]
1725				]
1726				.try_from_unbounded_paged()
1727				.unwrap()
1728			);
1729
1730			assert_eq!(
1731				paged.score,
1732				ElectionScore { minimal_stake: 15, sum_stake: 40, sum_stake_squared: 850 }
1733			);
1734		})
1735	}
1736
1737	#[test]
1738	fn mine_solution_2_out_of_3_pages() {
1739		ExtBuilder::mock_signed().pages(3).voter_per_page(4).build_and_execute(|| {
1740			roll_to_snapshot_created();
1741
1742			ensure_voters(3, 12);
1743			ensure_targets(1, 4);
1744
1745			assert_eq!(
1746				Snapshot::<Runtime>::voters(0)
1747					.unwrap()
1748					.into_iter()
1749					.map(|(x, _, _)| x)
1750					.collect::<Vec<_>>(),
1751				vec![10, 20, 30, 40]
1752			);
1753			assert_eq!(
1754				Snapshot::<Runtime>::voters(1)
1755					.unwrap()
1756					.into_iter()
1757					.map(|(x, _, _)| x)
1758					.collect::<Vec<_>>(),
1759				vec![5, 6, 7, 8]
1760			);
1761			assert_eq!(
1762				Snapshot::<Runtime>::voters(2)
1763					.unwrap()
1764					.into_iter()
1765					.map(|(x, _, _)| x)
1766					.collect::<Vec<_>>(),
1767				vec![1, 2, 3, 4]
1768			);
1769
1770			// now we ask for just 1 page of solution.
1771			let paged = mine_solution(2).unwrap();
1772
1773			// this solution must be feasible and submittable.
1774			OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1775
1776			assert_eq!(
1777				paged.solution_pages,
1778				vec![
1779					// this can be "pagified" to snapshot at index 1, which contains 5, 6, 7, 8
1780					// in which:
1781					// 6 (index:1) votes for 40 (index:3)
1782					// 8 (index:1) votes for 10 (index:0)
1783					// 5 votes for both 10 and 40
1784					TestNposSolution {
1785						votes1: vec![(1, 3), (3, 0)],
1786						votes2: vec![(0, [(0, PerU16::from_parts(32768))], 3)],
1787						..Default::default()
1788					},
1789					// this can be 'pagified" to snapshot at index 2, which contains 1, 2, 3, 4
1790					// in which:
1791					// 1 (index:0) votes for 10 (index:0)
1792					// 2 (index:1) votes for 40 (index:3)
1793					// 3 (index:2) votes for 40 (index:3)
1794					// 4 votes for both 10 and 40
1795					TestNposSolution {
1796						votes1: vec![(0, 0), (1, 3), (2, 3)],
1797						votes2: vec![(3, [(0, PerU16::from_parts(32768))], 3)],
1798						..Default::default()
1799					}
1800				]
1801			);
1802
1803			// this solution must be feasible and submittable.
1804			OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1805			// now do a realistic full verification.
1806			load_mock_signed_and_start(paged.clone());
1807			let supports = roll_to_full_verification();
1808
1809			assert_eq!(
1810				supports,
1811				vec![
1812					// empty page 0.
1813					vec![],
1814					// supports from voters 5, 6, 7, 8
1815					vec![
1816						(10, Support { total: 15, voters: vec![(5, 5), (8, 10)] }),
1817						(40, Support { total: 15, voters: vec![(5, 5), (6, 10)] })
1818					],
1819					// supports from voters 1, 2, 3, 4
1820					vec![
1821						(10, Support { total: 15, voters: vec![(1, 10), (4, 5)] }),
1822						(40, Support { total: 25, voters: vec![(2, 10), (3, 10), (4, 5)] })
1823					]
1824				]
1825				.try_from_unbounded_paged()
1826				.unwrap()
1827			);
1828
1829			assert_eq!(
1830				paged.score,
1831				ElectionScore { minimal_stake: 30, sum_stake: 70, sum_stake_squared: 2500 }
1832			);
1833		})
1834	}
1835
1836	#[test]
1837	fn can_reduce_solution() {
1838		ExtBuilder::mock_signed().build_and_execute(|| {
1839			roll_to_snapshot_created();
1840			let full_edges = OffchainWorkerMiner::<Runtime>::mine_solution(Pages::get(), false)
1841				.unwrap()
1842				.solution_pages
1843				.iter()
1844				.fold(0, |acc, x| acc + x.edge_count());
1845			let reduced_edges = OffchainWorkerMiner::<Runtime>::mine_solution(Pages::get(), true)
1846				.unwrap()
1847				.solution_pages
1848				.iter()
1849				.fold(0, |acc, x| acc + x.edge_count());
1850
1851			assert!(reduced_edges < full_edges, "{} < {} not fulfilled", reduced_edges, full_edges);
1852		})
1853	}
1854}
1855
1856#[cfg(test)]
1857mod offchain_worker_miner {
1858	use crate::{verifier::Verifier, CommonError};
1859	use frame_support::traits::Hooks;
1860	use sp_runtime::offchain::storage_lock::{BlockAndTime, StorageLock};
1861
1862	use super::*;
1863	use crate::mock::*;
1864
1865	#[test]
1866	fn lock_prevents_frequent_execution() {
1867		let (mut ext, _) = ExtBuilder::mock_signed().build_offchainify();
1868		ext.execute_with_sanity_checks(|| {
1869			let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
1870
1871			// first execution -- okay.
1872			assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(25).is_ok());
1873
1874			// next block: rejected.
1875			assert_noop!(
1876				OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(26),
1877				OffchainMinerError::Lock("recently executed.")
1878			);
1879
1880			// allowed after `OFFCHAIN_REPEAT`
1881			assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(
1882				(26 + offchain_repeat).into()
1883			)
1884			.is_ok());
1885
1886			// a fork like situation: re-execute last 3.
1887			assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(
1888				(26 + offchain_repeat - 3).into()
1889			)
1890			.is_err());
1891			assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(
1892				(26 + offchain_repeat - 2).into()
1893			)
1894			.is_err());
1895			assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(
1896				(26 + offchain_repeat - 1).into()
1897			)
1898			.is_err());
1899		})
1900	}
1901
1902	#[test]
1903	fn lock_released_after_successful_execution() {
1904		// first, ensure that a successful execution releases the lock
1905		let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
1906		ext.execute_with_sanity_checks(|| {
1907			let guard = StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_LOCK);
1908			let last_block =
1909				StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_LAST_BLOCK);
1910
1911			roll_to_unsigned_open();
1912
1913			// initially, the lock is not set.
1914			assert!(guard.get::<bool>().unwrap().is_none());
1915
1916			// a successful a-z execution.
1917			UnsignedPallet::offchain_worker(25);
1918			assert_eq!(pool.read().transactions.len(), 1);
1919
1920			// afterwards, the lock is not set either..
1921			assert!(guard.get::<bool>().unwrap().is_none());
1922			assert_eq!(last_block.get::<BlockNumber>().unwrap(), Some(25));
1923		});
1924	}
1925
1926	#[test]
1927	fn lock_prevents_overlapping_execution() {
1928		// ensure that if the guard is in hold, a new execution is not allowed.
1929		let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
1930		ext.execute_with_sanity_checks(|| {
1931			roll_to_unsigned_open();
1932
1933			// artificially set the value, as if another thread is mid-way.
1934			let mut lock = StorageLock::<BlockAndTime<System>>::with_block_deadline(
1935				OffchainWorkerMiner::<Runtime>::OFFCHAIN_LOCK,
1936				UnsignedPhase::get().saturated_into(),
1937			);
1938			let guard = lock.lock();
1939
1940			// nothing submitted.
1941			UnsignedPallet::offchain_worker(25);
1942			assert_eq!(pool.read().transactions.len(), 0);
1943			UnsignedPallet::offchain_worker(26);
1944			assert_eq!(pool.read().transactions.len(), 0);
1945
1946			drop(guard);
1947
1948			// ๐ŸŽ‰ !
1949			UnsignedPallet::offchain_worker(25);
1950			assert_eq!(pool.read().transactions.len(), 1);
1951		});
1952	}
1953
1954	#[test]
1955	fn initial_ocw_runs_and_saves_new_cache() {
1956		let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
1957		ext.execute_with_sanity_checks(|| {
1958			roll_to_unsigned_open();
1959
1960			let last_block =
1961				StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_LAST_BLOCK);
1962			let cache =
1963				StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
1964
1965			assert_eq!(last_block.get::<BlockNumber>(), Ok(None));
1966			assert_eq!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
1967
1968			// creates, caches, submits without expecting previous cache value
1969			UnsignedPallet::offchain_worker(25);
1970			assert_eq!(pool.read().transactions.len(), 1);
1971
1972			assert_eq!(last_block.get::<BlockNumber>(), Ok(Some(25)));
1973			assert!(matches!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
1974		})
1975	}
1976
1977	#[test]
1978	fn ocw_pool_submission_works() {
1979		let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
1980		ext.execute_with_sanity_checks(|| {
1981			roll_to_unsigned_open();
1982
1983			roll_next_with_ocw(Some(pool.clone()));
1984			// OCW must have submitted now
1985
1986			let encoded = pool.read().transactions[0].clone();
1987			let extrinsic: Extrinsic = codec::Decode::decode(&mut &*encoded).unwrap();
1988			let call = extrinsic.function;
1989			assert!(matches!(
1990				call,
1991				crate::mock::RuntimeCall::UnsignedPallet(
1992					crate::unsigned::Call::submit_unsigned { .. }
1993				)
1994			));
1995		})
1996	}
1997
1998	#[test]
1999	fn resubmits_after_offchain_repeat() {
2000		let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
2001		ext.execute_with_sanity_checks(|| {
2002			let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2003			roll_to_unsigned_open();
2004
2005			assert!(OffchainWorkerMiner::<Runtime>::cached_solution().is_none());
2006			// creates, caches, submits without expecting previous cache value
2007			UnsignedPallet::offchain_worker(25);
2008			assert_eq!(pool.read().transactions.len(), 1);
2009			let tx_cache = pool.read().transactions[0].clone();
2010			// assume that the tx has been processed
2011			pool.try_write().unwrap().transactions.clear();
2012
2013			// attempts to resubmit the tx after the threshold has expired.
2014			UnsignedPallet::offchain_worker(25 + 1 + offchain_repeat);
2015			assert_eq!(pool.read().transactions.len(), 1);
2016
2017			// resubmitted tx is identical to first submission
2018			let tx = &pool.read().transactions[0];
2019			assert_eq!(&tx_cache, tx);
2020		})
2021	}
2022
2023	#[test]
2024	fn regenerates_and_resubmits_after_offchain_repeat_if_no_cache() {
2025		let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
2026		ext.execute_with_sanity_checks(|| {
2027			let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2028			roll_to_unsigned_open();
2029
2030			assert!(OffchainWorkerMiner::<Runtime>::cached_solution().is_none());
2031			// creates, caches, submits without expecting previous cache value.
2032			UnsignedPallet::offchain_worker(25);
2033			assert_eq!(pool.read().transactions.len(), 1);
2034			let tx_cache = pool.read().transactions[0].clone();
2035			// assume that the tx has been processed
2036			pool.try_write().unwrap().transactions.clear();
2037
2038			// remove the cached submitted tx.
2039			// this ensures that when the resubmit window rolls around, we're ready to regenerate
2040			// from scratch if necessary
2041			let mut call_cache =
2042				StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
2043			assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2044			call_cache.clear();
2045
2046			// attempts to resubmit the tx after the threshold has expired
2047			UnsignedPallet::offchain_worker(25 + 1 + offchain_repeat);
2048			assert_eq!(pool.read().transactions.len(), 1);
2049
2050			// resubmitted tx is identical to first submission
2051			let tx = &pool.read().transactions[0];
2052			assert_eq!(&tx_cache, tx);
2053		})
2054	}
2055
2056	#[test]
2057	fn altering_snapshot_invalidates_solution_cache() {
2058		// by infeasible, we mean here that if the snapshot fingerprint has changed.
2059		let (mut ext, pool) = ExtBuilder::mock_signed().unsigned_phase(999).build_offchainify();
2060		ext.execute_with_sanity_checks(|| {
2061			let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2062			roll_to_unsigned_open();
2063			roll_next_with_ocw(None);
2064
2065			// something is submitted..
2066			assert_eq!(pool.read().transactions.len(), 1);
2067			pool.try_write().unwrap().transactions.clear();
2068
2069			// ..and cached
2070			let call_cache =
2071				StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
2072			assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2073
2074			// now change the snapshot, ofc this is rare in reality. This makes the cached call
2075			// infeasible.
2076			assert_eq!(crate::Snapshot::<Runtime>::targets().unwrap(), vec![10, 20, 30, 40]);
2077			let pre_fingerprint = crate::Snapshot::<Runtime>::fingerprint();
2078			crate::Snapshot::<Runtime>::remove_target(0);
2079			let post_fingerprint = crate::Snapshot::<Runtime>::fingerprint();
2080			assert_eq!(crate::Snapshot::<Runtime>::targets().unwrap(), vec![20, 30, 40]);
2081			assert_ne!(pre_fingerprint, post_fingerprint);
2082
2083			// now run ocw again
2084			let now = System::block_number();
2085			roll_to_with_ocw(now + offchain_repeat + 1, None);
2086			// nothing is submitted this time..
2087			assert_eq!(pool.read().transactions.len(), 0);
2088			// .. and the cache is gone.
2089			assert_eq!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
2090
2091			// upon the next run, we re-generate and submit something fresh again.
2092			roll_to_with_ocw(now + offchain_repeat + offchain_repeat + 2, None);
2093			assert_eq!(pool.read().transactions.len(), 1);
2094			assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2095		})
2096	}
2097
2098	#[test]
2099	fn wont_resubmit_if_weak_score() {
2100		// common case, if the score is weak, don't bother with anything, ideally check from the
2101		// logs that we don't run feasibility in this call path. Score check must come before.
2102		let (mut ext, pool) = ExtBuilder::mock_signed().unsigned_phase(999).build_offchainify();
2103		ext.execute_with_sanity_checks(|| {
2104			let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2105			// unfortunately there's no pretty way to run the ocw code such that it generates a
2106			// weak, but correct solution. We just write it to cache directly.
2107			roll_to_unsigned_open();
2108			roll_next_with_ocw(None);
2109
2110			// something is submitted..
2111			assert_eq!(pool.read().transactions.len(), 1);
2112
2113			// ..and cached
2114			let call_cache =
2115				StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
2116			assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2117
2118			// and replace it with something weak.
2119			let weak_solution = raw_paged_from_supports(
2120				vec![vec![(40, Support { total: 10, voters: vec![(3, 10)] })]],
2121				0,
2122			);
2123			let weak_call = crate::unsigned::Call::<T>::submit_unsigned {
2124				paged_solution: Box::new(weak_solution),
2125			};
2126			call_cache.set(&weak_call);
2127
2128			// run again
2129			roll_to_with_ocw(System::block_number() + offchain_repeat + 1, Some(pool.clone()));
2130			// nothing is submitted this time..
2131			assert_eq!(pool.read().transactions.len(), 0);
2132			// .. and the cache IS STILL THERE!
2133			assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2134		})
2135	}
2136
2137	#[test]
2138	fn ocw_submission_e2e_works() {
2139		let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
2140		ext.execute_with_sanity_checks(|| {
2141			assert!(VerifierPallet::queued_score().is_none());
2142			roll_to_with_ocw(25 + 1, Some(pool.clone()));
2143			assert!(VerifierPallet::queued_score().is_some());
2144
2145			// call is cached.
2146			let call_cache =
2147				StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
2148			assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2149
2150			// pool is empty
2151			assert_eq!(pool.read().transactions.len(), 0);
2152		})
2153	}
2154
2155	#[test]
2156	fn ocw_e2e_submits_and_queued_msp_only() {
2157		let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
2158		ext.execute_with_sanity_checks(|| {
2159			// roll to mine
2160			roll_to_unsigned_open_with_ocw(None);
2161			// one block to verify and submit.
2162			roll_next_with_ocw(Some(pool.clone()));
2163
2164			assert_eq!(
2165				multi_block_events(),
2166				vec![
2167					crate::Event::PhaseTransitioned {
2168						from: Phase::Off,
2169						to: Phase::Snapshot(Pages::get())
2170					},
2171					crate::Event::PhaseTransitioned {
2172						from: Phase::Snapshot(0),
2173						to: Phase::Unsigned(UnsignedPhase::get() - 1)
2174					}
2175				]
2176			);
2177			assert_eq!(
2178				verifier_events(),
2179				vec![
2180					crate::verifier::Event::Verified(2, 2),
2181					crate::verifier::Event::Queued(
2182						ElectionScore { minimal_stake: 15, sum_stake: 40, sum_stake_squared: 850 },
2183						None
2184					)
2185				]
2186			);
2187			assert!(VerifierPallet::queued_score().is_some());
2188
2189			// pool is empty
2190			assert_eq!(pool.read().transactions.len(), 0);
2191		})
2192	}
2193
2194	#[test]
2195	fn multi_page_ocw_e2e_submits_and_queued_msp_only() {
2196		let (mut ext, pool) = ExtBuilder::mock_signed().miner_pages(2).build_offchainify();
2197		ext.execute_with_sanity_checks(|| {
2198			// roll to mine
2199			roll_to_unsigned_open_with_ocw(None);
2200			// one block to verify and submit.
2201			roll_next_with_ocw(Some(pool.clone()));
2202
2203			assert_eq!(
2204				multi_block_events(),
2205				vec![
2206					crate::Event::PhaseTransitioned {
2207						from: Phase::Off,
2208						to: Phase::Snapshot(Pages::get())
2209					},
2210					crate::Event::PhaseTransitioned {
2211						from: Phase::Snapshot(0),
2212						to: Phase::Unsigned(UnsignedPhase::get() - 1)
2213					}
2214				]
2215			);
2216			assert_eq!(
2217				verifier_events(),
2218				vec![
2219					crate::verifier::Event::Verified(1, 2),
2220					crate::verifier::Event::Verified(2, 2),
2221					crate::verifier::Event::Queued(
2222						ElectionScore { minimal_stake: 30, sum_stake: 70, sum_stake_squared: 2500 },
2223						None
2224					)
2225				]
2226			);
2227			assert!(VerifierPallet::queued_score().is_some());
2228
2229			// pool is empty
2230			assert_eq!(pool.read().transactions.len(), 0);
2231		})
2232	}
2233
2234	#[test]
2235	fn full_multi_page_ocw_e2e_submits_and_queued_msp_only() {
2236		let (mut ext, pool) = ExtBuilder::mock_signed().miner_pages(3).build_offchainify();
2237		ext.execute_with_sanity_checks(|| {
2238			// roll to mine
2239			roll_to_unsigned_open_with_ocw(None);
2240			// one block to verify and submit.
2241			roll_next_with_ocw(Some(pool.clone()));
2242
2243			assert_eq!(
2244				multi_block_events(),
2245				vec![
2246					crate::Event::PhaseTransitioned {
2247						from: Phase::Off,
2248						to: Phase::Snapshot(Pages::get())
2249					},
2250					crate::Event::PhaseTransitioned {
2251						from: Phase::Snapshot(0),
2252						to: Phase::Unsigned(UnsignedPhase::get() - 1)
2253					}
2254				]
2255			);
2256			assert_eq!(
2257				verifier_events(),
2258				vec![
2259					crate::verifier::Event::Verified(0, 2),
2260					crate::verifier::Event::Verified(1, 2),
2261					crate::verifier::Event::Verified(2, 2),
2262					crate::verifier::Event::Queued(
2263						ElectionScore {
2264							minimal_stake: 55,
2265							sum_stake: 130,
2266							sum_stake_squared: 8650
2267						},
2268						None
2269					)
2270				]
2271			);
2272			assert!(VerifierPallet::queued_score().is_some());
2273
2274			// pool is empty
2275			assert_eq!(pool.read().transactions.len(), 0);
2276		})
2277	}
2278
2279	#[test]
2280	fn will_not_mine_if_not_enough_winners() {
2281		// also see `trim_weight_too_much_makes_solution_invalid`.
2282		let (mut ext, _) = ExtBuilder::mock_signed().desired_targets(77).build_offchainify();
2283		ext.execute_with_sanity_checks(|| {
2284			roll_to_unsigned_open();
2285			ensure_voters(3, 12);
2286
2287			// beautiful errors, isn't it?
2288			assert_eq!(
2289				OffchainWorkerMiner::<Runtime>::mine_checked_call().unwrap_err(),
2290				OffchainMinerError::Common(CommonError::WrongWinnerCount)
2291			);
2292		});
2293	}
2294
2295	mod no_storage {
2296		use super::*;
2297		#[test]
2298		fn ocw_never_uses_cache_on_initial_run_or_resubmission() {
2299			// When `T::OffchainStorage` is false, the offchain worker should never use cache:
2300			// - Initial run: mines and submits without caching
2301			// - Resubmission: re-mines fresh solution instead of restoring from cache
2302			let (mut ext, pool) =
2303				ExtBuilder::mock_signed().offchain_storage(false).build_offchainify();
2304			ext.execute_with_sanity_checks(|| {
2305				let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2306				roll_to_unsigned_open();
2307
2308				let last_block = StorageValueRef::persistent(
2309					&OffchainWorkerMiner::<Runtime>::OFFCHAIN_LAST_BLOCK,
2310				);
2311				let cache = StorageValueRef::persistent(
2312					&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL,
2313				);
2314
2315				// Initial state: no previous runs
2316				assert_eq!(last_block.get::<BlockNumber>(), Ok(None));
2317				assert_eq!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
2318
2319				// First run: mines and submits without caching
2320				UnsignedPallet::offchain_worker(25);
2321				assert_eq!(pool.read().transactions.len(), 1);
2322				let first_tx = pool.read().transactions[0].clone();
2323
2324				// Verify no cache is created or used
2325				assert_eq!(last_block.get::<BlockNumber>(), Ok(Some(25)));
2326				assert_eq!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
2327
2328				// Clear the pool to simulate transaction processing
2329				pool.try_write().unwrap().transactions.clear();
2330
2331				// Second run after repeat threshold: should re-mine instead of using cache
2332				UnsignedPallet::offchain_worker(25 + 1 + offchain_repeat);
2333				assert_eq!(pool.read().transactions.len(), 1);
2334				let second_tx = pool.read().transactions[0].clone();
2335
2336				// Verify still no cache is used throughout the process
2337				assert_eq!(last_block.get::<BlockNumber>(), Ok(Some(25 + 1 + offchain_repeat)));
2338				assert_eq!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
2339
2340				// Both transactions should be identical since the snapshot hasn't changed,
2341				// but they were generated independently (no cache reuse)
2342				assert_eq!(first_tx, second_tx);
2343			})
2344		}
2345	}
2346}