1use 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#[derive(Debug, Eq, PartialEq)]
54pub enum SnapshotType {
55	Voters(PageIndex),
57	Targets,
59	Metadata,
61	DesiredTargets,
63}
64
65pub(crate) type MinerSolverErrorOf<T> = <<T as MinerConfig>::Solver as NposSolver>::Error;
66
67#[derive(
69	frame_support::DebugNoBound, frame_support::EqNoBound, frame_support::PartialEqNoBound,
70)]
71pub enum MinerError<T: MinerConfig> {
72	NposElections(sp_npos_elections::Error),
74	Solver(MinerSolverErrorOf<T>),
76	SnapshotUnAvailable(SnapshotType),
78	Common(CommonError),
80	Feasibility(verifier::FeasibilityError),
82	InvalidPage,
84	TooManyWinnersRemoved,
86	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#[derive(
110	frame_support::DebugNoBound, frame_support::EqNoBound, frame_support::PartialEqNoBound,
111)]
112pub enum OffchainMinerError<T: Config> {
113	BaseMiner(MinerError<T::MinerConfig>),
115	Common(CommonError),
117	Lock(&'static str),
119	PoolSubmissionFailed,
121	NoStoredSolution,
123	SolutionCallInvalid,
125	FailedToStoreSolution,
127	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
143pub trait MinerConfig {
148	type AccountId: Ord + Clone + codec::Codec + core::fmt::Debug;
150	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	type Solver: NposSolver<AccountId = Self::AccountId>;
164	type MaxLength: Get<u32>;
170	type MaxVotesPerVoter: Get<u32>;
176	type MaxWinnersPerPage: Get<u32>;
182	type MaxBackersPerWinner: Get<u32>;
188	type MaxBackersPerWinnerFinal: Get<u32>;
194	type Pages: Get<u32>;
198	type VoterSnapshotPerBlock: Get<u32>;
202	type TargetSnapshotPerBlock: Get<u32>;
206	type Hash: Eq + PartialEq;
208}
209
210pub struct BaseMiner<T: MinerConfig>(sp_std::marker::PhantomData<T>);
213
214pub 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
224pub 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
233pub type FullSupportsOfMiner<T> = frame_election_provider_support::BoundedSupports<
239	<T as MinerConfig>::AccountId,
240	MaxWinnersFinal<T>,
241	<T as MinerConfig>::MaxBackersPerWinnerFinal,
242>;
243
244pub struct MineInput<T: MinerConfig> {
246	pub desired_targets: u32,
248	pub all_targets: BoundedVec<T::AccountId, T::TargetSnapshotPerBlock>,
250	pub voter_pages: AllVoterPagesOf<T>,
256	pub pages: PageIndex,
261	pub do_reduce: bool,
263	pub round: u32,
265}
266
267impl<T: MinerConfig> BaseMiner<T> {
268	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		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		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		let trimmed_assignments = {
313			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			let cache = helpers::generate_voter_cache::<T, _>(&all_voters);
325			let stake_of = helpers::stake_of_fn::<T, _>(&all_voters, &cache);
326
327			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				let count = if do_reduce { reduce(&mut staked) } else { 0 };
335				(count, staked)
336			};
337
338			let (_pre_score, final_trimmed_assignments, winners_removed, backers_removed) = {
340				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				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		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			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		let mut solution_pages: Vec<SolutionOf<T>> = paged_assignments
384			.into_iter()
385			.enumerate()
386			.map(|(page_index, assignment_page)| {
387				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				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		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		let mut paged = PagedRawSolution { round, solution_pages, score: Default::default() };
422
423		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	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		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				Ok(supports)
484			})
485	}
486
487	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		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		let supports = to_supports(&untrimmed_staked_assignments);
529		drop(untrimmed_staked_assignments);
530
531		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		let trimmed_staked_assignments = supports_to_staked_assignment(bounded.into());
546		let trimmed_assignments =
548			assignment_staked_to_ratio_normalized(trimmed_staked_assignments)?;
549
550		Ok(trimmed_assignments)
551	}
552
553	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			let needs_weight_trim = false;
587			needs_weight_trim || needs_len_trim
588		};
589
590		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		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				removed.saturating_inc();
641			} else {
642				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
667pub struct OffchainWorkerMiner<T: Config>(sp_std::marker::PhantomData<T>);
671
672impl<T: Config> OffchainWorkerMiner<T> {
673	pub(crate) const OFFCHAIN_LOCK: &'static [u8] = b"parity/multi-block-unsigned-election/lock";
675	const OFFCHAIN_LAST_BLOCK: &'static [u8] = b"parity/multi-block-unsigned-election";
677	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		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		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		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	fn mine_checked_call() -> Result<Call<T>, OffchainMinerError<T>> {
745		let reduce = true;
747
748		let paged_solution = Self::mine_solution(T::MinerPages::get(), reduce)
751			.map_err::<OffchainMinerError<T>, _>(Into::into)?;
752		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	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	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		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	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	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				if let Call::submit_unsigned { paged_solution, .. } = &call {
852					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						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						sublog!(warn, "unsigned::ocw-miner", "wiping infeasible solution ({:?}).", error);
891						Self::clear_offchain_solution_cache();
893
894						Err(error)
896					},
897					_ => {
898						sublog!(debug, "unsigned::ocw-miner", "unhandled error in restoring offchain solution {:?}", error);
899						Err(error)
901					},
902				}
903			})?;
904
905		Self::submit_call(call)
906	}
907
908	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					Ok(Some(head)) if now > head + threshold => {
931						Ok(now)
933					},
934					_ => {
935						Ok(now)
938					},
939				}
940			},
941		);
942
943		match mutate_stat {
944			Ok(_) => Ok(()),
946			Err(MutateStorageError::ConcurrentModification(_)) => Err(OffchainMinerError::Lock(
948				"failed to write to offchain db (concurrent modification).",
949			)),
950			Err(MutateStorageError::ValueFunctionFailed(why)) => Err(OffchainMinerError::Lock(why)),
952		}
953	}
954
955	fn save_solution(
957		call: &Call<T>,
958		snapshot_fingerprint: T::Hash,
959	) -> Result<(), OffchainMinerError<T>> {
960		sublog!(debug, "unsigned::ocw-miner", "saving a call to the offchain storage.");
961		let storage = StorageValueRef::persistent(&Self::OFFCHAIN_CACHED_CALL);
962		match storage.mutate::<_, (), _>(|_| Ok((call.clone(), snapshot_fingerprint))) {
963			Ok(_) => Ok(()),
964			Err(MutateStorageError::ConcurrentModification(_)) =>
965				Err(OffchainMinerError::FailedToStoreSolution),
966			Err(MutateStorageError::ValueFunctionFailed(_)) => {
967				Err(OffchainMinerError::FailedToStoreSolution)
972			},
973		}
974	}
975
976	fn restore_solution() -> Result<(Call<T>, T::Hash), OffchainMinerError<T>> {
978		StorageValueRef::persistent(&Self::OFFCHAIN_CACHED_CALL)
979			.get()
980			.ok()
981			.flatten()
982			.ok_or(OffchainMinerError::NoStoredSolution)
983	}
984
985	fn clear_offchain_solution_cache() {
987		sublog!(debug, "unsigned::ocw-miner", "clearing offchain call cache storage.");
988		let mut storage = StorageValueRef::persistent(&Self::OFFCHAIN_CACHED_CALL);
989		storage.clear();
990	}
991
992	#[cfg(test)]
993	fn cached_solution() -> Option<Call<T>> {
994		StorageValueRef::persistent(&Self::OFFCHAIN_CACHED_CALL)
995			.get::<Call<T>>()
996			.unwrap()
997	}
998}
999
1000#[cfg(test)]
1002mod trimming {
1003	use super::*;
1004	use crate::{mock::*, verifier::Verifier};
1005	use frame_election_provider_support::TryFromUnboundedPagedSupports;
1006	use sp_npos_elections::Support;
1007
1008	#[test]
1009	fn solution_without_any_trimming() {
1010		ExtBuilder::unsigned().build_and_execute(|| {
1011			let mut current_voters = Voters::get();
1013			current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1014			Voters::set(current_voters);
1015
1016			roll_to_snapshot_created();
1017
1018			let solution = mine_full_solution().unwrap();
1020			assert_eq!(
1021				solution.solution_pages.iter().map(|page| page.voter_count()).sum::<usize>(),
1022				8
1023			);
1024
1025			assert_eq!(solution.solution_pages.encoded_size(), 105);
1026
1027			load_mock_signed_and_start(solution);
1028			let supports = roll_to_full_verification();
1029
1030			assert!(VerifierPallet::queued_score().is_some());
1032
1033			assert_eq!(
1034				supports,
1035				vec![
1036					vec![
1037						(30, Support { total: 30, voters: vec![(30, 30)] }),
1038						(40, Support { total: 40, voters: vec![(40, 40)] })
1039					],
1040					vec![
1041						(30, Support { total: 11, voters: vec![(5, 2), (6, 2), (7, 7)] }),
1042						(40, Support { total: 7, voters: vec![(5, 3), (6, 4)] })
1043					],
1044					vec![(40, Support { total: 9, voters: vec![(2, 2), (3, 3), (4, 4)] })]
1045				]
1046				.try_from_unbounded_paged()
1047				.unwrap()
1048			);
1049		})
1050	}
1051
1052	#[test]
1053	fn trim_length() {
1054		ExtBuilder::unsigned().miner_max_length(104).build_and_execute(|| {
1055			let mut current_voters = Voters::get();
1057			current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1058			Voters::set(current_voters);
1059
1060			roll_to_snapshot_created();
1061			ensure_voters(3, 12);
1062
1063			let solution = mine_full_solution().unwrap();
1064
1065			assert_eq!(
1066				solution.solution_pages.iter().map(|page| page.voter_count()).sum::<usize>(),
1067				7
1068			);
1069
1070			assert_eq!(solution.solution_pages.encoded_size(), 99);
1071
1072			load_mock_signed_and_start(solution);
1073			let supports = roll_to_full_verification();
1074
1075			assert!(VerifierPallet::queued_score().is_some());
1077
1078			assert_eq!(
1079				supports,
1080				vec![
1081					vec![(40, Support { total: 40, voters: vec![(40, 40)] })],
1084					vec![
1085						(30, Support { total: 11, voters: vec![(5, 2), (6, 2), (7, 7)] }),
1086						(40, Support { total: 7, voters: vec![(5, 3), (6, 4)] })
1087					],
1088					vec![(40, Support { total: 9, voters: vec![(2, 2), (3, 3), (4, 4)] })]
1089				]
1090				.try_from_unbounded_paged()
1091				.unwrap()
1092			);
1093		});
1094	}
1095
1096	#[test]
1097	fn trim_length_2() {
1098		ExtBuilder::unsigned().miner_max_length(98).build_and_execute(|| {
1099			let mut current_voters = Voters::get();
1101			current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1102			Voters::set(current_voters);
1103
1104			roll_to_snapshot_created();
1105			ensure_voters(3, 12);
1106
1107			let solution = mine_full_solution().unwrap();
1108
1109			assert_eq!(
1110				solution.solution_pages.iter().map(|page| page.voter_count()).sum::<usize>(),
1111				6
1112			);
1113
1114			assert_eq!(solution.solution_pages.encoded_size(), 93);
1115
1116			load_mock_signed_and_start(solution);
1117			let supports = roll_to_full_verification();
1118
1119			assert!(VerifierPallet::queued_score().is_some());
1121
1122			assert_eq!(
1123				supports,
1124				vec![
1125					vec![],
1126					vec![
1127						(30, Support { total: 11, voters: vec![(5, 2), (6, 2), (7, 7)] }),
1128						(40, Support { total: 7, voters: vec![(5, 3), (6, 4)] })
1129					],
1130					vec![(40, Support { total: 9, voters: vec![(2, 2), (3, 3), (4, 4)] })]
1131				]
1132				.try_from_unbounded_paged()
1133				.unwrap()
1134			);
1135		});
1136	}
1137
1138	#[test]
1139	fn trim_length_3() {
1140		ExtBuilder::unsigned().miner_max_length(92).build_and_execute(|| {
1141			let mut current_voters = Voters::get();
1143			current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1144			Voters::set(current_voters);
1145
1146			roll_to_snapshot_created();
1147			ensure_voters(3, 12);
1148
1149			let solution = mine_full_solution().unwrap();
1150
1151			assert_eq!(
1152				solution.solution_pages.iter().map(|page| page.voter_count()).sum::<usize>(),
1153				5
1154			);
1155
1156			assert_eq!(solution.solution_pages.encoded_size(), 83);
1157
1158			load_mock_signed_and_start(solution);
1159			let supports = roll_to_full_verification();
1160
1161			assert!(VerifierPallet::queued_score().is_some());
1163
1164			assert_eq!(
1165				supports,
1166				vec![
1167					vec![],
1168					vec![
1169						(30, Support { total: 9, voters: vec![(6, 2), (7, 7)] }),
1170						(40, Support { total: 4, voters: vec![(6, 4)] })
1171					],
1172					vec![(40, Support { total: 9, voters: vec![(2, 2), (3, 3), (4, 4)] })]
1173				]
1174				.try_from_unbounded_paged()
1175				.unwrap()
1176			);
1177		});
1178	}
1179
1180	#[test]
1181	fn trim_backers_per_page_works() {
1182		ExtBuilder::unsigned().max_backers_per_winner(2).build_and_execute(|| {
1183			let mut current_voters = Voters::get();
1185			current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1186			Voters::set(current_voters);
1187
1188			roll_to_snapshot_created();
1189			ensure_voters(3, 12);
1190
1191			let solution = mine_full_solution().unwrap();
1192
1193			load_mock_signed_and_start(solution);
1194			let supports = roll_to_full_verification();
1195
1196			assert!(VerifierPallet::queued_score().is_some());
1198
1199			assert_eq!(
1201				supports,
1202				vec![
1203					vec![
1204						(30, Support { total: 30, voters: vec![(30, 30)] }),
1205						(40, Support { total: 40, voters: vec![(40, 40)] })
1206					],
1207					vec![
1208						(30, Support { total: 9, voters: vec![(6, 2), (7, 7)] }),
1209						(40, Support { total: 9, voters: vec![(5, 5), (6, 4)] }) ],
1214					vec![(40, Support { total: 7, voters: vec![(3, 3), (4, 4)] })]
1215				]
1216				.try_from_unbounded_paged()
1217				.unwrap()
1218			);
1219		})
1220	}
1221
1222	#[test]
1223	fn trim_backers_per_page_works_2() {
1224		ExtBuilder::unsigned().max_backers_per_winner(1).build_and_execute(|| {
1227			let mut current_voters = Voters::get();
1229			current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1230			Voters::set(current_voters);
1231
1232			roll_to_snapshot_created();
1233			ensure_voters(3, 12);
1234
1235			let solution = mine_full_solution().unwrap();
1236
1237			load_mock_signed_and_start(solution);
1238			let supports = roll_to_full_verification();
1239
1240			assert!(VerifierPallet::queued_score().is_some());
1242
1243			assert_eq!(
1245				supports,
1246				vec![
1247					vec![
1248						(30, Support { total: 30, voters: vec![(30, 30)] }),
1249						(40, Support { total: 40, voters: vec![(40, 40)] })
1250					],
1251					vec![
1252						(30, Support { total: 7, voters: vec![(7, 7)] }),
1253						(40, Support { total: 6, voters: vec![(6, 6)] })
1254					],
1255					vec![(40, Support { total: 4, voters: vec![(4, 4)] })]
1256				]
1257				.try_from_unbounded_paged()
1258				.unwrap()
1259			);
1260		})
1261	}
1262
1263	#[test]
1264	fn trim_backers_final_works() {
1265		ExtBuilder::unsigned()
1266			.max_backers_per_winner(4)
1267			.max_backers_per_winner_final(4)
1268			.build_and_execute(|| {
1269				let mut current_voters = Voters::get();
1271				current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1272				Voters::set(current_voters);
1273
1274				roll_to_snapshot_created();
1275				ensure_voters(3, 12);
1276
1277				let solution = mine_full_solution().unwrap();
1278
1279				load_mock_signed_and_start(solution);
1280				let supports = roll_to_full_verification();
1281
1282				assert!(VerifierPallet::queued_score().is_some());
1284
1285				assert_eq!(
1288					supports,
1289					vec![
1290						vec![
1291							(30, Support { total: 30, voters: vec![(30, 30)] }),
1292							(40, Support { total: 40, voters: vec![(40, 40)] })
1293						],
1294						vec![
1295							(30, Support { total: 14, voters: vec![(5, 5), (6, 2), (7, 7)] }),
1296							(40, Support { total: 4, voters: vec![(6, 4)] })
1297						],
1298						vec![(40, Support { total: 7, voters: vec![(3, 3), (4, 4)] })]
1299					]
1300					.try_from_unbounded_paged()
1301					.unwrap()
1302				);
1303			})
1304	}
1305
1306	#[test]
1307	fn trim_backers_per_page_and_final_works() {
1308		ExtBuilder::unsigned()
1309			.max_backers_per_winner_final(4)
1310			.max_backers_per_winner(2)
1311			.build_and_execute(|| {
1312				let mut current_voters = Voters::get();
1314				current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1315				Voters::set(current_voters);
1316
1317				roll_to_snapshot_created();
1318				ensure_voters(3, 12);
1319
1320				let solution = mine_full_solution().unwrap();
1321
1322				load_mock_signed_and_start(solution);
1323				let supports = roll_to_full_verification();
1324
1325				assert!(VerifierPallet::queued_score().is_some());
1327
1328				assert_eq!(
1330					supports,
1331					vec![
1332						vec![
1333							(30, Support { total: 30, voters: vec![(30, 30)] }),
1334							(40, Support { total: 40, voters: vec![(40, 40)] })
1335						],
1336						vec![
1337							(30, Support { total: 12, voters: vec![(5, 5), (7, 7)] }),
1338							(40, Support { total: 6, voters: vec![(6, 6)] })
1339						],
1340						vec![(40, Support { total: 7, voters: vec![(3, 3), (4, 4)] })]
1341					]
1342					.try_from_unbounded_paged()
1343					.unwrap()
1344				);
1345			})
1346	}
1347
1348	#[test]
1349	fn aggressive_backer_trimming_maintains_winner_count() {
1350		ExtBuilder::unsigned()
1353			.desired_targets(3)
1354			.max_winners_per_page(2)
1355			.pages(2)
1356			.max_backers_per_winner_final(1) .max_backers_per_winner(1) .build_and_execute(|| {
1359				let mut current_voters = Voters::get();
1363				current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1364				Voters::set(current_voters);
1365
1366				roll_to_snapshot_created();
1367
1368				let solution = mine_full_solution().unwrap();
1369
1370				assert!(solution.solution_pages.len() > 0);
1372
1373				let winner_count = solution
1374					.solution_pages
1375					.iter()
1376					.flat_map(|page| page.unique_targets())
1377					.collect::<std::collections::HashSet<_>>()
1378					.len();
1379
1380				assert_eq!(winner_count, 3);
1384
1385				load_mock_signed_and_start(solution);
1387				let _supports = roll_to_full_verification();
1388
1389				assert!(VerifierPallet::queued_score().is_some());
1391			})
1392	}
1393}
1394
1395#[cfg(test)]
1396mod base_miner {
1397	use std::vec;
1398
1399	use super::*;
1400	use crate::{mock::*, Snapshot};
1401	use frame_election_provider_support::TryFromUnboundedPagedSupports;
1402	use sp_npos_elections::Support;
1403	use sp_runtime::PerU16;
1404
1405	#[test]
1406	fn pagination_does_not_affect_score() {
1407		let score_1 = ExtBuilder::unsigned()
1408			.pages(1)
1409			.voter_per_page(12)
1410			.build_unchecked()
1411			.execute_with(|| {
1412				roll_to_snapshot_created();
1413				mine_full_solution().unwrap().score
1414			});
1415		let score_2 = ExtBuilder::unsigned()
1416			.pages(2)
1417			.voter_per_page(6)
1418			.build_unchecked()
1419			.execute_with(|| {
1420				roll_to_snapshot_created();
1421				mine_full_solution().unwrap().score
1422			});
1423		let score_3 = ExtBuilder::unsigned()
1424			.pages(3)
1425			.voter_per_page(4)
1426			.build_unchecked()
1427			.execute_with(|| {
1428				roll_to_snapshot_created();
1429				mine_full_solution().unwrap().score
1430			});
1431
1432		assert_eq!(score_1, score_2);
1433		assert_eq!(score_2, score_3);
1434	}
1435
1436	#[test]
1437	fn mine_solution_single_page_works() {
1438		ExtBuilder::unsigned().pages(1).voter_per_page(8).build_and_execute(|| {
1439			roll_to_snapshot_created();
1440
1441			ensure_voters(1, 8);
1442			ensure_targets(1, 4);
1443
1444			assert_eq!(
1445				Snapshot::<Runtime>::voters(0)
1446					.unwrap()
1447					.into_iter()
1448					.map(|(x, _, _)| x)
1449					.collect::<Vec<_>>(),
1450				vec![1, 2, 3, 4, 5, 6, 7, 8]
1451			);
1452
1453			let paged = mine_full_solution().unwrap();
1454			assert_eq!(paged.solution_pages.len(), 1);
1455
1456			OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1458
1459			load_mock_signed_and_start(paged.clone());
1461			let supports = roll_to_full_verification();
1462
1463			assert_eq!(
1464				supports,
1465				vec![vec![
1466					(10, Support { total: 30, voters: vec![(1, 10), (4, 5), (5, 5), (8, 10)] }),
1467					(
1468						40,
1469						Support {
1470							total: 40,
1471							voters: vec![(2, 10), (3, 10), (4, 5), (5, 5), (6, 10)]
1472						}
1473					)
1474				]]
1475				.try_from_unbounded_paged()
1476				.unwrap()
1477			);
1478
1479			assert_eq!(
1482				paged.score,
1483				ElectionScore { minimal_stake: 30, sum_stake: 70, sum_stake_squared: 2500 }
1484			);
1485		})
1486	}
1487
1488	#[test]
1489	fn mine_solution_double_page_works() {
1490		ExtBuilder::unsigned().pages(2).voter_per_page(4).build_and_execute(|| {
1491			roll_to_snapshot_created();
1492
1493			ensure_voters(2, 8);
1495			ensure_targets(1, 4);
1497
1498			assert_eq!(
1500				Snapshot::<Runtime>::voters(0)
1501					.unwrap()
1502					.into_iter()
1503					.map(|(x, _, _)| x)
1504					.collect::<Vec<_>>(),
1505				vec![5, 6, 7, 8]
1506			);
1507			assert_eq!(
1508				Snapshot::<Runtime>::voters(1)
1509					.unwrap()
1510					.into_iter()
1511					.map(|(x, _, _)| x)
1512					.collect::<Vec<_>>(),
1513				vec![1, 2, 3, 4]
1514			);
1515			assert_eq!(Snapshot::<Runtime>::targets().unwrap(), vec![10, 20, 30, 40]);
1517			let paged = mine_full_solution().unwrap();
1518
1519			assert_eq!(
1520				paged.solution_pages,
1521				vec![
1522					TestNposSolution {
1523						votes1: vec![(1, 3), (3, 0)],
1526						votes2: vec![(0, [(0, PerU16::from_parts(32768))], 3)],
1528						..Default::default()
1529					},
1530					TestNposSolution {
1531						votes1: vec![(0, 0), (1, 3), (2, 3)],
1535						votes2: vec![(3, [(0, PerU16::from_parts(32768))], 3)],
1537						..Default::default()
1538					},
1539				]
1540			);
1541
1542			OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, false).unwrap();
1544
1545			load_mock_signed_and_start(paged.clone());
1547			let supports = roll_to_full_verification();
1548
1549			assert_eq!(
1550				supports,
1551				vec![
1552					vec![
1554						(10, Support { total: 15, voters: vec![(5, 5), (8, 10)] }),
1555						(40, Support { total: 15, voters: vec![(5, 5), (6, 10)] })
1556					],
1557					vec![
1559						(10, Support { total: 15, voters: vec![(1, 10), (4, 5)] }),
1560						(40, Support { total: 25, voters: vec![(2, 10), (3, 10), (4, 5)] })
1561					]
1562				]
1563				.try_from_unbounded_paged()
1564				.unwrap()
1565			);
1566
1567			assert_eq!(
1568				paged.score,
1569				ElectionScore { minimal_stake: 30, sum_stake: 70, sum_stake_squared: 2500 }
1570			);
1571		})
1572	}
1573
1574	#[test]
1575	fn mine_solution_triple_page_works() {
1576		ExtBuilder::unsigned().pages(3).voter_per_page(4).build_and_execute(|| {
1577			roll_to_snapshot_created();
1578
1579			ensure_voters(3, 12);
1580			ensure_targets(1, 4);
1581
1582			assert_eq!(
1584				Snapshot::<Runtime>::voters(2)
1585					.unwrap()
1586					.into_iter()
1587					.map(|(x, _, _)| x)
1588					.collect::<Vec<_>>(),
1589				vec![1, 2, 3, 4]
1590			);
1591			assert_eq!(
1592				Snapshot::<Runtime>::voters(1)
1593					.unwrap()
1594					.into_iter()
1595					.map(|(x, _, _)| x)
1596					.collect::<Vec<_>>(),
1597				vec![5, 6, 7, 8]
1598			);
1599			assert_eq!(
1600				Snapshot::<Runtime>::voters(0)
1601					.unwrap()
1602					.into_iter()
1603					.map(|(x, _, _)| x)
1604					.collect::<Vec<_>>(),
1605				vec![10, 20, 30, 40]
1606			);
1607
1608			let paged = mine_full_solution().unwrap();
1609			assert_eq!(
1610				paged.solution_pages,
1611				vec![
1612					TestNposSolution { votes1: vec![(2, 2), (3, 3)], ..Default::default() },
1613					TestNposSolution {
1614						votes1: vec![(2, 2)],
1615						votes2: vec![
1616							(0, [(2, PerU16::from_parts(32768))], 3),
1617							(1, [(2, PerU16::from_parts(32768))], 3)
1618						],
1619						..Default::default()
1620					},
1621					TestNposSolution {
1622						votes1: vec![(2, 3), (3, 3)],
1623						votes2: vec![(1, [(2, PerU16::from_parts(32768))], 3)],
1624						..Default::default()
1625					},
1626				]
1627			);
1628
1629			OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1631			load_mock_signed_and_start(paged.clone());
1633			let supports = roll_to_full_verification();
1634
1635			assert_eq!(
1636				supports,
1637				vec![
1638					vec![
1640						(30, Support { total: 30, voters: vec![(30, 30)] }),
1641						(40, Support { total: 40, voters: vec![(40, 40)] })
1642					],
1643					vec![
1645						(30, Support { total: 20, voters: vec![(5, 5), (6, 5), (7, 10)] }),
1646						(40, Support { total: 10, voters: vec![(5, 5), (6, 5)] })
1647					],
1648					vec![
1650						(30, Support { total: 5, voters: vec![(2, 5)] }),
1651						(40, Support { total: 25, voters: vec![(2, 5), (3, 10), (4, 10)] })
1652					]
1653				]
1654				.try_from_unbounded_paged()
1655				.unwrap()
1656			);
1657
1658			assert_eq!(
1659				paged.score,
1660				ElectionScore { minimal_stake: 55, sum_stake: 130, sum_stake_squared: 8650 }
1661			);
1662		})
1663	}
1664
1665	#[test]
1666	fn mine_solution_choses_most_significant_pages() {
1667		ExtBuilder::unsigned().pages(2).voter_per_page(4).build_and_execute(|| {
1668			roll_to_snapshot_created();
1669
1670			ensure_voters(2, 8);
1671			ensure_targets(1, 4);
1672
1673			assert_eq!(
1675				Snapshot::<Runtime>::voters(0)
1676					.unwrap()
1677					.into_iter()
1678					.map(|(x, _, _)| x)
1679					.collect::<Vec<_>>(),
1680				vec![5, 6, 7, 8]
1681			);
1682			assert_eq!(
1684				Snapshot::<Runtime>::voters(1)
1685					.unwrap()
1686					.into_iter()
1687					.map(|(x, _, _)| x)
1688					.collect::<Vec<_>>(),
1689				vec![1, 2, 3, 4]
1690			);
1691
1692			let paged = mine_solution(1).unwrap();
1694
1695			assert_eq!(
1696				paged.solution_pages,
1697				vec![TestNposSolution {
1698					votes1: vec![(0, 0), (1, 3), (2, 3)],
1702					votes2: vec![(3, [(0, PerU16::from_parts(32768))], 3)],
1704					..Default::default()
1705				}]
1706			);
1707
1708			OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1710			load_mock_signed_and_start(paged.clone());
1712			let supports = roll_to_full_verification();
1713
1714			assert_eq!(
1715				supports,
1716				vec![
1717					vec![],
1719					vec![
1721						(10, Support { total: 15, voters: vec![(1, 10), (4, 5)] }),
1722						(40, Support { total: 25, voters: vec![(2, 10), (3, 10), (4, 5)] })
1723					]
1724				]
1725				.try_from_unbounded_paged()
1726				.unwrap()
1727			);
1728
1729			assert_eq!(
1730				paged.score,
1731				ElectionScore { minimal_stake: 15, sum_stake: 40, sum_stake_squared: 850 }
1732			);
1733		})
1734	}
1735
1736	#[test]
1737	fn mine_solution_2_out_of_3_pages() {
1738		ExtBuilder::unsigned().pages(3).voter_per_page(4).build_and_execute(|| {
1739			roll_to_snapshot_created();
1740
1741			ensure_voters(3, 12);
1742			ensure_targets(1, 4);
1743
1744			assert_eq!(
1745				Snapshot::<Runtime>::voters(0)
1746					.unwrap()
1747					.into_iter()
1748					.map(|(x, _, _)| x)
1749					.collect::<Vec<_>>(),
1750				vec![10, 20, 30, 40]
1751			);
1752			assert_eq!(
1753				Snapshot::<Runtime>::voters(1)
1754					.unwrap()
1755					.into_iter()
1756					.map(|(x, _, _)| x)
1757					.collect::<Vec<_>>(),
1758				vec![5, 6, 7, 8]
1759			);
1760			assert_eq!(
1761				Snapshot::<Runtime>::voters(2)
1762					.unwrap()
1763					.into_iter()
1764					.map(|(x, _, _)| x)
1765					.collect::<Vec<_>>(),
1766				vec![1, 2, 3, 4]
1767			);
1768
1769			let paged = mine_solution(2).unwrap();
1771
1772			OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1774
1775			assert_eq!(
1776				paged.solution_pages,
1777				vec![
1778					TestNposSolution {
1784						votes1: vec![(1, 3), (3, 0)],
1785						votes2: vec![(0, [(0, PerU16::from_parts(32768))], 3)],
1786						..Default::default()
1787					},
1788					TestNposSolution {
1795						votes1: vec![(0, 0), (1, 3), (2, 3)],
1796						votes2: vec![(3, [(0, PerU16::from_parts(32768))], 3)],
1797						..Default::default()
1798					}
1799				]
1800			);
1801
1802			OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1804			load_mock_signed_and_start(paged.clone());
1806			let supports = roll_to_full_verification();
1807
1808			assert_eq!(
1809				supports,
1810				vec![
1811					vec![],
1813					vec![
1815						(10, Support { total: 15, voters: vec![(5, 5), (8, 10)] }),
1816						(40, Support { total: 15, voters: vec![(5, 5), (6, 10)] })
1817					],
1818					vec![
1820						(10, Support { total: 15, voters: vec![(1, 10), (4, 5)] }),
1821						(40, Support { total: 25, voters: vec![(2, 10), (3, 10), (4, 5)] })
1822					]
1823				]
1824				.try_from_unbounded_paged()
1825				.unwrap()
1826			);
1827
1828			assert_eq!(
1829				paged.score,
1830				ElectionScore { minimal_stake: 30, sum_stake: 70, sum_stake_squared: 2500 }
1831			);
1832		})
1833	}
1834
1835	#[test]
1836	fn can_reduce_solution() {
1837		ExtBuilder::unsigned().build_and_execute(|| {
1838			roll_to_snapshot_created();
1839			let full_edges = OffchainWorkerMiner::<Runtime>::mine_solution(Pages::get(), false)
1840				.unwrap()
1841				.solution_pages
1842				.iter()
1843				.fold(0, |acc, x| acc + x.edge_count());
1844			let reduced_edges = OffchainWorkerMiner::<Runtime>::mine_solution(Pages::get(), true)
1845				.unwrap()
1846				.solution_pages
1847				.iter()
1848				.fold(0, |acc, x| acc + x.edge_count());
1849
1850			assert!(reduced_edges < full_edges, "{} < {} not fulfilled", reduced_edges, full_edges);
1851		})
1852	}
1853}
1854
1855#[cfg(test)]
1856mod offchain_worker_miner {
1857	use crate::{verifier::Verifier, CommonError};
1858	use frame_support::traits::Hooks;
1859	use sp_runtime::offchain::storage_lock::{BlockAndTime, StorageLock};
1860
1861	use super::*;
1862	use crate::mock::*;
1863
1864	#[test]
1865	fn lock_prevents_frequent_execution() {
1866		let (mut ext, _) = ExtBuilder::unsigned().build_offchainify();
1867		ext.execute_with_sanity_checks(|| {
1868			let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
1869
1870			assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(25).is_ok());
1872
1873			assert_noop!(
1875				OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(26),
1876				OffchainMinerError::Lock("recently executed.")
1877			);
1878
1879			assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(
1881				(26 + offchain_repeat).into()
1882			)
1883			.is_ok());
1884
1885			assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(
1887				(26 + offchain_repeat - 3).into()
1888			)
1889			.is_err());
1890			assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(
1891				(26 + offchain_repeat - 2).into()
1892			)
1893			.is_err());
1894			assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(
1895				(26 + offchain_repeat - 1).into()
1896			)
1897			.is_err());
1898		})
1899	}
1900
1901	#[test]
1902	fn lock_released_after_successful_execution() {
1903		let (mut ext, pool) = ExtBuilder::unsigned().build_offchainify();
1905		ext.execute_with_sanity_checks(|| {
1906			let guard = StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_LOCK);
1907			let last_block =
1908				StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_LAST_BLOCK);
1909
1910			roll_to_unsigned_open();
1911
1912			assert!(guard.get::<bool>().unwrap().is_none());
1914
1915			UnsignedPallet::offchain_worker(25);
1917			assert_eq!(pool.read().transactions.len(), 1);
1918
1919			assert!(guard.get::<bool>().unwrap().is_none());
1921			assert_eq!(last_block.get::<BlockNumber>().unwrap(), Some(25));
1922		});
1923	}
1924
1925	#[test]
1926	fn lock_prevents_overlapping_execution() {
1927		let (mut ext, pool) = ExtBuilder::unsigned().build_offchainify();
1929		ext.execute_with_sanity_checks(|| {
1930			roll_to_unsigned_open();
1931
1932			let mut lock = StorageLock::<BlockAndTime<System>>::with_block_deadline(
1934				OffchainWorkerMiner::<Runtime>::OFFCHAIN_LOCK,
1935				UnsignedPhase::get().saturated_into(),
1936			);
1937			let guard = lock.lock();
1938
1939			UnsignedPallet::offchain_worker(25);
1941			assert_eq!(pool.read().transactions.len(), 0);
1942			UnsignedPallet::offchain_worker(26);
1943			assert_eq!(pool.read().transactions.len(), 0);
1944
1945			drop(guard);
1946
1947			UnsignedPallet::offchain_worker(25);
1949			assert_eq!(pool.read().transactions.len(), 1);
1950		});
1951	}
1952
1953	#[test]
1954	fn initial_ocw_runs_and_saves_new_cache() {
1955		let (mut ext, pool) = ExtBuilder::unsigned().build_offchainify();
1956		ext.execute_with_sanity_checks(|| {
1957			roll_to_unsigned_open();
1958
1959			let last_block =
1960				StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_LAST_BLOCK);
1961			let cache =
1962				StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
1963
1964			assert_eq!(last_block.get::<BlockNumber>(), Ok(None));
1965			assert_eq!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
1966
1967			UnsignedPallet::offchain_worker(25);
1969			assert_eq!(pool.read().transactions.len(), 1);
1970
1971			assert_eq!(last_block.get::<BlockNumber>(), Ok(Some(25)));
1972			assert!(matches!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
1973		})
1974	}
1975
1976	#[test]
1977	fn ocw_pool_submission_works() {
1978		let (mut ext, pool) = ExtBuilder::unsigned().build_offchainify();
1979		ext.execute_with_sanity_checks(|| {
1980			roll_to_unsigned_open();
1981
1982			roll_next_with_ocw(Some(pool.clone()));
1983			let encoded = pool.read().transactions[0].clone();
1986			let extrinsic: Extrinsic = codec::Decode::decode(&mut &*encoded).unwrap();
1987			let call = extrinsic.function;
1988			assert!(matches!(
1989				call,
1990				crate::mock::RuntimeCall::UnsignedPallet(
1991					crate::unsigned::Call::submit_unsigned { .. }
1992				)
1993			));
1994		})
1995	}
1996
1997	#[test]
1998	fn resubmits_after_offchain_repeat() {
1999		let (mut ext, pool) = ExtBuilder::unsigned().build_offchainify();
2000		ext.execute_with_sanity_checks(|| {
2001			let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2002			roll_to_unsigned_open();
2003
2004			assert!(OffchainWorkerMiner::<Runtime>::cached_solution().is_none());
2005			UnsignedPallet::offchain_worker(25);
2007			assert_eq!(pool.read().transactions.len(), 1);
2008			let tx_cache = pool.read().transactions[0].clone();
2009			pool.try_write().unwrap().transactions.clear();
2011
2012			UnsignedPallet::offchain_worker(25 + 1 + offchain_repeat);
2014			assert_eq!(pool.read().transactions.len(), 1);
2015
2016			let tx = &pool.read().transactions[0];
2018			assert_eq!(&tx_cache, tx);
2019		})
2020	}
2021
2022	#[test]
2023	fn regenerates_and_resubmits_after_offchain_repeat_if_no_cache() {
2024		let (mut ext, pool) = ExtBuilder::unsigned().build_offchainify();
2025		ext.execute_with_sanity_checks(|| {
2026			let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2027			roll_to_unsigned_open();
2028
2029			assert!(OffchainWorkerMiner::<Runtime>::cached_solution().is_none());
2030			UnsignedPallet::offchain_worker(25);
2032			assert_eq!(pool.read().transactions.len(), 1);
2033			let tx_cache = pool.read().transactions[0].clone();
2034			pool.try_write().unwrap().transactions.clear();
2036
2037			let mut call_cache =
2041				StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
2042			assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2043			call_cache.clear();
2044
2045			UnsignedPallet::offchain_worker(25 + 1 + offchain_repeat);
2047			assert_eq!(pool.read().transactions.len(), 1);
2048
2049			let tx = &pool.read().transactions[0];
2051			assert_eq!(&tx_cache, tx);
2052		})
2053	}
2054
2055	#[test]
2056	fn altering_snapshot_invalidates_solution_cache() {
2057		let (mut ext, pool) = ExtBuilder::unsigned().unsigned_phase(999).build_offchainify();
2059		ext.execute_with_sanity_checks(|| {
2060			let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2061			roll_to_unsigned_open();
2062			roll_next_with_ocw(None);
2063
2064			assert_eq!(pool.read().transactions.len(), 1);
2066			pool.try_write().unwrap().transactions.clear();
2067
2068			let call_cache =
2070				StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
2071			assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2072
2073			assert_eq!(crate::Snapshot::<Runtime>::targets().unwrap(), vec![10, 20, 30, 40]);
2076			let pre_fingerprint = crate::Snapshot::<Runtime>::fingerprint();
2077			crate::Snapshot::<Runtime>::remove_target(0);
2078			let post_fingerprint = crate::Snapshot::<Runtime>::fingerprint();
2079			assert_eq!(crate::Snapshot::<Runtime>::targets().unwrap(), vec![20, 30, 40]);
2080			assert_ne!(pre_fingerprint, post_fingerprint);
2081
2082			let now = System::block_number();
2084			roll_to_with_ocw(now + offchain_repeat + 1, None);
2085			assert_eq!(pool.read().transactions.len(), 0);
2087			assert_eq!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
2089
2090			roll_to_with_ocw(now + offchain_repeat + offchain_repeat + 2, None);
2092			assert_eq!(pool.read().transactions.len(), 1);
2093			assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2094		})
2095	}
2096
2097	#[test]
2098	fn wont_resubmit_if_weak_score() {
2099		let (mut ext, pool) = ExtBuilder::unsigned().unsigned_phase(999).build_offchainify();
2102		ext.execute_with_sanity_checks(|| {
2103			let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2104			roll_to_unsigned_open();
2107			roll_next_with_ocw(None);
2108
2109			assert_eq!(pool.read().transactions.len(), 1);
2111
2112			let call_cache =
2114				StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
2115			assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2116
2117			let weak_solution = raw_paged_from_supports(
2119				vec![vec![(40, Support { total: 10, voters: vec![(3, 10)] })]],
2120				0,
2121			);
2122			let weak_call = crate::unsigned::Call::<T>::submit_unsigned {
2123				paged_solution: Box::new(weak_solution),
2124			};
2125			call_cache.set(&weak_call);
2126
2127			roll_to_with_ocw(System::block_number() + offchain_repeat + 1, Some(pool.clone()));
2129			assert_eq!(pool.read().transactions.len(), 0);
2131			assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2133		})
2134	}
2135
2136	#[test]
2137	fn ocw_submission_e2e_works() {
2138		let (mut ext, pool) = ExtBuilder::unsigned().build_offchainify();
2139		ext.execute_with_sanity_checks(|| {
2140			assert!(VerifierPallet::queued_score().is_none());
2141			roll_to_with_ocw(25 + 1, Some(pool.clone()));
2142			assert!(VerifierPallet::queued_score().is_some());
2143
2144			let call_cache =
2146				StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
2147			assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2148
2149			assert_eq!(pool.read().transactions.len(), 0);
2151		})
2152	}
2153
2154	#[test]
2155	fn ocw_e2e_submits_and_queued_msp_only() {
2156		let (mut ext, pool) = ExtBuilder::unsigned().build_offchainify();
2157		ext.execute_with_sanity_checks(|| {
2158			roll_to_unsigned_open_with_ocw(None);
2160			roll_next_with_ocw(Some(pool.clone()));
2162
2163			assert_eq!(
2164				multi_block_events(),
2165				vec![
2166					crate::Event::PhaseTransitioned {
2167						from: Phase::Off,
2168						to: Phase::Snapshot(Pages::get())
2169					},
2170					crate::Event::PhaseTransitioned {
2171						from: Phase::Snapshot(0),
2172						to: Phase::Unsigned(UnsignedPhase::get() - 1)
2173					}
2174				]
2175			);
2176			assert_eq!(
2177				verifier_events(),
2178				vec![
2179					crate::verifier::Event::Verified(2, 2),
2180					crate::verifier::Event::Queued(
2181						ElectionScore { minimal_stake: 15, sum_stake: 40, sum_stake_squared: 850 },
2182						None
2183					)
2184				]
2185			);
2186			assert!(VerifierPallet::queued_score().is_some());
2187
2188			assert_eq!(pool.read().transactions.len(), 0);
2190		})
2191	}
2192
2193	#[test]
2194	fn multi_page_ocw_e2e_submits_and_queued_msp_only() {
2195		let (mut ext, pool) = ExtBuilder::unsigned().miner_pages(2).build_offchainify();
2196		ext.execute_with_sanity_checks(|| {
2197			roll_to_unsigned_open_with_ocw(None);
2199			roll_next_with_ocw(Some(pool.clone()));
2201
2202			assert_eq!(
2203				multi_block_events(),
2204				vec![
2205					crate::Event::PhaseTransitioned {
2206						from: Phase::Off,
2207						to: Phase::Snapshot(Pages::get())
2208					},
2209					crate::Event::PhaseTransitioned {
2210						from: Phase::Snapshot(0),
2211						to: Phase::Unsigned(UnsignedPhase::get() - 1)
2212					}
2213				]
2214			);
2215			assert_eq!(
2216				verifier_events(),
2217				vec![
2218					crate::verifier::Event::Verified(1, 2),
2219					crate::verifier::Event::Verified(2, 2),
2220					crate::verifier::Event::Queued(
2221						ElectionScore { minimal_stake: 30, sum_stake: 70, sum_stake_squared: 2500 },
2222						None
2223					)
2224				]
2225			);
2226			assert!(VerifierPallet::queued_score().is_some());
2227
2228			assert_eq!(pool.read().transactions.len(), 0);
2230		})
2231	}
2232
2233	#[test]
2234	fn full_multi_page_ocw_e2e_submits_and_queued_msp_only() {
2235		let (mut ext, pool) = ExtBuilder::unsigned().miner_pages(3).build_offchainify();
2236		ext.execute_with_sanity_checks(|| {
2237			roll_to_unsigned_open_with_ocw(None);
2239			roll_next_with_ocw(Some(pool.clone()));
2241
2242			assert_eq!(
2243				multi_block_events(),
2244				vec![
2245					crate::Event::PhaseTransitioned {
2246						from: Phase::Off,
2247						to: Phase::Snapshot(Pages::get())
2248					},
2249					crate::Event::PhaseTransitioned {
2250						from: Phase::Snapshot(0),
2251						to: Phase::Unsigned(UnsignedPhase::get() - 1)
2252					}
2253				]
2254			);
2255			assert_eq!(
2256				verifier_events(),
2257				vec![
2258					crate::verifier::Event::Verified(0, 2),
2259					crate::verifier::Event::Verified(1, 2),
2260					crate::verifier::Event::Verified(2, 2),
2261					crate::verifier::Event::Queued(
2262						ElectionScore {
2263							minimal_stake: 55,
2264							sum_stake: 130,
2265							sum_stake_squared: 8650
2266						},
2267						None
2268					)
2269				]
2270			);
2271			assert!(VerifierPallet::queued_score().is_some());
2272
2273			assert_eq!(pool.read().transactions.len(), 0);
2275		})
2276	}
2277
2278	#[test]
2279	fn will_not_mine_if_not_enough_winners() {
2280		let (mut ext, _) = ExtBuilder::unsigned().desired_targets(77).build_offchainify();
2282		ext.execute_with_sanity_checks(|| {
2283			roll_to_unsigned_open();
2284			ensure_voters(3, 12);
2285
2286			assert_eq!(
2288				OffchainWorkerMiner::<Runtime>::mine_checked_call().unwrap_err(),
2289				OffchainMinerError::Common(CommonError::WrongWinnerCount)
2290			);
2291		});
2292	}
2293
2294	mod no_storage {
2295		use super::*;
2296		#[test]
2297		fn ocw_never_uses_cache_on_initial_run_or_resubmission() {
2298			let (mut ext, pool) =
2302				ExtBuilder::unsigned().offchain_storage(false).build_offchainify();
2303			ext.execute_with_sanity_checks(|| {
2304				let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2305				roll_to_unsigned_open();
2306
2307				let last_block = StorageValueRef::persistent(
2308					&OffchainWorkerMiner::<Runtime>::OFFCHAIN_LAST_BLOCK,
2309				);
2310				let cache = StorageValueRef::persistent(
2311					&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL,
2312				);
2313
2314				assert_eq!(last_block.get::<BlockNumber>(), Ok(None));
2316				assert_eq!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
2317
2318				UnsignedPallet::offchain_worker(25);
2320				assert_eq!(pool.read().transactions.len(), 1);
2321				let first_tx = pool.read().transactions[0].clone();
2322
2323				assert_eq!(last_block.get::<BlockNumber>(), Ok(Some(25)));
2325				assert_eq!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
2326
2327				pool.try_write().unwrap().transactions.clear();
2329
2330				UnsignedPallet::offchain_worker(25 + 1 + offchain_repeat);
2332				assert_eq!(pool.read().transactions.len(), 1);
2333				let second_tx = pool.read().transactions[0].clone();
2334
2335				assert_eq!(last_block.get::<BlockNumber>(), Ok(Some(25 + 1 + offchain_repeat)));
2337				assert_eq!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
2338
2339				assert_eq!(first_tx, second_tx);
2342			})
2343		}
2344	}
2345}