referrerpolicy=no-referrer-when-downgrade

pallet_election_provider_multi_phase/
unsigned.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 unsigned phase, and its miner.
19
20use crate::{
21	helpers, Call, Config, CurrentPhase, DesiredTargets, ElectionCompute, Error, FeasibilityError,
22	Pallet, QueuedSolution, RawSolution, ReadySolution, ReadySolutionOf, Round, RoundSnapshot,
23	Snapshot, SolutionAccuracyOf, SolutionOf, SolutionOrSnapshotSize, Weight,
24};
25use alloc::{boxed::Box, vec::Vec};
26use codec::Encode;
27use frame_election_provider_support::{NposSolution, NposSolver, PerThing128, VoteWeight};
28use frame_support::{
29	dispatch::DispatchResult,
30	ensure,
31	traits::{DefensiveResult, Get},
32	BoundedVec,
33};
34use frame_system::{
35	offchain::{CreateBare, SubmitTransaction},
36	pallet_prelude::BlockNumberFor,
37};
38use scale_info::TypeInfo;
39use sp_npos_elections::{
40	assignment_ratio_to_staked_normalized, assignment_staked_to_ratio_normalized, ElectionResult,
41	ElectionScore, EvaluateSupport,
42};
43use sp_runtime::{
44	offchain::storage::{MutateStorageError, StorageValueRef},
45	DispatchError, SaturatedConversion,
46};
47
48/// Storage key used to store the last block number at which offchain worker ran.
49pub(crate) const OFFCHAIN_LAST_BLOCK: &[u8] = b"parity/multi-phase-unsigned-election";
50/// Storage key used to store the offchain worker running status.
51pub(crate) const OFFCHAIN_LOCK: &[u8] = b"parity/multi-phase-unsigned-election/lock";
52
53/// Storage key used to cache the solution `call`.
54pub(crate) const OFFCHAIN_CACHED_CALL: &[u8] = b"parity/multi-phase-unsigned-election/call";
55
56/// A voter's fundamental data: their ID, their stake, and the list of candidates for whom they
57/// voted.
58pub type VoterOf<T> = frame_election_provider_support::VoterOf<<T as Config>::DataProvider>;
59
60/// Same as [`VoterOf`], but parameterized by the `MinerConfig`.
61pub type MinerVoterOf<T> = frame_election_provider_support::Voter<
62	<T as MinerConfig>::AccountId,
63	<T as MinerConfig>::MaxVotesPerVoter,
64>;
65
66/// The relative distribution of a voter's stake among the winning targets.
67pub type Assignment<T> =
68	sp_npos_elections::Assignment<<T as frame_system::Config>::AccountId, SolutionAccuracyOf<T>>;
69
70/// The [`IndexAssignment`][frame_election_provider_support::IndexAssignment] type specialized for a
71/// particular runtime `T`.
72pub type IndexAssignmentOf<T> = frame_election_provider_support::IndexAssignmentOf<SolutionOf<T>>;
73
74/// Error type of the pallet's [`crate::Config::Solver`].
75pub type SolverErrorOf<T> = <<T as Config>::Solver as NposSolver>::Error;
76/// Error type for operations related to the OCW npos solution miner.
77#[derive(frame_support::DebugNoBound, frame_support::PartialEqNoBound)]
78pub enum MinerError {
79	/// An internal error in the NPoS elections crate.
80	NposElections(sp_npos_elections::Error),
81	/// Snapshot data was unavailable unexpectedly.
82	SnapshotUnAvailable,
83	/// Submitting a transaction to the pool failed.
84	PoolSubmissionFailed,
85	/// The pre-dispatch checks failed for the mined solution.
86	PreDispatchChecksFailed(DispatchError),
87	/// The solution generated from the miner is not feasible.
88	Feasibility(FeasibilityError),
89	/// Something went wrong fetching the lock.
90	Lock(&'static str),
91	/// Cannot restore a solution that was not stored.
92	NoStoredSolution,
93	/// Cached solution is not a `submit_unsigned` call.
94	SolutionCallInvalid,
95	/// Failed to store a solution.
96	FailedToStoreSolution,
97	/// There are no more voters to remove to trim the solution.
98	NoMoreVoters,
99	/// An error from the solver.
100	Solver,
101	/// Desired targets are mire than the maximum allowed winners.
102	TooManyDesiredTargets,
103}
104
105impl From<sp_npos_elections::Error> for MinerError {
106	fn from(e: sp_npos_elections::Error) -> Self {
107		MinerError::NposElections(e)
108	}
109}
110
111impl From<FeasibilityError> for MinerError {
112	fn from(e: FeasibilityError) -> Self {
113		MinerError::Feasibility(e)
114	}
115}
116
117/// Reports the trimming result of a mined solution.
118#[derive(Debug, Clone)]
119pub struct TrimmingStatus {
120	/// Number of voters trimmed due to the solution weight limits.
121	weight: usize,
122	/// Number of voters trimmed due to the solution length limits.
123	length: usize,
124	/// Number of edges (voter -> target) trimmed due to the max backers per winner bound.
125	edges: usize,
126}
127
128impl TrimmingStatus {
129	pub fn is_trimmed(&self) -> bool {
130		self.weight > 0 || self.length > 0 || self.edges > 0
131	}
132
133	pub fn trimmed_weight(&self) -> usize {
134		self.weight
135	}
136
137	pub fn trimmed_length(&self) -> usize {
138		self.length
139	}
140
141	pub fn trimmed_edges(&self) -> usize {
142		self.edges
143	}
144}
145
146/// Save a given call into OCW storage.
147fn save_solution<T: Config>(call: &Call<T>) -> Result<(), MinerError> {
148	log!(debug, "saving a call to the offchain storage.");
149	let storage = StorageValueRef::persistent(OFFCHAIN_CACHED_CALL);
150	match storage.mutate::<_, (), _>(|_| Ok(call.clone())) {
151		Ok(_) => Ok(()),
152		Err(MutateStorageError::ConcurrentModification(_)) =>
153			Err(MinerError::FailedToStoreSolution),
154		Err(MutateStorageError::ValueFunctionFailed(_)) => {
155			// this branch should be unreachable according to the definition of
156			// `StorageValueRef::mutate`: that function should only ever `Err` if the closure we
157			// pass it returns an error. however, for safety in case the definition changes, we do
158			// not optimize the branch away or panic.
159			Err(MinerError::FailedToStoreSolution)
160		},
161	}
162}
163
164/// Get a saved solution from OCW storage if it exists.
165fn restore_solution<T: Config>() -> Result<Call<T>, MinerError> {
166	StorageValueRef::persistent(OFFCHAIN_CACHED_CALL)
167		.get()
168		.ok()
169		.flatten()
170		.ok_or(MinerError::NoStoredSolution)
171}
172
173/// Clear a saved solution from OCW storage.
174pub(super) fn kill_ocw_solution<T: Config>() {
175	log!(debug, "clearing offchain call cache storage.");
176	let mut storage = StorageValueRef::persistent(OFFCHAIN_CACHED_CALL);
177	storage.clear();
178}
179
180/// Clear the offchain repeat storage.
181///
182/// After calling this, the next offchain worker is guaranteed to work, with respect to the
183/// frequency repeat.
184fn clear_offchain_repeat_frequency() {
185	let mut last_block = StorageValueRef::persistent(OFFCHAIN_LAST_BLOCK);
186	last_block.clear();
187}
188
189/// `true` when OCW storage contains a solution
190#[cfg(test)]
191fn ocw_solution_exists<T: Config>() -> bool {
192	matches!(StorageValueRef::persistent(OFFCHAIN_CACHED_CALL).get::<Call<T>>(), Ok(Some(_)))
193}
194
195impl<T: Config + CreateBare<Call<T>>> Pallet<T> {
196	/// Mine a new npos solution.
197	///
198	/// The Npos Solver type, `S`, must have the same AccountId and Error type as the
199	/// [`crate::Config::Solver`] in order to create a unified return type.
200	pub fn mine_solution() -> Result<
201		(RawSolution<SolutionOf<T::MinerConfig>>, SolutionOrSnapshotSize, TrimmingStatus),
202		MinerError,
203	> {
204		let RoundSnapshot { voters, targets } =
205			Snapshot::<T>::get().ok_or(MinerError::SnapshotUnAvailable)?;
206		let desired_targets = DesiredTargets::<T>::get().ok_or(MinerError::SnapshotUnAvailable)?;
207		ensure!(desired_targets <= T::MaxWinners::get(), MinerError::TooManyDesiredTargets);
208		let (solution, score, size, is_trimmed) =
209			Miner::<T::MinerConfig>::mine_solution_with_snapshot::<T::Solver>(
210				voters,
211				targets,
212				desired_targets,
213			)?;
214		let round = Round::<T>::get();
215		Ok((RawSolution { solution, score, round }, size, is_trimmed))
216	}
217
218	/// Attempt to restore a solution from cache. Otherwise, compute it fresh. Either way, submit
219	/// if our call's score is greater than that of the cached solution.
220	pub fn restore_or_compute_then_maybe_submit() -> Result<(), MinerError> {
221		log!(debug, "miner attempting to restore or compute an unsigned solution.");
222
223		let call = restore_solution::<T>()
224			.and_then(|call| {
225				// ensure the cached call is still current before submitting
226				if let Call::submit_unsigned { raw_solution, .. } = &call {
227					// prevent errors arising from state changes in a forkful chain
228					Self::basic_checks(raw_solution, "restored")?;
229					Ok(call)
230				} else {
231					Err(MinerError::SolutionCallInvalid)
232				}
233			})
234			.or_else::<MinerError, _>(|error| {
235				log!(debug, "restoring solution failed due to {:?}", error);
236				match error {
237					MinerError::NoStoredSolution => {
238						log!(trace, "mining a new solution.");
239						// if not present or cache invalidated due to feasibility, regenerate.
240						// note that failing `Feasibility` can only mean that the solution was
241						// computed over a snapshot that has changed due to a fork.
242						let call = Self::mine_checked_call()?;
243						save_solution(&call)?;
244						Ok(call)
245					},
246					MinerError::Feasibility(_) => {
247						log!(trace, "wiping infeasible solution.");
248						// kill the infeasible solution, hopefully in the next runs (whenever they
249						// may be) we mine a new one.
250						kill_ocw_solution::<T>();
251						clear_offchain_repeat_frequency();
252						Err(error)
253					},
254					_ => {
255						// nothing to do. Return the error as-is.
256						Err(error)
257					},
258				}
259			})?;
260
261		Self::submit_call(call)
262	}
263
264	/// Mine a new solution, cache it, and submit it back to the chain as an unsigned transaction.
265	pub fn mine_check_save_submit() -> Result<(), MinerError> {
266		log!(debug, "miner attempting to compute an unsigned solution.");
267
268		let call = Self::mine_checked_call()?;
269		save_solution(&call)?;
270		Self::submit_call(call)
271	}
272
273	/// Mine a new solution as a call. Performs all checks.
274	pub fn mine_checked_call() -> Result<Call<T>, MinerError> {
275		// get the solution, with a load of checks to ensure if submitted, IT IS ABSOLUTELY VALID.
276		let (raw_solution, witness, _trimming) = Self::mine_and_check()?;
277
278		let score = raw_solution.score;
279		let call: Call<T> = Call::submit_unsigned { raw_solution: Box::new(raw_solution), witness };
280
281		log!(
282			debug,
283			"mined a solution with score {:?} and size {} and trimming {:?}",
284			score,
285			call.using_encoded(|b| b.len()),
286			_trimming
287		);
288
289		Ok(call)
290	}
291
292	fn submit_call(call: Call<T>) -> Result<(), MinerError> {
293		log!(debug, "miner submitting a solution as an unsigned transaction");
294
295		let xt = T::create_bare(call.into());
296		SubmitTransaction::<T, Call<T>>::submit_transaction(xt)
297			.map_err(|_| MinerError::PoolSubmissionFailed)
298	}
299
300	// perform basic checks of a solution's validity
301	//
302	// Performance: note that it internally clones the provided solution.
303	pub fn basic_checks(
304		raw_solution: &RawSolution<SolutionOf<T::MinerConfig>>,
305		solution_type: &str,
306	) -> Result<(), MinerError> {
307		Self::unsigned_pre_dispatch_checks(raw_solution).map_err(|err| {
308			log!(debug, "pre-dispatch checks failed for {} solution: {:?}", solution_type, err);
309			MinerError::PreDispatchChecksFailed(err)
310		})?;
311
312		Self::feasibility_check(raw_solution.clone(), ElectionCompute::Unsigned).map_err(
313			|err| {
314				log!(debug, "feasibility check failed for {} solution: {:?}", solution_type, err);
315				err
316			},
317		)?;
318
319		Ok(())
320	}
321
322	/// Mine a new npos solution, with all the relevant checks to make sure that it will be accepted
323	/// to the chain.
324	///
325	/// If you want an unchecked solution, use [`Pallet::mine_solution`].
326	/// If you want a checked solution and submit it at the same time, use
327	/// [`Pallet::mine_check_save_submit`].
328	pub fn mine_and_check() -> Result<
329		(RawSolution<SolutionOf<T::MinerConfig>>, SolutionOrSnapshotSize, TrimmingStatus),
330		MinerError,
331	> {
332		let (raw_solution, witness, is_trimmed) = Self::mine_solution()?;
333		Self::basic_checks(&raw_solution, "mined")?;
334		Ok((raw_solution, witness, is_trimmed))
335	}
336
337	/// Checks if an execution of the offchain worker is permitted at the given block number, or
338	/// not.
339	///
340	/// This makes sure that
341	/// 1. we don't run on previous blocks in case of a re-org
342	/// 2. we don't run twice within a window of length `T::OffchainRepeat`.
343	///
344	/// Returns `Ok(())` if offchain worker limit is respected, `Err(reason)` otherwise. If `Ok()`
345	/// is returned, `now` is written in storage and will be used in further calls as the baseline.
346	pub fn ensure_offchain_repeat_frequency(now: BlockNumberFor<T>) -> Result<(), MinerError> {
347		let threshold = T::OffchainRepeat::get();
348		let last_block = StorageValueRef::persistent(OFFCHAIN_LAST_BLOCK);
349
350		let mutate_stat = last_block.mutate::<_, &'static str, _>(
351			|maybe_head: Result<Option<BlockNumberFor<T>>, _>| {
352				match maybe_head {
353					Ok(Some(head)) if now < head => Err("fork."),
354					Ok(Some(head)) if now >= head && now <= head + threshold =>
355						Err("recently executed."),
356					Ok(Some(head)) if now > head + threshold => {
357						// we can run again now. Write the new head.
358						Ok(now)
359					},
360					_ => {
361						// value doesn't exists. Probably this node just booted up. Write, and run
362						Ok(now)
363					},
364				}
365			},
366		);
367
368		match mutate_stat {
369			// all good
370			Ok(_) => Ok(()),
371			// failed to write.
372			Err(MutateStorageError::ConcurrentModification(_)) =>
373				Err(MinerError::Lock("failed to write to offchain db (concurrent modification).")),
374			// fork etc.
375			Err(MutateStorageError::ValueFunctionFailed(why)) => Err(MinerError::Lock(why)),
376		}
377	}
378
379	/// Do the basics checks that MUST happen during the validation and pre-dispatch of an unsigned
380	/// transaction.
381	///
382	/// Can optionally also be called during dispatch, if needed.
383	///
384	/// NOTE: Ideally, these tests should move more and more outside of this and more to the miner's
385	/// code, so that we do less and less storage reads here.
386	pub fn unsigned_pre_dispatch_checks(
387		raw_solution: &RawSolution<SolutionOf<T::MinerConfig>>,
388	) -> DispatchResult {
389		// ensure solution is timely. Don't panic yet. This is a cheap check.
390		ensure!(
391			CurrentPhase::<T>::get().is_unsigned_open(),
392			Error::<T>::PreDispatchEarlySubmission
393		);
394
395		// ensure round is current
396		ensure!(Round::<T>::get() == raw_solution.round, Error::<T>::OcwCallWrongEra);
397
398		// ensure correct number of winners.
399		ensure!(
400			DesiredTargets::<T>::get().unwrap_or_default() ==
401				raw_solution.solution.unique_targets().len() as u32,
402			Error::<T>::PreDispatchWrongWinnerCount,
403		);
404
405		// ensure score is being improved. Panic henceforth.
406		ensure!(
407			QueuedSolution::<T>::get()
408				.map_or(true, |q: ReadySolution<_, _, _>| raw_solution.score > q.score),
409			Error::<T>::PreDispatchWeakSubmission,
410		);
411
412		Ok(())
413	}
414}
415
416/// Configurations for a miner that comes with this pallet.
417pub trait MinerConfig {
418	/// The account id type.
419	type AccountId: Ord + Clone + codec::Codec + core::fmt::Debug;
420	/// The solution that the miner is mining.
421	type Solution: codec::Codec
422		+ Default
423		+ PartialEq
424		+ Eq
425		+ Clone
426		+ core::fmt::Debug
427		+ Ord
428		+ NposSolution
429		+ TypeInfo;
430	/// Maximum number of votes per voter in the snapshots.
431	type MaxVotesPerVoter;
432	/// Maximum length of the solution that the miner is allowed to generate.
433	///
434	/// Solutions are trimmed to respect this.
435	type MaxLength: Get<u32>;
436	/// Maximum weight of the solution that the miner is allowed to generate.
437	///
438	/// Solutions are trimmed to respect this.
439	///
440	/// The weight is computed using `solution_weight`.
441	type MaxWeight: Get<Weight>;
442	/// The maximum number of winners that can be elected in the single page supported by this
443	/// pallet.
444	type MaxWinners: Get<u32>;
445	/// The maximum number of backers per winner in the last solution.
446	type MaxBackersPerWinner: Get<u32>;
447	/// Something that can compute the weight of a solution.
448	///
449	/// This weight estimate is then used to trim the solution, based on [`MinerConfig::MaxWeight`].
450	fn solution_weight(voters: u32, targets: u32, active_voters: u32, degree: u32) -> Weight;
451}
452
453/// A base miner, suitable to be used for both signed and unsigned submissions.
454pub struct Miner<T: MinerConfig>(core::marker::PhantomData<T>);
455impl<T: MinerConfig> Miner<T> {
456	/// Same as [`Pallet::mine_solution`], but the input snapshot data must be given.
457	pub fn mine_solution_with_snapshot<S>(
458		voters: Vec<(T::AccountId, VoteWeight, BoundedVec<T::AccountId, T::MaxVotesPerVoter>)>,
459		targets: Vec<T::AccountId>,
460		desired_targets: u32,
461	) -> Result<(SolutionOf<T>, ElectionScore, SolutionOrSnapshotSize, TrimmingStatus), MinerError>
462	where
463		S: NposSolver<AccountId = T::AccountId>,
464	{
465		S::solve(desired_targets as usize, targets.clone(), voters.clone())
466			.map_err(|e| {
467				log_no_system!(error, "solver error: {:?}", e);
468				MinerError::Solver
469			})
470			.and_then(|e| {
471				Self::prepare_election_result_with_snapshot::<S::Accuracy>(
472					e,
473					voters,
474					targets,
475					desired_targets,
476				)
477			})
478	}
479
480	/// Convert a raw solution from [`sp_npos_elections::ElectionResult`] to [`RawSolution`], which
481	/// is ready to be submitted to the chain.
482	///
483	/// Will always reduce the solution as well.
484	pub fn prepare_election_result_with_snapshot<Accuracy: PerThing128>(
485		election_result: ElectionResult<T::AccountId, Accuracy>,
486		voters: Vec<(T::AccountId, VoteWeight, BoundedVec<T::AccountId, T::MaxVotesPerVoter>)>,
487		targets: Vec<T::AccountId>,
488		desired_targets: u32,
489	) -> Result<(SolutionOf<T>, ElectionScore, SolutionOrSnapshotSize, TrimmingStatus), MinerError>
490	{
491		// now make some helper closures.
492		let cache = helpers::generate_voter_cache::<T>(&voters);
493		let voter_index = helpers::voter_index_fn::<T>(&cache);
494		let target_index = helpers::target_index_fn::<T>(&targets);
495		let voter_at = helpers::voter_at_fn::<T>(&voters);
496		let target_at = helpers::target_at_fn::<T>(&targets);
497		let stake_of = helpers::stake_of_fn::<T>(&voters, &cache);
498
499		// Compute the size of a solution comprised of the selected arguments.
500		//
501		// This function completes in `O(edges)`; it's expensive, but linear.
502		let encoded_size_of = |assignments: &[IndexAssignmentOf<T>]| {
503			SolutionOf::<T>::try_from(assignments).map(|s| s.encoded_size())
504		};
505
506		let ElectionResult { assignments, winners: _ } = election_result;
507
508		// keeps track of how many edges were trimmed out.
509		let mut edges_trimmed = 0;
510
511		// Reduce (requires round-trip to staked form) and ensures the max backer per winner bound
512		// requirements are met.
513		let sorted_assignments = {
514			// convert to staked and reduce.
515			let mut staked = assignment_ratio_to_staked_normalized(assignments, &stake_of)?;
516
517			// we reduce before sorting in order to ensure that the reduction process doesn't
518			// accidentally change the sort order
519			sp_npos_elections::reduce(&mut staked);
520
521			// Sort the assignments by reversed voter stake. This ensures that we can efficiently
522			// truncate the list.
523			staked.sort_by_key(
524				|sp_npos_elections::StakedAssignment::<T::AccountId> { who, .. }| {
525					// though staked assignments are expressed in terms of absolute stake, we'd
526					// still need to iterate over all votes in order to actually compute the total
527					// stake. it should be faster to look it up from the cache.
528					let stake = cache
529						.get(who)
530						.map(|idx| {
531							let (_, stake, _) = voters[*idx];
532							stake
533						})
534						.unwrap_or_default();
535					core::cmp::Reverse(stake)
536				},
537			);
538
539			// ensures that the max backers per winner bounds are respected given the supports
540			// generated from the assignments. We achieve that by removing edges (voter ->
541			// target) in the assignments with lower stake until the total number of backers per
542			// winner fits within the expected bounded supports. This should be performed *after*
543			// applying reduce over the assignments to avoid over-trimming.
544			//
545			// a potential trimming does not affect the desired targets of the solution as the
546			// targets have *too many* edges by definition if trimmed.
547			let max_backers_per_winner = T::MaxBackersPerWinner::get().saturated_into::<usize>();
548
549			let _ = sp_npos_elections::to_supports(&staked)
550				.iter_mut()
551				.filter(|(_, support)| support.voters.len() > max_backers_per_winner)
552				.for_each(|(target, ref mut support)| {
553					// first sort by support stake, lowest at the tail.
554					support.voters.sort_by(|a, b| b.1.cmp(&a.1));
555
556					// filter out lowest stake edge in this support.
557					// optimization note: collects edge voters to remove from assignments into a
558					// btree set to optimize the search in the next loop.
559					let filtered: alloc::collections::BTreeSet<_> = support
560						.voters
561						.split_off(max_backers_per_winner)
562						.into_iter()
563						.map(|(who, _stake)| who)
564						.collect();
565
566					// remove lowest stake edges calculated above from assignments.
567					staked.iter_mut().for_each(|assignment| {
568						if filtered.contains(&assignment.who) {
569							assignment.distribution.retain(|(t, _)| t != target);
570						}
571					});
572
573					edges_trimmed += filtered.len();
574				});
575
576			debug_assert!({
577				// at this point we expect the supports generated from the assignments to fit within
578				// the expected bounded supports.
579				let expected_ok: Result<
580					crate::BoundedSupports<_, T::MaxWinners, T::MaxBackersPerWinner>,
581					_,
582				> = sp_npos_elections::to_supports(&staked).try_into();
583				expected_ok.is_ok()
584			});
585
586			// convert back.
587			assignment_staked_to_ratio_normalized(staked)?
588		};
589
590		// convert to `IndexAssignment`. This improves the runtime complexity of repeatedly
591		// converting to `Solution`.
592		let mut index_assignments = sorted_assignments
593			.into_iter()
594			.map(|assignment| IndexAssignmentOf::<T>::new(&assignment, &voter_index, &target_index))
595			.collect::<Result<Vec<_>, _>>()?;
596
597		// trim assignments list for weight and length.
598		let size =
599			SolutionOrSnapshotSize { voters: voters.len() as u32, targets: targets.len() as u32 };
600		let weight_trimmed = Self::trim_assignments_weight(
601			desired_targets,
602			size,
603			T::MaxWeight::get(),
604			&mut index_assignments,
605		);
606		let length_trimmed = Self::trim_assignments_length(
607			T::MaxLength::get(),
608			&mut index_assignments,
609			&encoded_size_of,
610		)?;
611
612		// now make solution.
613		let solution = SolutionOf::<T>::try_from(&index_assignments)?;
614
615		// re-calc score.
616		let score = solution.clone().score(stake_of, voter_at, target_at)?;
617
618		let is_trimmed =
619			TrimmingStatus { weight: weight_trimmed, length: length_trimmed, edges: edges_trimmed };
620
621		log_no_system!(
622			debug,
623			"feasible solution mined: trimmed? {:?}, score: {:?}, encoded size: {:?}",
624			is_trimmed,
625			score,
626			solution.encoded_size()
627		);
628		Ok((solution, score, size, is_trimmed))
629	}
630
631	/// Greedily reduce the size of the solution to fit into the block w.r.t length.
632	///
633	/// The length of the solution is largely a function of the number of voters. The number of
634	/// winners cannot be changed. Thus, to reduce the solution size, we need to strip voters.
635	///
636	/// Note that this solution is already computed, and winners are elected based on the merit of
637	/// the total stake in the system. Nevertheless, some of the voters may be removed here.
638	///
639	/// Sometimes, removing a voter can cause a validator to also be implicitly removed, if
640	/// that voter was the only backer of that winner. In such cases, this solution is invalid,
641	/// which will be caught prior to submission.
642	///
643	/// The score must be computed **after** this step. If this step reduces the score too much,
644	/// then the solution must be discarded.
645	pub fn trim_assignments_length(
646		max_allowed_length: u32,
647		assignments: &mut Vec<IndexAssignmentOf<T>>,
648		encoded_size_of: impl Fn(&[IndexAssignmentOf<T>]) -> Result<usize, sp_npos_elections::Error>,
649	) -> Result<usize, MinerError> {
650		// Perform a binary search for the max subset of which can fit into the allowed
651		// length. Having discovered that, we can truncate efficiently.
652		let max_allowed_length: usize = max_allowed_length.saturated_into();
653		let mut high = assignments.len();
654		let mut low = 0;
655
656		// not much we can do if assignments are already empty.
657		if high == low {
658			return Ok(0)
659		}
660
661		while high - low > 1 {
662			let test = (high + low) / 2;
663			if encoded_size_of(&assignments[..test])? <= max_allowed_length {
664				low = test;
665			} else {
666				high = test;
667			}
668		}
669		let maximum_allowed_voters = if low < assignments.len() &&
670			encoded_size_of(&assignments[..low + 1])? <= max_allowed_length
671		{
672			low + 1
673		} else {
674			low
675		};
676
677		// ensure our post-conditions are correct
678		debug_assert!(
679			encoded_size_of(&assignments[..maximum_allowed_voters]).unwrap() <= max_allowed_length
680		);
681		debug_assert!(if maximum_allowed_voters < assignments.len() {
682			encoded_size_of(&assignments[..maximum_allowed_voters + 1]).unwrap() >
683				max_allowed_length
684		} else {
685			true
686		});
687
688		// NOTE: before this point, every access was immutable.
689		// after this point, we never error.
690		// check before edit.
691
692		let remove = assignments.len().saturating_sub(maximum_allowed_voters);
693
694		log_no_system!(
695			trace,
696			"from {} assignments, truncating to {} for length, removing {}",
697			assignments.len(),
698			maximum_allowed_voters,
699			remove
700		);
701		assignments.truncate(maximum_allowed_voters);
702
703		Ok(remove)
704	}
705
706	/// Greedily reduce the size of the solution to fit into the block w.r.t. weight.
707	///
708	/// The weight of the solution is foremost a function of the number of voters (i.e.
709	/// `assignments.len()`). Aside from this, the other components of the weight are invariant. The
710	/// number of winners shall not be changed (otherwise the solution is invalid) and the
711	/// `ElectionSize` is merely a representation of the total number of stakers.
712	///
713	/// Thus, we reside to stripping away some voters from the `assignments`.
714	///
715	/// Note that the solution is already computed, and the winners are elected based on the merit
716	/// of the entire stake in the system. Nonetheless, some of the voters will be removed further
717	/// down the line.
718	///
719	/// Indeed, the score must be computed **after** this step. If this step reduces the score too
720	/// much or remove a winner, then the solution must be discarded **after** this step.
721	pub fn trim_assignments_weight(
722		desired_targets: u32,
723		size: SolutionOrSnapshotSize,
724		max_weight: Weight,
725		assignments: &mut Vec<IndexAssignmentOf<T>>,
726	) -> usize {
727		let maximum_allowed_voters =
728			Self::maximum_voter_for_weight(desired_targets, size, max_weight);
729		let removing: usize =
730			assignments.len().saturating_sub(maximum_allowed_voters.saturated_into());
731		log_no_system!(
732			debug,
733			"from {} assignments, truncating to {} for weight, removing {}",
734			assignments.len(),
735			maximum_allowed_voters,
736			removing,
737		);
738		assignments.truncate(maximum_allowed_voters as usize);
739
740		removing
741	}
742
743	/// Find the maximum `len` that a solution can have in order to fit into the block weight.
744	///
745	/// This only returns a value between zero and `size.nominators`.
746	pub fn maximum_voter_for_weight(
747		desired_winners: u32,
748		size: SolutionOrSnapshotSize,
749		max_weight: Weight,
750	) -> u32 {
751		if size.voters < 1 {
752			return size.voters
753		}
754
755		let max_voters = size.voters.max(1);
756		let mut voters = max_voters;
757
758		// helper closures.
759		let weight_with = |active_voters: u32| -> Weight {
760			T::solution_weight(size.voters, size.targets, active_voters, desired_winners)
761		};
762
763		let next_voters = |current_weight: Weight, voters: u32, step: u32| -> Result<u32, ()> {
764			if current_weight.all_lt(max_weight) {
765				let next_voters = voters.checked_add(step);
766				match next_voters {
767					Some(voters) if voters < max_voters => Ok(voters),
768					_ => Err(()),
769				}
770			} else if current_weight.any_gt(max_weight) {
771				voters.checked_sub(step).ok_or(())
772			} else {
773				// If any of the constituent weights is equal to the max weight, we're at max
774				Ok(voters)
775			}
776		};
777
778		// First binary-search the right amount of voters
779		let mut step = voters / 2;
780		let mut current_weight = weight_with(voters);
781
782		while step > 0 {
783			match next_voters(current_weight, voters, step) {
784				// proceed with the binary search
785				Ok(next) if next != voters => {
786					voters = next;
787				},
788				// we are out of bounds, break out of the loop.
789				Err(()) => break,
790				// we found the right value - early exit the function.
791				Ok(next) => return next,
792			}
793			step /= 2;
794			current_weight = weight_with(voters);
795		}
796
797		// Time to finish. We might have reduced less than expected due to rounding error. Increase
798		// one last time if we have any room left, the reduce until we are sure we are below limit.
799		while voters < max_voters && weight_with(voters + 1).all_lt(max_weight) {
800			voters += 1;
801		}
802		while voters.checked_sub(1).is_some() && weight_with(voters).any_gt(max_weight) {
803			voters -= 1;
804		}
805
806		let final_decision = voters.min(size.voters);
807		debug_assert!(
808			weight_with(final_decision).all_lte(max_weight),
809			"weight_with({}) <= {}",
810			final_decision,
811			max_weight,
812		);
813		final_decision
814	}
815
816	/// Checks the feasibility of a solution.
817	pub fn feasibility_check(
818		raw_solution: RawSolution<SolutionOf<T>>,
819		compute: ElectionCompute,
820		desired_targets: u32,
821		snapshot: RoundSnapshot<T::AccountId, MinerVoterOf<T>>,
822		current_round: u32,
823		minimum_untrusted_score: Option<ElectionScore>,
824	) -> Result<ReadySolutionOf<T>, FeasibilityError> {
825		let RawSolution { solution, score, round } = raw_solution;
826		let RoundSnapshot { voters: snapshot_voters, targets: snapshot_targets } = snapshot;
827
828		// First, check round.
829		ensure!(current_round == round, FeasibilityError::InvalidRound);
830
831		// Winners are not directly encoded in the solution.
832		let winners = solution.unique_targets();
833
834		ensure!(winners.len() as u32 == desired_targets, FeasibilityError::WrongWinnerCount);
835		// Fail early if targets requested by data provider exceed maximum winners supported.
836		ensure!(desired_targets <= T::MaxWinners::get(), FeasibilityError::TooManyDesiredTargets);
837
838		// Ensure that the solution's score can pass absolute min-score.
839		let submitted_score = raw_solution.score;
840		ensure!(
841			minimum_untrusted_score
842				.map_or(true, |min_score| { submitted_score.strict_better(min_score) }),
843			FeasibilityError::UntrustedScoreTooLow
844		);
845
846		// ----- Start building. First, we need some closures.
847		let cache = helpers::generate_voter_cache::<T>(&snapshot_voters);
848		let voter_at = helpers::voter_at_fn::<T>(&snapshot_voters);
849		let target_at = helpers::target_at_fn::<T>(&snapshot_targets);
850		let voter_index = helpers::voter_index_fn_usize::<T>(&cache);
851
852		// Then convert solution -> assignment. This will fail if any of the indices are gibberish,
853		// namely any of the voters or targets.
854		let assignments = solution
855			.into_assignment(voter_at, target_at)
856			.map_err::<FeasibilityError, _>(Into::into)?;
857
858		// Ensure that assignments is correct.
859		assignments.iter().try_for_each(|assignment| {
860			// Check that assignment.who is actually a voter (defensive-only).
861			// NOTE: while using the index map from `voter_index` is better than a blind linear
862			// search, this *still* has room for optimization. Note that we had the index when
863			// we did `solution -> assignment` and we lost it. Ideal is to keep the index
864			// around.
865
866			// Defensive-only: must exist in the snapshot.
867			let snapshot_index =
868				voter_index(&assignment.who).ok_or(FeasibilityError::InvalidVoter)?;
869			// Defensive-only: index comes from the snapshot, must exist.
870			let (_voter, _stake, targets) =
871				snapshot_voters.get(snapshot_index).ok_or(FeasibilityError::InvalidVoter)?;
872
873			// Check that all of the targets are valid based on the snapshot.
874			if assignment.distribution.iter().any(|(d, _)| !targets.contains(d)) {
875				return Err(FeasibilityError::InvalidVote)
876			}
877			Ok(())
878		})?;
879
880		// ----- Start building support. First, we need one more closure.
881		let stake_of = helpers::stake_of_fn::<T>(&snapshot_voters, &cache);
882
883		// This might fail if the normalization fails. Very unlikely. See `integrity_test`.
884		let staked_assignments = assignment_ratio_to_staked_normalized(assignments, stake_of)
885			.map_err::<FeasibilityError, _>(Into::into)?;
886		let supports = sp_npos_elections::to_supports(&staked_assignments);
887
888		// Finally, check that the claimed score was indeed correct.
889		let known_score = supports.evaluate();
890
891		ensure!(known_score == score, FeasibilityError::InvalidScore);
892
893		// Size of winners in miner solution is equal to `desired_targets` <= `MaxWinners`. In
894		// addition, the miner should have ensured that the MaxBackerPerWinner bound in respected,
895		// thus this conversion should not fail.
896		let supports = supports
897			.try_into()
898			.defensive_map_err(|_| FeasibilityError::BoundedConversionFailed)?;
899
900		Ok(ReadySolution { supports, compute, score })
901	}
902}
903
904#[cfg(test)]
905mod max_weight {
906	#![allow(unused_variables)]
907	use super::*;
908	use crate::mock::{MockWeightInfo, Runtime};
909	#[test]
910	fn find_max_voter_binary_search_works() {
911		let w = SolutionOrSnapshotSize { voters: 10, targets: 0 };
912		MockWeightInfo::set(crate::mock::MockedWeightInfo::Complex);
913		assert_eq!(
914			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
915			0
916		);
917		assert_eq!(
918			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
919			0
920		);
921		assert_eq!(
922			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
923			0
924		);
925		assert_eq!(
926			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
927			1
928		);
929		assert_eq!(
930			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
931			1
932		);
933		assert_eq!(
934			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1990, u64::MAX)),
935			1
936		);
937		assert_eq!(
938			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
939			1
940		);
941		assert_eq!(
942			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
943			2
944		);
945		assert_eq!(
946			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
947			2
948		);
949		assert_eq!(
950			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
951			2
952		);
953		assert_eq!(
954			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2990, u64::MAX)),
955			2
956		);
957		assert_eq!(
958			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2999, u64::MAX)),
959			2
960		);
961		assert_eq!(
962			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3000, u64::MAX)),
963			3
964		);
965		assert_eq!(
966			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
967			3
968		);
969		assert_eq!(
970			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(5500, u64::MAX)),
971			5
972		);
973		assert_eq!(
974			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(7777, u64::MAX)),
975			7
976		);
977		assert_eq!(
978			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(9999, u64::MAX)),
979			9
980		);
981		assert_eq!(
982			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(10_000, u64::MAX)),
983			10
984		);
985		assert_eq!(
986			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(10_999, u64::MAX)),
987			10
988		);
989		assert_eq!(
990			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(11_000, u64::MAX)),
991			10
992		);
993		assert_eq!(
994			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(22_000, u64::MAX)),
995			10
996		);
997
998		let w = SolutionOrSnapshotSize { voters: 1, targets: 0 };
999
1000		assert_eq!(
1001			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
1002			0
1003		);
1004		assert_eq!(
1005			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
1006			0
1007		);
1008		assert_eq!(
1009			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
1010			0
1011		);
1012		assert_eq!(
1013			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
1014			1
1015		);
1016		assert_eq!(
1017			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
1018			1
1019		);
1020		assert_eq!(
1021			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1990, u64::MAX)),
1022			1
1023		);
1024		assert_eq!(
1025			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
1026			1
1027		);
1028		assert_eq!(
1029			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
1030			1
1031		);
1032		assert_eq!(
1033			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
1034			1
1035		);
1036		assert_eq!(
1037			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
1038			1
1039		);
1040		assert_eq!(
1041			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
1042			1
1043		);
1044
1045		let w = SolutionOrSnapshotSize { voters: 2, targets: 0 };
1046
1047		assert_eq!(
1048			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
1049			0
1050		);
1051		assert_eq!(
1052			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
1053			0
1054		);
1055		assert_eq!(
1056			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
1057			0
1058		);
1059		assert_eq!(
1060			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
1061			1
1062		);
1063		assert_eq!(
1064			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
1065			1
1066		);
1067		assert_eq!(
1068			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
1069			1
1070		);
1071		assert_eq!(
1072			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
1073			2
1074		);
1075		assert_eq!(
1076			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
1077			2
1078		);
1079		assert_eq!(
1080			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
1081			2
1082		);
1083		assert_eq!(
1084			Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
1085			2
1086		);
1087	}
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092	use super::*;
1093	use crate::{
1094		mock::{
1095			multi_phase_events, roll_to, roll_to_signed, roll_to_unsigned, roll_to_with_ocw,
1096			trim_helpers, witness, BlockNumber, ExtBuilder, Extrinsic, MinerMaxWeight, MultiPhase,
1097			Runtime, RuntimeCall, RuntimeOrigin, System, TestNposSolution, TrimHelpers,
1098			UnsignedPhase,
1099		},
1100		Event, InvalidTransaction, Phase, QueuedSolution, TransactionSource,
1101		TransactionValidityError,
1102	};
1103	use alloc::vec;
1104	use codec::Decode;
1105	use frame_election_provider_support::IndexAssignment;
1106	use frame_support::{assert_noop, assert_ok, traits::OffchainWorker};
1107	use sp_npos_elections::ElectionScore;
1108	use sp_runtime::{
1109		bounded_vec,
1110		offchain::storage_lock::{BlockAndTime, StorageLock},
1111		traits::{Dispatchable, ValidateUnsigned, Zero},
1112		ModuleError, PerU16,
1113	};
1114
1115	type Assignment = crate::unsigned::Assignment<Runtime>;
1116
1117	#[test]
1118	fn validate_unsigned_retracts_wrong_phase() {
1119		ExtBuilder::default().desired_targets(0).build_and_execute(|| {
1120			let solution = RawSolution::<TestNposSolution> {
1121				score: ElectionScore { minimal_stake: 5, ..Default::default() },
1122				..Default::default()
1123			};
1124			let call = Call::submit_unsigned {
1125				raw_solution: Box::new(solution.clone()),
1126				witness: witness(),
1127			};
1128
1129			// initial
1130			assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
1131			assert!(matches!(
1132				<MultiPhase as ValidateUnsigned>::validate_unsigned(
1133					TransactionSource::Local,
1134					&call
1135				)
1136				.unwrap_err(),
1137				TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1138			));
1139			assert!(matches!(
1140				<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1141				TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1142			));
1143
1144			// signed
1145			roll_to_signed();
1146			assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
1147			assert!(matches!(
1148				<MultiPhase as ValidateUnsigned>::validate_unsigned(
1149					TransactionSource::Local,
1150					&call
1151				)
1152				.unwrap_err(),
1153				TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1154			));
1155			assert!(matches!(
1156				<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1157				TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1158			));
1159
1160			// unsigned
1161			roll_to_unsigned();
1162			assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1163
1164			assert!(<MultiPhase as ValidateUnsigned>::validate_unsigned(
1165				TransactionSource::Local,
1166				&call
1167			)
1168			.is_ok());
1169			assert!(<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).is_ok());
1170
1171			// unsigned -- but not enabled.
1172			MultiPhase::phase_transition(Phase::Unsigned((false, 25)));
1173			assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1174			assert!(matches!(
1175				<MultiPhase as ValidateUnsigned>::validate_unsigned(
1176					TransactionSource::Local,
1177					&call
1178				)
1179				.unwrap_err(),
1180				TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1181			));
1182			assert!(matches!(
1183				<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1184				TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1185			));
1186		})
1187	}
1188
1189	#[test]
1190	fn validate_unsigned_retracts_low_score() {
1191		ExtBuilder::default().desired_targets(0).build_and_execute(|| {
1192			roll_to_unsigned();
1193			assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1194
1195			let solution = RawSolution::<TestNposSolution> {
1196				score: ElectionScore { minimal_stake: 5, ..Default::default() },
1197				..Default::default()
1198			};
1199			let call = Call::submit_unsigned {
1200				raw_solution: Box::new(solution.clone()),
1201				witness: witness(),
1202			};
1203
1204			// initial
1205			assert!(<MultiPhase as ValidateUnsigned>::validate_unsigned(
1206				TransactionSource::Local,
1207				&call
1208			)
1209			.is_ok());
1210			assert!(<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).is_ok());
1211
1212			// set a better score
1213			let ready = ReadySolution {
1214				score: ElectionScore { minimal_stake: 10, ..Default::default() },
1215				..Default::default()
1216			};
1217			QueuedSolution::<Runtime>::put(ready);
1218
1219			// won't work anymore.
1220			assert!(matches!(
1221				<MultiPhase as ValidateUnsigned>::validate_unsigned(
1222					TransactionSource::Local,
1223					&call
1224				)
1225				.unwrap_err(),
1226				TransactionValidityError::Invalid(InvalidTransaction::Custom(2))
1227			));
1228			assert!(matches!(
1229				<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1230				TransactionValidityError::Invalid(InvalidTransaction::Custom(2))
1231			));
1232		})
1233	}
1234
1235	#[test]
1236	fn validate_unsigned_retracts_incorrect_winner_count() {
1237		ExtBuilder::default().desired_targets(1).build_and_execute(|| {
1238			roll_to_unsigned();
1239			assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1240
1241			let raw = RawSolution::<TestNposSolution> {
1242				score: ElectionScore { minimal_stake: 5, ..Default::default() },
1243				..Default::default()
1244			};
1245			let call =
1246				Call::submit_unsigned { raw_solution: Box::new(raw.clone()), witness: witness() };
1247			assert_eq!(raw.solution.unique_targets().len(), 0);
1248
1249			// won't work anymore.
1250			assert!(matches!(
1251				<MultiPhase as ValidateUnsigned>::validate_unsigned(
1252					TransactionSource::Local,
1253					&call
1254				)
1255				.unwrap_err(),
1256				TransactionValidityError::Invalid(InvalidTransaction::Custom(1))
1257			));
1258		})
1259	}
1260
1261	#[test]
1262	fn priority_is_set() {
1263		ExtBuilder::default()
1264			.miner_tx_priority(20)
1265			.desired_targets(0)
1266			.build_and_execute(|| {
1267				roll_to_unsigned();
1268				assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1269
1270				let solution = RawSolution::<TestNposSolution> {
1271					score: ElectionScore { minimal_stake: 5, ..Default::default() },
1272					..Default::default()
1273				};
1274				let call = Call::submit_unsigned {
1275					raw_solution: Box::new(solution.clone()),
1276					witness: witness(),
1277				};
1278
1279				assert_eq!(
1280					<MultiPhase as ValidateUnsigned>::validate_unsigned(
1281						TransactionSource::Local,
1282						&call
1283					)
1284					.unwrap()
1285					.priority,
1286					25
1287				);
1288			})
1289	}
1290
1291	#[test]
1292	#[should_panic(expected = "Invalid unsigned submission must produce invalid block and \
1293	                           deprive validator from their authoring reward.: \
1294	                           Module(ModuleError { index: 2, error: [1, 0, 0, 0], message: \
1295	                           Some(\"PreDispatchWrongWinnerCount\") })")]
1296	fn unfeasible_solution_panics() {
1297		ExtBuilder::default().build_and_execute(|| {
1298			roll_to_unsigned();
1299			assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1300
1301			// This is in itself an invalid BS solution.
1302			let solution = RawSolution::<TestNposSolution> {
1303				score: ElectionScore { minimal_stake: 5, ..Default::default() },
1304				..Default::default()
1305			};
1306			let call = Call::submit_unsigned {
1307				raw_solution: Box::new(solution.clone()),
1308				witness: witness(),
1309			};
1310			let runtime_call: RuntimeCall = call.into();
1311			let _ = runtime_call.dispatch(RuntimeOrigin::none());
1312		})
1313	}
1314
1315	#[test]
1316	#[should_panic(expected = "Invalid unsigned submission must produce invalid block and \
1317	                           deprive validator from their authoring reward.")]
1318	fn wrong_witness_panics() {
1319		ExtBuilder::default().build_and_execute(|| {
1320			roll_to_unsigned();
1321			assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1322
1323			// This solution is unfeasible as well, but we won't even get there.
1324			let solution = RawSolution::<TestNposSolution> {
1325				score: ElectionScore { minimal_stake: 5, ..Default::default() },
1326				..Default::default()
1327			};
1328
1329			let mut correct_witness = witness();
1330			correct_witness.voters += 1;
1331			correct_witness.targets -= 1;
1332			let call = Call::submit_unsigned {
1333				raw_solution: Box::new(solution.clone()),
1334				witness: correct_witness,
1335			};
1336			let runtime_call: RuntimeCall = call.into();
1337			let _ = runtime_call.dispatch(RuntimeOrigin::none());
1338		})
1339	}
1340
1341	#[test]
1342	fn miner_works() {
1343		ExtBuilder::default().build_and_execute(|| {
1344			roll_to_unsigned();
1345			assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1346
1347			// ensure we have snapshots in place.
1348			assert!(Snapshot::<Runtime>::get().is_some());
1349			assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 2);
1350
1351			// mine seq_phragmen solution with 2 iters.
1352			let (solution, witness, _) = MultiPhase::mine_solution().unwrap();
1353
1354			// ensure this solution is valid.
1355			assert!(QueuedSolution::<Runtime>::get().is_none());
1356			assert_ok!(MultiPhase::submit_unsigned(
1357				RuntimeOrigin::none(),
1358				Box::new(solution),
1359				witness
1360			));
1361			assert!(QueuedSolution::<Runtime>::get().is_some());
1362			assert_eq!(
1363				multi_phase_events(),
1364				vec![
1365					Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1366					Event::PhaseTransitioned {
1367						from: Phase::Signed,
1368						to: Phase::Unsigned((true, 25)),
1369						round: 1
1370					},
1371					Event::SolutionStored {
1372						compute: ElectionCompute::Unsigned,
1373						origin: None,
1374						prev_ejected: false
1375					}
1376				]
1377			);
1378		})
1379	}
1380
1381	#[test]
1382	fn miner_trims_weight() {
1383		ExtBuilder::default()
1384			.miner_weight(Weight::from_parts(100, u64::MAX))
1385			.mock_weight_info(crate::mock::MockedWeightInfo::Basic)
1386			.build_and_execute(|| {
1387				roll_to_unsigned();
1388				assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1389
1390				let (raw, witness, t) = MultiPhase::mine_solution().unwrap();
1391				let solution_weight = <Runtime as MinerConfig>::solution_weight(
1392					witness.voters,
1393					witness.targets,
1394					raw.solution.voter_count() as u32,
1395					raw.solution.unique_targets().len() as u32,
1396				);
1397				// default solution will have 5 edges (5 * 5 + 10)
1398				assert_eq!(solution_weight, Weight::from_parts(35, 0));
1399				assert_eq!(raw.solution.voter_count(), 5);
1400				assert_eq!(t.trimmed_weight(), 0);
1401
1402				// now reduce the max weight
1403				<MinerMaxWeight>::set(Weight::from_parts(25, u64::MAX));
1404
1405				let (raw, witness, t) = MultiPhase::mine_solution().unwrap();
1406				let solution_weight = <Runtime as MinerConfig>::solution_weight(
1407					witness.voters,
1408					witness.targets,
1409					raw.solution.voter_count() as u32,
1410					raw.solution.unique_targets().len() as u32,
1411				);
1412				// default solution will have 5 edges (5 * 5 + 10)
1413				assert_eq!(solution_weight, Weight::from_parts(25, 0));
1414				assert_eq!(raw.solution.voter_count(), 3);
1415				assert_eq!(t.trimmed_weight(), 2);
1416			})
1417	}
1418
1419	#[test]
1420	fn miner_will_not_submit_if_not_enough_winners() {
1421		let (mut ext, _) = ExtBuilder::default().desired_targets(8).build_offchainify(0);
1422		ext.execute_with(|| {
1423			roll_to_unsigned();
1424			assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1425
1426			// Force the number of winners to be bigger to fail
1427			let (mut solution, _, _) = MultiPhase::mine_solution().unwrap();
1428			solution.solution.votes1[0].1 = 4;
1429
1430			assert_eq!(
1431				MultiPhase::basic_checks(&solution, "mined").unwrap_err(),
1432				MinerError::PreDispatchChecksFailed(DispatchError::Module(ModuleError {
1433					index: 2,
1434					error: [1, 0, 0, 0],
1435					message: Some("PreDispatchWrongWinnerCount"),
1436				})),
1437			);
1438		})
1439	}
1440
1441	#[test]
1442	fn unsigned_per_dispatch_checks_can_only_submit_threshold_better() {
1443		ExtBuilder::default()
1444			.desired_targets(1)
1445			.add_voter(7, 2, bounded_vec![10])
1446			.add_voter(8, 5, bounded_vec![10])
1447			.add_voter(9, 1, bounded_vec![10])
1448			.build_and_execute(|| {
1449				roll_to_unsigned();
1450				assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1451				assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 1);
1452
1453				// an initial solution
1454				let result = ElectionResult {
1455					winners: vec![(10, 12)],
1456					assignments: vec![
1457						Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1458						Assignment {
1459							who: 7,
1460							// note: this percent doesn't even matter, in solution it is 100%.
1461							distribution: vec![(10, PerU16::one())],
1462						},
1463					],
1464				};
1465
1466				let RoundSnapshot { voters, targets } = Snapshot::<Runtime>::get().unwrap();
1467				let desired_targets = DesiredTargets::<Runtime>::get().unwrap();
1468
1469				let (raw, score, witness, _) =
1470					Miner::<Runtime>::prepare_election_result_with_snapshot(
1471						result,
1472						voters.clone(),
1473						targets.clone(),
1474						desired_targets,
1475					)
1476					.unwrap();
1477				let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1478				assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
1479				assert_ok!(MultiPhase::submit_unsigned(
1480					RuntimeOrigin::none(),
1481					Box::new(solution),
1482					witness
1483				));
1484				assert_eq!(QueuedSolution::<Runtime>::get().unwrap().score.minimal_stake, 12);
1485
1486				// trial 1: a solution who's minimal stake is 10, i.e. worse than the first solution
1487				// of 12.
1488				let result = ElectionResult {
1489					winners: vec![(10, 10)],
1490					assignments: vec![Assignment {
1491						who: 10,
1492						distribution: vec![(10, PerU16::one())],
1493					}],
1494				};
1495				let (raw, score, _, _) = Miner::<Runtime>::prepare_election_result_with_snapshot(
1496					result,
1497					voters.clone(),
1498					targets.clone(),
1499					desired_targets,
1500				)
1501				.unwrap();
1502				let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1503				// 10 is not better than 12
1504				assert_eq!(solution.score.minimal_stake, 10);
1505				// submitting this will actually panic.
1506				assert_noop!(
1507					MultiPhase::unsigned_pre_dispatch_checks(&solution),
1508					Error::<Runtime>::PreDispatchWeakSubmission,
1509				);
1510
1511				// trial 2: try resubmitting another solution with same score (12) as the queued
1512				// solution.
1513				let result = ElectionResult {
1514					winners: vec![(10, 12)],
1515					assignments: vec![
1516						Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1517						Assignment {
1518							who: 7,
1519							// note: this percent doesn't even matter, in solution it is 100%.
1520							distribution: vec![(10, PerU16::one())],
1521						},
1522					],
1523				};
1524
1525				let (raw, score, _, _) = Miner::<Runtime>::prepare_election_result_with_snapshot(
1526					result,
1527					voters.clone(),
1528					targets.clone(),
1529					desired_targets,
1530				)
1531				.unwrap();
1532				let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1533				// 12 is not better than 12. We need score of at least 13 to be accepted.
1534				assert_eq!(solution.score.minimal_stake, 12);
1535				// submitting this will panic.
1536				assert_noop!(
1537					MultiPhase::unsigned_pre_dispatch_checks(&solution),
1538					Error::<Runtime>::PreDispatchWeakSubmission,
1539				);
1540
1541				// trial 3: a solution who's minimal stake is 13, i.e. 1 better than the queued
1542				// solution of 12.
1543				let result = ElectionResult {
1544					winners: vec![(10, 12)],
1545					assignments: vec![
1546						Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1547						Assignment { who: 7, distribution: vec![(10, PerU16::one())] },
1548						Assignment { who: 9, distribution: vec![(10, PerU16::one())] },
1549					],
1550				};
1551				let (raw, score, witness, _) =
1552					Miner::<Runtime>::prepare_election_result_with_snapshot(
1553						result,
1554						voters.clone(),
1555						targets.clone(),
1556						desired_targets,
1557					)
1558					.unwrap();
1559				let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1560				assert_eq!(solution.score.minimal_stake, 13);
1561
1562				// this should work
1563				assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
1564				assert_ok!(MultiPhase::submit_unsigned(
1565					RuntimeOrigin::none(),
1566					Box::new(solution),
1567					witness
1568				));
1569
1570				// trial 4: a solution who's minimal stake is 17, i.e. 4 better than the last
1571				// solution.
1572				let result = ElectionResult {
1573					winners: vec![(10, 12)],
1574					assignments: vec![
1575						Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1576						Assignment { who: 7, distribution: vec![(10, PerU16::one())] },
1577						Assignment {
1578							who: 8,
1579							// note: this percent doesn't even matter, in solution it is 100%.
1580							distribution: vec![(10, PerU16::one())],
1581						},
1582					],
1583				};
1584				let (raw, score, witness, _) =
1585					Miner::<Runtime>::prepare_election_result_with_snapshot(
1586						result,
1587						voters.clone(),
1588						targets.clone(),
1589						desired_targets,
1590					)
1591					.unwrap();
1592				let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1593				assert_eq!(solution.score.minimal_stake, 17);
1594
1595				// and it is fine
1596				assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
1597				assert_ok!(MultiPhase::submit_unsigned(
1598					RuntimeOrigin::none(),
1599					Box::new(solution),
1600					witness
1601				));
1602			})
1603	}
1604
1605	#[test]
1606	fn ocw_lock_prevents_frequent_execution() {
1607		let (mut ext, _) = ExtBuilder::default().build_offchainify(0);
1608		ext.execute_with(|| {
1609			let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1610
1611			roll_to_unsigned();
1612			assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1613
1614			// first execution -- okay.
1615			assert!(MultiPhase::ensure_offchain_repeat_frequency(25).is_ok());
1616
1617			// next block: rejected.
1618			assert_noop!(
1619				MultiPhase::ensure_offchain_repeat_frequency(26),
1620				MinerError::Lock("recently executed.")
1621			);
1622
1623			// allowed after `OFFCHAIN_REPEAT`
1624			assert!(
1625				MultiPhase::ensure_offchain_repeat_frequency((26 + offchain_repeat).into()).is_ok()
1626			);
1627
1628			// a fork like situation: re-execute last 3.
1629			assert!(MultiPhase::ensure_offchain_repeat_frequency(
1630				(26 + offchain_repeat - 3).into()
1631			)
1632			.is_err());
1633			assert!(MultiPhase::ensure_offchain_repeat_frequency(
1634				(26 + offchain_repeat - 2).into()
1635			)
1636			.is_err());
1637			assert!(MultiPhase::ensure_offchain_repeat_frequency(
1638				(26 + offchain_repeat - 1).into()
1639			)
1640			.is_err());
1641		})
1642	}
1643
1644	#[test]
1645	fn ocw_lock_released_after_successful_execution() {
1646		// first, ensure that a successful execution releases the lock
1647		let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1648		ext.execute_with(|| {
1649			let guard = StorageValueRef::persistent(&OFFCHAIN_LOCK);
1650			let last_block = StorageValueRef::persistent(OFFCHAIN_LAST_BLOCK);
1651
1652			roll_to_unsigned();
1653			assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1654
1655			// initially, the lock is not set.
1656			assert!(guard.get::<bool>().unwrap().is_none());
1657
1658			// a successful a-z execution.
1659			MultiPhase::offchain_worker(25);
1660			assert_eq!(pool.read().transactions.len(), 1);
1661
1662			// afterwards, the lock is not set either..
1663			assert!(guard.get::<bool>().unwrap().is_none());
1664			assert_eq!(last_block.get::<BlockNumber>().unwrap(), Some(25));
1665		});
1666	}
1667
1668	#[test]
1669	fn ocw_lock_prevents_overlapping_execution() {
1670		// ensure that if the guard is in hold, a new execution is not allowed.
1671		let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1672		ext.execute_with(|| {
1673			roll_to_unsigned();
1674			assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1675
1676			// artificially set the value, as if another thread is mid-way.
1677			let mut lock = StorageLock::<BlockAndTime<System>>::with_block_deadline(
1678				OFFCHAIN_LOCK,
1679				UnsignedPhase::get().saturated_into(),
1680			);
1681			let guard = lock.lock();
1682
1683			// nothing submitted.
1684			MultiPhase::offchain_worker(25);
1685			assert_eq!(pool.read().transactions.len(), 0);
1686			MultiPhase::offchain_worker(26);
1687			assert_eq!(pool.read().transactions.len(), 0);
1688
1689			drop(guard);
1690
1691			// ๐ŸŽ‰ !
1692			MultiPhase::offchain_worker(25);
1693			assert_eq!(pool.read().transactions.len(), 1);
1694		});
1695	}
1696
1697	#[test]
1698	fn ocw_only_runs_when_unsigned_open_now() {
1699		let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1700		ext.execute_with(|| {
1701			roll_to_unsigned();
1702			assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
1703
1704			// we must clear the offchain storage to ensure the offchain execution check doesn't get
1705			// in the way.
1706			let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
1707
1708			MultiPhase::offchain_worker(24);
1709			assert!(pool.read().transactions.len().is_zero());
1710			storage.clear();
1711
1712			// creates, caches, submits without expecting previous cache value
1713			MultiPhase::offchain_worker(25);
1714			assert_eq!(pool.read().transactions.len(), 1);
1715			// assume that the tx has been processed
1716			pool.try_write().unwrap().transactions.clear();
1717
1718			// locked, but also, has previously cached.
1719			MultiPhase::offchain_worker(26);
1720			assert!(pool.read().transactions.len().is_zero());
1721		})
1722	}
1723
1724	#[test]
1725	fn ocw_clears_cache_on_unsigned_phase_open() {
1726		let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1727		ext.execute_with(|| {
1728			const BLOCK: u64 = 25;
1729			let block_plus = |delta: u64| BLOCK + delta;
1730			let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1731
1732			roll_to(BLOCK);
1733			// we are on the first block of the unsigned phase
1734			assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
1735
1736			assert!(
1737				!ocw_solution_exists::<Runtime>(),
1738				"no solution should be present before we mine one",
1739			);
1740
1741			// create and cache a solution on the first block of the unsigned phase
1742			MultiPhase::offchain_worker(BLOCK);
1743			assert!(
1744				ocw_solution_exists::<Runtime>(),
1745				"a solution must be cached after running the worker",
1746			);
1747
1748			// record the submitted tx,
1749			let tx_cache_1 = pool.read().transactions[0].clone();
1750			// and assume it has been processed.
1751			pool.try_write().unwrap().transactions.clear();
1752
1753			// after an election, the solution is not cleared
1754			// we don't actually care about the result of the election
1755			let _ = MultiPhase::do_elect();
1756			MultiPhase::offchain_worker(block_plus(1));
1757			assert!(ocw_solution_exists::<Runtime>(), "elections does not clear the ocw cache");
1758
1759			// submit a solution with the offchain worker after the repeat interval
1760			MultiPhase::offchain_worker(block_plus(offchain_repeat + 1));
1761
1762			// record the submitted tx,
1763			let tx_cache_2 = pool.read().transactions[0].clone();
1764			// and assume it has been processed.
1765			pool.try_write().unwrap().transactions.clear();
1766
1767			// the OCW submitted the same solution twice since the cache was not cleared.
1768			assert_eq!(tx_cache_1, tx_cache_2);
1769
1770			let current_block = block_plus(offchain_repeat * 2 + 2);
1771			// force the unsigned phase to start on the current block.
1772			MultiPhase::phase_transition(Phase::Unsigned((true, current_block)));
1773
1774			// clear the cache and create a solution since we are on the first block of the unsigned
1775			// phase.
1776			MultiPhase::offchain_worker(current_block);
1777			let tx_cache_3 = pool.read().transactions[0].clone();
1778
1779			// the submitted solution changes because the cache was cleared.
1780			assert_eq!(tx_cache_1, tx_cache_3);
1781			assert_eq!(
1782				multi_phase_events(),
1783				vec![
1784					Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1785					Event::PhaseTransitioned {
1786						from: Phase::Signed,
1787						to: Phase::Unsigned((true, 25)),
1788						round: 1
1789					},
1790					Event::ElectionFinalized {
1791						compute: ElectionCompute::Fallback,
1792						score: ElectionScore {
1793							minimal_stake: 0,
1794							sum_stake: 0,
1795							sum_stake_squared: 0
1796						}
1797					},
1798					Event::PhaseTransitioned {
1799						from: Phase::Unsigned((true, 25)),
1800						to: Phase::Unsigned((true, 37)),
1801						round: 1
1802					},
1803				]
1804			);
1805		})
1806	}
1807
1808	#[test]
1809	fn ocw_resubmits_after_offchain_repeat() {
1810		let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1811		ext.execute_with(|| {
1812			const BLOCK: u64 = 25;
1813			let block_plus = |delta: i32| ((BLOCK as i32) + delta) as u64;
1814			let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1815
1816			roll_to(BLOCK);
1817			assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
1818
1819			// we must clear the offchain storage to ensure the offchain execution check doesn't get
1820			// in the way.
1821			let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
1822
1823			MultiPhase::offchain_worker(block_plus(-1));
1824			assert!(pool.read().transactions.len().is_zero());
1825			storage.clear();
1826
1827			// creates, caches, submits without expecting previous cache value
1828			MultiPhase::offchain_worker(BLOCK);
1829			assert_eq!(pool.read().transactions.len(), 1);
1830			let tx_cache = pool.read().transactions[0].clone();
1831			// assume that the tx has been processed
1832			pool.try_write().unwrap().transactions.clear();
1833
1834			// attempts to resubmit the tx after the threshold has expired
1835			// note that we have to add 1: the semantics forbid resubmission at
1836			// BLOCK + offchain_repeat
1837			MultiPhase::offchain_worker(block_plus(1 + offchain_repeat as i32));
1838			assert_eq!(pool.read().transactions.len(), 1);
1839
1840			// resubmitted tx is identical to first submission
1841			let tx = &pool.read().transactions[0];
1842			assert_eq!(&tx_cache, tx);
1843		})
1844	}
1845
1846	#[test]
1847	fn ocw_regenerates_and_resubmits_after_offchain_repeat() {
1848		let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1849		ext.execute_with(|| {
1850			const BLOCK: u64 = 25;
1851			let block_plus = |delta: i32| ((BLOCK as i32) + delta) as u64;
1852			let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1853
1854			roll_to(BLOCK);
1855			assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
1856
1857			// we must clear the offchain storage to ensure the offchain execution check doesn't get
1858			// in the way.
1859			let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
1860
1861			MultiPhase::offchain_worker(block_plus(-1));
1862			assert!(pool.read().transactions.len().is_zero());
1863			storage.clear();
1864
1865			// creates, caches, submits without expecting previous cache value
1866			MultiPhase::offchain_worker(BLOCK);
1867			assert_eq!(pool.read().transactions.len(), 1);
1868			let tx_cache = pool.read().transactions[0].clone();
1869			// assume that the tx has been processed
1870			pool.try_write().unwrap().transactions.clear();
1871
1872			// remove the cached submitted tx
1873			// this ensures that when the resubmit window rolls around, we're ready to regenerate
1874			// from scratch if necessary
1875			let mut call_cache = StorageValueRef::persistent(&OFFCHAIN_CACHED_CALL);
1876			assert!(matches!(call_cache.get::<Call<Runtime>>(), Ok(Some(_call))));
1877			call_cache.clear();
1878
1879			// attempts to resubmit the tx after the threshold has expired
1880			// note that we have to add 1: the semantics forbid resubmission at
1881			// BLOCK + offchain_repeat
1882			MultiPhase::offchain_worker(block_plus(1 + offchain_repeat as i32));
1883			assert_eq!(pool.read().transactions.len(), 1);
1884
1885			// resubmitted tx is identical to first submission
1886			let tx = &pool.read().transactions[0];
1887			assert_eq!(&tx_cache, tx);
1888		})
1889	}
1890
1891	#[test]
1892	fn ocw_can_submit_to_pool() {
1893		let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1894		ext.execute_with(|| {
1895			roll_to_with_ocw(25);
1896			assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
1897			// OCW must have submitted now
1898
1899			let encoded = pool.read().transactions[0].clone();
1900			let extrinsic: Extrinsic = codec::Decode::decode(&mut &*encoded).unwrap();
1901			let call = extrinsic.function;
1902			assert!(matches!(call, RuntimeCall::MultiPhase(Call::submit_unsigned { .. })));
1903		})
1904	}
1905
1906	#[test]
1907	fn ocw_solution_must_have_correct_round() {
1908		let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1909		ext.execute_with(|| {
1910			roll_to_with_ocw(25);
1911			assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
1912			// OCW must have submitted now
1913			// now, before we check the call, update the round
1914			crate::Round::<Runtime>::mutate(|round| *round += 1);
1915
1916			let encoded = pool.read().transactions[0].clone();
1917			let extrinsic = Extrinsic::decode(&mut &*encoded).unwrap();
1918			let call = match extrinsic.function {
1919				RuntimeCall::MultiPhase(call @ Call::submit_unsigned { .. }) => call,
1920				_ => panic!("bad call: unexpected submission"),
1921			};
1922
1923			// Custom(7) maps to PreDispatchChecksFailed
1924			let pre_dispatch_check_error =
1925				TransactionValidityError::Invalid(InvalidTransaction::Custom(7));
1926			assert_eq!(
1927				<MultiPhase as ValidateUnsigned>::validate_unsigned(
1928					TransactionSource::Local,
1929					&call,
1930				)
1931				.unwrap_err(),
1932				pre_dispatch_check_error,
1933			);
1934			assert_eq!(
1935				<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1936				pre_dispatch_check_error,
1937			);
1938		})
1939	}
1940
1941	#[test]
1942	fn mine_solution_always_respects_max_backers_per_winner() {
1943		use crate::mock::MaxBackersPerWinner;
1944		use frame_election_provider_support::BoundedSupport;
1945
1946		let targets = vec![10, 20, 30, 40];
1947		let voters = vec![
1948			(1, 11, bounded_vec![10, 20, 30]),
1949			(2, 12, bounded_vec![10, 20, 30]),
1950			(3, 13, bounded_vec![10, 20, 30]),
1951			(4, 14, bounded_vec![10, 20, 30]),
1952			(5, 15, bounded_vec![10, 20, 40]),
1953		];
1954		let snapshot = RoundSnapshot { voters: voters.clone(), targets: targets.clone() };
1955		let (round, desired_targets) = (1, 3);
1956
1957		// election with unbounded max backers per winnner.
1958		ExtBuilder::default().max_backers_per_winner(u32::MAX).build_and_execute(|| {
1959			assert_eq!(MaxBackersPerWinner::get(), u32::MAX);
1960
1961			let (solution, expected_score_unbounded, _, trimming_status) =
1962				Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
1963					voters.clone(),
1964					targets.clone(),
1965					desired_targets,
1966				)
1967				.unwrap();
1968
1969			let ready_solution = Miner::<Runtime>::feasibility_check(
1970				RawSolution { solution, score: expected_score_unbounded, round },
1971				Default::default(),
1972				desired_targets,
1973				snapshot.clone(),
1974				round,
1975				Default::default(),
1976			)
1977			.unwrap();
1978
1979			assert_eq!(
1980				ready_solution.supports.into_iter().collect::<Vec<_>>(),
1981				vec![
1982					(
1983						10,
1984						BoundedSupport { total: 25, voters: bounded_vec![(1, 11), (4, 9), (5, 5)] }
1985					),
1986					(20, BoundedSupport { total: 22, voters: bounded_vec![(2, 12), (5, 10)] }),
1987					(30, BoundedSupport { total: 18, voters: bounded_vec![(3, 13), (4, 5)] })
1988				]
1989			);
1990
1991			// no trimmed edges.
1992			assert_eq!(trimming_status.trimmed_edges(), 0);
1993		});
1994
1995		// election with max 1 backer per winnner.
1996		ExtBuilder::default().max_backers_per_winner(1).build_and_execute(|| {
1997			assert_eq!(MaxBackersPerWinner::get(), 1);
1998
1999			let (solution, expected_score_bounded, _, trimming_status) =
2000				Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
2001					voters,
2002					targets,
2003					desired_targets,
2004				)
2005				.unwrap();
2006
2007			let ready_solution = Miner::<Runtime>::feasibility_check(
2008				RawSolution { solution, score: expected_score_bounded, round },
2009				Default::default(),
2010				desired_targets,
2011				snapshot,
2012				round,
2013				Default::default(),
2014			)
2015			.unwrap();
2016
2017			for (_, supports) in ready_solution.supports.iter() {
2018				assert!((supports.voters.len() as u32) <= MaxBackersPerWinner::get());
2019			}
2020
2021			assert_eq!(
2022				ready_solution.supports.into_iter().collect::<Vec<_>>(),
2023				vec![
2024					(10, BoundedSupport { total: 11, voters: bounded_vec![(1, 11)] }),
2025					(20, BoundedSupport { total: 12, voters: bounded_vec![(2, 12)] }),
2026					(30, BoundedSupport { total: 13, voters: bounded_vec![(3, 13)] })
2027				]
2028			);
2029
2030			// four trimmed edges.
2031			assert_eq!(trimming_status.trimmed_edges(), 4);
2032		});
2033	}
2034
2035	#[test]
2036	fn max_backers_edges_trims_lowest_stake() {
2037		use crate::mock::MaxBackersPerWinner;
2038
2039		ExtBuilder::default().build_and_execute(|| {
2040			let targets = vec![10, 20, 30, 40];
2041
2042			let voters = vec![
2043				(1, 100, bounded_vec![10, 20]),
2044				(2, 200, bounded_vec![10, 20, 30]),
2045				(3, 300, bounded_vec![10, 30]),
2046				(4, 400, bounded_vec![10, 30]),
2047				(5, 500, bounded_vec![10, 20, 30]),
2048				(6, 600, bounded_vec![10, 20, 30, 40]),
2049			];
2050			let snapshot = RoundSnapshot { voters: voters.clone(), targets: targets.clone() };
2051			let (round, desired_targets) = (1, 4);
2052
2053			let max_backers_bound = u32::MAX;
2054			let trim_backers_bound = 2;
2055
2056			// election with unbounded max backers per winnner.
2057			MaxBackersPerWinner::set(max_backers_bound);
2058			let (solution, score, _, trimming_status) =
2059				Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
2060					voters.clone(),
2061					targets.clone(),
2062					desired_targets,
2063				)
2064				.unwrap();
2065
2066			assert_eq!(trimming_status.trimmed_edges(), 0);
2067
2068			let ready_solution = Miner::<Runtime>::feasibility_check(
2069				RawSolution { solution, score, round },
2070				Default::default(),
2071				desired_targets,
2072				snapshot.clone(),
2073				round,
2074				Default::default(),
2075			)
2076			.unwrap();
2077
2078			let full_supports = ready_solution.supports.into_iter().collect::<Vec<_>>();
2079
2080			// gather the expected trimmed supports (lowest stake from supports with more backers
2081			// than expected when MaxBackersPerWinner is 2) from the full, unbounded supports.
2082			let expected_trimmed_supports = full_supports
2083				.into_iter()
2084				.filter(|(_, s)| s.voters.len() as u32 > trim_backers_bound)
2085				.map(|(t, s)| (t, s.voters.into_iter().min_by(|a, b| a.1.cmp(&b.1)).unwrap()))
2086				.collect::<Vec<_>>();
2087
2088			// election with bounded 2 max backers per winnner.
2089			MaxBackersPerWinner::set(trim_backers_bound);
2090			let (solution, score, _, trimming_status) =
2091				Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
2092					voters.clone(),
2093					targets.clone(),
2094					desired_targets,
2095				)
2096				.unwrap();
2097
2098			assert_eq!(trimming_status.trimmed_edges(), 2);
2099
2100			let ready_solution = Miner::<Runtime>::feasibility_check(
2101				RawSolution { solution, score, round },
2102				Default::default(),
2103				desired_targets,
2104				snapshot.clone(),
2105				round,
2106				Default::default(),
2107			)
2108			.unwrap();
2109
2110			let trimmed_supports = ready_solution.supports.into_iter().collect::<Vec<_>>();
2111
2112			// gather all trimmed_supports edges from the trimmed solution.
2113			let mut trimmed_supports_edges_full = vec![];
2114			for (t, s) in trimmed_supports {
2115				for v in s.voters {
2116					trimmed_supports_edges_full.push((t, v));
2117				}
2118			}
2119
2120			// expected trimmed supports set should be disjoint to the trimmed_supports full set of
2121			// edges.
2122			for edge in trimmed_supports_edges_full {
2123				assert!(!expected_trimmed_supports.contains(&edge));
2124			}
2125		})
2126	}
2127
2128	#[test]
2129	fn trim_assignments_length_does_not_modify_when_short_enough() {
2130		ExtBuilder::default().build_and_execute(|| {
2131			roll_to_unsigned();
2132
2133			// given
2134			let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
2135			let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2136			let encoded_len = solution.encoded_size() as u32;
2137			let solution_clone = solution.clone();
2138
2139			// when
2140			let trimmed_len = Miner::<Runtime>::trim_assignments_length(
2141				encoded_len,
2142				&mut assignments,
2143				encoded_size_of,
2144			)
2145			.unwrap();
2146
2147			// then
2148			let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2149			assert_eq!(solution, solution_clone);
2150			assert_eq!(trimmed_len, 0);
2151		});
2152	}
2153
2154	#[test]
2155	fn trim_assignments_length_modifies_when_too_long() {
2156		ExtBuilder::default().build().execute_with(|| {
2157			roll_to_unsigned();
2158
2159			// given
2160			let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
2161			let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2162			let encoded_len = solution.encoded_size();
2163			let solution_clone = solution.clone();
2164
2165			// when
2166			let trimmed_len = Miner::<Runtime>::trim_assignments_length(
2167				encoded_len as u32 - 1,
2168				&mut assignments,
2169				encoded_size_of,
2170			)
2171			.unwrap();
2172
2173			// then
2174			let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2175			assert_ne!(solution, solution_clone);
2176			assert!(solution.encoded_size() < encoded_len);
2177			assert_eq!(trimmed_len, 1);
2178		});
2179	}
2180
2181	#[test]
2182	fn trim_assignments_length_trims_lowest_stake() {
2183		ExtBuilder::default().build().execute_with(|| {
2184			roll_to_unsigned();
2185
2186			// given
2187			let TrimHelpers { voters, mut assignments, encoded_size_of, voter_index } =
2188				trim_helpers();
2189			let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2190			let encoded_len = solution.encoded_size() as u32;
2191			let count = assignments.len();
2192			let min_stake_voter = voters
2193				.iter()
2194				.map(|(id, weight, _)| (weight, id))
2195				.min()
2196				.and_then(|(_, id)| voter_index(id))
2197				.unwrap();
2198
2199			// when
2200			Miner::<Runtime>::trim_assignments_length(
2201				encoded_len - 1,
2202				&mut assignments,
2203				encoded_size_of,
2204			)
2205			.unwrap();
2206
2207			// then
2208			assert_eq!(assignments.len(), count - 1, "we must have removed exactly one assignment");
2209			assert!(
2210				assignments.iter().all(|IndexAssignment { who, .. }| *who != min_stake_voter),
2211				"min_stake_voter must no longer be in the set of voters",
2212			);
2213		});
2214	}
2215
2216	#[test]
2217	fn trim_assignments_length_wont_panic() {
2218		// we shan't panic if assignments are initially empty.
2219		ExtBuilder::default().build_and_execute(|| {
2220			let encoded_size_of = Box::new(|assignments: &[IndexAssignmentOf<Runtime>]| {
2221				SolutionOf::<Runtime>::try_from(assignments).map(|solution| solution.encoded_size())
2222			});
2223
2224			let mut assignments = vec![];
2225
2226			// since we have 16 fields, we need to store the length fields of 16 vecs, thus 16 bytes
2227			// minimum.
2228			let min_solution_size = encoded_size_of(&assignments).unwrap();
2229			assert_eq!(min_solution_size, SolutionOf::<Runtime>::LIMIT);
2230
2231			// all of this should not panic.
2232			Miner::<Runtime>::trim_assignments_length(0, &mut assignments, encoded_size_of.clone())
2233				.unwrap();
2234			Miner::<Runtime>::trim_assignments_length(1, &mut assignments, encoded_size_of.clone())
2235				.unwrap();
2236			Miner::<Runtime>::trim_assignments_length(
2237				min_solution_size as u32,
2238				&mut assignments,
2239				encoded_size_of,
2240			)
2241			.unwrap();
2242		});
2243
2244		// or when we trim it to zero.
2245		ExtBuilder::default().build_and_execute(|| {
2246			// we need snapshot for `trim_helpers` to work.
2247			roll_to_unsigned();
2248			let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
2249			assert!(assignments.len() > 0);
2250
2251			// trim to min solution size.
2252			let min_solution_size = SolutionOf::<Runtime>::LIMIT as u32;
2253			Miner::<Runtime>::trim_assignments_length(
2254				min_solution_size,
2255				&mut assignments,
2256				encoded_size_of,
2257			)
2258			.unwrap();
2259			assert_eq!(assignments.len(), 0);
2260		});
2261	}
2262
2263	// all the other solution-generation functions end up delegating to `mine_solution`, so if we
2264	// demonstrate that `mine_solution` solutions are all trimmed to an acceptable length, then
2265	// we know that higher-level functions will all also have short-enough solutions.
2266	#[test]
2267	fn mine_solution_solutions_always_within_acceptable_length() {
2268		ExtBuilder::default().build_and_execute(|| {
2269			roll_to_unsigned();
2270
2271			// how long would the default solution be?
2272			let solution = MultiPhase::mine_solution().unwrap();
2273			let max_length = <Runtime as MinerConfig>::MaxLength::get();
2274			let solution_size = solution.0.solution.encoded_size();
2275			assert!(solution_size <= max_length as usize);
2276
2277			// now set the max size to less than the actual size and regenerate
2278			<Runtime as MinerConfig>::MaxLength::set(solution_size as u32 - 1);
2279			let solution = MultiPhase::mine_solution().unwrap();
2280			let max_length = <Runtime as MinerConfig>::MaxLength::get();
2281			let solution_size = solution.0.solution.encoded_size();
2282			assert!(solution_size <= max_length as usize);
2283		});
2284	}
2285}