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 Err(MutateStorageError::ValueFunctionFailed(_)) => {
155 Err(MinerError::FailedToStoreSolution)
160 },
161 }
162}
163
164fn restore_solution<T: Config>() -> Result<Call<T>, MinerError> {
166 StorageValueRef::persistent(OFFCHAIN_CACHED_CALL)
167 .get()
168 .ok()
169 .flatten()
170 .ok_or(MinerError::NoStoredSolution)
171}
172
173pub(super) fn kill_ocw_solution<T: Config>() {
175 log!(debug, "clearing offchain call cache storage.");
176 let mut storage = StorageValueRef::persistent(OFFCHAIN_CACHED_CALL);
177 storage.clear();
178}
179
180fn clear_offchain_repeat_frequency() {
185 let mut last_block = StorageValueRef::persistent(OFFCHAIN_LAST_BLOCK);
186 last_block.clear();
187}
188
189#[cfg(test)]
191fn ocw_solution_exists<T: Config>() -> bool {
192 matches!(StorageValueRef::persistent(OFFCHAIN_CACHED_CALL).get::<Call<T>>(), Ok(Some(_)))
193}
194
195impl<T: Config + CreateBare<Call<T>>> Pallet<T> {
196 pub fn mine_solution() -> Result<
201 (RawSolution<SolutionOf<T::MinerConfig>>, SolutionOrSnapshotSize, TrimmingStatus),
202 MinerError,
203 > {
204 let RoundSnapshot { voters, targets } =
205 Snapshot::<T>::get().ok_or(MinerError::SnapshotUnAvailable)?;
206 let desired_targets = DesiredTargets::<T>::get().ok_or(MinerError::SnapshotUnAvailable)?;
207 ensure!(desired_targets <= T::MaxWinners::get(), MinerError::TooManyDesiredTargets);
208 let (solution, score, size, is_trimmed) =
209 Miner::<T::MinerConfig>::mine_solution_with_snapshot::<T::Solver>(
210 voters,
211 targets,
212 desired_targets,
213 )?;
214 let round = Round::<T>::get();
215 Ok((RawSolution { solution, score, round }, size, is_trimmed))
216 }
217
218 pub fn restore_or_compute_then_maybe_submit() -> Result<(), MinerError> {
221 log!(debug, "miner attempting to restore or compute an unsigned solution.");
222
223 let call = restore_solution::<T>()
224 .and_then(|call| {
225 if let Call::submit_unsigned { raw_solution, .. } = &call {
227 Self::basic_checks(raw_solution, "restored")?;
229 Ok(call)
230 } else {
231 Err(MinerError::SolutionCallInvalid)
232 }
233 })
234 .or_else::<MinerError, _>(|error| {
235 log!(debug, "restoring solution failed due to {:?}", error);
236 match error {
237 MinerError::NoStoredSolution => {
238 log!(trace, "mining a new solution.");
239 let call = Self::mine_checked_call()?;
243 save_solution(&call)?;
244 Ok(call)
245 },
246 MinerError::Feasibility(_) => {
247 log!(trace, "wiping infeasible solution.");
248 kill_ocw_solution::<T>();
251 clear_offchain_repeat_frequency();
252 Err(error)
253 },
254 _ => {
255 Err(error)
257 },
258 }
259 })?;
260
261 Self::submit_call(call)
262 }
263
264 pub fn mine_check_save_submit() -> Result<(), MinerError> {
266 log!(debug, "miner attempting to compute an unsigned solution.");
267
268 let call = Self::mine_checked_call()?;
269 save_solution(&call)?;
270 Self::submit_call(call)
271 }
272
273 pub fn mine_checked_call() -> Result<Call<T>, MinerError> {
275 let (raw_solution, witness, _trimming) = Self::mine_and_check()?;
277
278 let score = raw_solution.score;
279 let call: Call<T> = Call::submit_unsigned { raw_solution: Box::new(raw_solution), witness };
280
281 log!(
282 debug,
283 "mined a solution with score {:?} and size {} and trimming {:?}",
284 score,
285 call.using_encoded(|b| b.len()),
286 _trimming
287 );
288
289 Ok(call)
290 }
291
292 fn submit_call(call: Call<T>) -> Result<(), MinerError> {
293 log!(debug, "miner submitting a solution as an unsigned transaction");
294
295 let xt = T::create_bare(call.into());
296 SubmitTransaction::<T, Call<T>>::submit_transaction(xt)
297 .map_err(|_| MinerError::PoolSubmissionFailed)
298 }
299
300 pub fn basic_checks(
304 raw_solution: &RawSolution<SolutionOf<T::MinerConfig>>,
305 solution_type: &str,
306 ) -> Result<(), MinerError> {
307 Self::unsigned_pre_dispatch_checks(raw_solution).map_err(|err| {
308 log!(debug, "pre-dispatch checks failed for {} solution: {:?}", solution_type, err);
309 MinerError::PreDispatchChecksFailed(err)
310 })?;
311
312 Self::feasibility_check(raw_solution.clone(), ElectionCompute::Unsigned).map_err(
313 |err| {
314 log!(debug, "feasibility check failed for {} solution: {:?}", solution_type, err);
315 err
316 },
317 )?;
318
319 Ok(())
320 }
321
322 pub fn mine_and_check() -> Result<
329 (RawSolution<SolutionOf<T::MinerConfig>>, SolutionOrSnapshotSize, TrimmingStatus),
330 MinerError,
331 > {
332 let (raw_solution, witness, is_trimmed) = Self::mine_solution()?;
333 Self::basic_checks(&raw_solution, "mined")?;
334 Ok((raw_solution, witness, is_trimmed))
335 }
336
337 pub fn ensure_offchain_repeat_frequency(now: BlockNumberFor<T>) -> Result<(), MinerError> {
347 let threshold = T::OffchainRepeat::get();
348 let last_block = StorageValueRef::persistent(OFFCHAIN_LAST_BLOCK);
349
350 let mutate_stat = last_block.mutate::<_, &'static str, _>(
351 |maybe_head: Result<Option<BlockNumberFor<T>>, _>| {
352 match maybe_head {
353 Ok(Some(head)) if now < head => Err("fork."),
354 Ok(Some(head)) if now >= head && now <= head + threshold =>
355 Err("recently executed."),
356 Ok(Some(head)) if now > head + threshold => {
357 Ok(now)
359 },
360 _ => {
361 Ok(now)
363 },
364 }
365 },
366 );
367
368 match mutate_stat {
369 Ok(_) => Ok(()),
371 Err(MutateStorageError::ConcurrentModification(_)) =>
373 Err(MinerError::Lock("failed to write to offchain db (concurrent modification).")),
374 Err(MutateStorageError::ValueFunctionFailed(why)) => Err(MinerError::Lock(why)),
376 }
377 }
378
379 pub fn unsigned_pre_dispatch_checks(
387 raw_solution: &RawSolution<SolutionOf<T::MinerConfig>>,
388 ) -> DispatchResult {
389 ensure!(
391 CurrentPhase::<T>::get().is_unsigned_open(),
392 Error::<T>::PreDispatchEarlySubmission
393 );
394
395 ensure!(Round::<T>::get() == raw_solution.round, Error::<T>::OcwCallWrongEra);
397
398 ensure!(
400 DesiredTargets::<T>::get().unwrap_or_default() ==
401 raw_solution.solution.unique_targets().len() as u32,
402 Error::<T>::PreDispatchWrongWinnerCount,
403 );
404
405 ensure!(
407 QueuedSolution::<T>::get()
408 .map_or(true, |q: ReadySolution<_, _, _>| raw_solution.score > q.score),
409 Error::<T>::PreDispatchWeakSubmission,
410 );
411
412 Ok(())
413 }
414}
415
416pub trait MinerConfig {
418 type AccountId: Ord + Clone + codec::Codec + core::fmt::Debug;
420 type Solution: codec::Codec
422 + Default
423 + PartialEq
424 + Eq
425 + Clone
426 + core::fmt::Debug
427 + Ord
428 + NposSolution
429 + TypeInfo;
430 type MaxVotesPerVoter;
432 type MaxLength: Get<u32>;
436 type MaxWeight: Get<Weight>;
442 type MaxWinners: Get<u32>;
445 type MaxBackersPerWinner: Get<u32>;
447 fn solution_weight(voters: u32, targets: u32, active_voters: u32, degree: u32) -> Weight;
451}
452
453pub struct Miner<T: MinerConfig>(core::marker::PhantomData<T>);
455impl<T: MinerConfig> Miner<T> {
456 pub fn mine_solution_with_snapshot<S>(
458 voters: Vec<(T::AccountId, VoteWeight, BoundedVec<T::AccountId, T::MaxVotesPerVoter>)>,
459 targets: Vec<T::AccountId>,
460 desired_targets: u32,
461 ) -> Result<(SolutionOf<T>, ElectionScore, SolutionOrSnapshotSize, TrimmingStatus), MinerError>
462 where
463 S: NposSolver<AccountId = T::AccountId>,
464 {
465 S::solve(desired_targets as usize, targets.clone(), voters.clone())
466 .map_err(|e| {
467 log_no_system!(error, "solver error: {:?}", e);
468 MinerError::Solver
469 })
470 .and_then(|e| {
471 Self::prepare_election_result_with_snapshot::<S::Accuracy>(
472 e,
473 voters,
474 targets,
475 desired_targets,
476 )
477 })
478 }
479
480 pub fn prepare_election_result_with_snapshot<Accuracy: PerThing128>(
485 election_result: ElectionResult<T::AccountId, Accuracy>,
486 voters: Vec<(T::AccountId, VoteWeight, BoundedVec<T::AccountId, T::MaxVotesPerVoter>)>,
487 targets: Vec<T::AccountId>,
488 desired_targets: u32,
489 ) -> Result<(SolutionOf<T>, ElectionScore, SolutionOrSnapshotSize, TrimmingStatus), MinerError>
490 {
491 let cache = helpers::generate_voter_cache::<T>(&voters);
493 let voter_index = helpers::voter_index_fn::<T>(&cache);
494 let target_index = helpers::target_index_fn::<T>(&targets);
495 let voter_at = helpers::voter_at_fn::<T>(&voters);
496 let target_at = helpers::target_at_fn::<T>(&targets);
497 let stake_of = helpers::stake_of_fn::<T>(&voters, &cache);
498
499 let encoded_size_of = |assignments: &[IndexAssignmentOf<T>]| {
503 SolutionOf::<T>::try_from(assignments).map(|s| s.encoded_size())
504 };
505
506 let ElectionResult { assignments, winners: _ } = election_result;
507
508 let mut edges_trimmed = 0;
510
511 let sorted_assignments = {
514 let mut staked = assignment_ratio_to_staked_normalized(assignments, &stake_of)?;
516
517 sp_npos_elections::reduce(&mut staked);
520
521 staked.sort_by_key(
524 |sp_npos_elections::StakedAssignment::<T::AccountId> { who, .. }| {
525 let stake = cache
529 .get(who)
530 .map(|idx| {
531 let (_, stake, _) = voters[*idx];
532 stake
533 })
534 .unwrap_or_default();
535 core::cmp::Reverse(stake)
536 },
537 );
538
539 let max_backers_per_winner = T::MaxBackersPerWinner::get().saturated_into::<usize>();
548
549 let _ = sp_npos_elections::to_supports(&staked)
550 .iter_mut()
551 .filter(|(_, support)| support.voters.len() > max_backers_per_winner)
552 .for_each(|(target, ref mut support)| {
553 support.voters.sort_by(|a, b| b.1.cmp(&a.1));
555
556 let filtered: alloc::collections::BTreeSet<_> = support
560 .voters
561 .split_off(max_backers_per_winner)
562 .into_iter()
563 .map(|(who, _stake)| who)
564 .collect();
565
566 staked.iter_mut().for_each(|assignment| {
568 if filtered.contains(&assignment.who) {
569 assignment.distribution.retain(|(t, _)| t != target);
570 }
571 });
572
573 edges_trimmed += filtered.len();
574 });
575
576 debug_assert!({
577 let expected_ok: Result<
580 crate::BoundedSupports<_, T::MaxWinners, T::MaxBackersPerWinner>,
581 _,
582 > = sp_npos_elections::to_supports(&staked).try_into();
583 expected_ok.is_ok()
584 });
585
586 assignment_staked_to_ratio_normalized(staked)?
588 };
589
590 let mut index_assignments = sorted_assignments
593 .into_iter()
594 .map(|assignment| IndexAssignmentOf::<T>::new(&assignment, &voter_index, &target_index))
595 .collect::<Result<Vec<_>, _>>()?;
596
597 let size =
599 SolutionOrSnapshotSize { voters: voters.len() as u32, targets: targets.len() as u32 };
600 let weight_trimmed = Self::trim_assignments_weight(
601 desired_targets,
602 size,
603 T::MaxWeight::get(),
604 &mut index_assignments,
605 );
606 let length_trimmed = Self::trim_assignments_length(
607 T::MaxLength::get(),
608 &mut index_assignments,
609 &encoded_size_of,
610 )?;
611
612 let solution = SolutionOf::<T>::try_from(&index_assignments)?;
614
615 let score = solution.clone().score(stake_of, voter_at, target_at)?;
617
618 let is_trimmed =
619 TrimmingStatus { weight: weight_trimmed, length: length_trimmed, edges: edges_trimmed };
620
621 log_no_system!(
622 debug,
623 "feasible solution mined: trimmed? {:?}, score: {:?}, encoded size: {:?}",
624 is_trimmed,
625 score,
626 solution.encoded_size()
627 );
628 Ok((solution, score, size, is_trimmed))
629 }
630
631 pub fn trim_assignments_length(
646 max_allowed_length: u32,
647 assignments: &mut Vec<IndexAssignmentOf<T>>,
648 encoded_size_of: impl Fn(&[IndexAssignmentOf<T>]) -> Result<usize, sp_npos_elections::Error>,
649 ) -> Result<usize, MinerError> {
650 let max_allowed_length: usize = max_allowed_length.saturated_into();
653 let mut high = assignments.len();
654 let mut low = 0;
655
656 if high == low {
658 return Ok(0)
659 }
660
661 while high - low > 1 {
662 let test = (high + low) / 2;
663 if encoded_size_of(&assignments[..test])? <= max_allowed_length {
664 low = test;
665 } else {
666 high = test;
667 }
668 }
669 let maximum_allowed_voters = if low < assignments.len() &&
670 encoded_size_of(&assignments[..low + 1])? <= max_allowed_length
671 {
672 low + 1
673 } else {
674 low
675 };
676
677 debug_assert!(
679 encoded_size_of(&assignments[..maximum_allowed_voters]).unwrap() <= max_allowed_length
680 );
681 debug_assert!(if maximum_allowed_voters < assignments.len() {
682 encoded_size_of(&assignments[..maximum_allowed_voters + 1]).unwrap() >
683 max_allowed_length
684 } else {
685 true
686 });
687
688 let remove = assignments.len().saturating_sub(maximum_allowed_voters);
693
694 log_no_system!(
695 trace,
696 "from {} assignments, truncating to {} for length, removing {}",
697 assignments.len(),
698 maximum_allowed_voters,
699 remove
700 );
701 assignments.truncate(maximum_allowed_voters);
702
703 Ok(remove)
704 }
705
706 pub fn trim_assignments_weight(
722 desired_targets: u32,
723 size: SolutionOrSnapshotSize,
724 max_weight: Weight,
725 assignments: &mut Vec<IndexAssignmentOf<T>>,
726 ) -> usize {
727 let maximum_allowed_voters =
728 Self::maximum_voter_for_weight(desired_targets, size, max_weight);
729 let removing: usize =
730 assignments.len().saturating_sub(maximum_allowed_voters.saturated_into());
731 log_no_system!(
732 debug,
733 "from {} assignments, truncating to {} for weight, removing {}",
734 assignments.len(),
735 maximum_allowed_voters,
736 removing,
737 );
738 assignments.truncate(maximum_allowed_voters as usize);
739
740 removing
741 }
742
743 pub fn maximum_voter_for_weight(
747 desired_winners: u32,
748 size: SolutionOrSnapshotSize,
749 max_weight: Weight,
750 ) -> u32 {
751 if size.voters < 1 {
752 return size.voters
753 }
754
755 let max_voters = size.voters.max(1);
756 let mut voters = max_voters;
757
758 let weight_with = |active_voters: u32| -> Weight {
760 T::solution_weight(size.voters, size.targets, active_voters, desired_winners)
761 };
762
763 let next_voters = |current_weight: Weight, voters: u32, step: u32| -> Result<u32, ()> {
764 if current_weight.all_lt(max_weight) {
765 let next_voters = voters.checked_add(step);
766 match next_voters {
767 Some(voters) if voters < max_voters => Ok(voters),
768 _ => Err(()),
769 }
770 } else if current_weight.any_gt(max_weight) {
771 voters.checked_sub(step).ok_or(())
772 } else {
773 Ok(voters)
775 }
776 };
777
778 let mut step = voters / 2;
780 let mut current_weight = weight_with(voters);
781
782 while step > 0 {
783 match next_voters(current_weight, voters, step) {
784 Ok(next) if next != voters => {
786 voters = next;
787 },
788 Err(()) => break,
790 Ok(next) => return next,
792 }
793 step /= 2;
794 current_weight = weight_with(voters);
795 }
796
797 while voters < max_voters && weight_with(voters + 1).all_lt(max_weight) {
800 voters += 1;
801 }
802 while voters.checked_sub(1).is_some() && weight_with(voters).any_gt(max_weight) {
803 voters -= 1;
804 }
805
806 let final_decision = voters.min(size.voters);
807 debug_assert!(
808 weight_with(final_decision).all_lte(max_weight),
809 "weight_with({}) <= {}",
810 final_decision,
811 max_weight,
812 );
813 final_decision
814 }
815
816 pub fn feasibility_check(
818 raw_solution: RawSolution<SolutionOf<T>>,
819 compute: ElectionCompute,
820 desired_targets: u32,
821 snapshot: RoundSnapshot<T::AccountId, MinerVoterOf<T>>,
822 current_round: u32,
823 minimum_untrusted_score: Option<ElectionScore>,
824 ) -> Result<ReadySolutionOf<T>, FeasibilityError> {
825 let RawSolution { solution, score, round } = raw_solution;
826 let RoundSnapshot { voters: snapshot_voters, targets: snapshot_targets } = snapshot;
827
828 ensure!(current_round == round, FeasibilityError::InvalidRound);
830
831 let winners = solution.unique_targets();
833
834 ensure!(winners.len() as u32 == desired_targets, FeasibilityError::WrongWinnerCount);
835 ensure!(desired_targets <= T::MaxWinners::get(), FeasibilityError::TooManyDesiredTargets);
837
838 let submitted_score = raw_solution.score;
840 ensure!(
841 minimum_untrusted_score
842 .map_or(true, |min_score| { submitted_score.strict_better(min_score) }),
843 FeasibilityError::UntrustedScoreTooLow
844 );
845
846 let cache = helpers::generate_voter_cache::<T>(&snapshot_voters);
848 let voter_at = helpers::voter_at_fn::<T>(&snapshot_voters);
849 let target_at = helpers::target_at_fn::<T>(&snapshot_targets);
850 let voter_index = helpers::voter_index_fn_usize::<T>(&cache);
851
852 let assignments = solution
855 .into_assignment(voter_at, target_at)
856 .map_err::<FeasibilityError, _>(Into::into)?;
857
858 assignments.iter().try_for_each(|assignment| {
860 let snapshot_index =
868 voter_index(&assignment.who).ok_or(FeasibilityError::InvalidVoter)?;
869 let (_voter, _stake, targets) =
871 snapshot_voters.get(snapshot_index).ok_or(FeasibilityError::InvalidVoter)?;
872
873 if assignment.distribution.iter().any(|(d, _)| !targets.contains(d)) {
875 return Err(FeasibilityError::InvalidVote)
876 }
877 Ok(())
878 })?;
879
880 let stake_of = helpers::stake_of_fn::<T>(&snapshot_voters, &cache);
882
883 let staked_assignments = assignment_ratio_to_staked_normalized(assignments, stake_of)
885 .map_err::<FeasibilityError, _>(Into::into)?;
886 let supports = sp_npos_elections::to_supports(&staked_assignments);
887
888 let known_score = supports.evaluate();
890
891 ensure!(known_score == score, FeasibilityError::InvalidScore);
892
893 let supports = supports
897 .try_into()
898 .defensive_map_err(|_| FeasibilityError::BoundedConversionFailed)?;
899
900 Ok(ReadySolution { supports, compute, score })
901 }
902}
903
904#[cfg(test)]
905mod max_weight {
906 #![allow(unused_variables)]
907 use super::*;
908 use crate::mock::{MockWeightInfo, Runtime};
909 #[test]
910 fn find_max_voter_binary_search_works() {
911 let w = SolutionOrSnapshotSize { voters: 10, targets: 0 };
912 MockWeightInfo::set(crate::mock::MockedWeightInfo::Complex);
913 assert_eq!(
914 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
915 0
916 );
917 assert_eq!(
918 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
919 0
920 );
921 assert_eq!(
922 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
923 0
924 );
925 assert_eq!(
926 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
927 1
928 );
929 assert_eq!(
930 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
931 1
932 );
933 assert_eq!(
934 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1990, u64::MAX)),
935 1
936 );
937 assert_eq!(
938 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
939 1
940 );
941 assert_eq!(
942 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
943 2
944 );
945 assert_eq!(
946 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
947 2
948 );
949 assert_eq!(
950 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
951 2
952 );
953 assert_eq!(
954 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2990, u64::MAX)),
955 2
956 );
957 assert_eq!(
958 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2999, u64::MAX)),
959 2
960 );
961 assert_eq!(
962 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3000, u64::MAX)),
963 3
964 );
965 assert_eq!(
966 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
967 3
968 );
969 assert_eq!(
970 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(5500, u64::MAX)),
971 5
972 );
973 assert_eq!(
974 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(7777, u64::MAX)),
975 7
976 );
977 assert_eq!(
978 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(9999, u64::MAX)),
979 9
980 );
981 assert_eq!(
982 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(10_000, u64::MAX)),
983 10
984 );
985 assert_eq!(
986 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(10_999, u64::MAX)),
987 10
988 );
989 assert_eq!(
990 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(11_000, u64::MAX)),
991 10
992 );
993 assert_eq!(
994 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(22_000, u64::MAX)),
995 10
996 );
997
998 let w = SolutionOrSnapshotSize { voters: 1, targets: 0 };
999
1000 assert_eq!(
1001 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
1002 0
1003 );
1004 assert_eq!(
1005 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
1006 0
1007 );
1008 assert_eq!(
1009 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
1010 0
1011 );
1012 assert_eq!(
1013 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
1014 1
1015 );
1016 assert_eq!(
1017 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
1018 1
1019 );
1020 assert_eq!(
1021 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1990, u64::MAX)),
1022 1
1023 );
1024 assert_eq!(
1025 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
1026 1
1027 );
1028 assert_eq!(
1029 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
1030 1
1031 );
1032 assert_eq!(
1033 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
1034 1
1035 );
1036 assert_eq!(
1037 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
1038 1
1039 );
1040 assert_eq!(
1041 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
1042 1
1043 );
1044
1045 let w = SolutionOrSnapshotSize { voters: 2, targets: 0 };
1046
1047 assert_eq!(
1048 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
1049 0
1050 );
1051 assert_eq!(
1052 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
1053 0
1054 );
1055 assert_eq!(
1056 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
1057 0
1058 );
1059 assert_eq!(
1060 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
1061 1
1062 );
1063 assert_eq!(
1064 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
1065 1
1066 );
1067 assert_eq!(
1068 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
1069 1
1070 );
1071 assert_eq!(
1072 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
1073 2
1074 );
1075 assert_eq!(
1076 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
1077 2
1078 );
1079 assert_eq!(
1080 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
1081 2
1082 );
1083 assert_eq!(
1084 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
1085 2
1086 );
1087 }
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092 use super::*;
1093 use crate::{
1094 mock::{
1095 multi_phase_events, roll_to, roll_to_signed, roll_to_unsigned, roll_to_with_ocw,
1096 trim_helpers, witness, BlockNumber, ExtBuilder, Extrinsic, MinerMaxWeight, MultiPhase,
1097 Runtime, RuntimeCall, RuntimeOrigin, System, TestNposSolution, TrimHelpers,
1098 UnsignedPhase,
1099 },
1100 Event, InvalidTransaction, Phase, QueuedSolution, TransactionSource,
1101 TransactionValidityError,
1102 };
1103 use alloc::vec;
1104 use codec::Decode;
1105 use frame_election_provider_support::IndexAssignment;
1106 use frame_support::{assert_noop, assert_ok, traits::OffchainWorker};
1107 use sp_npos_elections::ElectionScore;
1108 use sp_runtime::{
1109 bounded_vec,
1110 offchain::storage_lock::{BlockAndTime, StorageLock},
1111 traits::{Dispatchable, ValidateUnsigned, Zero},
1112 ModuleError, PerU16,
1113 };
1114
1115 type Assignment = crate::unsigned::Assignment<Runtime>;
1116
1117 #[test]
1118 fn validate_unsigned_retracts_wrong_phase() {
1119 ExtBuilder::default().desired_targets(0).build_and_execute(|| {
1120 let solution = RawSolution::<TestNposSolution> {
1121 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1122 ..Default::default()
1123 };
1124 let call = Call::submit_unsigned {
1125 raw_solution: Box::new(solution.clone()),
1126 witness: witness(),
1127 };
1128
1129 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
1131 assert!(matches!(
1132 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1133 TransactionSource::Local,
1134 &call
1135 )
1136 .unwrap_err(),
1137 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1138 ));
1139 assert!(matches!(
1140 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1141 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1142 ));
1143
1144 roll_to_signed();
1146 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
1147 assert!(matches!(
1148 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1149 TransactionSource::Local,
1150 &call
1151 )
1152 .unwrap_err(),
1153 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1154 ));
1155 assert!(matches!(
1156 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1157 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1158 ));
1159
1160 roll_to_unsigned();
1162 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1163
1164 assert!(<MultiPhase as ValidateUnsigned>::validate_unsigned(
1165 TransactionSource::Local,
1166 &call
1167 )
1168 .is_ok());
1169 assert!(<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).is_ok());
1170
1171 MultiPhase::phase_transition(Phase::Unsigned((false, 25)));
1173 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1174 assert!(matches!(
1175 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1176 TransactionSource::Local,
1177 &call
1178 )
1179 .unwrap_err(),
1180 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1181 ));
1182 assert!(matches!(
1183 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1184 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1185 ));
1186 })
1187 }
1188
1189 #[test]
1190 fn validate_unsigned_retracts_low_score() {
1191 ExtBuilder::default().desired_targets(0).build_and_execute(|| {
1192 roll_to_unsigned();
1193 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1194
1195 let solution = RawSolution::<TestNposSolution> {
1196 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1197 ..Default::default()
1198 };
1199 let call = Call::submit_unsigned {
1200 raw_solution: Box::new(solution.clone()),
1201 witness: witness(),
1202 };
1203
1204 assert!(<MultiPhase as ValidateUnsigned>::validate_unsigned(
1206 TransactionSource::Local,
1207 &call
1208 )
1209 .is_ok());
1210 assert!(<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).is_ok());
1211
1212 let ready = ReadySolution {
1214 score: ElectionScore { minimal_stake: 10, ..Default::default() },
1215 ..Default::default()
1216 };
1217 QueuedSolution::<Runtime>::put(ready);
1218
1219 assert!(matches!(
1221 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1222 TransactionSource::Local,
1223 &call
1224 )
1225 .unwrap_err(),
1226 TransactionValidityError::Invalid(InvalidTransaction::Custom(2))
1227 ));
1228 assert!(matches!(
1229 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1230 TransactionValidityError::Invalid(InvalidTransaction::Custom(2))
1231 ));
1232 })
1233 }
1234
1235 #[test]
1236 fn validate_unsigned_retracts_incorrect_winner_count() {
1237 ExtBuilder::default().desired_targets(1).build_and_execute(|| {
1238 roll_to_unsigned();
1239 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1240
1241 let raw = RawSolution::<TestNposSolution> {
1242 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1243 ..Default::default()
1244 };
1245 let call =
1246 Call::submit_unsigned { raw_solution: Box::new(raw.clone()), witness: witness() };
1247 assert_eq!(raw.solution.unique_targets().len(), 0);
1248
1249 assert!(matches!(
1251 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1252 TransactionSource::Local,
1253 &call
1254 )
1255 .unwrap_err(),
1256 TransactionValidityError::Invalid(InvalidTransaction::Custom(1))
1257 ));
1258 })
1259 }
1260
1261 #[test]
1262 fn priority_is_set() {
1263 ExtBuilder::default()
1264 .miner_tx_priority(20)
1265 .desired_targets(0)
1266 .build_and_execute(|| {
1267 roll_to_unsigned();
1268 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1269
1270 let solution = RawSolution::<TestNposSolution> {
1271 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1272 ..Default::default()
1273 };
1274 let call = Call::submit_unsigned {
1275 raw_solution: Box::new(solution.clone()),
1276 witness: witness(),
1277 };
1278
1279 assert_eq!(
1280 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1281 TransactionSource::Local,
1282 &call
1283 )
1284 .unwrap()
1285 .priority,
1286 25
1287 );
1288 })
1289 }
1290
1291 #[test]
1292 #[should_panic(expected = "Invalid unsigned submission must produce invalid block and \
1293 deprive validator from their authoring reward.: \
1294 Module(ModuleError { index: 2, error: [1, 0, 0, 0], message: \
1295 Some(\"PreDispatchWrongWinnerCount\") })")]
1296 fn unfeasible_solution_panics() {
1297 ExtBuilder::default().build_and_execute(|| {
1298 roll_to_unsigned();
1299 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1300
1301 let solution = RawSolution::<TestNposSolution> {
1303 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1304 ..Default::default()
1305 };
1306 let call = Call::submit_unsigned {
1307 raw_solution: Box::new(solution.clone()),
1308 witness: witness(),
1309 };
1310 let runtime_call: RuntimeCall = call.into();
1311 let _ = runtime_call.dispatch(RuntimeOrigin::none());
1312 })
1313 }
1314
1315 #[test]
1316 #[should_panic(expected = "Invalid unsigned submission must produce invalid block and \
1317 deprive validator from their authoring reward.")]
1318 fn wrong_witness_panics() {
1319 ExtBuilder::default().build_and_execute(|| {
1320 roll_to_unsigned();
1321 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1322
1323 let solution = RawSolution::<TestNposSolution> {
1325 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1326 ..Default::default()
1327 };
1328
1329 let mut correct_witness = witness();
1330 correct_witness.voters += 1;
1331 correct_witness.targets -= 1;
1332 let call = Call::submit_unsigned {
1333 raw_solution: Box::new(solution.clone()),
1334 witness: correct_witness,
1335 };
1336 let runtime_call: RuntimeCall = call.into();
1337 let _ = runtime_call.dispatch(RuntimeOrigin::none());
1338 })
1339 }
1340
1341 #[test]
1342 fn miner_works() {
1343 ExtBuilder::default().build_and_execute(|| {
1344 roll_to_unsigned();
1345 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1346
1347 assert!(Snapshot::<Runtime>::get().is_some());
1349 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 2);
1350
1351 let (solution, witness, _) = MultiPhase::mine_solution().unwrap();
1353
1354 assert!(QueuedSolution::<Runtime>::get().is_none());
1356 assert_ok!(MultiPhase::submit_unsigned(
1357 RuntimeOrigin::none(),
1358 Box::new(solution),
1359 witness
1360 ));
1361 assert!(QueuedSolution::<Runtime>::get().is_some());
1362 assert_eq!(
1363 multi_phase_events(),
1364 vec![
1365 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1366 Event::PhaseTransitioned {
1367 from: Phase::Signed,
1368 to: Phase::Unsigned((true, 25)),
1369 round: 1
1370 },
1371 Event::SolutionStored {
1372 compute: ElectionCompute::Unsigned,
1373 origin: None,
1374 prev_ejected: false
1375 }
1376 ]
1377 );
1378 })
1379 }
1380
1381 #[test]
1382 fn miner_trims_weight() {
1383 ExtBuilder::default()
1384 .miner_weight(Weight::from_parts(100, u64::MAX))
1385 .mock_weight_info(crate::mock::MockedWeightInfo::Basic)
1386 .build_and_execute(|| {
1387 roll_to_unsigned();
1388 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1389
1390 let (raw, witness, t) = MultiPhase::mine_solution().unwrap();
1391 let solution_weight = <Runtime as MinerConfig>::solution_weight(
1392 witness.voters,
1393 witness.targets,
1394 raw.solution.voter_count() as u32,
1395 raw.solution.unique_targets().len() as u32,
1396 );
1397 assert_eq!(solution_weight, Weight::from_parts(35, 0));
1399 assert_eq!(raw.solution.voter_count(), 5);
1400 assert_eq!(t.trimmed_weight(), 0);
1401
1402 <MinerMaxWeight>::set(Weight::from_parts(25, u64::MAX));
1404
1405 let (raw, witness, t) = MultiPhase::mine_solution().unwrap();
1406 let solution_weight = <Runtime as MinerConfig>::solution_weight(
1407 witness.voters,
1408 witness.targets,
1409 raw.solution.voter_count() as u32,
1410 raw.solution.unique_targets().len() as u32,
1411 );
1412 assert_eq!(solution_weight, Weight::from_parts(25, 0));
1414 assert_eq!(raw.solution.voter_count(), 3);
1415 assert_eq!(t.trimmed_weight(), 2);
1416 })
1417 }
1418
1419 #[test]
1420 fn miner_will_not_submit_if_not_enough_winners() {
1421 let (mut ext, _) = ExtBuilder::default().desired_targets(8).build_offchainify(0);
1422 ext.execute_with(|| {
1423 roll_to_unsigned();
1424 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1425
1426 let (mut solution, _, _) = MultiPhase::mine_solution().unwrap();
1428 solution.solution.votes1[0].1 = 4;
1429
1430 assert_eq!(
1431 MultiPhase::basic_checks(&solution, "mined").unwrap_err(),
1432 MinerError::PreDispatchChecksFailed(DispatchError::Module(ModuleError {
1433 index: 2,
1434 error: [1, 0, 0, 0],
1435 message: Some("PreDispatchWrongWinnerCount"),
1436 })),
1437 );
1438 })
1439 }
1440
1441 #[test]
1442 fn unsigned_per_dispatch_checks_can_only_submit_threshold_better() {
1443 ExtBuilder::default()
1444 .desired_targets(1)
1445 .add_voter(7, 2, bounded_vec![10])
1446 .add_voter(8, 5, bounded_vec![10])
1447 .add_voter(9, 1, bounded_vec![10])
1448 .build_and_execute(|| {
1449 roll_to_unsigned();
1450 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1451 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 1);
1452
1453 let result = ElectionResult {
1455 winners: vec![(10, 12)],
1456 assignments: vec![
1457 Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1458 Assignment {
1459 who: 7,
1460 distribution: vec![(10, PerU16::one())],
1462 },
1463 ],
1464 };
1465
1466 let RoundSnapshot { voters, targets } = Snapshot::<Runtime>::get().unwrap();
1467 let desired_targets = DesiredTargets::<Runtime>::get().unwrap();
1468
1469 let (raw, score, witness, _) =
1470 Miner::<Runtime>::prepare_election_result_with_snapshot(
1471 result,
1472 voters.clone(),
1473 targets.clone(),
1474 desired_targets,
1475 )
1476 .unwrap();
1477 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1478 assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
1479 assert_ok!(MultiPhase::submit_unsigned(
1480 RuntimeOrigin::none(),
1481 Box::new(solution),
1482 witness
1483 ));
1484 assert_eq!(QueuedSolution::<Runtime>::get().unwrap().score.minimal_stake, 12);
1485
1486 let result = ElectionResult {
1489 winners: vec![(10, 10)],
1490 assignments: vec![Assignment {
1491 who: 10,
1492 distribution: vec![(10, PerU16::one())],
1493 }],
1494 };
1495 let (raw, score, _, _) = Miner::<Runtime>::prepare_election_result_with_snapshot(
1496 result,
1497 voters.clone(),
1498 targets.clone(),
1499 desired_targets,
1500 )
1501 .unwrap();
1502 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1503 assert_eq!(solution.score.minimal_stake, 10);
1505 assert_noop!(
1507 MultiPhase::unsigned_pre_dispatch_checks(&solution),
1508 Error::<Runtime>::PreDispatchWeakSubmission,
1509 );
1510
1511 let result = ElectionResult {
1514 winners: vec![(10, 12)],
1515 assignments: vec![
1516 Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1517 Assignment {
1518 who: 7,
1519 distribution: vec![(10, PerU16::one())],
1521 },
1522 ],
1523 };
1524
1525 let (raw, score, _, _) = Miner::<Runtime>::prepare_election_result_with_snapshot(
1526 result,
1527 voters.clone(),
1528 targets.clone(),
1529 desired_targets,
1530 )
1531 .unwrap();
1532 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1533 assert_eq!(solution.score.minimal_stake, 12);
1535 assert_noop!(
1537 MultiPhase::unsigned_pre_dispatch_checks(&solution),
1538 Error::<Runtime>::PreDispatchWeakSubmission,
1539 );
1540
1541 let result = ElectionResult {
1544 winners: vec![(10, 12)],
1545 assignments: vec![
1546 Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1547 Assignment { who: 7, distribution: vec![(10, PerU16::one())] },
1548 Assignment { who: 9, distribution: vec![(10, PerU16::one())] },
1549 ],
1550 };
1551 let (raw, score, witness, _) =
1552 Miner::<Runtime>::prepare_election_result_with_snapshot(
1553 result,
1554 voters.clone(),
1555 targets.clone(),
1556 desired_targets,
1557 )
1558 .unwrap();
1559 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1560 assert_eq!(solution.score.minimal_stake, 13);
1561
1562 assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
1564 assert_ok!(MultiPhase::submit_unsigned(
1565 RuntimeOrigin::none(),
1566 Box::new(solution),
1567 witness
1568 ));
1569
1570 let result = ElectionResult {
1573 winners: vec![(10, 12)],
1574 assignments: vec![
1575 Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1576 Assignment { who: 7, distribution: vec![(10, PerU16::one())] },
1577 Assignment {
1578 who: 8,
1579 distribution: vec![(10, PerU16::one())],
1581 },
1582 ],
1583 };
1584 let (raw, score, witness, _) =
1585 Miner::<Runtime>::prepare_election_result_with_snapshot(
1586 result,
1587 voters.clone(),
1588 targets.clone(),
1589 desired_targets,
1590 )
1591 .unwrap();
1592 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1593 assert_eq!(solution.score.minimal_stake, 17);
1594
1595 assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
1597 assert_ok!(MultiPhase::submit_unsigned(
1598 RuntimeOrigin::none(),
1599 Box::new(solution),
1600 witness
1601 ));
1602 })
1603 }
1604
1605 #[test]
1606 fn ocw_lock_prevents_frequent_execution() {
1607 let (mut ext, _) = ExtBuilder::default().build_offchainify(0);
1608 ext.execute_with(|| {
1609 let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1610
1611 roll_to_unsigned();
1612 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1613
1614 assert!(MultiPhase::ensure_offchain_repeat_frequency(25).is_ok());
1616
1617 assert_noop!(
1619 MultiPhase::ensure_offchain_repeat_frequency(26),
1620 MinerError::Lock("recently executed.")
1621 );
1622
1623 assert!(
1625 MultiPhase::ensure_offchain_repeat_frequency((26 + offchain_repeat).into()).is_ok()
1626 );
1627
1628 assert!(MultiPhase::ensure_offchain_repeat_frequency(
1630 (26 + offchain_repeat - 3).into()
1631 )
1632 .is_err());
1633 assert!(MultiPhase::ensure_offchain_repeat_frequency(
1634 (26 + offchain_repeat - 2).into()
1635 )
1636 .is_err());
1637 assert!(MultiPhase::ensure_offchain_repeat_frequency(
1638 (26 + offchain_repeat - 1).into()
1639 )
1640 .is_err());
1641 })
1642 }
1643
1644 #[test]
1645 fn ocw_lock_released_after_successful_execution() {
1646 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1648 ext.execute_with(|| {
1649 let guard = StorageValueRef::persistent(&OFFCHAIN_LOCK);
1650 let last_block = StorageValueRef::persistent(OFFCHAIN_LAST_BLOCK);
1651
1652 roll_to_unsigned();
1653 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1654
1655 assert!(guard.get::<bool>().unwrap().is_none());
1657
1658 MultiPhase::offchain_worker(25);
1660 assert_eq!(pool.read().transactions.len(), 1);
1661
1662 assert!(guard.get::<bool>().unwrap().is_none());
1664 assert_eq!(last_block.get::<BlockNumber>().unwrap(), Some(25));
1665 });
1666 }
1667
1668 #[test]
1669 fn ocw_lock_prevents_overlapping_execution() {
1670 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1672 ext.execute_with(|| {
1673 roll_to_unsigned();
1674 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1675
1676 let mut lock = StorageLock::<BlockAndTime<System>>::with_block_deadline(
1678 OFFCHAIN_LOCK,
1679 UnsignedPhase::get().saturated_into(),
1680 );
1681 let guard = lock.lock();
1682
1683 MultiPhase::offchain_worker(25);
1685 assert_eq!(pool.read().transactions.len(), 0);
1686 MultiPhase::offchain_worker(26);
1687 assert_eq!(pool.read().transactions.len(), 0);
1688
1689 drop(guard);
1690
1691 MultiPhase::offchain_worker(25);
1693 assert_eq!(pool.read().transactions.len(), 1);
1694 });
1695 }
1696
1697 #[test]
1698 fn ocw_only_runs_when_unsigned_open_now() {
1699 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1700 ext.execute_with(|| {
1701 roll_to_unsigned();
1702 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
1703
1704 let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
1707
1708 MultiPhase::offchain_worker(24);
1709 assert!(pool.read().transactions.len().is_zero());
1710 storage.clear();
1711
1712 MultiPhase::offchain_worker(25);
1714 assert_eq!(pool.read().transactions.len(), 1);
1715 pool.try_write().unwrap().transactions.clear();
1717
1718 MultiPhase::offchain_worker(26);
1720 assert!(pool.read().transactions.len().is_zero());
1721 })
1722 }
1723
1724 #[test]
1725 fn ocw_clears_cache_on_unsigned_phase_open() {
1726 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1727 ext.execute_with(|| {
1728 const BLOCK: u64 = 25;
1729 let block_plus = |delta: u64| BLOCK + delta;
1730 let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1731
1732 roll_to(BLOCK);
1733 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
1735
1736 assert!(
1737 !ocw_solution_exists::<Runtime>(),
1738 "no solution should be present before we mine one",
1739 );
1740
1741 MultiPhase::offchain_worker(BLOCK);
1743 assert!(
1744 ocw_solution_exists::<Runtime>(),
1745 "a solution must be cached after running the worker",
1746 );
1747
1748 let tx_cache_1 = pool.read().transactions[0].clone();
1750 pool.try_write().unwrap().transactions.clear();
1752
1753 let _ = MultiPhase::do_elect();
1756 MultiPhase::offchain_worker(block_plus(1));
1757 assert!(ocw_solution_exists::<Runtime>(), "elections does not clear the ocw cache");
1758
1759 MultiPhase::offchain_worker(block_plus(offchain_repeat + 1));
1761
1762 let tx_cache_2 = pool.read().transactions[0].clone();
1764 pool.try_write().unwrap().transactions.clear();
1766
1767 assert_eq!(tx_cache_1, tx_cache_2);
1769
1770 let current_block = block_plus(offchain_repeat * 2 + 2);
1771 MultiPhase::phase_transition(Phase::Unsigned((true, current_block)));
1773
1774 MultiPhase::offchain_worker(current_block);
1777 let tx_cache_3 = pool.read().transactions[0].clone();
1778
1779 assert_eq!(tx_cache_1, tx_cache_3);
1781 assert_eq!(
1782 multi_phase_events(),
1783 vec![
1784 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1785 Event::PhaseTransitioned {
1786 from: Phase::Signed,
1787 to: Phase::Unsigned((true, 25)),
1788 round: 1
1789 },
1790 Event::ElectionFinalized {
1791 compute: ElectionCompute::Fallback,
1792 score: ElectionScore {
1793 minimal_stake: 0,
1794 sum_stake: 0,
1795 sum_stake_squared: 0
1796 }
1797 },
1798 Event::PhaseTransitioned {
1799 from: Phase::Unsigned((true, 25)),
1800 to: Phase::Unsigned((true, 37)),
1801 round: 1
1802 },
1803 ]
1804 );
1805 })
1806 }
1807
1808 #[test]
1809 fn ocw_resubmits_after_offchain_repeat() {
1810 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1811 ext.execute_with(|| {
1812 const BLOCK: u64 = 25;
1813 let block_plus = |delta: i32| ((BLOCK as i32) + delta) as u64;
1814 let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1815
1816 roll_to(BLOCK);
1817 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
1818
1819 let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
1822
1823 MultiPhase::offchain_worker(block_plus(-1));
1824 assert!(pool.read().transactions.len().is_zero());
1825 storage.clear();
1826
1827 MultiPhase::offchain_worker(BLOCK);
1829 assert_eq!(pool.read().transactions.len(), 1);
1830 let tx_cache = pool.read().transactions[0].clone();
1831 pool.try_write().unwrap().transactions.clear();
1833
1834 MultiPhase::offchain_worker(block_plus(1 + offchain_repeat as i32));
1838 assert_eq!(pool.read().transactions.len(), 1);
1839
1840 let tx = &pool.read().transactions[0];
1842 assert_eq!(&tx_cache, tx);
1843 })
1844 }
1845
1846 #[test]
1847 fn ocw_regenerates_and_resubmits_after_offchain_repeat() {
1848 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1849 ext.execute_with(|| {
1850 const BLOCK: u64 = 25;
1851 let block_plus = |delta: i32| ((BLOCK as i32) + delta) as u64;
1852 let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1853
1854 roll_to(BLOCK);
1855 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
1856
1857 let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
1860
1861 MultiPhase::offchain_worker(block_plus(-1));
1862 assert!(pool.read().transactions.len().is_zero());
1863 storage.clear();
1864
1865 MultiPhase::offchain_worker(BLOCK);
1867 assert_eq!(pool.read().transactions.len(), 1);
1868 let tx_cache = pool.read().transactions[0].clone();
1869 pool.try_write().unwrap().transactions.clear();
1871
1872 let mut call_cache = StorageValueRef::persistent(&OFFCHAIN_CACHED_CALL);
1876 assert!(matches!(call_cache.get::<Call<Runtime>>(), Ok(Some(_call))));
1877 call_cache.clear();
1878
1879 MultiPhase::offchain_worker(block_plus(1 + offchain_repeat as i32));
1883 assert_eq!(pool.read().transactions.len(), 1);
1884
1885 let tx = &pool.read().transactions[0];
1887 assert_eq!(&tx_cache, tx);
1888 })
1889 }
1890
1891 #[test]
1892 fn ocw_can_submit_to_pool() {
1893 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1894 ext.execute_with(|| {
1895 roll_to_with_ocw(25);
1896 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
1897 let encoded = pool.read().transactions[0].clone();
1900 let extrinsic: Extrinsic = codec::Decode::decode(&mut &*encoded).unwrap();
1901 let call = extrinsic.function;
1902 assert!(matches!(call, RuntimeCall::MultiPhase(Call::submit_unsigned { .. })));
1903 })
1904 }
1905
1906 #[test]
1907 fn ocw_solution_must_have_correct_round() {
1908 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1909 ext.execute_with(|| {
1910 roll_to_with_ocw(25);
1911 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
1912 crate::Round::<Runtime>::mutate(|round| *round += 1);
1915
1916 let encoded = pool.read().transactions[0].clone();
1917 let extrinsic = Extrinsic::decode(&mut &*encoded).unwrap();
1918 let call = match extrinsic.function {
1919 RuntimeCall::MultiPhase(call @ Call::submit_unsigned { .. }) => call,
1920 _ => panic!("bad call: unexpected submission"),
1921 };
1922
1923 let pre_dispatch_check_error =
1925 TransactionValidityError::Invalid(InvalidTransaction::Custom(7));
1926 assert_eq!(
1927 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1928 TransactionSource::Local,
1929 &call,
1930 )
1931 .unwrap_err(),
1932 pre_dispatch_check_error,
1933 );
1934 assert_eq!(
1935 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1936 pre_dispatch_check_error,
1937 );
1938 })
1939 }
1940
1941 #[test]
1942 fn mine_solution_always_respects_max_backers_per_winner() {
1943 use crate::mock::MaxBackersPerWinner;
1944 use frame_election_provider_support::BoundedSupport;
1945
1946 let targets = vec![10, 20, 30, 40];
1947 let voters = vec![
1948 (1, 11, bounded_vec![10, 20, 30]),
1949 (2, 12, bounded_vec![10, 20, 30]),
1950 (3, 13, bounded_vec![10, 20, 30]),
1951 (4, 14, bounded_vec![10, 20, 30]),
1952 (5, 15, bounded_vec![10, 20, 40]),
1953 ];
1954 let snapshot = RoundSnapshot { voters: voters.clone(), targets: targets.clone() };
1955 let (round, desired_targets) = (1, 3);
1956
1957 ExtBuilder::default().max_backers_per_winner(u32::MAX).build_and_execute(|| {
1959 assert_eq!(MaxBackersPerWinner::get(), u32::MAX);
1960
1961 let (solution, expected_score_unbounded, _, trimming_status) =
1962 Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
1963 voters.clone(),
1964 targets.clone(),
1965 desired_targets,
1966 )
1967 .unwrap();
1968
1969 let ready_solution = Miner::<Runtime>::feasibility_check(
1970 RawSolution { solution, score: expected_score_unbounded, round },
1971 Default::default(),
1972 desired_targets,
1973 snapshot.clone(),
1974 round,
1975 Default::default(),
1976 )
1977 .unwrap();
1978
1979 assert_eq!(
1980 ready_solution.supports.into_iter().collect::<Vec<_>>(),
1981 vec![
1982 (
1983 10,
1984 BoundedSupport { total: 25, voters: bounded_vec![(1, 11), (4, 9), (5, 5)] }
1985 ),
1986 (20, BoundedSupport { total: 22, voters: bounded_vec![(2, 12), (5, 10)] }),
1987 (30, BoundedSupport { total: 18, voters: bounded_vec![(3, 13), (4, 5)] })
1988 ]
1989 );
1990
1991 assert_eq!(trimming_status.trimmed_edges(), 0);
1993 });
1994
1995 ExtBuilder::default().max_backers_per_winner(1).build_and_execute(|| {
1997 assert_eq!(MaxBackersPerWinner::get(), 1);
1998
1999 let (solution, expected_score_bounded, _, trimming_status) =
2000 Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
2001 voters,
2002 targets,
2003 desired_targets,
2004 )
2005 .unwrap();
2006
2007 let ready_solution = Miner::<Runtime>::feasibility_check(
2008 RawSolution { solution, score: expected_score_bounded, round },
2009 Default::default(),
2010 desired_targets,
2011 snapshot,
2012 round,
2013 Default::default(),
2014 )
2015 .unwrap();
2016
2017 for (_, supports) in ready_solution.supports.iter() {
2018 assert!((supports.voters.len() as u32) <= MaxBackersPerWinner::get());
2019 }
2020
2021 assert_eq!(
2022 ready_solution.supports.into_iter().collect::<Vec<_>>(),
2023 vec![
2024 (10, BoundedSupport { total: 11, voters: bounded_vec![(1, 11)] }),
2025 (20, BoundedSupport { total: 12, voters: bounded_vec![(2, 12)] }),
2026 (30, BoundedSupport { total: 13, voters: bounded_vec![(3, 13)] })
2027 ]
2028 );
2029
2030 assert_eq!(trimming_status.trimmed_edges(), 4);
2032 });
2033 }
2034
2035 #[test]
2036 fn max_backers_edges_trims_lowest_stake() {
2037 use crate::mock::MaxBackersPerWinner;
2038
2039 ExtBuilder::default().build_and_execute(|| {
2040 let targets = vec![10, 20, 30, 40];
2041
2042 let voters = vec![
2043 (1, 100, bounded_vec![10, 20]),
2044 (2, 200, bounded_vec![10, 20, 30]),
2045 (3, 300, bounded_vec![10, 30]),
2046 (4, 400, bounded_vec![10, 30]),
2047 (5, 500, bounded_vec![10, 20, 30]),
2048 (6, 600, bounded_vec![10, 20, 30, 40]),
2049 ];
2050 let snapshot = RoundSnapshot { voters: voters.clone(), targets: targets.clone() };
2051 let (round, desired_targets) = (1, 4);
2052
2053 let max_backers_bound = u32::MAX;
2054 let trim_backers_bound = 2;
2055
2056 MaxBackersPerWinner::set(max_backers_bound);
2058 let (solution, score, _, trimming_status) =
2059 Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
2060 voters.clone(),
2061 targets.clone(),
2062 desired_targets,
2063 )
2064 .unwrap();
2065
2066 assert_eq!(trimming_status.trimmed_edges(), 0);
2067
2068 let ready_solution = Miner::<Runtime>::feasibility_check(
2069 RawSolution { solution, score, round },
2070 Default::default(),
2071 desired_targets,
2072 snapshot.clone(),
2073 round,
2074 Default::default(),
2075 )
2076 .unwrap();
2077
2078 let full_supports = ready_solution.supports.into_iter().collect::<Vec<_>>();
2079
2080 let expected_trimmed_supports = full_supports
2083 .into_iter()
2084 .filter(|(_, s)| s.voters.len() as u32 > trim_backers_bound)
2085 .map(|(t, s)| (t, s.voters.into_iter().min_by(|a, b| a.1.cmp(&b.1)).unwrap()))
2086 .collect::<Vec<_>>();
2087
2088 MaxBackersPerWinner::set(trim_backers_bound);
2090 let (solution, score, _, trimming_status) =
2091 Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
2092 voters.clone(),
2093 targets.clone(),
2094 desired_targets,
2095 )
2096 .unwrap();
2097
2098 assert_eq!(trimming_status.trimmed_edges(), 2);
2099
2100 let ready_solution = Miner::<Runtime>::feasibility_check(
2101 RawSolution { solution, score, round },
2102 Default::default(),
2103 desired_targets,
2104 snapshot.clone(),
2105 round,
2106 Default::default(),
2107 )
2108 .unwrap();
2109
2110 let trimmed_supports = ready_solution.supports.into_iter().collect::<Vec<_>>();
2111
2112 let mut trimmed_supports_edges_full = vec![];
2114 for (t, s) in trimmed_supports {
2115 for v in s.voters {
2116 trimmed_supports_edges_full.push((t, v));
2117 }
2118 }
2119
2120 for edge in trimmed_supports_edges_full {
2123 assert!(!expected_trimmed_supports.contains(&edge));
2124 }
2125 })
2126 }
2127
2128 #[test]
2129 fn trim_assignments_length_does_not_modify_when_short_enough() {
2130 ExtBuilder::default().build_and_execute(|| {
2131 roll_to_unsigned();
2132
2133 let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
2135 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2136 let encoded_len = solution.encoded_size() as u32;
2137 let solution_clone = solution.clone();
2138
2139 let trimmed_len = Miner::<Runtime>::trim_assignments_length(
2141 encoded_len,
2142 &mut assignments,
2143 encoded_size_of,
2144 )
2145 .unwrap();
2146
2147 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2149 assert_eq!(solution, solution_clone);
2150 assert_eq!(trimmed_len, 0);
2151 });
2152 }
2153
2154 #[test]
2155 fn trim_assignments_length_modifies_when_too_long() {
2156 ExtBuilder::default().build().execute_with(|| {
2157 roll_to_unsigned();
2158
2159 let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
2161 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2162 let encoded_len = solution.encoded_size();
2163 let solution_clone = solution.clone();
2164
2165 let trimmed_len = Miner::<Runtime>::trim_assignments_length(
2167 encoded_len as u32 - 1,
2168 &mut assignments,
2169 encoded_size_of,
2170 )
2171 .unwrap();
2172
2173 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2175 assert_ne!(solution, solution_clone);
2176 assert!(solution.encoded_size() < encoded_len);
2177 assert_eq!(trimmed_len, 1);
2178 });
2179 }
2180
2181 #[test]
2182 fn trim_assignments_length_trims_lowest_stake() {
2183 ExtBuilder::default().build().execute_with(|| {
2184 roll_to_unsigned();
2185
2186 let TrimHelpers { voters, mut assignments, encoded_size_of, voter_index } =
2188 trim_helpers();
2189 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2190 let encoded_len = solution.encoded_size() as u32;
2191 let count = assignments.len();
2192 let min_stake_voter = voters
2193 .iter()
2194 .map(|(id, weight, _)| (weight, id))
2195 .min()
2196 .and_then(|(_, id)| voter_index(id))
2197 .unwrap();
2198
2199 Miner::<Runtime>::trim_assignments_length(
2201 encoded_len - 1,
2202 &mut assignments,
2203 encoded_size_of,
2204 )
2205 .unwrap();
2206
2207 assert_eq!(assignments.len(), count - 1, "we must have removed exactly one assignment");
2209 assert!(
2210 assignments.iter().all(|IndexAssignment { who, .. }| *who != min_stake_voter),
2211 "min_stake_voter must no longer be in the set of voters",
2212 );
2213 });
2214 }
2215
2216 #[test]
2217 fn trim_assignments_length_wont_panic() {
2218 ExtBuilder::default().build_and_execute(|| {
2220 let encoded_size_of = Box::new(|assignments: &[IndexAssignmentOf<Runtime>]| {
2221 SolutionOf::<Runtime>::try_from(assignments).map(|solution| solution.encoded_size())
2222 });
2223
2224 let mut assignments = vec![];
2225
2226 let min_solution_size = encoded_size_of(&assignments).unwrap();
2229 assert_eq!(min_solution_size, SolutionOf::<Runtime>::LIMIT);
2230
2231 Miner::<Runtime>::trim_assignments_length(0, &mut assignments, encoded_size_of.clone())
2233 .unwrap();
2234 Miner::<Runtime>::trim_assignments_length(1, &mut assignments, encoded_size_of.clone())
2235 .unwrap();
2236 Miner::<Runtime>::trim_assignments_length(
2237 min_solution_size as u32,
2238 &mut assignments,
2239 encoded_size_of,
2240 )
2241 .unwrap();
2242 });
2243
2244 ExtBuilder::default().build_and_execute(|| {
2246 roll_to_unsigned();
2248 let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
2249 assert!(assignments.len() > 0);
2250
2251 let min_solution_size = SolutionOf::<Runtime>::LIMIT as u32;
2253 Miner::<Runtime>::trim_assignments_length(
2254 min_solution_size,
2255 &mut assignments,
2256 encoded_size_of,
2257 )
2258 .unwrap();
2259 assert_eq!(assignments.len(), 0);
2260 });
2261 }
2262
2263 #[test]
2267 fn mine_solution_solutions_always_within_acceptable_length() {
2268 ExtBuilder::default().build_and_execute(|| {
2269 roll_to_unsigned();
2270
2271 let solution = MultiPhase::mine_solution().unwrap();
2273 let max_length = <Runtime as MinerConfig>::MaxLength::get();
2274 let solution_size = solution.0.solution.encoded_size();
2275 assert!(solution_size <= max_length as usize);
2276
2277 <Runtime as MinerConfig>::MaxLength::set(solution_size as u32 - 1);
2279 let solution = MultiPhase::mine_solution().unwrap();
2280 let max_length = <Runtime as MinerConfig>::MaxLength::get();
2281 let solution_size = solution.0.solution.encoded_size();
2282 assert!(solution_size <= max_length as usize);
2283 });
2284 }
2285}