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 },
931 Ok(Some(head)) if now > head + threshold => {
932 Ok(now)
934 },
935 _ => {
936 Ok(now)
939 },
940 }
941 },
942 );
943
944 match mutate_stat {
945 Ok(_) => Ok(()),
947 Err(MutateStorageError::ConcurrentModification(_)) => Err(OffchainMinerError::Lock(
949 "failed to write to offchain db (concurrent modification).",
950 )),
951 Err(MutateStorageError::ValueFunctionFailed(why)) => Err(OffchainMinerError::Lock(why)),
953 }
954 }
955
956 fn save_solution(
958 call: &Call<T>,
959 snapshot_fingerprint: T::Hash,
960 ) -> Result<(), OffchainMinerError<T>> {
961 sublog!(debug, "unsigned::ocw-miner", "saving a call to the offchain storage.");
962 let storage = StorageValueRef::persistent(&Self::OFFCHAIN_CACHED_CALL);
963 match storage.mutate::<_, (), _>(|_| Ok((call.clone(), snapshot_fingerprint))) {
964 Ok(_) => Ok(()),
965 Err(MutateStorageError::ConcurrentModification(_)) => {
966 Err(OffchainMinerError::FailedToStoreSolution)
967 },
968 Err(MutateStorageError::ValueFunctionFailed(_)) => {
969 Err(OffchainMinerError::FailedToStoreSolution)
974 },
975 }
976 }
977
978 fn restore_solution() -> Result<(Call<T>, T::Hash), OffchainMinerError<T>> {
980 StorageValueRef::persistent(&Self::OFFCHAIN_CACHED_CALL)
981 .get()
982 .ok()
983 .flatten()
984 .ok_or(OffchainMinerError::NoStoredSolution)
985 }
986
987 fn clear_offchain_solution_cache() {
989 sublog!(debug, "unsigned::ocw-miner", "clearing offchain call cache storage.");
990 let mut storage = StorageValueRef::persistent(&Self::OFFCHAIN_CACHED_CALL);
991 storage.clear();
992 }
993
994 #[cfg(test)]
995 fn cached_solution() -> Option<Call<T>> {
996 StorageValueRef::persistent(&Self::OFFCHAIN_CACHED_CALL)
997 .get::<Call<T>>()
998 .unwrap()
999 }
1000}
1001
1002#[cfg(test)]
1004mod trimming {
1005 use super::*;
1006 use crate::{mock::*, verifier::Verifier};
1007 use frame_election_provider_support::TryFromUnboundedPagedSupports;
1008 use sp_npos_elections::Support;
1009
1010 #[test]
1011 fn solution_without_any_trimming() {
1012 ExtBuilder::mock_signed().build_and_execute(|| {
1013 let mut current_voters = Voters::get();
1015 current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1016 Voters::set(current_voters);
1017
1018 roll_to_snapshot_created();
1019
1020 let solution = mine_full_solution().unwrap();
1022 assert_eq!(
1023 solution.solution_pages.iter().map(|page| page.voter_count()).sum::<usize>(),
1024 8
1025 );
1026
1027 assert_eq!(solution.solution_pages.encoded_size(), 105);
1028 load_mock_signed_and_start(solution);
1029 let supports = roll_to_full_verification();
1030
1031 assert!(VerifierPallet::queued_score().is_some());
1033
1034 assert_eq!(
1035 supports,
1036 vec![
1037 vec![
1038 (30, Support { total: 30, voters: vec![(30, 30)] }),
1039 (40, Support { total: 40, voters: vec![(40, 40)] })
1040 ],
1041 vec![
1042 (30, Support { total: 11, voters: vec![(5, 2), (6, 2), (7, 7)] }),
1043 (40, Support { total: 7, voters: vec![(5, 3), (6, 4)] })
1044 ],
1045 vec![(40, Support { total: 9, voters: vec![(2, 2), (3, 3), (4, 4)] })]
1046 ]
1047 .try_from_unbounded_paged()
1048 .unwrap()
1049 );
1050 })
1051 }
1052
1053 #[test]
1054 fn trim_length() {
1055 ExtBuilder::mock_signed().miner_max_length(104).build_and_execute(|| {
1056 let mut current_voters = Voters::get();
1058 current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1059 Voters::set(current_voters);
1060
1061 roll_to_snapshot_created();
1062 ensure_voters(3, 12);
1063
1064 let solution = mine_full_solution().unwrap();
1065
1066 assert_eq!(
1067 solution.solution_pages.iter().map(|page| page.voter_count()).sum::<usize>(),
1068 7
1069 );
1070
1071 assert_eq!(solution.solution_pages.encoded_size(), 99);
1072
1073 load_mock_signed_and_start(solution);
1074 let supports = roll_to_full_verification();
1075
1076 assert!(VerifierPallet::queued_score().is_some());
1078
1079 assert_eq!(
1080 supports,
1081 vec![
1082 vec![(40, Support { total: 40, voters: vec![(40, 40)] })],
1085 vec![
1086 (30, Support { total: 11, voters: vec![(5, 2), (6, 2), (7, 7)] }),
1087 (40, Support { total: 7, voters: vec![(5, 3), (6, 4)] })
1088 ],
1089 vec![(40, Support { total: 9, voters: vec![(2, 2), (3, 3), (4, 4)] })]
1090 ]
1091 .try_from_unbounded_paged()
1092 .unwrap()
1093 );
1094 });
1095 }
1096
1097 #[test]
1098 fn trim_length_2() {
1099 ExtBuilder::mock_signed().miner_max_length(98).build_and_execute(|| {
1100 let mut current_voters = Voters::get();
1102 current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1103 Voters::set(current_voters);
1104
1105 roll_to_snapshot_created();
1106 ensure_voters(3, 12);
1107
1108 let solution = mine_full_solution().unwrap();
1109
1110 assert_eq!(
1111 solution.solution_pages.iter().map(|page| page.voter_count()).sum::<usize>(),
1112 6
1113 );
1114
1115 assert_eq!(solution.solution_pages.encoded_size(), 93);
1116
1117 load_mock_signed_and_start(solution);
1118 let supports = roll_to_full_verification();
1119
1120 assert!(VerifierPallet::queued_score().is_some());
1122
1123 assert_eq!(
1124 supports,
1125 vec![
1126 vec![],
1127 vec![
1128 (30, Support { total: 11, voters: vec![(5, 2), (6, 2), (7, 7)] }),
1129 (40, Support { total: 7, voters: vec![(5, 3), (6, 4)] })
1130 ],
1131 vec![(40, Support { total: 9, voters: vec![(2, 2), (3, 3), (4, 4)] })]
1132 ]
1133 .try_from_unbounded_paged()
1134 .unwrap()
1135 );
1136 });
1137 }
1138
1139 #[test]
1140 fn trim_length_3() {
1141 ExtBuilder::mock_signed().miner_max_length(92).build_and_execute(|| {
1142 let mut current_voters = Voters::get();
1144 current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1145 Voters::set(current_voters);
1146
1147 roll_to_snapshot_created();
1148 ensure_voters(3, 12);
1149
1150 let solution = mine_full_solution().unwrap();
1151
1152 assert_eq!(
1153 solution.solution_pages.iter().map(|page| page.voter_count()).sum::<usize>(),
1154 5
1155 );
1156
1157 assert_eq!(solution.solution_pages.encoded_size(), 83);
1158
1159 load_mock_signed_and_start(solution);
1160 let supports = roll_to_full_verification();
1161
1162 assert!(VerifierPallet::queued_score().is_some());
1164
1165 assert_eq!(
1166 supports,
1167 vec![
1168 vec![],
1169 vec![
1170 (30, Support { total: 9, voters: vec![(6, 2), (7, 7)] }),
1171 (40, Support { total: 4, voters: vec![(6, 4)] })
1172 ],
1173 vec![(40, Support { total: 9, voters: vec![(2, 2), (3, 3), (4, 4)] })]
1174 ]
1175 .try_from_unbounded_paged()
1176 .unwrap()
1177 );
1178 });
1179 }
1180
1181 #[test]
1182 fn trim_backers_per_page_works() {
1183 ExtBuilder::mock_signed().max_backers_per_winner(2).build_and_execute(|| {
1184 let mut current_voters = Voters::get();
1186 current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1187 Voters::set(current_voters);
1188
1189 roll_to_snapshot_created();
1190 ensure_voters(3, 12);
1191
1192 let solution = mine_full_solution().unwrap();
1193
1194 load_mock_signed_and_start(solution);
1195 let supports = roll_to_full_verification();
1196
1197 assert!(VerifierPallet::queued_score().is_some());
1199
1200 assert_eq!(
1202 supports,
1203 vec![
1204 vec![
1205 (30, Support { total: 30, voters: vec![(30, 30)] }),
1206 (40, Support { total: 40, voters: vec![(40, 40)] })
1207 ],
1208 vec![
1209 (30, Support { total: 9, voters: vec![(6, 2), (7, 7)] }),
1210 (40, Support { total: 9, voters: vec![(5, 5), (6, 4)] }) ],
1215 vec![(40, Support { total: 7, voters: vec![(3, 3), (4, 4)] })]
1216 ]
1217 .try_from_unbounded_paged()
1218 .unwrap()
1219 );
1220 })
1221 }
1222
1223 #[test]
1224 fn trim_backers_per_page_works_2() {
1225 ExtBuilder::mock_signed().max_backers_per_winner(1).build_and_execute(|| {
1228 let mut current_voters = Voters::get();
1230 current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1231 Voters::set(current_voters);
1232
1233 roll_to_snapshot_created();
1234 ensure_voters(3, 12);
1235
1236 let solution = mine_full_solution().unwrap();
1237
1238 load_mock_signed_and_start(solution);
1239 let supports = roll_to_full_verification();
1240
1241 assert!(VerifierPallet::queued_score().is_some());
1243
1244 assert_eq!(
1246 supports,
1247 vec![
1248 vec![
1249 (30, Support { total: 30, voters: vec![(30, 30)] }),
1250 (40, Support { total: 40, voters: vec![(40, 40)] })
1251 ],
1252 vec![
1253 (30, Support { total: 7, voters: vec![(7, 7)] }),
1254 (40, Support { total: 6, voters: vec![(6, 6)] })
1255 ],
1256 vec![(40, Support { total: 4, voters: vec![(4, 4)] })]
1257 ]
1258 .try_from_unbounded_paged()
1259 .unwrap()
1260 );
1261 })
1262 }
1263
1264 #[test]
1265 fn trim_backers_final_works() {
1266 ExtBuilder::mock_signed()
1267 .max_backers_per_winner(4)
1268 .max_backers_per_winner_final(4)
1269 .build_and_execute(|| {
1270 let mut current_voters = Voters::get();
1272 current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1273 Voters::set(current_voters);
1274
1275 roll_to_snapshot_created();
1276 ensure_voters(3, 12);
1277
1278 let solution = mine_full_solution().unwrap();
1279
1280 load_mock_signed_and_start(solution);
1281 let supports = roll_to_full_verification();
1282
1283 assert!(VerifierPallet::queued_score().is_some());
1285
1286 assert_eq!(
1289 supports,
1290 vec![
1291 vec![
1292 (30, Support { total: 30, voters: vec![(30, 30)] }),
1293 (40, Support { total: 40, voters: vec![(40, 40)] })
1294 ],
1295 vec![
1296 (30, Support { total: 14, voters: vec![(5, 5), (6, 2), (7, 7)] }),
1297 (40, Support { total: 4, voters: vec![(6, 4)] })
1298 ],
1299 vec![(40, Support { total: 7, voters: vec![(3, 3), (4, 4)] })]
1300 ]
1301 .try_from_unbounded_paged()
1302 .unwrap()
1303 );
1304 })
1305 }
1306
1307 #[test]
1308 fn trim_backers_per_page_and_final_works() {
1309 ExtBuilder::mock_signed()
1310 .max_backers_per_winner_final(4)
1311 .max_backers_per_winner(2)
1312 .build_and_execute(|| {
1313 let mut current_voters = Voters::get();
1315 current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1316 Voters::set(current_voters);
1317
1318 roll_to_snapshot_created();
1319 ensure_voters(3, 12);
1320
1321 let solution = mine_full_solution().unwrap();
1322
1323 load_mock_signed_and_start(solution);
1324 let supports = roll_to_full_verification();
1325
1326 assert!(VerifierPallet::queued_score().is_some());
1328
1329 assert_eq!(
1331 supports,
1332 vec![
1333 vec![
1334 (30, Support { total: 30, voters: vec![(30, 30)] }),
1335 (40, Support { total: 40, voters: vec![(40, 40)] })
1336 ],
1337 vec![
1338 (30, Support { total: 12, voters: vec![(5, 5), (7, 7)] }),
1339 (40, Support { total: 6, voters: vec![(6, 6)] })
1340 ],
1341 vec![(40, Support { total: 7, voters: vec![(3, 3), (4, 4)] })]
1342 ]
1343 .try_from_unbounded_paged()
1344 .unwrap()
1345 );
1346 })
1347 }
1348
1349 #[test]
1350 fn aggressive_backer_trimming_maintains_winner_count() {
1351 ExtBuilder::mock_signed()
1354 .desired_targets(3)
1355 .max_winners_per_page(2)
1356 .pages(2)
1357 .max_backers_per_winner_final(1) .max_backers_per_winner(1) .build_and_execute(|| {
1360 let mut current_voters = Voters::get();
1364 current_voters.iter_mut().for_each(|(who, stake, ..)| *stake = *who);
1365 Voters::set(current_voters);
1366
1367 roll_to_snapshot_created();
1368
1369 let solution = mine_full_solution().unwrap();
1370
1371 assert!(solution.solution_pages.len() > 0);
1373
1374 let winner_count = solution
1375 .solution_pages
1376 .iter()
1377 .flat_map(|page| page.unique_targets())
1378 .collect::<std::collections::HashSet<_>>()
1379 .len();
1380
1381 assert_eq!(winner_count, 3);
1385
1386 load_mock_signed_and_start(solution);
1388 let _supports = roll_to_full_verification();
1389
1390 assert!(VerifierPallet::queued_score().is_some());
1392 })
1393 }
1394}
1395
1396#[cfg(test)]
1397mod base_miner {
1398 use std::vec;
1399
1400 use super::*;
1401 use crate::{mock::*, Snapshot};
1402 use frame_election_provider_support::TryFromUnboundedPagedSupports;
1403 use sp_npos_elections::Support;
1404 use sp_runtime::PerU16;
1405
1406 #[test]
1407 fn pagination_does_not_affect_score() {
1408 let score_1 = ExtBuilder::mock_signed()
1409 .pages(1)
1410 .voter_per_page(12)
1411 .build_unchecked()
1412 .execute_with(|| {
1413 roll_to_snapshot_created();
1414 mine_full_solution().unwrap().score
1415 });
1416 let score_2 = ExtBuilder::mock_signed()
1417 .pages(2)
1418 .voter_per_page(6)
1419 .build_unchecked()
1420 .execute_with(|| {
1421 roll_to_snapshot_created();
1422 mine_full_solution().unwrap().score
1423 });
1424 let score_3 = ExtBuilder::mock_signed()
1425 .pages(3)
1426 .voter_per_page(4)
1427 .build_unchecked()
1428 .execute_with(|| {
1429 roll_to_snapshot_created();
1430 mine_full_solution().unwrap().score
1431 });
1432
1433 assert_eq!(score_1, score_2);
1434 assert_eq!(score_2, score_3);
1435 }
1436
1437 #[test]
1438 fn mine_solution_single_page_works() {
1439 ExtBuilder::mock_signed().pages(1).voter_per_page(8).build_and_execute(|| {
1440 roll_to_snapshot_created();
1441
1442 ensure_voters(1, 8);
1443 ensure_targets(1, 4);
1444
1445 assert_eq!(
1446 Snapshot::<Runtime>::voters(0)
1447 .unwrap()
1448 .into_iter()
1449 .map(|(x, _, _)| x)
1450 .collect::<Vec<_>>(),
1451 vec![1, 2, 3, 4, 5, 6, 7, 8]
1452 );
1453
1454 let paged = mine_full_solution().unwrap();
1455 assert_eq!(paged.solution_pages.len(), 1);
1456
1457 OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1459
1460 load_mock_signed_and_start(paged.clone());
1462 let supports = roll_to_full_verification();
1463
1464 assert_eq!(
1465 supports,
1466 vec![vec![
1467 (10, Support { total: 30, voters: vec![(1, 10), (4, 5), (5, 5), (8, 10)] }),
1468 (
1469 40,
1470 Support {
1471 total: 40,
1472 voters: vec![(2, 10), (3, 10), (4, 5), (5, 5), (6, 10)]
1473 }
1474 )
1475 ]]
1476 .try_from_unbounded_paged()
1477 .unwrap()
1478 );
1479
1480 assert_eq!(
1483 paged.score,
1484 ElectionScore { minimal_stake: 30, sum_stake: 70, sum_stake_squared: 2500 }
1485 );
1486 })
1487 }
1488
1489 #[test]
1490 fn mine_solution_double_page_works() {
1491 ExtBuilder::mock_signed().pages(2).voter_per_page(4).build_and_execute(|| {
1492 roll_to_snapshot_created();
1493
1494 ensure_voters(2, 8);
1496 ensure_targets(1, 4);
1498
1499 assert_eq!(
1501 Snapshot::<Runtime>::voters(0)
1502 .unwrap()
1503 .into_iter()
1504 .map(|(x, _, _)| x)
1505 .collect::<Vec<_>>(),
1506 vec![5, 6, 7, 8]
1507 );
1508 assert_eq!(
1509 Snapshot::<Runtime>::voters(1)
1510 .unwrap()
1511 .into_iter()
1512 .map(|(x, _, _)| x)
1513 .collect::<Vec<_>>(),
1514 vec![1, 2, 3, 4]
1515 );
1516 assert_eq!(Snapshot::<Runtime>::targets().unwrap(), vec![10, 20, 30, 40]);
1518 let paged = mine_full_solution().unwrap();
1519
1520 assert_eq!(
1521 paged.solution_pages,
1522 vec![
1523 TestNposSolution {
1524 votes1: vec![(1, 3), (3, 0)],
1527 votes2: vec![(0, [(0, PerU16::from_parts(32768))], 3)],
1529 ..Default::default()
1530 },
1531 TestNposSolution {
1532 votes1: vec![(0, 0), (1, 3), (2, 3)],
1536 votes2: vec![(3, [(0, PerU16::from_parts(32768))], 3)],
1538 ..Default::default()
1539 },
1540 ]
1541 );
1542
1543 OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, false).unwrap();
1545
1546 load_mock_signed_and_start(paged.clone());
1548 let supports = roll_to_full_verification();
1549
1550 assert_eq!(
1551 supports,
1552 vec![
1553 vec![
1555 (10, Support { total: 15, voters: vec![(5, 5), (8, 10)] }),
1556 (40, Support { total: 15, voters: vec![(5, 5), (6, 10)] })
1557 ],
1558 vec![
1560 (10, Support { total: 15, voters: vec![(1, 10), (4, 5)] }),
1561 (40, Support { total: 25, voters: vec![(2, 10), (3, 10), (4, 5)] })
1562 ]
1563 ]
1564 .try_from_unbounded_paged()
1565 .unwrap()
1566 );
1567
1568 assert_eq!(
1569 paged.score,
1570 ElectionScore { minimal_stake: 30, sum_stake: 70, sum_stake_squared: 2500 }
1571 );
1572 })
1573 }
1574
1575 #[test]
1576 fn mine_solution_triple_page_works() {
1577 ExtBuilder::mock_signed().pages(3).voter_per_page(4).build_and_execute(|| {
1578 roll_to_snapshot_created();
1579
1580 ensure_voters(3, 12);
1581 ensure_targets(1, 4);
1582
1583 assert_eq!(
1585 Snapshot::<Runtime>::voters(2)
1586 .unwrap()
1587 .into_iter()
1588 .map(|(x, _, _)| x)
1589 .collect::<Vec<_>>(),
1590 vec![1, 2, 3, 4]
1591 );
1592 assert_eq!(
1593 Snapshot::<Runtime>::voters(1)
1594 .unwrap()
1595 .into_iter()
1596 .map(|(x, _, _)| x)
1597 .collect::<Vec<_>>(),
1598 vec![5, 6, 7, 8]
1599 );
1600 assert_eq!(
1601 Snapshot::<Runtime>::voters(0)
1602 .unwrap()
1603 .into_iter()
1604 .map(|(x, _, _)| x)
1605 .collect::<Vec<_>>(),
1606 vec![10, 20, 30, 40]
1607 );
1608
1609 let paged = mine_full_solution().unwrap();
1610 assert_eq!(
1611 paged.solution_pages,
1612 vec![
1613 TestNposSolution { votes1: vec![(2, 2), (3, 3)], ..Default::default() },
1614 TestNposSolution {
1615 votes1: vec![(2, 2)],
1616 votes2: vec![
1617 (0, [(2, PerU16::from_parts(32768))], 3),
1618 (1, [(2, PerU16::from_parts(32768))], 3)
1619 ],
1620 ..Default::default()
1621 },
1622 TestNposSolution {
1623 votes1: vec![(2, 3), (3, 3)],
1624 votes2: vec![(1, [(2, PerU16::from_parts(32768))], 3)],
1625 ..Default::default()
1626 },
1627 ]
1628 );
1629
1630 OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1632 load_mock_signed_and_start(paged.clone());
1634 let supports = roll_to_full_verification();
1635
1636 assert_eq!(
1637 supports,
1638 vec![
1639 vec![
1641 (30, Support { total: 30, voters: vec![(30, 30)] }),
1642 (40, Support { total: 40, voters: vec![(40, 40)] })
1643 ],
1644 vec![
1646 (30, Support { total: 20, voters: vec![(5, 5), (6, 5), (7, 10)] }),
1647 (40, Support { total: 10, voters: vec![(5, 5), (6, 5)] })
1648 ],
1649 vec![
1651 (30, Support { total: 5, voters: vec![(2, 5)] }),
1652 (40, Support { total: 25, voters: vec![(2, 5), (3, 10), (4, 10)] })
1653 ]
1654 ]
1655 .try_from_unbounded_paged()
1656 .unwrap()
1657 );
1658
1659 assert_eq!(
1660 paged.score,
1661 ElectionScore { minimal_stake: 55, sum_stake: 130, sum_stake_squared: 8650 }
1662 );
1663 })
1664 }
1665
1666 #[test]
1667 fn mine_solution_choses_most_significant_pages() {
1668 ExtBuilder::mock_signed().pages(2).voter_per_page(4).build_and_execute(|| {
1669 roll_to_snapshot_created();
1670
1671 ensure_voters(2, 8);
1672 ensure_targets(1, 4);
1673
1674 assert_eq!(
1676 Snapshot::<Runtime>::voters(0)
1677 .unwrap()
1678 .into_iter()
1679 .map(|(x, _, _)| x)
1680 .collect::<Vec<_>>(),
1681 vec![5, 6, 7, 8]
1682 );
1683 assert_eq!(
1685 Snapshot::<Runtime>::voters(1)
1686 .unwrap()
1687 .into_iter()
1688 .map(|(x, _, _)| x)
1689 .collect::<Vec<_>>(),
1690 vec![1, 2, 3, 4]
1691 );
1692
1693 let paged = mine_solution(1).unwrap();
1695
1696 assert_eq!(
1697 paged.solution_pages,
1698 vec![TestNposSolution {
1699 votes1: vec![(0, 0), (1, 3), (2, 3)],
1703 votes2: vec![(3, [(0, PerU16::from_parts(32768))], 3)],
1705 ..Default::default()
1706 }]
1707 );
1708
1709 OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1711 load_mock_signed_and_start(paged.clone());
1713 let supports = roll_to_full_verification();
1714
1715 assert_eq!(
1716 supports,
1717 vec![
1718 vec![],
1720 vec![
1722 (10, Support { total: 15, voters: vec![(1, 10), (4, 5)] }),
1723 (40, Support { total: 25, voters: vec![(2, 10), (3, 10), (4, 5)] })
1724 ]
1725 ]
1726 .try_from_unbounded_paged()
1727 .unwrap()
1728 );
1729
1730 assert_eq!(
1731 paged.score,
1732 ElectionScore { minimal_stake: 15, sum_stake: 40, sum_stake_squared: 850 }
1733 );
1734 })
1735 }
1736
1737 #[test]
1738 fn mine_solution_2_out_of_3_pages() {
1739 ExtBuilder::mock_signed().pages(3).voter_per_page(4).build_and_execute(|| {
1740 roll_to_snapshot_created();
1741
1742 ensure_voters(3, 12);
1743 ensure_targets(1, 4);
1744
1745 assert_eq!(
1746 Snapshot::<Runtime>::voters(0)
1747 .unwrap()
1748 .into_iter()
1749 .map(|(x, _, _)| x)
1750 .collect::<Vec<_>>(),
1751 vec![10, 20, 30, 40]
1752 );
1753 assert_eq!(
1754 Snapshot::<Runtime>::voters(1)
1755 .unwrap()
1756 .into_iter()
1757 .map(|(x, _, _)| x)
1758 .collect::<Vec<_>>(),
1759 vec![5, 6, 7, 8]
1760 );
1761 assert_eq!(
1762 Snapshot::<Runtime>::voters(2)
1763 .unwrap()
1764 .into_iter()
1765 .map(|(x, _, _)| x)
1766 .collect::<Vec<_>>(),
1767 vec![1, 2, 3, 4]
1768 );
1769
1770 let paged = mine_solution(2).unwrap();
1772
1773 OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1775
1776 assert_eq!(
1777 paged.solution_pages,
1778 vec![
1779 TestNposSolution {
1785 votes1: vec![(1, 3), (3, 0)],
1786 votes2: vec![(0, [(0, PerU16::from_parts(32768))], 3)],
1787 ..Default::default()
1788 },
1789 TestNposSolution {
1796 votes1: vec![(0, 0), (1, 3), (2, 3)],
1797 votes2: vec![(3, [(0, PerU16::from_parts(32768))], 3)],
1798 ..Default::default()
1799 }
1800 ]
1801 );
1802
1803 OffchainWorkerMiner::<Runtime>::base_check_solution(&paged, None, true).unwrap();
1805 load_mock_signed_and_start(paged.clone());
1807 let supports = roll_to_full_verification();
1808
1809 assert_eq!(
1810 supports,
1811 vec![
1812 vec![],
1814 vec![
1816 (10, Support { total: 15, voters: vec![(5, 5), (8, 10)] }),
1817 (40, Support { total: 15, voters: vec![(5, 5), (6, 10)] })
1818 ],
1819 vec![
1821 (10, Support { total: 15, voters: vec![(1, 10), (4, 5)] }),
1822 (40, Support { total: 25, voters: vec![(2, 10), (3, 10), (4, 5)] })
1823 ]
1824 ]
1825 .try_from_unbounded_paged()
1826 .unwrap()
1827 );
1828
1829 assert_eq!(
1830 paged.score,
1831 ElectionScore { minimal_stake: 30, sum_stake: 70, sum_stake_squared: 2500 }
1832 );
1833 })
1834 }
1835
1836 #[test]
1837 fn can_reduce_solution() {
1838 ExtBuilder::mock_signed().build_and_execute(|| {
1839 roll_to_snapshot_created();
1840 let full_edges = OffchainWorkerMiner::<Runtime>::mine_solution(Pages::get(), false)
1841 .unwrap()
1842 .solution_pages
1843 .iter()
1844 .fold(0, |acc, x| acc + x.edge_count());
1845 let reduced_edges = OffchainWorkerMiner::<Runtime>::mine_solution(Pages::get(), true)
1846 .unwrap()
1847 .solution_pages
1848 .iter()
1849 .fold(0, |acc, x| acc + x.edge_count());
1850
1851 assert!(reduced_edges < full_edges, "{} < {} not fulfilled", reduced_edges, full_edges);
1852 })
1853 }
1854}
1855
1856#[cfg(test)]
1857mod offchain_worker_miner {
1858 use crate::{verifier::Verifier, CommonError};
1859 use frame_support::traits::Hooks;
1860 use sp_runtime::offchain::storage_lock::{BlockAndTime, StorageLock};
1861
1862 use super::*;
1863 use crate::mock::*;
1864
1865 #[test]
1866 fn lock_prevents_frequent_execution() {
1867 let (mut ext, _) = ExtBuilder::mock_signed().build_offchainify();
1868 ext.execute_with_sanity_checks(|| {
1869 let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
1870
1871 assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(25).is_ok());
1873
1874 assert_noop!(
1876 OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(26),
1877 OffchainMinerError::Lock("recently executed.")
1878 );
1879
1880 assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(
1882 (26 + offchain_repeat).into()
1883 )
1884 .is_ok());
1885
1886 assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(
1888 (26 + offchain_repeat - 3).into()
1889 )
1890 .is_err());
1891 assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(
1892 (26 + offchain_repeat - 2).into()
1893 )
1894 .is_err());
1895 assert!(OffchainWorkerMiner::<Runtime>::ensure_offchain_repeat_frequency(
1896 (26 + offchain_repeat - 1).into()
1897 )
1898 .is_err());
1899 })
1900 }
1901
1902 #[test]
1903 fn lock_released_after_successful_execution() {
1904 let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
1906 ext.execute_with_sanity_checks(|| {
1907 let guard = StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_LOCK);
1908 let last_block =
1909 StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_LAST_BLOCK);
1910
1911 roll_to_unsigned_open();
1912
1913 assert!(guard.get::<bool>().unwrap().is_none());
1915
1916 UnsignedPallet::offchain_worker(25);
1918 assert_eq!(pool.read().transactions.len(), 1);
1919
1920 assert!(guard.get::<bool>().unwrap().is_none());
1922 assert_eq!(last_block.get::<BlockNumber>().unwrap(), Some(25));
1923 });
1924 }
1925
1926 #[test]
1927 fn lock_prevents_overlapping_execution() {
1928 let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
1930 ext.execute_with_sanity_checks(|| {
1931 roll_to_unsigned_open();
1932
1933 let mut lock = StorageLock::<BlockAndTime<System>>::with_block_deadline(
1935 OffchainWorkerMiner::<Runtime>::OFFCHAIN_LOCK,
1936 UnsignedPhase::get().saturated_into(),
1937 );
1938 let guard = lock.lock();
1939
1940 UnsignedPallet::offchain_worker(25);
1942 assert_eq!(pool.read().transactions.len(), 0);
1943 UnsignedPallet::offchain_worker(26);
1944 assert_eq!(pool.read().transactions.len(), 0);
1945
1946 drop(guard);
1947
1948 UnsignedPallet::offchain_worker(25);
1950 assert_eq!(pool.read().transactions.len(), 1);
1951 });
1952 }
1953
1954 #[test]
1955 fn initial_ocw_runs_and_saves_new_cache() {
1956 let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
1957 ext.execute_with_sanity_checks(|| {
1958 roll_to_unsigned_open();
1959
1960 let last_block =
1961 StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_LAST_BLOCK);
1962 let cache =
1963 StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
1964
1965 assert_eq!(last_block.get::<BlockNumber>(), Ok(None));
1966 assert_eq!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
1967
1968 UnsignedPallet::offchain_worker(25);
1970 assert_eq!(pool.read().transactions.len(), 1);
1971
1972 assert_eq!(last_block.get::<BlockNumber>(), Ok(Some(25)));
1973 assert!(matches!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
1974 })
1975 }
1976
1977 #[test]
1978 fn ocw_pool_submission_works() {
1979 let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
1980 ext.execute_with_sanity_checks(|| {
1981 roll_to_unsigned_open();
1982
1983 roll_next_with_ocw(Some(pool.clone()));
1984 let encoded = pool.read().transactions[0].clone();
1987 let extrinsic: Extrinsic = codec::Decode::decode(&mut &*encoded).unwrap();
1988 let call = extrinsic.function;
1989 assert!(matches!(
1990 call,
1991 crate::mock::RuntimeCall::UnsignedPallet(
1992 crate::unsigned::Call::submit_unsigned { .. }
1993 )
1994 ));
1995 })
1996 }
1997
1998 #[test]
1999 fn resubmits_after_offchain_repeat() {
2000 let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
2001 ext.execute_with_sanity_checks(|| {
2002 let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2003 roll_to_unsigned_open();
2004
2005 assert!(OffchainWorkerMiner::<Runtime>::cached_solution().is_none());
2006 UnsignedPallet::offchain_worker(25);
2008 assert_eq!(pool.read().transactions.len(), 1);
2009 let tx_cache = pool.read().transactions[0].clone();
2010 pool.try_write().unwrap().transactions.clear();
2012
2013 UnsignedPallet::offchain_worker(25 + 1 + offchain_repeat);
2015 assert_eq!(pool.read().transactions.len(), 1);
2016
2017 let tx = &pool.read().transactions[0];
2019 assert_eq!(&tx_cache, tx);
2020 })
2021 }
2022
2023 #[test]
2024 fn regenerates_and_resubmits_after_offchain_repeat_if_no_cache() {
2025 let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
2026 ext.execute_with_sanity_checks(|| {
2027 let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2028 roll_to_unsigned_open();
2029
2030 assert!(OffchainWorkerMiner::<Runtime>::cached_solution().is_none());
2031 UnsignedPallet::offchain_worker(25);
2033 assert_eq!(pool.read().transactions.len(), 1);
2034 let tx_cache = pool.read().transactions[0].clone();
2035 pool.try_write().unwrap().transactions.clear();
2037
2038 let mut call_cache =
2042 StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
2043 assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2044 call_cache.clear();
2045
2046 UnsignedPallet::offchain_worker(25 + 1 + offchain_repeat);
2048 assert_eq!(pool.read().transactions.len(), 1);
2049
2050 let tx = &pool.read().transactions[0];
2052 assert_eq!(&tx_cache, tx);
2053 })
2054 }
2055
2056 #[test]
2057 fn altering_snapshot_invalidates_solution_cache() {
2058 let (mut ext, pool) = ExtBuilder::mock_signed().unsigned_phase(999).build_offchainify();
2060 ext.execute_with_sanity_checks(|| {
2061 let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2062 roll_to_unsigned_open();
2063 roll_next_with_ocw(None);
2064
2065 assert_eq!(pool.read().transactions.len(), 1);
2067 pool.try_write().unwrap().transactions.clear();
2068
2069 let call_cache =
2071 StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
2072 assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2073
2074 assert_eq!(crate::Snapshot::<Runtime>::targets().unwrap(), vec![10, 20, 30, 40]);
2077 let pre_fingerprint = crate::Snapshot::<Runtime>::fingerprint();
2078 crate::Snapshot::<Runtime>::remove_target(0);
2079 let post_fingerprint = crate::Snapshot::<Runtime>::fingerprint();
2080 assert_eq!(crate::Snapshot::<Runtime>::targets().unwrap(), vec![20, 30, 40]);
2081 assert_ne!(pre_fingerprint, post_fingerprint);
2082
2083 let now = System::block_number();
2085 roll_to_with_ocw(now + offchain_repeat + 1, None);
2086 assert_eq!(pool.read().transactions.len(), 0);
2088 assert_eq!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
2090
2091 roll_to_with_ocw(now + offchain_repeat + offchain_repeat + 2, None);
2093 assert_eq!(pool.read().transactions.len(), 1);
2094 assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2095 })
2096 }
2097
2098 #[test]
2099 fn wont_resubmit_if_weak_score() {
2100 let (mut ext, pool) = ExtBuilder::mock_signed().unsigned_phase(999).build_offchainify();
2103 ext.execute_with_sanity_checks(|| {
2104 let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2105 roll_to_unsigned_open();
2108 roll_next_with_ocw(None);
2109
2110 assert_eq!(pool.read().transactions.len(), 1);
2112
2113 let call_cache =
2115 StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
2116 assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2117
2118 let weak_solution = raw_paged_from_supports(
2120 vec![vec![(40, Support { total: 10, voters: vec![(3, 10)] })]],
2121 0,
2122 );
2123 let weak_call = crate::unsigned::Call::<T>::submit_unsigned {
2124 paged_solution: Box::new(weak_solution),
2125 };
2126 call_cache.set(&weak_call);
2127
2128 roll_to_with_ocw(System::block_number() + offchain_repeat + 1, Some(pool.clone()));
2130 assert_eq!(pool.read().transactions.len(), 0);
2132 assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2134 })
2135 }
2136
2137 #[test]
2138 fn ocw_submission_e2e_works() {
2139 let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
2140 ext.execute_with_sanity_checks(|| {
2141 assert!(VerifierPallet::queued_score().is_none());
2142 roll_to_with_ocw(25 + 1, Some(pool.clone()));
2143 assert!(VerifierPallet::queued_score().is_some());
2144
2145 let call_cache =
2147 StorageValueRef::persistent(&OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL);
2148 assert!(matches!(call_cache.get::<crate::unsigned::Call<Runtime>>(), Ok(Some(_))));
2149
2150 assert_eq!(pool.read().transactions.len(), 0);
2152 })
2153 }
2154
2155 #[test]
2156 fn ocw_e2e_submits_and_queued_msp_only() {
2157 let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
2158 ext.execute_with_sanity_checks(|| {
2159 roll_to_unsigned_open_with_ocw(None);
2161 roll_next_with_ocw(Some(pool.clone()));
2163
2164 assert_eq!(
2165 multi_block_events(),
2166 vec![
2167 crate::Event::PhaseTransitioned {
2168 from: Phase::Off,
2169 to: Phase::Snapshot(Pages::get())
2170 },
2171 crate::Event::PhaseTransitioned {
2172 from: Phase::Snapshot(0),
2173 to: Phase::Unsigned(UnsignedPhase::get() - 1)
2174 }
2175 ]
2176 );
2177 assert_eq!(
2178 verifier_events(),
2179 vec![
2180 crate::verifier::Event::Verified(2, 2),
2181 crate::verifier::Event::Queued(
2182 ElectionScore { minimal_stake: 15, sum_stake: 40, sum_stake_squared: 850 },
2183 None
2184 )
2185 ]
2186 );
2187 assert!(VerifierPallet::queued_score().is_some());
2188
2189 assert_eq!(pool.read().transactions.len(), 0);
2191 })
2192 }
2193
2194 #[test]
2195 fn multi_page_ocw_e2e_submits_and_queued_msp_only() {
2196 let (mut ext, pool) = ExtBuilder::mock_signed().miner_pages(2).build_offchainify();
2197 ext.execute_with_sanity_checks(|| {
2198 roll_to_unsigned_open_with_ocw(None);
2200 roll_next_with_ocw(Some(pool.clone()));
2202
2203 assert_eq!(
2204 multi_block_events(),
2205 vec![
2206 crate::Event::PhaseTransitioned {
2207 from: Phase::Off,
2208 to: Phase::Snapshot(Pages::get())
2209 },
2210 crate::Event::PhaseTransitioned {
2211 from: Phase::Snapshot(0),
2212 to: Phase::Unsigned(UnsignedPhase::get() - 1)
2213 }
2214 ]
2215 );
2216 assert_eq!(
2217 verifier_events(),
2218 vec![
2219 crate::verifier::Event::Verified(1, 2),
2220 crate::verifier::Event::Verified(2, 2),
2221 crate::verifier::Event::Queued(
2222 ElectionScore { minimal_stake: 30, sum_stake: 70, sum_stake_squared: 2500 },
2223 None
2224 )
2225 ]
2226 );
2227 assert!(VerifierPallet::queued_score().is_some());
2228
2229 assert_eq!(pool.read().transactions.len(), 0);
2231 })
2232 }
2233
2234 #[test]
2235 fn full_multi_page_ocw_e2e_submits_and_queued_msp_only() {
2236 let (mut ext, pool) = ExtBuilder::mock_signed().miner_pages(3).build_offchainify();
2237 ext.execute_with_sanity_checks(|| {
2238 roll_to_unsigned_open_with_ocw(None);
2240 roll_next_with_ocw(Some(pool.clone()));
2242
2243 assert_eq!(
2244 multi_block_events(),
2245 vec![
2246 crate::Event::PhaseTransitioned {
2247 from: Phase::Off,
2248 to: Phase::Snapshot(Pages::get())
2249 },
2250 crate::Event::PhaseTransitioned {
2251 from: Phase::Snapshot(0),
2252 to: Phase::Unsigned(UnsignedPhase::get() - 1)
2253 }
2254 ]
2255 );
2256 assert_eq!(
2257 verifier_events(),
2258 vec![
2259 crate::verifier::Event::Verified(0, 2),
2260 crate::verifier::Event::Verified(1, 2),
2261 crate::verifier::Event::Verified(2, 2),
2262 crate::verifier::Event::Queued(
2263 ElectionScore {
2264 minimal_stake: 55,
2265 sum_stake: 130,
2266 sum_stake_squared: 8650
2267 },
2268 None
2269 )
2270 ]
2271 );
2272 assert!(VerifierPallet::queued_score().is_some());
2273
2274 assert_eq!(pool.read().transactions.len(), 0);
2276 })
2277 }
2278
2279 #[test]
2280 fn will_not_mine_if_not_enough_winners() {
2281 let (mut ext, _) = ExtBuilder::mock_signed().desired_targets(77).build_offchainify();
2283 ext.execute_with_sanity_checks(|| {
2284 roll_to_unsigned_open();
2285 ensure_voters(3, 12);
2286
2287 assert_eq!(
2289 OffchainWorkerMiner::<Runtime>::mine_checked_call().unwrap_err(),
2290 OffchainMinerError::Common(CommonError::WrongWinnerCount)
2291 );
2292 });
2293 }
2294
2295 mod no_storage {
2296 use super::*;
2297 #[test]
2298 fn ocw_never_uses_cache_on_initial_run_or_resubmission() {
2299 let (mut ext, pool) =
2303 ExtBuilder::mock_signed().offchain_storage(false).build_offchainify();
2304 ext.execute_with_sanity_checks(|| {
2305 let offchain_repeat = <Runtime as crate::unsigned::Config>::OffchainRepeat::get();
2306 roll_to_unsigned_open();
2307
2308 let last_block = StorageValueRef::persistent(
2309 &OffchainWorkerMiner::<Runtime>::OFFCHAIN_LAST_BLOCK,
2310 );
2311 let cache = StorageValueRef::persistent(
2312 &OffchainWorkerMiner::<Runtime>::OFFCHAIN_CACHED_CALL,
2313 );
2314
2315 assert_eq!(last_block.get::<BlockNumber>(), Ok(None));
2317 assert_eq!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
2318
2319 UnsignedPallet::offchain_worker(25);
2321 assert_eq!(pool.read().transactions.len(), 1);
2322 let first_tx = pool.read().transactions[0].clone();
2323
2324 assert_eq!(last_block.get::<BlockNumber>(), Ok(Some(25)));
2326 assert_eq!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
2327
2328 pool.try_write().unwrap().transactions.clear();
2330
2331 UnsignedPallet::offchain_worker(25 + 1 + offchain_repeat);
2333 assert_eq!(pool.read().transactions.len(), 1);
2334 let second_tx = pool.read().transactions[0].clone();
2335
2336 assert_eq!(last_block.get::<BlockNumber>(), Ok(Some(25 + 1 + offchain_repeat)));
2338 assert_eq!(cache.get::<crate::unsigned::Call<Runtime>>(), Ok(None));
2339
2340 assert_eq!(first_tx, second_tx);
2343 })
2344 }
2345 }
2346}