1use crate::{
21 helpers, Call, Config, CurrentPhase, DesiredTargets, ElectionCompute, Error, FeasibilityError,
22 Pallet, QueuedSolution, RawSolution, ReadySolution, ReadySolutionOf, Round, RoundSnapshot,
23 Snapshot, SolutionAccuracyOf, SolutionOf, SolutionOrSnapshotSize, Weight,
24};
25use alloc::{boxed::Box, vec::Vec};
26use codec::Encode;
27use frame_election_provider_support::{NposSolution, NposSolver, PerThing128, VoteWeight};
28use frame_support::{
29 dispatch::DispatchResult,
30 ensure,
31 traits::{DefensiveResult, Get},
32 BoundedVec,
33};
34use frame_system::{
35 offchain::{CreateBare, SubmitTransaction},
36 pallet_prelude::BlockNumberFor,
37};
38use scale_info::TypeInfo;
39use sp_npos_elections::{
40 assignment_ratio_to_staked_normalized, assignment_staked_to_ratio_normalized, ElectionResult,
41 ElectionScore, EvaluateSupport,
42};
43use sp_runtime::{
44 offchain::storage::{MutateStorageError, StorageValueRef},
45 DispatchError, SaturatedConversion,
46};
47
48pub(crate) const OFFCHAIN_LAST_BLOCK: &[u8] = b"parity/multi-phase-unsigned-election";
50pub(crate) const OFFCHAIN_LOCK: &[u8] = b"parity/multi-phase-unsigned-election/lock";
52
53pub(crate) const OFFCHAIN_CACHED_CALL: &[u8] = b"parity/multi-phase-unsigned-election/call";
55
56pub type VoterOf<T> = frame_election_provider_support::VoterOf<<T as Config>::DataProvider>;
59
60pub type MinerVoterOf<T> = frame_election_provider_support::Voter<
62 <T as MinerConfig>::AccountId,
63 <T as MinerConfig>::MaxVotesPerVoter,
64>;
65
66pub type Assignment<T> =
68 sp_npos_elections::Assignment<<T as frame_system::Config>::AccountId, SolutionAccuracyOf<T>>;
69
70pub type IndexAssignmentOf<T> = frame_election_provider_support::IndexAssignmentOf<SolutionOf<T>>;
73
74pub type SolverErrorOf<T> = <<T as Config>::Solver as NposSolver>::Error;
76#[derive(frame_support::DebugNoBound, frame_support::PartialEqNoBound)]
78pub enum MinerError {
79 NposElections(sp_npos_elections::Error),
81 SnapshotUnAvailable,
83 PoolSubmissionFailed,
85 PreDispatchChecksFailed(DispatchError),
87 Feasibility(FeasibilityError),
89 Lock(&'static str),
91 NoStoredSolution,
93 SolutionCallInvalid,
95 FailedToStoreSolution,
97 NoMoreVoters,
99 Solver,
101 TooManyDesiredTargets,
103}
104
105impl From<sp_npos_elections::Error> for MinerError {
106 fn from(e: sp_npos_elections::Error) -> Self {
107 MinerError::NposElections(e)
108 }
109}
110
111impl From<FeasibilityError> for MinerError {
112 fn from(e: FeasibilityError) -> Self {
113 MinerError::Feasibility(e)
114 }
115}
116
117#[derive(Debug, Clone)]
119pub struct TrimmingStatus {
120 weight: usize,
122 length: usize,
124 edges: usize,
126}
127
128impl TrimmingStatus {
129 pub fn is_trimmed(&self) -> bool {
130 self.weight > 0 || self.length > 0 || self.edges > 0
131 }
132
133 pub fn trimmed_weight(&self) -> usize {
134 self.weight
135 }
136
137 pub fn trimmed_length(&self) -> usize {
138 self.length
139 }
140
141 pub fn trimmed_edges(&self) -> usize {
142 self.edges
143 }
144}
145
146fn save_solution<T: Config>(call: &Call<T>) -> Result<(), MinerError> {
148 log!(debug, "saving a call to the offchain storage.");
149 let storage = StorageValueRef::persistent(OFFCHAIN_CACHED_CALL);
150 match storage.mutate::<_, (), _>(|_| Ok(call.clone())) {
151 Ok(_) => Ok(()),
152 Err(MutateStorageError::ConcurrentModification(_)) => {
153 Err(MinerError::FailedToStoreSolution)
154 },
155 Err(MutateStorageError::ValueFunctionFailed(_)) => {
156 Err(MinerError::FailedToStoreSolution)
161 },
162 }
163}
164
165fn restore_solution<T: Config>() -> Result<Call<T>, MinerError> {
167 StorageValueRef::persistent(OFFCHAIN_CACHED_CALL)
168 .get()
169 .ok()
170 .flatten()
171 .ok_or(MinerError::NoStoredSolution)
172}
173
174pub(super) fn kill_ocw_solution<T: Config>() {
176 log!(debug, "clearing offchain call cache storage.");
177 let mut storage = StorageValueRef::persistent(OFFCHAIN_CACHED_CALL);
178 storage.clear();
179}
180
181fn clear_offchain_repeat_frequency() {
186 let mut last_block = StorageValueRef::persistent(OFFCHAIN_LAST_BLOCK);
187 last_block.clear();
188}
189
190#[cfg(test)]
192fn ocw_solution_exists<T: Config>() -> bool {
193 matches!(StorageValueRef::persistent(OFFCHAIN_CACHED_CALL).get::<Call<T>>(), Ok(Some(_)))
194}
195
196impl<T: Config + CreateBare<Call<T>>> Pallet<T> {
197 pub fn mine_solution() -> Result<
202 (RawSolution<SolutionOf<T::MinerConfig>>, SolutionOrSnapshotSize, TrimmingStatus),
203 MinerError,
204 > {
205 let RoundSnapshot { voters, targets } =
206 Snapshot::<T>::get().ok_or(MinerError::SnapshotUnAvailable)?;
207 let desired_targets = DesiredTargets::<T>::get().ok_or(MinerError::SnapshotUnAvailable)?;
208 ensure!(desired_targets <= T::MaxWinners::get(), MinerError::TooManyDesiredTargets);
209 let (solution, score, size, is_trimmed) =
210 Miner::<T::MinerConfig>::mine_solution_with_snapshot::<T::Solver>(
211 voters,
212 targets,
213 desired_targets,
214 )?;
215 let round = Round::<T>::get();
216 Ok((RawSolution { solution, score, round }, size, is_trimmed))
217 }
218
219 pub fn restore_or_compute_then_maybe_submit() -> Result<(), MinerError> {
222 log!(debug, "miner attempting to restore or compute an unsigned solution.");
223
224 let call = restore_solution::<T>()
225 .and_then(|call| {
226 if let Call::submit_unsigned { raw_solution, .. } = &call {
228 Self::basic_checks(raw_solution, "restored")?;
230 Ok(call)
231 } else {
232 Err(MinerError::SolutionCallInvalid)
233 }
234 })
235 .or_else::<MinerError, _>(|error| {
236 log!(debug, "restoring solution failed due to {:?}", error);
237 match error {
238 MinerError::NoStoredSolution => {
239 log!(trace, "mining a new solution.");
240 let call = Self::mine_checked_call()?;
244 save_solution(&call)?;
245 Ok(call)
246 },
247 MinerError::Feasibility(_) => {
248 log!(trace, "wiping infeasible solution.");
249 kill_ocw_solution::<T>();
252 clear_offchain_repeat_frequency();
253 Err(error)
254 },
255 _ => {
256 Err(error)
258 },
259 }
260 })?;
261
262 Self::submit_call(call)
263 }
264
265 pub fn mine_check_save_submit() -> Result<(), MinerError> {
267 log!(debug, "miner attempting to compute an unsigned solution.");
268
269 let call = Self::mine_checked_call()?;
270 save_solution(&call)?;
271 Self::submit_call(call)
272 }
273
274 pub fn mine_checked_call() -> Result<Call<T>, MinerError> {
276 let (raw_solution, witness, _trimming) = Self::mine_and_check()?;
278
279 let score = raw_solution.score;
280 let call: Call<T> = Call::submit_unsigned { raw_solution: Box::new(raw_solution), witness };
281
282 log!(
283 debug,
284 "mined a solution with score {:?} and size {} and trimming {:?}",
285 score,
286 call.using_encoded(|b| b.len()),
287 _trimming
288 );
289
290 Ok(call)
291 }
292
293 fn submit_call(call: Call<T>) -> Result<(), MinerError> {
294 log!(debug, "miner submitting a solution as an unsigned transaction");
295
296 let xt = T::create_bare(call.into());
297 SubmitTransaction::<T, Call<T>>::submit_transaction(xt)
298 .map_err(|_| MinerError::PoolSubmissionFailed)
299 }
300
301 pub fn basic_checks(
305 raw_solution: &RawSolution<SolutionOf<T::MinerConfig>>,
306 solution_type: &str,
307 ) -> Result<(), MinerError> {
308 Self::unsigned_pre_dispatch_checks(raw_solution).map_err(|err| {
309 log!(debug, "pre-dispatch checks failed for {} solution: {:?}", solution_type, err);
310 MinerError::PreDispatchChecksFailed(err)
311 })?;
312
313 Self::feasibility_check(raw_solution.clone(), ElectionCompute::Unsigned).map_err(
314 |err| {
315 log!(debug, "feasibility check failed for {} solution: {:?}", solution_type, err);
316 err
317 },
318 )?;
319
320 Ok(())
321 }
322
323 pub fn mine_and_check() -> Result<
330 (RawSolution<SolutionOf<T::MinerConfig>>, SolutionOrSnapshotSize, TrimmingStatus),
331 MinerError,
332 > {
333 let (raw_solution, witness, is_trimmed) = Self::mine_solution()?;
334 Self::basic_checks(&raw_solution, "mined")?;
335 Ok((raw_solution, witness, is_trimmed))
336 }
337
338 pub fn ensure_offchain_repeat_frequency(now: BlockNumberFor<T>) -> Result<(), MinerError> {
348 let threshold = T::OffchainRepeat::get();
349 let last_block = StorageValueRef::persistent(OFFCHAIN_LAST_BLOCK);
350
351 let mutate_stat = last_block.mutate::<_, &'static str, _>(
352 |maybe_head: Result<Option<BlockNumberFor<T>>, _>| {
353 match maybe_head {
354 Ok(Some(head)) if now < head => Err("fork."),
355 Ok(Some(head)) if now >= head && now <= head + threshold => {
356 Err("recently executed.")
357 },
358 Ok(Some(head)) if now > head + threshold => {
359 Ok(now)
361 },
362 _ => {
363 Ok(now)
365 },
366 }
367 },
368 );
369
370 match mutate_stat {
371 Ok(_) => Ok(()),
373 Err(MutateStorageError::ConcurrentModification(_)) => {
375 Err(MinerError::Lock("failed to write to offchain db (concurrent modification)."))
376 },
377 Err(MutateStorageError::ValueFunctionFailed(why)) => Err(MinerError::Lock(why)),
379 }
380 }
381
382 pub fn unsigned_pre_dispatch_checks(
390 raw_solution: &RawSolution<SolutionOf<T::MinerConfig>>,
391 ) -> DispatchResult {
392 ensure!(
394 CurrentPhase::<T>::get().is_unsigned_open(),
395 Error::<T>::PreDispatchEarlySubmission
396 );
397
398 ensure!(Round::<T>::get() == raw_solution.round, Error::<T>::OcwCallWrongEra);
400
401 ensure!(
403 DesiredTargets::<T>::get().unwrap_or_default() ==
404 raw_solution.solution.unique_targets().len() as u32,
405 Error::<T>::PreDispatchWrongWinnerCount,
406 );
407
408 ensure!(
410 QueuedSolution::<T>::get()
411 .map_or(true, |q: ReadySolution<_, _, _>| raw_solution.score > q.score),
412 Error::<T>::PreDispatchWeakSubmission,
413 );
414
415 Ok(())
416 }
417}
418
419pub trait MinerConfig {
421 type AccountId: Ord + Clone + codec::Codec + core::fmt::Debug;
423 type Solution: codec::Codec
425 + Default
426 + PartialEq
427 + Eq
428 + Clone
429 + core::fmt::Debug
430 + Ord
431 + NposSolution
432 + TypeInfo;
433 type MaxVotesPerVoter;
435 type MaxLength: Get<u32>;
439 type MaxWeight: Get<Weight>;
445 type MaxWinners: Get<u32>;
448 type MaxBackersPerWinner: Get<u32>;
450 fn solution_weight(voters: u32, targets: u32, active_voters: u32, degree: u32) -> Weight;
454}
455
456pub struct Miner<T: MinerConfig>(core::marker::PhantomData<T>);
458impl<T: MinerConfig> Miner<T> {
459 pub fn mine_solution_with_snapshot<S>(
461 voters: Vec<(T::AccountId, VoteWeight, BoundedVec<T::AccountId, T::MaxVotesPerVoter>)>,
462 targets: Vec<T::AccountId>,
463 desired_targets: u32,
464 ) -> Result<(SolutionOf<T>, ElectionScore, SolutionOrSnapshotSize, TrimmingStatus), MinerError>
465 where
466 S: NposSolver<AccountId = T::AccountId>,
467 {
468 S::solve(desired_targets as usize, targets.clone(), voters.clone())
469 .map_err(|e| {
470 log_no_system!(error, "solver error: {:?}", e);
471 MinerError::Solver
472 })
473 .and_then(|e| {
474 Self::prepare_election_result_with_snapshot::<S::Accuracy>(
475 e,
476 voters,
477 targets,
478 desired_targets,
479 )
480 })
481 }
482
483 pub fn prepare_election_result_with_snapshot<Accuracy: PerThing128>(
488 election_result: ElectionResult<T::AccountId, Accuracy>,
489 voters: Vec<(T::AccountId, VoteWeight, BoundedVec<T::AccountId, T::MaxVotesPerVoter>)>,
490 targets: Vec<T::AccountId>,
491 desired_targets: u32,
492 ) -> Result<(SolutionOf<T>, ElectionScore, SolutionOrSnapshotSize, TrimmingStatus), MinerError>
493 {
494 let cache = helpers::generate_voter_cache::<T>(&voters);
496 let voter_index = helpers::voter_index_fn::<T>(&cache);
497 let target_index = helpers::target_index_fn::<T>(&targets);
498 let voter_at = helpers::voter_at_fn::<T>(&voters);
499 let target_at = helpers::target_at_fn::<T>(&targets);
500 let stake_of = helpers::stake_of_fn::<T>(&voters, &cache);
501
502 let encoded_size_of = |assignments: &[IndexAssignmentOf<T>]| {
506 SolutionOf::<T>::try_from(assignments).map(|s| s.encoded_size())
507 };
508
509 let ElectionResult { assignments, winners: _ } = election_result;
510
511 let mut edges_trimmed = 0;
513
514 let sorted_assignments = {
517 let mut staked = assignment_ratio_to_staked_normalized(assignments, &stake_of)?;
519
520 sp_npos_elections::reduce(&mut staked);
523
524 staked.sort_by_key(
527 |sp_npos_elections::StakedAssignment::<T::AccountId> { who, .. }| {
528 let stake = cache
532 .get(who)
533 .map(|idx| {
534 let (_, stake, _) = voters[*idx];
535 stake
536 })
537 .unwrap_or_default();
538 core::cmp::Reverse(stake)
539 },
540 );
541
542 let max_backers_per_winner = T::MaxBackersPerWinner::get().saturated_into::<usize>();
551
552 let _ = sp_npos_elections::to_supports(&staked)
553 .iter_mut()
554 .filter(|(_, support)| support.voters.len() > max_backers_per_winner)
555 .for_each(|(target, ref mut support)| {
556 support.voters.sort_by(|a, b| b.1.cmp(&a.1));
558
559 let filtered: alloc::collections::BTreeSet<_> = support
563 .voters
564 .split_off(max_backers_per_winner)
565 .into_iter()
566 .map(|(who, _stake)| who)
567 .collect();
568
569 staked.iter_mut().for_each(|assignment| {
571 if filtered.contains(&assignment.who) {
572 assignment.distribution.retain(|(t, _)| t != target);
573 }
574 });
575
576 edges_trimmed += filtered.len();
577 });
578
579 debug_assert!({
580 let expected_ok: Result<
583 crate::BoundedSupports<_, T::MaxWinners, T::MaxBackersPerWinner>,
584 _,
585 > = sp_npos_elections::to_supports(&staked).try_into();
586 expected_ok.is_ok()
587 });
588
589 assignment_staked_to_ratio_normalized(staked)?
591 };
592
593 let mut index_assignments = sorted_assignments
596 .into_iter()
597 .map(|assignment| IndexAssignmentOf::<T>::new(&assignment, &voter_index, &target_index))
598 .collect::<Result<Vec<_>, _>>()?;
599
600 let size =
602 SolutionOrSnapshotSize { voters: voters.len() as u32, targets: targets.len() as u32 };
603 let weight_trimmed = Self::trim_assignments_weight(
604 desired_targets,
605 size,
606 T::MaxWeight::get(),
607 &mut index_assignments,
608 );
609 let length_trimmed = Self::trim_assignments_length(
610 T::MaxLength::get(),
611 &mut index_assignments,
612 &encoded_size_of,
613 )?;
614
615 let solution = SolutionOf::<T>::try_from(&index_assignments)?;
617
618 let score = solution.clone().score(stake_of, voter_at, target_at)?;
620
621 let is_trimmed =
622 TrimmingStatus { weight: weight_trimmed, length: length_trimmed, edges: edges_trimmed };
623
624 log_no_system!(
625 debug,
626 "feasible solution mined: trimmed? {:?}, score: {:?}, encoded size: {:?}",
627 is_trimmed,
628 score,
629 solution.encoded_size()
630 );
631 Ok((solution, score, size, is_trimmed))
632 }
633
634 pub fn trim_assignments_length(
649 max_allowed_length: u32,
650 assignments: &mut Vec<IndexAssignmentOf<T>>,
651 encoded_size_of: impl Fn(&[IndexAssignmentOf<T>]) -> Result<usize, sp_npos_elections::Error>,
652 ) -> Result<usize, MinerError> {
653 let max_allowed_length: usize = max_allowed_length.saturated_into();
656 let mut high = assignments.len();
657 let mut low = 0;
658
659 if high == low {
661 return Ok(0);
662 }
663
664 while high - low > 1 {
665 let test = (high + low) / 2;
666 if encoded_size_of(&assignments[..test])? <= max_allowed_length {
667 low = test;
668 } else {
669 high = test;
670 }
671 }
672 let maximum_allowed_voters = if low < assignments.len() &&
673 encoded_size_of(&assignments[..low + 1])? <= max_allowed_length
674 {
675 low + 1
676 } else {
677 low
678 };
679
680 debug_assert!(
682 encoded_size_of(&assignments[..maximum_allowed_voters]).unwrap() <= max_allowed_length
683 );
684 debug_assert!(if maximum_allowed_voters < assignments.len() {
685 encoded_size_of(&assignments[..maximum_allowed_voters + 1]).unwrap() >
686 max_allowed_length
687 } else {
688 true
689 });
690
691 let remove = assignments.len().saturating_sub(maximum_allowed_voters);
696
697 log_no_system!(
698 trace,
699 "from {} assignments, truncating to {} for length, removing {}",
700 assignments.len(),
701 maximum_allowed_voters,
702 remove
703 );
704 assignments.truncate(maximum_allowed_voters);
705
706 Ok(remove)
707 }
708
709 pub fn trim_assignments_weight(
725 desired_targets: u32,
726 size: SolutionOrSnapshotSize,
727 max_weight: Weight,
728 assignments: &mut Vec<IndexAssignmentOf<T>>,
729 ) -> usize {
730 let maximum_allowed_voters =
731 Self::maximum_voter_for_weight(desired_targets, size, max_weight);
732 let removing: usize =
733 assignments.len().saturating_sub(maximum_allowed_voters.saturated_into());
734 log_no_system!(
735 debug,
736 "from {} assignments, truncating to {} for weight, removing {}",
737 assignments.len(),
738 maximum_allowed_voters,
739 removing,
740 );
741 assignments.truncate(maximum_allowed_voters as usize);
742
743 removing
744 }
745
746 pub fn maximum_voter_for_weight(
750 desired_winners: u32,
751 size: SolutionOrSnapshotSize,
752 max_weight: Weight,
753 ) -> u32 {
754 if size.voters < 1 {
755 return size.voters;
756 }
757
758 let max_voters = size.voters.max(1);
759 let mut voters = max_voters;
760
761 let weight_with = |active_voters: u32| -> Weight {
763 T::solution_weight(size.voters, size.targets, active_voters, desired_winners)
764 };
765
766 let next_voters = |current_weight: Weight, voters: u32, step: u32| -> Result<u32, ()> {
767 if current_weight.all_lt(max_weight) {
768 let next_voters = voters.checked_add(step);
769 match next_voters {
770 Some(voters) if voters < max_voters => Ok(voters),
771 _ => Err(()),
772 }
773 } else if current_weight.any_gt(max_weight) {
774 voters.checked_sub(step).ok_or(())
775 } else {
776 Ok(voters)
778 }
779 };
780
781 let mut step = voters / 2;
783 let mut current_weight = weight_with(voters);
784
785 while step > 0 {
786 match next_voters(current_weight, voters, step) {
787 Ok(next) if next != voters => {
789 voters = next;
790 },
791 Err(()) => break,
793 Ok(next) => return next,
795 }
796 step /= 2;
797 current_weight = weight_with(voters);
798 }
799
800 while voters < max_voters && weight_with(voters + 1).all_lt(max_weight) {
803 voters += 1;
804 }
805 while voters.checked_sub(1).is_some() && weight_with(voters).any_gt(max_weight) {
806 voters -= 1;
807 }
808
809 let final_decision = voters.min(size.voters);
810 debug_assert!(
811 weight_with(final_decision).all_lte(max_weight),
812 "weight_with({}) <= {}",
813 final_decision,
814 max_weight,
815 );
816 final_decision
817 }
818
819 pub fn feasibility_check(
821 raw_solution: RawSolution<SolutionOf<T>>,
822 compute: ElectionCompute,
823 desired_targets: u32,
824 snapshot: RoundSnapshot<T::AccountId, MinerVoterOf<T>>,
825 current_round: u32,
826 minimum_untrusted_score: Option<ElectionScore>,
827 ) -> Result<ReadySolutionOf<T>, FeasibilityError> {
828 let RawSolution { solution, score, round } = raw_solution;
829 let RoundSnapshot { voters: snapshot_voters, targets: snapshot_targets } = snapshot;
830
831 ensure!(current_round == round, FeasibilityError::InvalidRound);
833
834 let winners = solution.unique_targets();
836
837 ensure!(winners.len() as u32 == desired_targets, FeasibilityError::WrongWinnerCount);
838 ensure!(desired_targets <= T::MaxWinners::get(), FeasibilityError::TooManyDesiredTargets);
840
841 let submitted_score = raw_solution.score;
843 ensure!(
844 minimum_untrusted_score
845 .map_or(true, |min_score| { submitted_score.strict_better(min_score) }),
846 FeasibilityError::UntrustedScoreTooLow
847 );
848
849 let cache = helpers::generate_voter_cache::<T>(&snapshot_voters);
851 let voter_at = helpers::voter_at_fn::<T>(&snapshot_voters);
852 let target_at = helpers::target_at_fn::<T>(&snapshot_targets);
853 let voter_index = helpers::voter_index_fn_usize::<T>(&cache);
854
855 let assignments = solution
858 .into_assignment(voter_at, target_at)
859 .map_err::<FeasibilityError, _>(Into::into)?;
860
861 assignments.iter().try_for_each(|assignment| {
863 let snapshot_index =
871 voter_index(&assignment.who).ok_or(FeasibilityError::InvalidVoter)?;
872 let (_voter, _stake, targets) =
874 snapshot_voters.get(snapshot_index).ok_or(FeasibilityError::InvalidVoter)?;
875
876 if assignment.distribution.iter().any(|(d, _)| !targets.contains(d)) {
878 return Err(FeasibilityError::InvalidVote);
879 }
880 Ok(())
881 })?;
882
883 let stake_of = helpers::stake_of_fn::<T>(&snapshot_voters, &cache);
885
886 let staked_assignments = assignment_ratio_to_staked_normalized(assignments, stake_of)
888 .map_err::<FeasibilityError, _>(Into::into)?;
889 let supports = sp_npos_elections::to_supports(&staked_assignments);
890
891 let known_score = supports.evaluate();
893
894 ensure!(known_score == score, FeasibilityError::InvalidScore);
895
896 let supports = supports
900 .try_into()
901 .defensive_map_err(|_| FeasibilityError::BoundedConversionFailed)?;
902
903 Ok(ReadySolution { supports, compute, score })
904 }
905}
906
907#[cfg(test)]
908mod max_weight {
909 #![allow(unused_variables)]
910 use super::*;
911 use crate::mock::{MockWeightInfo, Runtime};
912 #[test]
913 fn find_max_voter_binary_search_works() {
914 let w = SolutionOrSnapshotSize { voters: 10, targets: 0 };
915 MockWeightInfo::set(crate::mock::MockedWeightInfo::Complex);
916 assert_eq!(
917 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
918 0
919 );
920 assert_eq!(
921 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
922 0
923 );
924 assert_eq!(
925 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
926 0
927 );
928 assert_eq!(
929 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
930 1
931 );
932 assert_eq!(
933 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
934 1
935 );
936 assert_eq!(
937 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1990, u64::MAX)),
938 1
939 );
940 assert_eq!(
941 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
942 1
943 );
944 assert_eq!(
945 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
946 2
947 );
948 assert_eq!(
949 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
950 2
951 );
952 assert_eq!(
953 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
954 2
955 );
956 assert_eq!(
957 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2990, u64::MAX)),
958 2
959 );
960 assert_eq!(
961 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2999, u64::MAX)),
962 2
963 );
964 assert_eq!(
965 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3000, u64::MAX)),
966 3
967 );
968 assert_eq!(
969 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
970 3
971 );
972 assert_eq!(
973 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(5500, u64::MAX)),
974 5
975 );
976 assert_eq!(
977 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(7777, u64::MAX)),
978 7
979 );
980 assert_eq!(
981 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(9999, u64::MAX)),
982 9
983 );
984 assert_eq!(
985 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(10_000, u64::MAX)),
986 10
987 );
988 assert_eq!(
989 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(10_999, u64::MAX)),
990 10
991 );
992 assert_eq!(
993 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(11_000, u64::MAX)),
994 10
995 );
996 assert_eq!(
997 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(22_000, u64::MAX)),
998 10
999 );
1000
1001 let w = SolutionOrSnapshotSize { voters: 1, targets: 0 };
1002
1003 assert_eq!(
1004 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
1005 0
1006 );
1007 assert_eq!(
1008 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
1009 0
1010 );
1011 assert_eq!(
1012 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
1013 0
1014 );
1015 assert_eq!(
1016 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
1017 1
1018 );
1019 assert_eq!(
1020 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
1021 1
1022 );
1023 assert_eq!(
1024 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1990, u64::MAX)),
1025 1
1026 );
1027 assert_eq!(
1028 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
1029 1
1030 );
1031 assert_eq!(
1032 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
1033 1
1034 );
1035 assert_eq!(
1036 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
1037 1
1038 );
1039 assert_eq!(
1040 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
1041 1
1042 );
1043 assert_eq!(
1044 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
1045 1
1046 );
1047
1048 let w = SolutionOrSnapshotSize { voters: 2, targets: 0 };
1049
1050 assert_eq!(
1051 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
1052 0
1053 );
1054 assert_eq!(
1055 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
1056 0
1057 );
1058 assert_eq!(
1059 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
1060 0
1061 );
1062 assert_eq!(
1063 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
1064 1
1065 );
1066 assert_eq!(
1067 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
1068 1
1069 );
1070 assert_eq!(
1071 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
1072 1
1073 );
1074 assert_eq!(
1075 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
1076 2
1077 );
1078 assert_eq!(
1079 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
1080 2
1081 );
1082 assert_eq!(
1083 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
1084 2
1085 );
1086 assert_eq!(
1087 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
1088 2
1089 );
1090 }
1091}
1092
1093#[cfg(test)]
1094mod tests {
1095 use super::*;
1096 use crate::{
1097 mock::{
1098 multi_phase_events, roll_to, roll_to_signed, roll_to_unsigned, roll_to_with_ocw,
1099 trim_helpers, witness, BlockNumber, ExtBuilder, Extrinsic, MinerMaxWeight, MultiPhase,
1100 Runtime, RuntimeCall, RuntimeOrigin, System, TestNposSolution, TrimHelpers,
1101 UnsignedPhase,
1102 },
1103 Event, InvalidTransaction, Phase, QueuedSolution, TransactionSource,
1104 TransactionValidityError,
1105 };
1106 use alloc::vec;
1107 use codec::Decode;
1108 use frame_election_provider_support::IndexAssignment;
1109 use frame_support::{assert_noop, assert_ok, traits::OffchainWorker};
1110 use sp_npos_elections::ElectionScore;
1111 use sp_runtime::{
1112 bounded_vec,
1113 offchain::storage_lock::{BlockAndTime, StorageLock},
1114 traits::{Dispatchable, Zero},
1115 ModuleError, PerU16,
1116 };
1117
1118 #[allow(deprecated)]
1119 use sp_runtime::traits::ValidateUnsigned;
1120
1121 type Assignment = crate::unsigned::Assignment<Runtime>;
1122
1123 #[test]
1124 #[allow(deprecated)]
1125 fn validate_unsigned_retracts_wrong_phase() {
1126 ExtBuilder::default().desired_targets(0).build_and_execute(|| {
1127 let solution = RawSolution::<TestNposSolution> {
1128 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1129 ..Default::default()
1130 };
1131 let call = Call::submit_unsigned {
1132 raw_solution: Box::new(solution.clone()),
1133 witness: witness(),
1134 };
1135
1136 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
1138 assert!(matches!(
1139 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1140 TransactionSource::Local,
1141 &call
1142 )
1143 .unwrap_err(),
1144 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1145 ));
1146 assert!(matches!(
1147 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1148 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1149 ));
1150
1151 roll_to_signed();
1153 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
1154 assert!(matches!(
1155 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1156 TransactionSource::Local,
1157 &call
1158 )
1159 .unwrap_err(),
1160 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1161 ));
1162 assert!(matches!(
1163 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1164 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1165 ));
1166
1167 roll_to_unsigned();
1169 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1170
1171 assert!(<MultiPhase as ValidateUnsigned>::validate_unsigned(
1172 TransactionSource::Local,
1173 &call
1174 )
1175 .is_ok());
1176 assert!(<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).is_ok());
1177
1178 MultiPhase::phase_transition(Phase::Unsigned((false, 25)));
1180 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1181 assert!(matches!(
1182 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1183 TransactionSource::Local,
1184 &call
1185 )
1186 .unwrap_err(),
1187 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1188 ));
1189 assert!(matches!(
1190 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1191 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1192 ));
1193 })
1194 }
1195
1196 #[test]
1197 #[allow(deprecated)]
1198 fn validate_unsigned_retracts_low_score() {
1199 ExtBuilder::default().desired_targets(0).build_and_execute(|| {
1200 roll_to_unsigned();
1201 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1202
1203 let solution = RawSolution::<TestNposSolution> {
1204 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1205 ..Default::default()
1206 };
1207 let call = Call::submit_unsigned {
1208 raw_solution: Box::new(solution.clone()),
1209 witness: witness(),
1210 };
1211
1212 assert!(<MultiPhase as ValidateUnsigned>::validate_unsigned(
1214 TransactionSource::Local,
1215 &call
1216 )
1217 .is_ok());
1218 assert!(<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).is_ok());
1219
1220 let ready = ReadySolution {
1222 score: ElectionScore { minimal_stake: 10, ..Default::default() },
1223 ..Default::default()
1224 };
1225 QueuedSolution::<Runtime>::put(ready);
1226
1227 assert!(matches!(
1229 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1230 TransactionSource::Local,
1231 &call
1232 )
1233 .unwrap_err(),
1234 TransactionValidityError::Invalid(InvalidTransaction::Custom(2))
1235 ));
1236 assert!(matches!(
1237 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1238 TransactionValidityError::Invalid(InvalidTransaction::Custom(2))
1239 ));
1240 })
1241 }
1242
1243 #[test]
1244 #[allow(deprecated)]
1245 fn validate_unsigned_retracts_incorrect_winner_count() {
1246 ExtBuilder::default().desired_targets(1).build_and_execute(|| {
1247 roll_to_unsigned();
1248 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1249
1250 let raw = RawSolution::<TestNposSolution> {
1251 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1252 ..Default::default()
1253 };
1254 let call =
1255 Call::submit_unsigned { raw_solution: Box::new(raw.clone()), witness: witness() };
1256 assert_eq!(raw.solution.unique_targets().len(), 0);
1257
1258 assert!(matches!(
1260 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1261 TransactionSource::Local,
1262 &call
1263 )
1264 .unwrap_err(),
1265 TransactionValidityError::Invalid(InvalidTransaction::Custom(1))
1266 ));
1267 })
1268 }
1269
1270 #[test]
1271 #[allow(deprecated)]
1272 fn priority_is_set() {
1273 ExtBuilder::default()
1274 .miner_tx_priority(20)
1275 .desired_targets(0)
1276 .build_and_execute(|| {
1277 roll_to_unsigned();
1278 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1279
1280 let solution = RawSolution::<TestNposSolution> {
1281 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1282 ..Default::default()
1283 };
1284 let call = Call::submit_unsigned {
1285 raw_solution: Box::new(solution.clone()),
1286 witness: witness(),
1287 };
1288
1289 assert_eq!(
1290 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1291 TransactionSource::Local,
1292 &call
1293 )
1294 .unwrap()
1295 .priority,
1296 25
1297 );
1298 })
1299 }
1300
1301 #[test]
1302 #[should_panic(expected = "Invalid unsigned submission must produce invalid block and \
1303 deprive validator from their authoring reward.: \
1304 Module(ModuleError { index: 2, error: [1, 0, 0, 0], message: \
1305 Some(\"PreDispatchWrongWinnerCount\") })")]
1306 fn unfeasible_solution_panics() {
1307 ExtBuilder::default().build_and_execute(|| {
1308 roll_to_unsigned();
1309 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1310
1311 let solution = RawSolution::<TestNposSolution> {
1313 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1314 ..Default::default()
1315 };
1316 let call = Call::submit_unsigned {
1317 raw_solution: Box::new(solution.clone()),
1318 witness: witness(),
1319 };
1320 let runtime_call: RuntimeCall = call.into();
1321 let _ = runtime_call.dispatch(RuntimeOrigin::none());
1322 })
1323 }
1324
1325 #[test]
1326 #[should_panic(expected = "Invalid unsigned submission must produce invalid block and \
1327 deprive validator from their authoring reward.")]
1328 fn wrong_witness_panics() {
1329 ExtBuilder::default().build_and_execute(|| {
1330 roll_to_unsigned();
1331 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1332
1333 let solution = RawSolution::<TestNposSolution> {
1335 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1336 ..Default::default()
1337 };
1338
1339 let mut correct_witness = witness();
1340 correct_witness.voters += 1;
1341 correct_witness.targets -= 1;
1342 let call = Call::submit_unsigned {
1343 raw_solution: Box::new(solution.clone()),
1344 witness: correct_witness,
1345 };
1346 let runtime_call: RuntimeCall = call.into();
1347 let _ = runtime_call.dispatch(RuntimeOrigin::none());
1348 })
1349 }
1350
1351 #[test]
1352 fn miner_works() {
1353 ExtBuilder::default().build_and_execute(|| {
1354 roll_to_unsigned();
1355 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1356
1357 assert!(Snapshot::<Runtime>::get().is_some());
1359 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 2);
1360
1361 let (solution, witness, _) = MultiPhase::mine_solution().unwrap();
1363
1364 assert!(QueuedSolution::<Runtime>::get().is_none());
1366 assert_ok!(MultiPhase::submit_unsigned(
1367 RuntimeOrigin::none(),
1368 Box::new(solution),
1369 witness
1370 ));
1371 assert!(QueuedSolution::<Runtime>::get().is_some());
1372 assert_eq!(
1373 multi_phase_events(),
1374 vec![
1375 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1376 Event::PhaseTransitioned {
1377 from: Phase::Signed,
1378 to: Phase::Unsigned((true, 25)),
1379 round: 1
1380 },
1381 Event::SolutionStored {
1382 compute: ElectionCompute::Unsigned,
1383 origin: None,
1384 prev_ejected: false
1385 }
1386 ]
1387 );
1388 })
1389 }
1390
1391 #[test]
1392 fn miner_trims_weight() {
1393 ExtBuilder::default()
1394 .miner_weight(Weight::from_parts(100, u64::MAX))
1395 .mock_weight_info(crate::mock::MockedWeightInfo::Basic)
1396 .build_and_execute(|| {
1397 roll_to_unsigned();
1398 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1399
1400 let (raw, witness, t) = MultiPhase::mine_solution().unwrap();
1401 let solution_weight = <Runtime as MinerConfig>::solution_weight(
1402 witness.voters,
1403 witness.targets,
1404 raw.solution.voter_count() as u32,
1405 raw.solution.unique_targets().len() as u32,
1406 );
1407 assert_eq!(solution_weight, Weight::from_parts(35, 0));
1409 assert_eq!(raw.solution.voter_count(), 5);
1410 assert_eq!(t.trimmed_weight(), 0);
1411
1412 <MinerMaxWeight>::set(Weight::from_parts(25, u64::MAX));
1414
1415 let (raw, witness, t) = MultiPhase::mine_solution().unwrap();
1416 let solution_weight = <Runtime as MinerConfig>::solution_weight(
1417 witness.voters,
1418 witness.targets,
1419 raw.solution.voter_count() as u32,
1420 raw.solution.unique_targets().len() as u32,
1421 );
1422 assert_eq!(solution_weight, Weight::from_parts(25, 0));
1424 assert_eq!(raw.solution.voter_count(), 3);
1425 assert_eq!(t.trimmed_weight(), 2);
1426 })
1427 }
1428
1429 #[test]
1430 fn miner_will_not_submit_if_not_enough_winners() {
1431 let (mut ext, _) = ExtBuilder::default().desired_targets(8).build_offchainify(0);
1432 ext.execute_with(|| {
1433 roll_to_unsigned();
1434 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1435
1436 let (mut solution, _, _) = MultiPhase::mine_solution().unwrap();
1438 solution.solution.votes1[0].1 = 4;
1439
1440 assert_eq!(
1441 MultiPhase::basic_checks(&solution, "mined").unwrap_err(),
1442 MinerError::PreDispatchChecksFailed(DispatchError::Module(ModuleError {
1443 index: 2,
1444 error: [1, 0, 0, 0],
1445 message: Some("PreDispatchWrongWinnerCount"),
1446 })),
1447 );
1448 })
1449 }
1450
1451 #[test]
1452 fn unsigned_per_dispatch_checks_can_only_submit_threshold_better() {
1453 ExtBuilder::default()
1454 .desired_targets(1)
1455 .add_voter(7, 2, bounded_vec![10])
1456 .add_voter(8, 5, bounded_vec![10])
1457 .add_voter(9, 1, bounded_vec![10])
1458 .build_and_execute(|| {
1459 roll_to_unsigned();
1460 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1461 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 1);
1462
1463 let result = ElectionResult {
1465 winners: vec![(10, 12)],
1466 assignments: vec![
1467 Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1468 Assignment {
1469 who: 7,
1470 distribution: vec![(10, PerU16::one())],
1472 },
1473 ],
1474 };
1475
1476 let RoundSnapshot { voters, targets } = Snapshot::<Runtime>::get().unwrap();
1477 let desired_targets = DesiredTargets::<Runtime>::get().unwrap();
1478
1479 let (raw, score, witness, _) =
1480 Miner::<Runtime>::prepare_election_result_with_snapshot(
1481 result,
1482 voters.clone(),
1483 targets.clone(),
1484 desired_targets,
1485 )
1486 .unwrap();
1487 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1488 assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
1489 assert_ok!(MultiPhase::submit_unsigned(
1490 RuntimeOrigin::none(),
1491 Box::new(solution),
1492 witness
1493 ));
1494 assert_eq!(QueuedSolution::<Runtime>::get().unwrap().score.minimal_stake, 12);
1495
1496 let result = ElectionResult {
1499 winners: vec![(10, 10)],
1500 assignments: vec![Assignment {
1501 who: 10,
1502 distribution: vec![(10, PerU16::one())],
1503 }],
1504 };
1505 let (raw, score, _, _) = Miner::<Runtime>::prepare_election_result_with_snapshot(
1506 result,
1507 voters.clone(),
1508 targets.clone(),
1509 desired_targets,
1510 )
1511 .unwrap();
1512 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1513 assert_eq!(solution.score.minimal_stake, 10);
1515 assert_noop!(
1517 MultiPhase::unsigned_pre_dispatch_checks(&solution),
1518 Error::<Runtime>::PreDispatchWeakSubmission,
1519 );
1520
1521 let result = ElectionResult {
1524 winners: vec![(10, 12)],
1525 assignments: vec![
1526 Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1527 Assignment {
1528 who: 7,
1529 distribution: vec![(10, PerU16::one())],
1531 },
1532 ],
1533 };
1534
1535 let (raw, score, _, _) = Miner::<Runtime>::prepare_election_result_with_snapshot(
1536 result,
1537 voters.clone(),
1538 targets.clone(),
1539 desired_targets,
1540 )
1541 .unwrap();
1542 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1543 assert_eq!(solution.score.minimal_stake, 12);
1545 assert_noop!(
1547 MultiPhase::unsigned_pre_dispatch_checks(&solution),
1548 Error::<Runtime>::PreDispatchWeakSubmission,
1549 );
1550
1551 let result = ElectionResult {
1554 winners: vec![(10, 12)],
1555 assignments: vec![
1556 Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1557 Assignment { who: 7, distribution: vec![(10, PerU16::one())] },
1558 Assignment { who: 9, distribution: vec![(10, PerU16::one())] },
1559 ],
1560 };
1561 let (raw, score, witness, _) =
1562 Miner::<Runtime>::prepare_election_result_with_snapshot(
1563 result,
1564 voters.clone(),
1565 targets.clone(),
1566 desired_targets,
1567 )
1568 .unwrap();
1569 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1570 assert_eq!(solution.score.minimal_stake, 13);
1571
1572 assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
1574 assert_ok!(MultiPhase::submit_unsigned(
1575 RuntimeOrigin::none(),
1576 Box::new(solution),
1577 witness
1578 ));
1579
1580 let result = ElectionResult {
1583 winners: vec![(10, 12)],
1584 assignments: vec![
1585 Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1586 Assignment { who: 7, distribution: vec![(10, PerU16::one())] },
1587 Assignment {
1588 who: 8,
1589 distribution: vec![(10, PerU16::one())],
1591 },
1592 ],
1593 };
1594 let (raw, score, witness, _) =
1595 Miner::<Runtime>::prepare_election_result_with_snapshot(
1596 result,
1597 voters.clone(),
1598 targets.clone(),
1599 desired_targets,
1600 )
1601 .unwrap();
1602 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1603 assert_eq!(solution.score.minimal_stake, 17);
1604
1605 assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
1607 assert_ok!(MultiPhase::submit_unsigned(
1608 RuntimeOrigin::none(),
1609 Box::new(solution),
1610 witness
1611 ));
1612 })
1613 }
1614
1615 #[test]
1616 fn ocw_lock_prevents_frequent_execution() {
1617 let (mut ext, _) = ExtBuilder::default().build_offchainify(0);
1618 ext.execute_with(|| {
1619 let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1620
1621 roll_to_unsigned();
1622 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1623
1624 assert!(MultiPhase::ensure_offchain_repeat_frequency(25).is_ok());
1626
1627 assert_noop!(
1629 MultiPhase::ensure_offchain_repeat_frequency(26),
1630 MinerError::Lock("recently executed.")
1631 );
1632
1633 assert!(
1635 MultiPhase::ensure_offchain_repeat_frequency((26 + offchain_repeat).into()).is_ok()
1636 );
1637
1638 assert!(MultiPhase::ensure_offchain_repeat_frequency(
1640 (26 + offchain_repeat - 3).into()
1641 )
1642 .is_err());
1643 assert!(MultiPhase::ensure_offchain_repeat_frequency(
1644 (26 + offchain_repeat - 2).into()
1645 )
1646 .is_err());
1647 assert!(MultiPhase::ensure_offchain_repeat_frequency(
1648 (26 + offchain_repeat - 1).into()
1649 )
1650 .is_err());
1651 })
1652 }
1653
1654 #[test]
1655 fn ocw_lock_released_after_successful_execution() {
1656 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1658 ext.execute_with(|| {
1659 let guard = StorageValueRef::persistent(&OFFCHAIN_LOCK);
1660 let last_block = StorageValueRef::persistent(OFFCHAIN_LAST_BLOCK);
1661
1662 roll_to_unsigned();
1663 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1664
1665 assert!(guard.get::<bool>().unwrap().is_none());
1667
1668 MultiPhase::offchain_worker(25);
1670 assert_eq!(pool.read().transactions.len(), 1);
1671
1672 assert!(guard.get::<bool>().unwrap().is_none());
1674 assert_eq!(last_block.get::<BlockNumber>().unwrap(), Some(25));
1675 });
1676 }
1677
1678 #[test]
1679 fn ocw_lock_prevents_overlapping_execution() {
1680 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1682 ext.execute_with(|| {
1683 roll_to_unsigned();
1684 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1685
1686 let mut lock = StorageLock::<BlockAndTime<System>>::with_block_deadline(
1688 OFFCHAIN_LOCK,
1689 UnsignedPhase::get().saturated_into(),
1690 );
1691 let guard = lock.lock();
1692
1693 MultiPhase::offchain_worker(25);
1695 assert_eq!(pool.read().transactions.len(), 0);
1696 MultiPhase::offchain_worker(26);
1697 assert_eq!(pool.read().transactions.len(), 0);
1698
1699 drop(guard);
1700
1701 MultiPhase::offchain_worker(25);
1703 assert_eq!(pool.read().transactions.len(), 1);
1704 });
1705 }
1706
1707 #[test]
1708 fn ocw_only_runs_when_unsigned_open_now() {
1709 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1710 ext.execute_with(|| {
1711 roll_to_unsigned();
1712 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
1713
1714 let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
1717
1718 MultiPhase::offchain_worker(24);
1719 assert!(pool.read().transactions.len().is_zero());
1720 storage.clear();
1721
1722 MultiPhase::offchain_worker(25);
1724 assert_eq!(pool.read().transactions.len(), 1);
1725 pool.try_write().unwrap().transactions.clear();
1727
1728 MultiPhase::offchain_worker(26);
1730 assert!(pool.read().transactions.len().is_zero());
1731 })
1732 }
1733
1734 #[test]
1735 fn ocw_clears_cache_on_unsigned_phase_open() {
1736 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1737 ext.execute_with(|| {
1738 const BLOCK: u64 = 25;
1739 let block_plus = |delta: u64| BLOCK + delta;
1740 let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1741
1742 roll_to(BLOCK);
1743 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
1745
1746 assert!(
1747 !ocw_solution_exists::<Runtime>(),
1748 "no solution should be present before we mine one",
1749 );
1750
1751 MultiPhase::offchain_worker(BLOCK);
1753 assert!(
1754 ocw_solution_exists::<Runtime>(),
1755 "a solution must be cached after running the worker",
1756 );
1757
1758 let tx_cache_1 = pool.read().transactions[0].clone();
1760 pool.try_write().unwrap().transactions.clear();
1762
1763 let _ = MultiPhase::do_elect();
1766 MultiPhase::offchain_worker(block_plus(1));
1767 assert!(ocw_solution_exists::<Runtime>(), "elections does not clear the ocw cache");
1768
1769 MultiPhase::offchain_worker(block_plus(offchain_repeat + 1));
1771
1772 let tx_cache_2 = pool.read().transactions[0].clone();
1774 pool.try_write().unwrap().transactions.clear();
1776
1777 assert_eq!(tx_cache_1, tx_cache_2);
1779
1780 let current_block = block_plus(offchain_repeat * 2 + 2);
1781 MultiPhase::phase_transition(Phase::Unsigned((true, current_block)));
1783
1784 MultiPhase::offchain_worker(current_block);
1787 let tx_cache_3 = pool.read().transactions[0].clone();
1788
1789 assert_eq!(tx_cache_1, tx_cache_3);
1791 assert_eq!(
1792 multi_phase_events(),
1793 vec![
1794 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1795 Event::PhaseTransitioned {
1796 from: Phase::Signed,
1797 to: Phase::Unsigned((true, 25)),
1798 round: 1
1799 },
1800 Event::ElectionFinalized {
1801 compute: ElectionCompute::Fallback,
1802 score: ElectionScore {
1803 minimal_stake: 0,
1804 sum_stake: 0,
1805 sum_stake_squared: 0
1806 }
1807 },
1808 Event::PhaseTransitioned {
1809 from: Phase::Unsigned((true, 25)),
1810 to: Phase::Unsigned((true, 37)),
1811 round: 1
1812 },
1813 ]
1814 );
1815 })
1816 }
1817
1818 #[test]
1819 fn ocw_resubmits_after_offchain_repeat() {
1820 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1821 ext.execute_with(|| {
1822 const BLOCK: u64 = 25;
1823 let block_plus = |delta: i32| ((BLOCK as i32) + delta) as u64;
1824 let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1825
1826 roll_to(BLOCK);
1827 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
1828
1829 let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
1832
1833 MultiPhase::offchain_worker(block_plus(-1));
1834 assert!(pool.read().transactions.len().is_zero());
1835 storage.clear();
1836
1837 MultiPhase::offchain_worker(BLOCK);
1839 assert_eq!(pool.read().transactions.len(), 1);
1840 let tx_cache = pool.read().transactions[0].clone();
1841 pool.try_write().unwrap().transactions.clear();
1843
1844 MultiPhase::offchain_worker(block_plus(1 + offchain_repeat as i32));
1848 assert_eq!(pool.read().transactions.len(), 1);
1849
1850 let tx = &pool.read().transactions[0];
1852 assert_eq!(&tx_cache, tx);
1853 })
1854 }
1855
1856 #[test]
1857 fn ocw_regenerates_and_resubmits_after_offchain_repeat() {
1858 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1859 ext.execute_with(|| {
1860 const BLOCK: u64 = 25;
1861 let block_plus = |delta: i32| ((BLOCK as i32) + delta) as u64;
1862 let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1863
1864 roll_to(BLOCK);
1865 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
1866
1867 let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
1870
1871 MultiPhase::offchain_worker(block_plus(-1));
1872 assert!(pool.read().transactions.len().is_zero());
1873 storage.clear();
1874
1875 MultiPhase::offchain_worker(BLOCK);
1877 assert_eq!(pool.read().transactions.len(), 1);
1878 let tx_cache = pool.read().transactions[0].clone();
1879 pool.try_write().unwrap().transactions.clear();
1881
1882 let mut call_cache = StorageValueRef::persistent(&OFFCHAIN_CACHED_CALL);
1886 assert!(matches!(call_cache.get::<Call<Runtime>>(), Ok(Some(_call))));
1887 call_cache.clear();
1888
1889 MultiPhase::offchain_worker(block_plus(1 + offchain_repeat as i32));
1893 assert_eq!(pool.read().transactions.len(), 1);
1894
1895 let tx = &pool.read().transactions[0];
1897 assert_eq!(&tx_cache, tx);
1898 })
1899 }
1900
1901 #[test]
1902 fn ocw_can_submit_to_pool() {
1903 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1904 ext.execute_with(|| {
1905 roll_to_with_ocw(25);
1906 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
1907 let encoded = pool.read().transactions[0].clone();
1910 let extrinsic: Extrinsic = codec::Decode::decode(&mut &*encoded).unwrap();
1911 let call = extrinsic.function;
1912 assert!(matches!(call, RuntimeCall::MultiPhase(Call::submit_unsigned { .. })));
1913 })
1914 }
1915
1916 #[test]
1917 #[allow(deprecated)]
1918 fn ocw_solution_must_have_correct_round() {
1919 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1920 ext.execute_with(|| {
1921 roll_to_with_ocw(25);
1922 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
1923 crate::Round::<Runtime>::mutate(|round| *round += 1);
1926
1927 let encoded = pool.read().transactions[0].clone();
1928 let extrinsic = Extrinsic::decode(&mut &*encoded).unwrap();
1929 let call = match extrinsic.function {
1930 RuntimeCall::MultiPhase(call @ Call::submit_unsigned { .. }) => call,
1931 _ => panic!("bad call: unexpected submission"),
1932 };
1933
1934 let pre_dispatch_check_error =
1936 TransactionValidityError::Invalid(InvalidTransaction::Custom(7));
1937 assert_eq!(
1938 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1939 TransactionSource::Local,
1940 &call,
1941 )
1942 .unwrap_err(),
1943 pre_dispatch_check_error,
1944 );
1945 assert_eq!(
1946 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1947 pre_dispatch_check_error,
1948 );
1949 })
1950 }
1951
1952 #[test]
1953 fn mine_solution_always_respects_max_backers_per_winner() {
1954 use crate::mock::MaxBackersPerWinner;
1955 use frame_election_provider_support::BoundedSupport;
1956
1957 let targets = vec![10, 20, 30, 40];
1958 let voters = vec![
1959 (1, 11, bounded_vec![10, 20, 30]),
1960 (2, 12, bounded_vec![10, 20, 30]),
1961 (3, 13, bounded_vec![10, 20, 30]),
1962 (4, 14, bounded_vec![10, 20, 30]),
1963 (5, 15, bounded_vec![10, 20, 40]),
1964 ];
1965 let snapshot = RoundSnapshot { voters: voters.clone(), targets: targets.clone() };
1966 let (round, desired_targets) = (1, 3);
1967
1968 ExtBuilder::default().max_backers_per_winner(u32::MAX).build_and_execute(|| {
1970 assert_eq!(MaxBackersPerWinner::get(), u32::MAX);
1971
1972 let (solution, expected_score_unbounded, _, trimming_status) =
1973 Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
1974 voters.clone(),
1975 targets.clone(),
1976 desired_targets,
1977 )
1978 .unwrap();
1979
1980 let ready_solution = Miner::<Runtime>::feasibility_check(
1981 RawSolution { solution, score: expected_score_unbounded, round },
1982 Default::default(),
1983 desired_targets,
1984 snapshot.clone(),
1985 round,
1986 Default::default(),
1987 )
1988 .unwrap();
1989
1990 assert_eq!(
1991 ready_solution.supports.into_iter().collect::<Vec<_>>(),
1992 vec![
1993 (
1994 10,
1995 BoundedSupport { total: 25, voters: bounded_vec![(1, 11), (4, 9), (5, 5)] }
1996 ),
1997 (20, BoundedSupport { total: 22, voters: bounded_vec![(2, 12), (5, 10)] }),
1998 (30, BoundedSupport { total: 18, voters: bounded_vec![(3, 13), (4, 5)] })
1999 ]
2000 );
2001
2002 assert_eq!(trimming_status.trimmed_edges(), 0);
2004 });
2005
2006 ExtBuilder::default().max_backers_per_winner(1).build_and_execute(|| {
2008 assert_eq!(MaxBackersPerWinner::get(), 1);
2009
2010 let (solution, expected_score_bounded, _, trimming_status) =
2011 Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
2012 voters,
2013 targets,
2014 desired_targets,
2015 )
2016 .unwrap();
2017
2018 let ready_solution = Miner::<Runtime>::feasibility_check(
2019 RawSolution { solution, score: expected_score_bounded, round },
2020 Default::default(),
2021 desired_targets,
2022 snapshot,
2023 round,
2024 Default::default(),
2025 )
2026 .unwrap();
2027
2028 for (_, supports) in ready_solution.supports.iter() {
2029 assert!((supports.voters.len() as u32) <= MaxBackersPerWinner::get());
2030 }
2031
2032 assert_eq!(
2033 ready_solution.supports.into_iter().collect::<Vec<_>>(),
2034 vec![
2035 (10, BoundedSupport { total: 11, voters: bounded_vec![(1, 11)] }),
2036 (20, BoundedSupport { total: 12, voters: bounded_vec![(2, 12)] }),
2037 (30, BoundedSupport { total: 13, voters: bounded_vec![(3, 13)] })
2038 ]
2039 );
2040
2041 assert_eq!(trimming_status.trimmed_edges(), 4);
2043 });
2044 }
2045
2046 #[test]
2047 fn max_backers_edges_trims_lowest_stake() {
2048 use crate::mock::MaxBackersPerWinner;
2049
2050 ExtBuilder::default().build_and_execute(|| {
2051 let targets = vec![10, 20, 30, 40];
2052
2053 let voters = vec![
2054 (1, 100, bounded_vec![10, 20]),
2055 (2, 200, bounded_vec![10, 20, 30]),
2056 (3, 300, bounded_vec![10, 30]),
2057 (4, 400, bounded_vec![10, 30]),
2058 (5, 500, bounded_vec![10, 20, 30]),
2059 (6, 600, bounded_vec![10, 20, 30, 40]),
2060 ];
2061 let snapshot = RoundSnapshot { voters: voters.clone(), targets: targets.clone() };
2062 let (round, desired_targets) = (1, 4);
2063
2064 let max_backers_bound = u32::MAX;
2065 let trim_backers_bound = 2;
2066
2067 MaxBackersPerWinner::set(max_backers_bound);
2069 let (solution, score, _, trimming_status) =
2070 Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
2071 voters.clone(),
2072 targets.clone(),
2073 desired_targets,
2074 )
2075 .unwrap();
2076
2077 assert_eq!(trimming_status.trimmed_edges(), 0);
2078
2079 let ready_solution = Miner::<Runtime>::feasibility_check(
2080 RawSolution { solution, score, round },
2081 Default::default(),
2082 desired_targets,
2083 snapshot.clone(),
2084 round,
2085 Default::default(),
2086 )
2087 .unwrap();
2088
2089 let full_supports = ready_solution.supports.into_iter().collect::<Vec<_>>();
2090
2091 let expected_trimmed_supports = full_supports
2094 .into_iter()
2095 .filter(|(_, s)| s.voters.len() as u32 > trim_backers_bound)
2096 .map(|(t, s)| (t, s.voters.into_iter().min_by(|a, b| a.1.cmp(&b.1)).unwrap()))
2097 .collect::<Vec<_>>();
2098
2099 MaxBackersPerWinner::set(trim_backers_bound);
2101 let (solution, score, _, trimming_status) =
2102 Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
2103 voters.clone(),
2104 targets.clone(),
2105 desired_targets,
2106 )
2107 .unwrap();
2108
2109 assert_eq!(trimming_status.trimmed_edges(), 2);
2110
2111 let ready_solution = Miner::<Runtime>::feasibility_check(
2112 RawSolution { solution, score, round },
2113 Default::default(),
2114 desired_targets,
2115 snapshot.clone(),
2116 round,
2117 Default::default(),
2118 )
2119 .unwrap();
2120
2121 let trimmed_supports = ready_solution.supports.into_iter().collect::<Vec<_>>();
2122
2123 let mut trimmed_supports_edges_full = vec![];
2125 for (t, s) in trimmed_supports {
2126 for v in s.voters {
2127 trimmed_supports_edges_full.push((t, v));
2128 }
2129 }
2130
2131 for edge in trimmed_supports_edges_full {
2134 assert!(!expected_trimmed_supports.contains(&edge));
2135 }
2136 })
2137 }
2138
2139 #[test]
2140 fn trim_assignments_length_does_not_modify_when_short_enough() {
2141 ExtBuilder::default().build_and_execute(|| {
2142 roll_to_unsigned();
2143
2144 let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
2146 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2147 let encoded_len = solution.encoded_size() as u32;
2148 let solution_clone = solution.clone();
2149
2150 let trimmed_len = Miner::<Runtime>::trim_assignments_length(
2152 encoded_len,
2153 &mut assignments,
2154 encoded_size_of,
2155 )
2156 .unwrap();
2157
2158 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2160 assert_eq!(solution, solution_clone);
2161 assert_eq!(trimmed_len, 0);
2162 });
2163 }
2164
2165 #[test]
2166 fn trim_assignments_length_modifies_when_too_long() {
2167 ExtBuilder::default().build().execute_with(|| {
2168 roll_to_unsigned();
2169
2170 let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
2172 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2173 let encoded_len = solution.encoded_size();
2174 let solution_clone = solution.clone();
2175
2176 let trimmed_len = Miner::<Runtime>::trim_assignments_length(
2178 encoded_len as u32 - 1,
2179 &mut assignments,
2180 encoded_size_of,
2181 )
2182 .unwrap();
2183
2184 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2186 assert_ne!(solution, solution_clone);
2187 assert!(solution.encoded_size() < encoded_len);
2188 assert_eq!(trimmed_len, 1);
2189 });
2190 }
2191
2192 #[test]
2193 fn trim_assignments_length_trims_lowest_stake() {
2194 ExtBuilder::default().build().execute_with(|| {
2195 roll_to_unsigned();
2196
2197 let TrimHelpers { voters, mut assignments, encoded_size_of, voter_index } =
2199 trim_helpers();
2200 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2201 let encoded_len = solution.encoded_size() as u32;
2202 let count = assignments.len();
2203 let min_stake_voter = voters
2204 .iter()
2205 .map(|(id, weight, _)| (weight, id))
2206 .min()
2207 .and_then(|(_, id)| voter_index(id))
2208 .unwrap();
2209
2210 Miner::<Runtime>::trim_assignments_length(
2212 encoded_len - 1,
2213 &mut assignments,
2214 encoded_size_of,
2215 )
2216 .unwrap();
2217
2218 assert_eq!(assignments.len(), count - 1, "we must have removed exactly one assignment");
2220 assert!(
2221 assignments.iter().all(|IndexAssignment { who, .. }| *who != min_stake_voter),
2222 "min_stake_voter must no longer be in the set of voters",
2223 );
2224 });
2225 }
2226
2227 #[test]
2228 fn trim_assignments_length_wont_panic() {
2229 ExtBuilder::default().build_and_execute(|| {
2231 let encoded_size_of = Box::new(|assignments: &[IndexAssignmentOf<Runtime>]| {
2232 SolutionOf::<Runtime>::try_from(assignments).map(|solution| solution.encoded_size())
2233 });
2234
2235 let mut assignments = vec![];
2236
2237 let min_solution_size = encoded_size_of(&assignments).unwrap();
2240 assert_eq!(min_solution_size, SolutionOf::<Runtime>::LIMIT);
2241
2242 Miner::<Runtime>::trim_assignments_length(0, &mut assignments, encoded_size_of.clone())
2244 .unwrap();
2245 Miner::<Runtime>::trim_assignments_length(1, &mut assignments, encoded_size_of.clone())
2246 .unwrap();
2247 Miner::<Runtime>::trim_assignments_length(
2248 min_solution_size as u32,
2249 &mut assignments,
2250 encoded_size_of,
2251 )
2252 .unwrap();
2253 });
2254
2255 ExtBuilder::default().build_and_execute(|| {
2257 roll_to_unsigned();
2259 let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
2260 assert!(assignments.len() > 0);
2261
2262 let min_solution_size = SolutionOf::<Runtime>::LIMIT as u32;
2264 Miner::<Runtime>::trim_assignments_length(
2265 min_solution_size,
2266 &mut assignments,
2267 encoded_size_of,
2268 )
2269 .unwrap();
2270 assert_eq!(assignments.len(), 0);
2271 });
2272 }
2273
2274 #[test]
2278 fn mine_solution_solutions_always_within_acceptable_length() {
2279 ExtBuilder::default().build_and_execute(|| {
2280 roll_to_unsigned();
2281
2282 let solution = MultiPhase::mine_solution().unwrap();
2284 let max_length = <Runtime as MinerConfig>::MaxLength::get();
2285 let solution_size = solution.0.solution.encoded_size();
2286 assert!(solution_size <= max_length as usize);
2287
2288 <Runtime as MinerConfig>::MaxLength::set(solution_size as u32 - 1);
2290 let solution = MultiPhase::mine_solution().unwrap();
2291 let max_length = <Runtime as MinerConfig>::MaxLength::get();
2292 let solution_size = solution.0.solution.encoded_size();
2293 assert!(solution_size <= max_length as usize);
2294 });
2295 }
2296}