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.map_or(true, |min_score| {
842 submitted_score.strict_threshold_better(min_score, sp_runtime::Perbill::zero())
843 }),
844 FeasibilityError::UntrustedScoreTooLow
845 );
846
847 let cache = helpers::generate_voter_cache::<T>(&snapshot_voters);
849 let voter_at = helpers::voter_at_fn::<T>(&snapshot_voters);
850 let target_at = helpers::target_at_fn::<T>(&snapshot_targets);
851 let voter_index = helpers::voter_index_fn_usize::<T>(&cache);
852
853 let assignments = solution
856 .into_assignment(voter_at, target_at)
857 .map_err::<FeasibilityError, _>(Into::into)?;
858
859 assignments.iter().try_for_each(|assignment| {
861 let snapshot_index =
869 voter_index(&assignment.who).ok_or(FeasibilityError::InvalidVoter)?;
870 let (_voter, _stake, targets) =
872 snapshot_voters.get(snapshot_index).ok_or(FeasibilityError::InvalidVoter)?;
873
874 if assignment.distribution.iter().any(|(d, _)| !targets.contains(d)) {
876 return Err(FeasibilityError::InvalidVote)
877 }
878 Ok(())
879 })?;
880
881 let stake_of = helpers::stake_of_fn::<T>(&snapshot_voters, &cache);
883
884 let staked_assignments = assignment_ratio_to_staked_normalized(assignments, stake_of)
886 .map_err::<FeasibilityError, _>(Into::into)?;
887 let supports = sp_npos_elections::to_supports(&staked_assignments);
888
889 let known_score = supports.evaluate();
891
892 ensure!(known_score == score, FeasibilityError::InvalidScore);
893
894 let supports = supports
898 .try_into()
899 .defensive_map_err(|_| FeasibilityError::BoundedConversionFailed)?;
900
901 Ok(ReadySolution { supports, compute, score })
902 }
903}
904
905#[cfg(test)]
906mod max_weight {
907 #![allow(unused_variables)]
908 use super::*;
909 use crate::mock::{MockWeightInfo, Runtime};
910 #[test]
911 fn find_max_voter_binary_search_works() {
912 let w = SolutionOrSnapshotSize { voters: 10, targets: 0 };
913 MockWeightInfo::set(crate::mock::MockedWeightInfo::Complex);
914 assert_eq!(
915 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
916 0
917 );
918 assert_eq!(
919 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
920 0
921 );
922 assert_eq!(
923 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
924 0
925 );
926 assert_eq!(
927 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
928 1
929 );
930 assert_eq!(
931 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
932 1
933 );
934 assert_eq!(
935 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1990, u64::MAX)),
936 1
937 );
938 assert_eq!(
939 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
940 1
941 );
942 assert_eq!(
943 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
944 2
945 );
946 assert_eq!(
947 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
948 2
949 );
950 assert_eq!(
951 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
952 2
953 );
954 assert_eq!(
955 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2990, u64::MAX)),
956 2
957 );
958 assert_eq!(
959 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2999, u64::MAX)),
960 2
961 );
962 assert_eq!(
963 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3000, u64::MAX)),
964 3
965 );
966 assert_eq!(
967 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
968 3
969 );
970 assert_eq!(
971 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(5500, u64::MAX)),
972 5
973 );
974 assert_eq!(
975 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(7777, u64::MAX)),
976 7
977 );
978 assert_eq!(
979 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(9999, u64::MAX)),
980 9
981 );
982 assert_eq!(
983 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(10_000, u64::MAX)),
984 10
985 );
986 assert_eq!(
987 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(10_999, u64::MAX)),
988 10
989 );
990 assert_eq!(
991 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(11_000, u64::MAX)),
992 10
993 );
994 assert_eq!(
995 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(22_000, u64::MAX)),
996 10
997 );
998
999 let w = SolutionOrSnapshotSize { voters: 1, targets: 0 };
1000
1001 assert_eq!(
1002 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
1003 0
1004 );
1005 assert_eq!(
1006 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
1007 0
1008 );
1009 assert_eq!(
1010 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
1011 0
1012 );
1013 assert_eq!(
1014 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
1015 1
1016 );
1017 assert_eq!(
1018 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
1019 1
1020 );
1021 assert_eq!(
1022 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1990, u64::MAX)),
1023 1
1024 );
1025 assert_eq!(
1026 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
1027 1
1028 );
1029 assert_eq!(
1030 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
1031 1
1032 );
1033 assert_eq!(
1034 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
1035 1
1036 );
1037 assert_eq!(
1038 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
1039 1
1040 );
1041 assert_eq!(
1042 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
1043 1
1044 );
1045
1046 let w = SolutionOrSnapshotSize { voters: 2, targets: 0 };
1047
1048 assert_eq!(
1049 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
1050 0
1051 );
1052 assert_eq!(
1053 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
1054 0
1055 );
1056 assert_eq!(
1057 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
1058 0
1059 );
1060 assert_eq!(
1061 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
1062 1
1063 );
1064 assert_eq!(
1065 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
1066 1
1067 );
1068 assert_eq!(
1069 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
1070 1
1071 );
1072 assert_eq!(
1073 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
1074 2
1075 );
1076 assert_eq!(
1077 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
1078 2
1079 );
1080 assert_eq!(
1081 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
1082 2
1083 );
1084 assert_eq!(
1085 Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
1086 2
1087 );
1088 }
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093 use super::*;
1094 use crate::{
1095 mock::{
1096 multi_phase_events, roll_to, roll_to_signed, roll_to_unsigned, roll_to_with_ocw,
1097 trim_helpers, witness, BlockNumber, ExtBuilder, Extrinsic, MinerMaxWeight, MultiPhase,
1098 Runtime, RuntimeCall, RuntimeOrigin, System, TestNposSolution, TrimHelpers,
1099 UnsignedPhase,
1100 },
1101 Event, InvalidTransaction, Phase, QueuedSolution, TransactionSource,
1102 TransactionValidityError,
1103 };
1104 use alloc::vec;
1105 use codec::Decode;
1106 use frame_election_provider_support::IndexAssignment;
1107 use frame_support::{assert_noop, assert_ok, traits::OffchainWorker};
1108 use sp_npos_elections::ElectionScore;
1109 use sp_runtime::{
1110 bounded_vec,
1111 offchain::storage_lock::{BlockAndTime, StorageLock},
1112 traits::{Dispatchable, ValidateUnsigned, Zero},
1113 ModuleError, PerU16,
1114 };
1115
1116 type Assignment = crate::unsigned::Assignment<Runtime>;
1117
1118 #[test]
1119 fn validate_unsigned_retracts_wrong_phase() {
1120 ExtBuilder::default().desired_targets(0).build_and_execute(|| {
1121 let solution = RawSolution::<TestNposSolution> {
1122 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1123 ..Default::default()
1124 };
1125 let call = Call::submit_unsigned {
1126 raw_solution: Box::new(solution.clone()),
1127 witness: witness(),
1128 };
1129
1130 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
1132 assert!(matches!(
1133 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1134 TransactionSource::Local,
1135 &call
1136 )
1137 .unwrap_err(),
1138 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1139 ));
1140 assert!(matches!(
1141 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1142 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1143 ));
1144
1145 roll_to_signed();
1147 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
1148 assert!(matches!(
1149 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1150 TransactionSource::Local,
1151 &call
1152 )
1153 .unwrap_err(),
1154 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1155 ));
1156 assert!(matches!(
1157 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1158 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1159 ));
1160
1161 roll_to_unsigned();
1163 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1164
1165 assert!(<MultiPhase as ValidateUnsigned>::validate_unsigned(
1166 TransactionSource::Local,
1167 &call
1168 )
1169 .is_ok());
1170 assert!(<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).is_ok());
1171
1172 MultiPhase::phase_transition(Phase::Unsigned((false, 25)));
1174 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1175 assert!(matches!(
1176 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1177 TransactionSource::Local,
1178 &call
1179 )
1180 .unwrap_err(),
1181 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1182 ));
1183 assert!(matches!(
1184 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1185 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
1186 ));
1187 })
1188 }
1189
1190 #[test]
1191 fn validate_unsigned_retracts_low_score() {
1192 ExtBuilder::default().desired_targets(0).build_and_execute(|| {
1193 roll_to_unsigned();
1194 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1195
1196 let solution = RawSolution::<TestNposSolution> {
1197 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1198 ..Default::default()
1199 };
1200 let call = Call::submit_unsigned {
1201 raw_solution: Box::new(solution.clone()),
1202 witness: witness(),
1203 };
1204
1205 assert!(<MultiPhase as ValidateUnsigned>::validate_unsigned(
1207 TransactionSource::Local,
1208 &call
1209 )
1210 .is_ok());
1211 assert!(<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).is_ok());
1212
1213 let ready = ReadySolution {
1215 score: ElectionScore { minimal_stake: 10, ..Default::default() },
1216 ..Default::default()
1217 };
1218 QueuedSolution::<Runtime>::put(ready);
1219
1220 assert!(matches!(
1222 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1223 TransactionSource::Local,
1224 &call
1225 )
1226 .unwrap_err(),
1227 TransactionValidityError::Invalid(InvalidTransaction::Custom(2))
1228 ));
1229 assert!(matches!(
1230 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1231 TransactionValidityError::Invalid(InvalidTransaction::Custom(2))
1232 ));
1233 })
1234 }
1235
1236 #[test]
1237 fn validate_unsigned_retracts_incorrect_winner_count() {
1238 ExtBuilder::default().desired_targets(1).build_and_execute(|| {
1239 roll_to_unsigned();
1240 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1241
1242 let raw = RawSolution::<TestNposSolution> {
1243 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1244 ..Default::default()
1245 };
1246 let call =
1247 Call::submit_unsigned { raw_solution: Box::new(raw.clone()), witness: witness() };
1248 assert_eq!(raw.solution.unique_targets().len(), 0);
1249
1250 assert!(matches!(
1252 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1253 TransactionSource::Local,
1254 &call
1255 )
1256 .unwrap_err(),
1257 TransactionValidityError::Invalid(InvalidTransaction::Custom(1))
1258 ));
1259 })
1260 }
1261
1262 #[test]
1263 fn priority_is_set() {
1264 ExtBuilder::default()
1265 .miner_tx_priority(20)
1266 .desired_targets(0)
1267 .build_and_execute(|| {
1268 roll_to_unsigned();
1269 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1270
1271 let solution = RawSolution::<TestNposSolution> {
1272 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1273 ..Default::default()
1274 };
1275 let call = Call::submit_unsigned {
1276 raw_solution: Box::new(solution.clone()),
1277 witness: witness(),
1278 };
1279
1280 assert_eq!(
1281 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1282 TransactionSource::Local,
1283 &call
1284 )
1285 .unwrap()
1286 .priority,
1287 25
1288 );
1289 })
1290 }
1291
1292 #[test]
1293 #[should_panic(expected = "Invalid unsigned submission must produce invalid block and \
1294 deprive validator from their authoring reward.: \
1295 Module(ModuleError { index: 2, error: [1, 0, 0, 0], message: \
1296 Some(\"PreDispatchWrongWinnerCount\") })")]
1297 fn unfeasible_solution_panics() {
1298 ExtBuilder::default().build_and_execute(|| {
1299 roll_to_unsigned();
1300 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1301
1302 let solution = RawSolution::<TestNposSolution> {
1304 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1305 ..Default::default()
1306 };
1307 let call = Call::submit_unsigned {
1308 raw_solution: Box::new(solution.clone()),
1309 witness: witness(),
1310 };
1311 let runtime_call: RuntimeCall = call.into();
1312 let _ = runtime_call.dispatch(RuntimeOrigin::none());
1313 })
1314 }
1315
1316 #[test]
1317 #[should_panic(expected = "Invalid unsigned submission must produce invalid block and \
1318 deprive validator from their authoring reward.")]
1319 fn wrong_witness_panics() {
1320 ExtBuilder::default().build_and_execute(|| {
1321 roll_to_unsigned();
1322 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1323
1324 let solution = RawSolution::<TestNposSolution> {
1326 score: ElectionScore { minimal_stake: 5, ..Default::default() },
1327 ..Default::default()
1328 };
1329
1330 let mut correct_witness = witness();
1331 correct_witness.voters += 1;
1332 correct_witness.targets -= 1;
1333 let call = Call::submit_unsigned {
1334 raw_solution: Box::new(solution.clone()),
1335 witness: correct_witness,
1336 };
1337 let runtime_call: RuntimeCall = call.into();
1338 let _ = runtime_call.dispatch(RuntimeOrigin::none());
1339 })
1340 }
1341
1342 #[test]
1343 fn miner_works() {
1344 ExtBuilder::default().build_and_execute(|| {
1345 roll_to_unsigned();
1346 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1347
1348 assert!(Snapshot::<Runtime>::get().is_some());
1350 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 2);
1351
1352 let (solution, witness, _) = MultiPhase::mine_solution().unwrap();
1354
1355 assert!(QueuedSolution::<Runtime>::get().is_none());
1357 assert_ok!(MultiPhase::submit_unsigned(
1358 RuntimeOrigin::none(),
1359 Box::new(solution),
1360 witness
1361 ));
1362 assert!(QueuedSolution::<Runtime>::get().is_some());
1363 assert_eq!(
1364 multi_phase_events(),
1365 vec![
1366 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1367 Event::PhaseTransitioned {
1368 from: Phase::Signed,
1369 to: Phase::Unsigned((true, 25)),
1370 round: 1
1371 },
1372 Event::SolutionStored {
1373 compute: ElectionCompute::Unsigned,
1374 origin: None,
1375 prev_ejected: false
1376 }
1377 ]
1378 );
1379 })
1380 }
1381
1382 #[test]
1383 fn miner_trims_weight() {
1384 ExtBuilder::default()
1385 .miner_weight(Weight::from_parts(100, u64::MAX))
1386 .mock_weight_info(crate::mock::MockedWeightInfo::Basic)
1387 .build_and_execute(|| {
1388 roll_to_unsigned();
1389 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1390
1391 let (raw, witness, t) = MultiPhase::mine_solution().unwrap();
1392 let solution_weight = <Runtime as MinerConfig>::solution_weight(
1393 witness.voters,
1394 witness.targets,
1395 raw.solution.voter_count() as u32,
1396 raw.solution.unique_targets().len() as u32,
1397 );
1398 assert_eq!(solution_weight, Weight::from_parts(35, 0));
1400 assert_eq!(raw.solution.voter_count(), 5);
1401 assert_eq!(t.trimmed_weight(), 0);
1402
1403 <MinerMaxWeight>::set(Weight::from_parts(25, u64::MAX));
1405
1406 let (raw, witness, t) = MultiPhase::mine_solution().unwrap();
1407 let solution_weight = <Runtime as MinerConfig>::solution_weight(
1408 witness.voters,
1409 witness.targets,
1410 raw.solution.voter_count() as u32,
1411 raw.solution.unique_targets().len() as u32,
1412 );
1413 assert_eq!(solution_weight, Weight::from_parts(25, 0));
1415 assert_eq!(raw.solution.voter_count(), 3);
1416 assert_eq!(t.trimmed_weight(), 2);
1417 })
1418 }
1419
1420 #[test]
1421 fn miner_will_not_submit_if_not_enough_winners() {
1422 let (mut ext, _) = ExtBuilder::default().desired_targets(8).build_offchainify(0);
1423 ext.execute_with(|| {
1424 roll_to_unsigned();
1425 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1426
1427 let (mut solution, _, _) = MultiPhase::mine_solution().unwrap();
1429 solution.solution.votes1[0].1 = 4;
1430
1431 assert_eq!(
1432 MultiPhase::basic_checks(&solution, "mined").unwrap_err(),
1433 MinerError::PreDispatchChecksFailed(DispatchError::Module(ModuleError {
1434 index: 2,
1435 error: [1, 0, 0, 0],
1436 message: Some("PreDispatchWrongWinnerCount"),
1437 })),
1438 );
1439 })
1440 }
1441
1442 #[test]
1443 fn unsigned_per_dispatch_checks_can_only_submit_threshold_better() {
1444 ExtBuilder::default()
1445 .desired_targets(1)
1446 .add_voter(7, 2, bounded_vec![10])
1447 .add_voter(8, 5, bounded_vec![10])
1448 .add_voter(9, 1, bounded_vec![10])
1449 .build_and_execute(|| {
1450 roll_to_unsigned();
1451 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1452 assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 1);
1453
1454 let result = ElectionResult {
1456 winners: vec![(10, 12)],
1457 assignments: vec![
1458 Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1459 Assignment {
1460 who: 7,
1461 distribution: vec![(10, PerU16::one())],
1463 },
1464 ],
1465 };
1466
1467 let RoundSnapshot { voters, targets } = Snapshot::<Runtime>::get().unwrap();
1468 let desired_targets = DesiredTargets::<Runtime>::get().unwrap();
1469
1470 let (raw, score, witness, _) =
1471 Miner::<Runtime>::prepare_election_result_with_snapshot(
1472 result,
1473 voters.clone(),
1474 targets.clone(),
1475 desired_targets,
1476 )
1477 .unwrap();
1478 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1479 assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
1480 assert_ok!(MultiPhase::submit_unsigned(
1481 RuntimeOrigin::none(),
1482 Box::new(solution),
1483 witness
1484 ));
1485 assert_eq!(QueuedSolution::<Runtime>::get().unwrap().score.minimal_stake, 12);
1486
1487 let result = ElectionResult {
1490 winners: vec![(10, 10)],
1491 assignments: vec![Assignment {
1492 who: 10,
1493 distribution: vec![(10, PerU16::one())],
1494 }],
1495 };
1496 let (raw, score, _, _) = Miner::<Runtime>::prepare_election_result_with_snapshot(
1497 result,
1498 voters.clone(),
1499 targets.clone(),
1500 desired_targets,
1501 )
1502 .unwrap();
1503 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1504 assert_eq!(solution.score.minimal_stake, 10);
1506 assert_noop!(
1508 MultiPhase::unsigned_pre_dispatch_checks(&solution),
1509 Error::<Runtime>::PreDispatchWeakSubmission,
1510 );
1511
1512 let result = ElectionResult {
1515 winners: vec![(10, 12)],
1516 assignments: vec![
1517 Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1518 Assignment {
1519 who: 7,
1520 distribution: vec![(10, PerU16::one())],
1522 },
1523 ],
1524 };
1525
1526 let (raw, score, _, _) = Miner::<Runtime>::prepare_election_result_with_snapshot(
1527 result,
1528 voters.clone(),
1529 targets.clone(),
1530 desired_targets,
1531 )
1532 .unwrap();
1533 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1534 assert_eq!(solution.score.minimal_stake, 12);
1536 assert_noop!(
1538 MultiPhase::unsigned_pre_dispatch_checks(&solution),
1539 Error::<Runtime>::PreDispatchWeakSubmission,
1540 );
1541
1542 let result = ElectionResult {
1545 winners: vec![(10, 12)],
1546 assignments: vec![
1547 Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1548 Assignment { who: 7, distribution: vec![(10, PerU16::one())] },
1549 Assignment { who: 9, distribution: vec![(10, PerU16::one())] },
1550 ],
1551 };
1552 let (raw, score, witness, _) =
1553 Miner::<Runtime>::prepare_election_result_with_snapshot(
1554 result,
1555 voters.clone(),
1556 targets.clone(),
1557 desired_targets,
1558 )
1559 .unwrap();
1560 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1561 assert_eq!(solution.score.minimal_stake, 13);
1562
1563 assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
1565 assert_ok!(MultiPhase::submit_unsigned(
1566 RuntimeOrigin::none(),
1567 Box::new(solution),
1568 witness
1569 ));
1570
1571 let result = ElectionResult {
1574 winners: vec![(10, 12)],
1575 assignments: vec![
1576 Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
1577 Assignment { who: 7, distribution: vec![(10, PerU16::one())] },
1578 Assignment {
1579 who: 8,
1580 distribution: vec![(10, PerU16::one())],
1582 },
1583 ],
1584 };
1585 let (raw, score, witness, _) =
1586 Miner::<Runtime>::prepare_election_result_with_snapshot(
1587 result,
1588 voters.clone(),
1589 targets.clone(),
1590 desired_targets,
1591 )
1592 .unwrap();
1593 let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
1594 assert_eq!(solution.score.minimal_stake, 17);
1595
1596 assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
1598 assert_ok!(MultiPhase::submit_unsigned(
1599 RuntimeOrigin::none(),
1600 Box::new(solution),
1601 witness
1602 ));
1603 })
1604 }
1605
1606 #[test]
1607 fn ocw_lock_prevents_frequent_execution() {
1608 let (mut ext, _) = ExtBuilder::default().build_offchainify(0);
1609 ext.execute_with(|| {
1610 let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1611
1612 roll_to_unsigned();
1613 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1614
1615 assert!(MultiPhase::ensure_offchain_repeat_frequency(25).is_ok());
1617
1618 assert_noop!(
1620 MultiPhase::ensure_offchain_repeat_frequency(26),
1621 MinerError::Lock("recently executed.")
1622 );
1623
1624 assert!(
1626 MultiPhase::ensure_offchain_repeat_frequency((26 + offchain_repeat).into()).is_ok()
1627 );
1628
1629 assert!(MultiPhase::ensure_offchain_repeat_frequency(
1631 (26 + offchain_repeat - 3).into()
1632 )
1633 .is_err());
1634 assert!(MultiPhase::ensure_offchain_repeat_frequency(
1635 (26 + offchain_repeat - 2).into()
1636 )
1637 .is_err());
1638 assert!(MultiPhase::ensure_offchain_repeat_frequency(
1639 (26 + offchain_repeat - 1).into()
1640 )
1641 .is_err());
1642 })
1643 }
1644
1645 #[test]
1646 fn ocw_lock_released_after_successful_execution() {
1647 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1649 ext.execute_with(|| {
1650 let guard = StorageValueRef::persistent(&OFFCHAIN_LOCK);
1651 let last_block = StorageValueRef::persistent(OFFCHAIN_LAST_BLOCK);
1652
1653 roll_to_unsigned();
1654 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1655
1656 assert!(guard.get::<bool>().unwrap().is_none());
1658
1659 MultiPhase::offchain_worker(25);
1661 assert_eq!(pool.read().transactions.len(), 1);
1662
1663 assert!(guard.get::<bool>().unwrap().is_none());
1665 assert_eq!(last_block.get::<BlockNumber>().unwrap(), Some(25));
1666 });
1667 }
1668
1669 #[test]
1670 fn ocw_lock_prevents_overlapping_execution() {
1671 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1673 ext.execute_with(|| {
1674 roll_to_unsigned();
1675 assert!(CurrentPhase::<Runtime>::get().is_unsigned());
1676
1677 let mut lock = StorageLock::<BlockAndTime<System>>::with_block_deadline(
1679 OFFCHAIN_LOCK,
1680 UnsignedPhase::get().saturated_into(),
1681 );
1682 let guard = lock.lock();
1683
1684 MultiPhase::offchain_worker(25);
1686 assert_eq!(pool.read().transactions.len(), 0);
1687 MultiPhase::offchain_worker(26);
1688 assert_eq!(pool.read().transactions.len(), 0);
1689
1690 drop(guard);
1691
1692 MultiPhase::offchain_worker(25);
1694 assert_eq!(pool.read().transactions.len(), 1);
1695 });
1696 }
1697
1698 #[test]
1699 fn ocw_only_runs_when_unsigned_open_now() {
1700 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1701 ext.execute_with(|| {
1702 roll_to_unsigned();
1703 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
1704
1705 let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
1708
1709 MultiPhase::offchain_worker(24);
1710 assert!(pool.read().transactions.len().is_zero());
1711 storage.clear();
1712
1713 MultiPhase::offchain_worker(25);
1715 assert_eq!(pool.read().transactions.len(), 1);
1716 pool.try_write().unwrap().transactions.clear();
1718
1719 MultiPhase::offchain_worker(26);
1721 assert!(pool.read().transactions.len().is_zero());
1722 })
1723 }
1724
1725 #[test]
1726 fn ocw_clears_cache_on_unsigned_phase_open() {
1727 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1728 ext.execute_with(|| {
1729 const BLOCK: u64 = 25;
1730 let block_plus = |delta: u64| BLOCK + delta;
1731 let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1732
1733 roll_to(BLOCK);
1734 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
1736
1737 assert!(
1738 !ocw_solution_exists::<Runtime>(),
1739 "no solution should be present before we mine one",
1740 );
1741
1742 MultiPhase::offchain_worker(BLOCK);
1744 assert!(
1745 ocw_solution_exists::<Runtime>(),
1746 "a solution must be cached after running the worker",
1747 );
1748
1749 let tx_cache_1 = pool.read().transactions[0].clone();
1751 pool.try_write().unwrap().transactions.clear();
1753
1754 let _ = MultiPhase::do_elect();
1757 MultiPhase::offchain_worker(block_plus(1));
1758 assert!(ocw_solution_exists::<Runtime>(), "elections does not clear the ocw cache");
1759
1760 MultiPhase::offchain_worker(block_plus(offchain_repeat + 1));
1762
1763 let tx_cache_2 = pool.read().transactions[0].clone();
1765 pool.try_write().unwrap().transactions.clear();
1767
1768 assert_eq!(tx_cache_1, tx_cache_2);
1770
1771 let current_block = block_plus(offchain_repeat * 2 + 2);
1772 MultiPhase::phase_transition(Phase::Unsigned((true, current_block)));
1774
1775 MultiPhase::offchain_worker(current_block);
1778 let tx_cache_3 = pool.read().transactions[0].clone();
1779
1780 assert_eq!(tx_cache_1, tx_cache_3);
1782 assert_eq!(
1783 multi_phase_events(),
1784 vec![
1785 Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1786 Event::PhaseTransitioned {
1787 from: Phase::Signed,
1788 to: Phase::Unsigned((true, 25)),
1789 round: 1
1790 },
1791 Event::ElectionFinalized {
1792 compute: ElectionCompute::Fallback,
1793 score: ElectionScore {
1794 minimal_stake: 0,
1795 sum_stake: 0,
1796 sum_stake_squared: 0
1797 }
1798 },
1799 Event::PhaseTransitioned {
1800 from: Phase::Unsigned((true, 25)),
1801 to: Phase::Unsigned((true, 37)),
1802 round: 1
1803 },
1804 ]
1805 );
1806 })
1807 }
1808
1809 #[test]
1810 fn ocw_resubmits_after_offchain_repeat() {
1811 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1812 ext.execute_with(|| {
1813 const BLOCK: u64 = 25;
1814 let block_plus = |delta: i32| ((BLOCK as i32) + delta) as u64;
1815 let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1816
1817 roll_to(BLOCK);
1818 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
1819
1820 let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
1823
1824 MultiPhase::offchain_worker(block_plus(-1));
1825 assert!(pool.read().transactions.len().is_zero());
1826 storage.clear();
1827
1828 MultiPhase::offchain_worker(BLOCK);
1830 assert_eq!(pool.read().transactions.len(), 1);
1831 let tx_cache = pool.read().transactions[0].clone();
1832 pool.try_write().unwrap().transactions.clear();
1834
1835 MultiPhase::offchain_worker(block_plus(1 + offchain_repeat as i32));
1839 assert_eq!(pool.read().transactions.len(), 1);
1840
1841 let tx = &pool.read().transactions[0];
1843 assert_eq!(&tx_cache, tx);
1844 })
1845 }
1846
1847 #[test]
1848 fn ocw_regenerates_and_resubmits_after_offchain_repeat() {
1849 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1850 ext.execute_with(|| {
1851 const BLOCK: u64 = 25;
1852 let block_plus = |delta: i32| ((BLOCK as i32) + delta) as u64;
1853 let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
1854
1855 roll_to(BLOCK);
1856 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
1857
1858 let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
1861
1862 MultiPhase::offchain_worker(block_plus(-1));
1863 assert!(pool.read().transactions.len().is_zero());
1864 storage.clear();
1865
1866 MultiPhase::offchain_worker(BLOCK);
1868 assert_eq!(pool.read().transactions.len(), 1);
1869 let tx_cache = pool.read().transactions[0].clone();
1870 pool.try_write().unwrap().transactions.clear();
1872
1873 let mut call_cache = StorageValueRef::persistent(&OFFCHAIN_CACHED_CALL);
1877 assert!(matches!(call_cache.get::<Call<Runtime>>(), Ok(Some(_call))));
1878 call_cache.clear();
1879
1880 MultiPhase::offchain_worker(block_plus(1 + offchain_repeat as i32));
1884 assert_eq!(pool.read().transactions.len(), 1);
1885
1886 let tx = &pool.read().transactions[0];
1888 assert_eq!(&tx_cache, tx);
1889 })
1890 }
1891
1892 #[test]
1893 fn ocw_can_submit_to_pool() {
1894 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1895 ext.execute_with(|| {
1896 roll_to_with_ocw(25);
1897 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
1898 let encoded = pool.read().transactions[0].clone();
1901 let extrinsic: Extrinsic = codec::Decode::decode(&mut &*encoded).unwrap();
1902 let call = extrinsic.function;
1903 assert!(matches!(call, RuntimeCall::MultiPhase(Call::submit_unsigned { .. })));
1904 })
1905 }
1906
1907 #[test]
1908 fn ocw_solution_must_have_correct_round() {
1909 let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
1910 ext.execute_with(|| {
1911 roll_to_with_ocw(25);
1912 assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
1913 crate::Round::<Runtime>::mutate(|round| *round += 1);
1916
1917 let encoded = pool.read().transactions[0].clone();
1918 let extrinsic = Extrinsic::decode(&mut &*encoded).unwrap();
1919 let call = match extrinsic.function {
1920 RuntimeCall::MultiPhase(call @ Call::submit_unsigned { .. }) => call,
1921 _ => panic!("bad call: unexpected submission"),
1922 };
1923
1924 let pre_dispatch_check_error =
1926 TransactionValidityError::Invalid(InvalidTransaction::Custom(7));
1927 assert_eq!(
1928 <MultiPhase as ValidateUnsigned>::validate_unsigned(
1929 TransactionSource::Local,
1930 &call,
1931 )
1932 .unwrap_err(),
1933 pre_dispatch_check_error,
1934 );
1935 assert_eq!(
1936 <MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
1937 pre_dispatch_check_error,
1938 );
1939 })
1940 }
1941
1942 #[test]
1943 fn mine_solution_always_respects_max_backers_per_winner() {
1944 use crate::mock::MaxBackersPerWinner;
1945 use frame_election_provider_support::BoundedSupport;
1946
1947 let targets = vec![10, 20, 30, 40];
1948 let voters = vec![
1949 (1, 11, bounded_vec![10, 20, 30]),
1950 (2, 12, bounded_vec![10, 20, 30]),
1951 (3, 13, bounded_vec![10, 20, 30]),
1952 (4, 14, bounded_vec![10, 20, 30]),
1953 (5, 15, bounded_vec![10, 20, 40]),
1954 ];
1955 let snapshot = RoundSnapshot { voters: voters.clone(), targets: targets.clone() };
1956 let (round, desired_targets) = (1, 3);
1957
1958 ExtBuilder::default().max_backers_per_winner(u32::MAX).build_and_execute(|| {
1960 assert_eq!(MaxBackersPerWinner::get(), u32::MAX);
1961
1962 let (solution, expected_score_unbounded, _, trimming_status) =
1963 Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
1964 voters.clone(),
1965 targets.clone(),
1966 desired_targets,
1967 )
1968 .unwrap();
1969
1970 let ready_solution = Miner::<Runtime>::feasibility_check(
1971 RawSolution { solution, score: expected_score_unbounded, round },
1972 Default::default(),
1973 desired_targets,
1974 snapshot.clone(),
1975 round,
1976 Default::default(),
1977 )
1978 .unwrap();
1979
1980 assert_eq!(
1981 ready_solution.supports.into_iter().collect::<Vec<_>>(),
1982 vec![
1983 (
1984 10,
1985 BoundedSupport { total: 25, voters: bounded_vec![(1, 11), (4, 9), (5, 5)] }
1986 ),
1987 (20, BoundedSupport { total: 22, voters: bounded_vec![(2, 12), (5, 10)] }),
1988 (30, BoundedSupport { total: 18, voters: bounded_vec![(3, 13), (4, 5)] })
1989 ]
1990 );
1991
1992 assert_eq!(trimming_status.trimmed_edges(), 0);
1994 });
1995
1996 ExtBuilder::default().max_backers_per_winner(1).build_and_execute(|| {
1998 assert_eq!(MaxBackersPerWinner::get(), 1);
1999
2000 let (solution, expected_score_bounded, _, trimming_status) =
2001 Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
2002 voters,
2003 targets,
2004 desired_targets,
2005 )
2006 .unwrap();
2007
2008 let ready_solution = Miner::<Runtime>::feasibility_check(
2009 RawSolution { solution, score: expected_score_bounded, round },
2010 Default::default(),
2011 desired_targets,
2012 snapshot,
2013 round,
2014 Default::default(),
2015 )
2016 .unwrap();
2017
2018 for (_, supports) in ready_solution.supports.iter() {
2019 assert!((supports.voters.len() as u32) <= MaxBackersPerWinner::get());
2020 }
2021
2022 assert_eq!(
2023 ready_solution.supports.into_iter().collect::<Vec<_>>(),
2024 vec![
2025 (10, BoundedSupport { total: 11, voters: bounded_vec![(1, 11)] }),
2026 (20, BoundedSupport { total: 12, voters: bounded_vec![(2, 12)] }),
2027 (30, BoundedSupport { total: 13, voters: bounded_vec![(3, 13)] })
2028 ]
2029 );
2030
2031 assert_eq!(trimming_status.trimmed_edges(), 4);
2033 });
2034 }
2035
2036 #[test]
2037 fn max_backers_edges_trims_lowest_stake() {
2038 use crate::mock::MaxBackersPerWinner;
2039
2040 ExtBuilder::default().build_and_execute(|| {
2041 let targets = vec![10, 20, 30, 40];
2042
2043 let voters = vec![
2044 (1, 100, bounded_vec![10, 20]),
2045 (2, 200, bounded_vec![10, 20, 30]),
2046 (3, 300, bounded_vec![10, 30]),
2047 (4, 400, bounded_vec![10, 30]),
2048 (5, 500, bounded_vec![10, 20, 30]),
2049 (6, 600, bounded_vec![10, 20, 30, 40]),
2050 ];
2051 let snapshot = RoundSnapshot { voters: voters.clone(), targets: targets.clone() };
2052 let (round, desired_targets) = (1, 4);
2053
2054 let max_backers_bound = u32::MAX;
2055 let trim_backers_bound = 2;
2056
2057 MaxBackersPerWinner::set(max_backers_bound);
2059 let (solution, score, _, trimming_status) =
2060 Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
2061 voters.clone(),
2062 targets.clone(),
2063 desired_targets,
2064 )
2065 .unwrap();
2066
2067 assert_eq!(trimming_status.trimmed_edges(), 0);
2068
2069 let ready_solution = Miner::<Runtime>::feasibility_check(
2070 RawSolution { solution, score, round },
2071 Default::default(),
2072 desired_targets,
2073 snapshot.clone(),
2074 round,
2075 Default::default(),
2076 )
2077 .unwrap();
2078
2079 let full_supports = ready_solution.supports.into_iter().collect::<Vec<_>>();
2080
2081 let expected_trimmed_supports = full_supports
2084 .into_iter()
2085 .filter(|(_, s)| s.voters.len() as u32 > trim_backers_bound)
2086 .map(|(t, s)| (t, s.voters.into_iter().min_by(|a, b| a.1.cmp(&b.1)).unwrap()))
2087 .collect::<Vec<_>>();
2088
2089 MaxBackersPerWinner::set(trim_backers_bound);
2091 let (solution, score, _, trimming_status) =
2092 Miner::<Runtime>::mine_solution_with_snapshot::<<Runtime as Config>::Solver>(
2093 voters.clone(),
2094 targets.clone(),
2095 desired_targets,
2096 )
2097 .unwrap();
2098
2099 assert_eq!(trimming_status.trimmed_edges(), 2);
2100
2101 let ready_solution = Miner::<Runtime>::feasibility_check(
2102 RawSolution { solution, score, round },
2103 Default::default(),
2104 desired_targets,
2105 snapshot.clone(),
2106 round,
2107 Default::default(),
2108 )
2109 .unwrap();
2110
2111 let trimmed_supports = ready_solution.supports.into_iter().collect::<Vec<_>>();
2112
2113 let mut trimmed_supports_edges_full = vec![];
2115 for (t, s) in trimmed_supports {
2116 for v in s.voters {
2117 trimmed_supports_edges_full.push((t, v));
2118 }
2119 }
2120
2121 for edge in trimmed_supports_edges_full {
2124 assert!(!expected_trimmed_supports.contains(&edge));
2125 }
2126 })
2127 }
2128
2129 #[test]
2130 fn trim_assignments_length_does_not_modify_when_short_enough() {
2131 ExtBuilder::default().build_and_execute(|| {
2132 roll_to_unsigned();
2133
2134 let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
2136 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2137 let encoded_len = solution.encoded_size() as u32;
2138 let solution_clone = solution.clone();
2139
2140 let trimmed_len = Miner::<Runtime>::trim_assignments_length(
2142 encoded_len,
2143 &mut assignments,
2144 encoded_size_of,
2145 )
2146 .unwrap();
2147
2148 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2150 assert_eq!(solution, solution_clone);
2151 assert_eq!(trimmed_len, 0);
2152 });
2153 }
2154
2155 #[test]
2156 fn trim_assignments_length_modifies_when_too_long() {
2157 ExtBuilder::default().build().execute_with(|| {
2158 roll_to_unsigned();
2159
2160 let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
2162 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2163 let encoded_len = solution.encoded_size();
2164 let solution_clone = solution.clone();
2165
2166 let trimmed_len = Miner::<Runtime>::trim_assignments_length(
2168 encoded_len as u32 - 1,
2169 &mut assignments,
2170 encoded_size_of,
2171 )
2172 .unwrap();
2173
2174 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2176 assert_ne!(solution, solution_clone);
2177 assert!(solution.encoded_size() < encoded_len);
2178 assert_eq!(trimmed_len, 1);
2179 });
2180 }
2181
2182 #[test]
2183 fn trim_assignments_length_trims_lowest_stake() {
2184 ExtBuilder::default().build().execute_with(|| {
2185 roll_to_unsigned();
2186
2187 let TrimHelpers { voters, mut assignments, encoded_size_of, voter_index } =
2189 trim_helpers();
2190 let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
2191 let encoded_len = solution.encoded_size() as u32;
2192 let count = assignments.len();
2193 let min_stake_voter = voters
2194 .iter()
2195 .map(|(id, weight, _)| (weight, id))
2196 .min()
2197 .and_then(|(_, id)| voter_index(id))
2198 .unwrap();
2199
2200 Miner::<Runtime>::trim_assignments_length(
2202 encoded_len - 1,
2203 &mut assignments,
2204 encoded_size_of,
2205 )
2206 .unwrap();
2207
2208 assert_eq!(assignments.len(), count - 1, "we must have removed exactly one assignment");
2210 assert!(
2211 assignments.iter().all(|IndexAssignment { who, .. }| *who != min_stake_voter),
2212 "min_stake_voter must no longer be in the set of voters",
2213 );
2214 });
2215 }
2216
2217 #[test]
2218 fn trim_assignments_length_wont_panic() {
2219 ExtBuilder::default().build_and_execute(|| {
2221 let encoded_size_of = Box::new(|assignments: &[IndexAssignmentOf<Runtime>]| {
2222 SolutionOf::<Runtime>::try_from(assignments).map(|solution| solution.encoded_size())
2223 });
2224
2225 let mut assignments = vec![];
2226
2227 let min_solution_size = encoded_size_of(&assignments).unwrap();
2230 assert_eq!(min_solution_size, SolutionOf::<Runtime>::LIMIT);
2231
2232 Miner::<Runtime>::trim_assignments_length(0, &mut assignments, encoded_size_of.clone())
2234 .unwrap();
2235 Miner::<Runtime>::trim_assignments_length(1, &mut assignments, encoded_size_of.clone())
2236 .unwrap();
2237 Miner::<Runtime>::trim_assignments_length(
2238 min_solution_size as u32,
2239 &mut assignments,
2240 encoded_size_of,
2241 )
2242 .unwrap();
2243 });
2244
2245 ExtBuilder::default().build_and_execute(|| {
2247 roll_to_unsigned();
2249 let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
2250 assert!(assignments.len() > 0);
2251
2252 let min_solution_size = SolutionOf::<Runtime>::LIMIT as u32;
2254 Miner::<Runtime>::trim_assignments_length(
2255 min_solution_size,
2256 &mut assignments,
2257 encoded_size_of,
2258 )
2259 .unwrap();
2260 assert_eq!(assignments.len(), 0);
2261 });
2262 }
2263
2264 #[test]
2268 fn mine_solution_solutions_always_within_acceptable_length() {
2269 ExtBuilder::default().build_and_execute(|| {
2270 roll_to_unsigned();
2271
2272 let solution = MultiPhase::mine_solution().unwrap();
2274 let max_length = <Runtime as MinerConfig>::MaxLength::get();
2275 let solution_size = solution.0.solution.encoded_size();
2276 assert!(solution_size <= max_length as usize);
2277
2278 <Runtime as MinerConfig>::MaxLength::set(solution_size as u32 - 1);
2280 let solution = MultiPhase::mine_solution().unwrap();
2281 let max_length = <Runtime as MinerConfig>::MaxLength::get();
2282 let solution_size = solution.0.solution.encoded_size();
2283 assert!(solution_size <= max_length as usize);
2284 });
2285 }
2286}