referrerpolicy=no-referrer-when-downgrade

pallet_election_provider_multi_phase/
signed.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! The signed phase implementation.
19
20use core::marker::PhantomData;
21
22use crate::{
23	unsigned::MinerConfig, Config, ElectionCompute, Pallet, QueuedSolution, RawSolution,
24	ReadySolutionOf, SignedSubmissionIndices, SignedSubmissionNextIndex, SignedSubmissionsMap,
25	SnapshotMetadata, SolutionOf, SolutionOrSnapshotSize, Weight, WeightInfo,
26};
27use alloc::{
28	collections::{btree_map::BTreeMap, btree_set::BTreeSet},
29	vec::Vec,
30};
31use codec::{Decode, Encode, HasCompact};
32use core::cmp::Ordering;
33use frame_election_provider_support::NposSolution;
34use frame_support::traits::{
35	defensive_prelude::*, Currency, Get, OnUnbalanced, ReservableCurrency,
36};
37use frame_system::pallet_prelude::BlockNumberFor;
38use sp_arithmetic::traits::SaturatedConversion;
39use sp_core::bounded::BoundedVec;
40use sp_npos_elections::ElectionScore;
41use sp_runtime::{
42	traits::{Convert, Saturating, Zero},
43	FixedPointNumber, FixedPointOperand, FixedU128, Percent, RuntimeDebug,
44};
45
46/// A raw, unchecked signed submission.
47///
48/// This is just a wrapper around [`RawSolution`] and some additional info.
49#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo)]
50pub struct SignedSubmission<AccountId, Balance: HasCompact, Solution> {
51	/// Who submitted this solution.
52	pub who: AccountId,
53	/// The deposit reserved for storing this solution.
54	pub deposit: Balance,
55	/// The raw solution itself.
56	pub raw_solution: RawSolution<Solution>,
57	// The estimated fee `who` paid to submit the solution.
58	pub call_fee: Balance,
59}
60
61impl<AccountId, Balance, Solution> Ord for SignedSubmission<AccountId, Balance, Solution>
62where
63	AccountId: Ord,
64	Balance: Ord + HasCompact,
65	Solution: Ord,
66	RawSolution<Solution>: Ord,
67{
68	fn cmp(&self, other: &Self) -> Ordering {
69		self.raw_solution
70			.score
71			.cmp(&other.raw_solution.score)
72			.then_with(|| self.raw_solution.cmp(&other.raw_solution))
73			.then_with(|| self.deposit.cmp(&other.deposit))
74			.then_with(|| self.who.cmp(&other.who))
75	}
76}
77
78impl<AccountId, Balance, Solution> PartialOrd for SignedSubmission<AccountId, Balance, Solution>
79where
80	AccountId: Ord,
81	Balance: Ord + HasCompact,
82	Solution: Ord,
83	RawSolution<Solution>: Ord,
84{
85	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
86		Some(self.cmp(other))
87	}
88}
89
90pub type BalanceOf<T> =
91	<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
92pub type PositiveImbalanceOf<T> = <<T as Config>::Currency as Currency<
93	<T as frame_system::Config>::AccountId,
94>>::PositiveImbalance;
95pub type NegativeImbalanceOf<T> = <<T as Config>::Currency as Currency<
96	<T as frame_system::Config>::AccountId,
97>>::NegativeImbalance;
98pub type SignedSubmissionOf<T> = SignedSubmission<
99	<T as frame_system::Config>::AccountId,
100	BalanceOf<T>,
101	<<T as crate::Config>::MinerConfig as MinerConfig>::Solution,
102>;
103
104/// Always sorted vector of a score, submitted at the given block number, which can be found at the
105/// given index (`u32`) of the `SignedSubmissionsMap`.
106pub type SubmissionIndicesOf<T> =
107	BoundedVec<(ElectionScore, BlockNumberFor<T>, u32), <T as Config>::SignedMaxSubmissions>;
108
109/// Outcome of [`SignedSubmissions::insert`].
110pub enum InsertResult<T: Config> {
111	/// The submission was not inserted because the queue was full and the submission had
112	/// insufficient score to eject a prior solution from the queue.
113	NotInserted,
114	/// The submission was inserted successfully without ejecting a solution.
115	Inserted,
116	/// The submission was inserted successfully. As the queue was full, this operation ejected a
117	/// prior solution, contained in this variant.
118	InsertedEjecting(SignedSubmissionOf<T>),
119}
120
121/// Mask type which pretends to be a set of `SignedSubmissionOf<T>`, while in fact delegating to the
122/// actual implementations in `SignedSubmissionIndices<T>`, `SignedSubmissionsMap<T>`, and
123/// `SignedSubmissionNextIndex<T>`.
124#[cfg_attr(feature = "std", derive(frame_support::DebugNoBound))]
125pub struct SignedSubmissions<T: Config> {
126	indices: SubmissionIndicesOf<T>,
127	next_idx: u32,
128	insertion_overlay: BTreeMap<u32, SignedSubmissionOf<T>>,
129	deletion_overlay: BTreeSet<u32>,
130}
131
132impl<T: Config> SignedSubmissions<T> {
133	/// `true` if the structure is empty.
134	pub fn is_empty(&self) -> bool {
135		self.indices.is_empty()
136	}
137
138	/// Get the length of submitted solutions.
139	pub fn len(&self) -> usize {
140		self.indices.len()
141	}
142
143	/// Get the signed submissions from storage.
144	pub fn get() -> Self {
145		let submissions = SignedSubmissions {
146			indices: SignedSubmissionIndices::<T>::get(),
147			next_idx: SignedSubmissionNextIndex::<T>::get(),
148			insertion_overlay: BTreeMap::new(),
149			deletion_overlay: BTreeSet::new(),
150		};
151
152		// validate that the stored state is sane
153		debug_assert!(submissions
154			.indices
155			.iter()
156			.map(|(_, _, index)| index)
157			.copied()
158			.max()
159			.map_or(true, |max_idx| submissions.next_idx > max_idx,));
160		submissions
161	}
162
163	/// Put the signed submissions back into storage.
164	pub fn put(mut self) {
165		// validate that we're going to write only sane things to storage
166		debug_assert!(self
167			.insertion_overlay
168			.keys()
169			.copied()
170			.max()
171			.map_or(true, |max_idx| self.next_idx > max_idx,));
172		debug_assert!(self
173			.indices
174			.iter()
175			.map(|(_, _, index)| index)
176			.copied()
177			.max()
178			.map_or(true, |max_idx| self.next_idx > max_idx,));
179
180		SignedSubmissionIndices::<T>::put(self.indices);
181		SignedSubmissionNextIndex::<T>::put(self.next_idx);
182		for key in self.deletion_overlay {
183			self.insertion_overlay.remove(&key);
184			SignedSubmissionsMap::<T>::remove(key);
185		}
186		for (key, value) in self.insertion_overlay {
187			SignedSubmissionsMap::<T>::insert(key, value);
188		}
189	}
190
191	/// Get the submission at a particular index.
192	fn get_submission(&self, index: u32) -> Option<SignedSubmissionOf<T>> {
193		if self.deletion_overlay.contains(&index) {
194			// Note: can't actually remove the item from the insertion overlay (if present) because
195			// we don't want to use `&mut self` here. There may be some kind of `RefCell`
196			// optimization possible here in the future.
197			None
198		} else {
199			self.insertion_overlay
200				.get(&index)
201				.cloned()
202				.or_else(|| SignedSubmissionsMap::<T>::get(index))
203		}
204	}
205
206	/// Perform three operations:
207	///
208	/// - Remove the solution at the given position of `self.indices`.
209	/// - Insert a new submission (identified by score and insertion index), if provided.
210	/// - Return the submission which was removed, if any.
211	///
212	/// The call site must ensure that `remove_pos` is a valid index. If otherwise, `None` is
213	/// silently returned.
214	///
215	/// Note: this doesn't insert into `insertion_overlay`, the optional new insertion must be
216	/// inserted into `insertion_overlay` to keep the variable `self` in a valid state.
217	fn swap_out_submission(
218		&mut self,
219		remove_pos: usize,
220		insert: Option<(ElectionScore, BlockNumberFor<T>, u32)>,
221	) -> Option<SignedSubmissionOf<T>> {
222		if remove_pos >= self.indices.len() {
223			return None
224		}
225
226		// safe: index was just checked in the line above.
227		let (_, _, remove_index) = self.indices.remove(remove_pos);
228
229		if let Some((insert_score, block_number, insert_idx)) = insert {
230			self.indices
231				.try_push((insert_score, block_number, insert_idx))
232				.expect("just removed an item, we must be under capacity; qed");
233		}
234
235		self.insertion_overlay.remove(&remove_index).or_else(|| {
236			(!self.deletion_overlay.contains(&remove_index))
237				.then(|| {
238					self.deletion_overlay.insert(remove_index);
239					SignedSubmissionsMap::<T>::get(remove_index)
240				})
241				.flatten()
242		})
243	}
244
245	/// Remove the signed submission with the highest score from the set.
246	pub fn pop_last(&mut self) -> Option<SignedSubmissionOf<T>> {
247		let best_index = self.indices.len().checked_sub(1)?;
248		self.swap_out_submission(best_index, None)
249	}
250
251	/// Iterate through the set of signed submissions in order of increasing score.
252	pub fn iter(&self) -> impl '_ + Iterator<Item = SignedSubmissionOf<T>> {
253		self.indices
254			.iter()
255			.filter_map(move |(_score, _bn, idx)| self.get_submission(*idx).defensive())
256	}
257
258	/// Empty the set of signed submissions, returning an iterator of signed submissions in
259	/// order of submission.
260	///
261	/// Note that if the iterator is dropped without consuming all elements, not all may be removed
262	/// from the underlying `SignedSubmissionsMap`, putting the storages into an invalid state.
263	///
264	/// Note that, like `put`, this function consumes `Self` and modifies storage.
265	fn drain_submitted_order(mut self) -> impl Iterator<Item = SignedSubmissionOf<T>> {
266		let mut keys = SignedSubmissionsMap::<T>::iter_keys()
267			.filter(|k| {
268				if self.deletion_overlay.contains(k) {
269					// Remove submissions that should be deleted.
270					SignedSubmissionsMap::<T>::remove(k);
271					false
272				} else {
273					true
274				}
275			})
276			.chain(self.insertion_overlay.keys().copied())
277			.collect::<Vec<_>>();
278		keys.sort();
279
280		SignedSubmissionIndices::<T>::kill();
281		SignedSubmissionNextIndex::<T>::kill();
282
283		keys.into_iter().filter_map(move |index| {
284			SignedSubmissionsMap::<T>::take(index).or_else(|| self.insertion_overlay.remove(&index))
285		})
286	}
287
288	/// Decode the length of the signed submissions without actually reading the entire struct into
289	/// memory.
290	///
291	/// Note that if you hold an instance of `SignedSubmissions`, this function does _not_
292	/// track its current length. This only decodes what is currently stored in memory.
293	pub fn decode_len() -> Option<usize> {
294		SignedSubmissionIndices::<T>::decode_len()
295	}
296
297	/// Insert a new signed submission into the set.
298	///
299	/// In the event that the new submission is not better than the current weakest according
300	/// to `is_score_better`, we do not change anything.
301	pub fn insert(&mut self, submission: SignedSubmissionOf<T>) -> InsertResult<T> {
302		// verify the expectation that we never reuse an index
303		debug_assert!(!self.indices.iter().map(|(_, _, x)| x).any(|&idx| idx == self.next_idx));
304		let block_number = frame_system::Pallet::<T>::block_number();
305
306		let maybe_weakest = match self.indices.try_push((
307			submission.raw_solution.score,
308			block_number,
309			self.next_idx,
310		)) {
311			Ok(_) => None,
312			Err(_) => {
313				// the queue is full -- if this is better, insert it.
314				let weakest_score = match self.indices.iter().next().defensive() {
315					None => return InsertResult::NotInserted,
316					Some((score, _, _)) => *score,
317				};
318				let threshold = T::BetterSignedThreshold::get();
319
320				// if we haven't improved on the weakest score, don't change anything.
321				if !submission.raw_solution.score.strict_threshold_better(weakest_score, threshold)
322				{
323					return InsertResult::NotInserted
324				}
325
326				self.swap_out_submission(
327					0, // swap out the worse one, which is always index 0.
328					Some((submission.raw_solution.score, block_number, self.next_idx)),
329				)
330			},
331		};
332
333		// this is the ONLY place that we insert, and we sort post insertion. If scores are the
334		// same, we sort based on reverse of submission block number.
335		self.indices
336			.sort_by(|(score1, bn1, _), (score2, bn2, _)| match score1.cmp(score2) {
337				Ordering::Equal => bn1.cmp(&bn2).reverse(),
338				x => x,
339			});
340
341		// we've taken out the weakest, so update the storage map and the next index
342		debug_assert!(!self.insertion_overlay.contains_key(&self.next_idx));
343		self.insertion_overlay.insert(self.next_idx, submission);
344		debug_assert!(!self.deletion_overlay.contains(&self.next_idx));
345		self.next_idx += 1;
346		match maybe_weakest {
347			Some(weakest) => InsertResult::InsertedEjecting(weakest),
348			None => InsertResult::Inserted,
349		}
350	}
351}
352
353/// Type that can be used to calculate the deposit base for signed submissions.
354///
355/// The deposit base is calculated as a geometric progression based on the number of signed
356/// submissions in the queue. The size of the queue represents the progression term.
357pub struct GeometricDepositBase<Balance, Fixed, Inc> {
358	_marker: (PhantomData<Balance>, PhantomData<Fixed>, PhantomData<Inc>),
359}
360
361impl<Balance, Fixed, Inc> Convert<usize, Balance> for GeometricDepositBase<Balance, Fixed, Inc>
362where
363	Balance: FixedPointOperand,
364	Fixed: Get<Balance>,
365	Inc: Get<Percent>,
366{
367	// Calculates the base deposit as a geometric progression based on the number of signed
368	// submissions.
369	//
370	// The nth term is obtained by calculating `base * (1 + increase_factor)^nth`. Example: factor
371	// 5, with initial deposit of 1000 and 10% of increase factor is 1000 * (1 + 0.1)^5.
372	fn convert(queue_len: usize) -> Balance {
373		let increase_factor: FixedU128 = FixedU128::from_u32(1) + Inc::get().into();
374
375		increase_factor.saturating_pow(queue_len).saturating_mul_int(Fixed::get())
376	}
377}
378
379impl<T: Config> Pallet<T> {
380	/// `Self` accessor for `SignedSubmission<T>`.
381	pub fn signed_submissions() -> SignedSubmissions<T> {
382		SignedSubmissions::<T>::get()
383	}
384
385	/// Finish the signed phase. Process the signed submissions from best to worse until a valid one
386	/// is found, rewarding the best one and slashing the invalid ones along the way.
387	///
388	/// Returns true if we have a good solution in the signed phase.
389	///
390	/// This drains the [`SignedSubmissions`], potentially storing the best valid one in
391	/// [`QueuedSolution`].
392	///
393	/// This is a *self-weighing* function, it automatically registers its weight internally when
394	/// being called.
395	pub fn finalize_signed_phase() -> bool {
396		let (weight, found_solution) = Self::finalize_signed_phase_internal();
397		Self::register_weight(weight);
398		found_solution
399	}
400
401	/// The guts of [`finalized_signed_phase`], that does everything except registering its weight.
402	pub(crate) fn finalize_signed_phase_internal() -> (Weight, bool) {
403		let mut all_submissions = Self::signed_submissions();
404		let mut found_solution = false;
405		let mut weight = T::DbWeight::get().reads(1);
406
407		let SolutionOrSnapshotSize { voters, targets } =
408			SnapshotMetadata::<T>::get().unwrap_or_default();
409
410		while let Some(best) = all_submissions.pop_last() {
411			log!(
412				debug,
413				"finalized_signed: trying to verify from {:?} score {:?}",
414				best.who,
415				best.raw_solution.score
416			);
417			let SignedSubmission { raw_solution, who, deposit, call_fee } = best;
418			let active_voters = raw_solution.solution.voter_count() as u32;
419			let feasibility_weight = {
420				// defensive only: at the end of signed phase, snapshot will exits.
421				let desired_targets =
422					crate::DesiredTargets::<T>::get().defensive_unwrap_or_default();
423				T::WeightInfo::feasibility_check(voters, targets, active_voters, desired_targets)
424			};
425
426			// the feasibility check itself has some weight
427			weight = weight.saturating_add(feasibility_weight);
428			match Self::feasibility_check(raw_solution, ElectionCompute::Signed) {
429				Ok(ready_solution) => {
430					Self::finalize_signed_phase_accept_solution(
431						ready_solution,
432						&who,
433						deposit,
434						call_fee,
435					);
436					found_solution = true;
437					log!(debug, "finalized_signed: found a valid solution");
438
439					weight = weight
440						.saturating_add(T::WeightInfo::finalize_signed_phase_accept_solution());
441					break
442				},
443				Err(_) => {
444					log!(warn, "finalized_signed: invalid signed submission found, slashing.");
445					Self::finalize_signed_phase_reject_solution(&who, deposit);
446					weight = weight
447						.saturating_add(T::WeightInfo::finalize_signed_phase_reject_solution());
448				},
449			}
450		}
451
452		// Any unprocessed solution is pointless to even consider. Feasible or malicious,
453		// they didn't end up being used. Unreserve the bonds.
454		let discarded = all_submissions.len();
455		let mut refund_count = 0;
456		let max_refunds = T::SignedMaxRefunds::get();
457
458		for SignedSubmission { who, deposit, call_fee, .. } in
459			all_submissions.drain_submitted_order()
460		{
461			if refund_count < max_refunds {
462				// Refund fee
463				let positive_imbalance = T::Currency::deposit_creating(&who, call_fee);
464				T::RewardHandler::on_unbalanced(positive_imbalance);
465				refund_count += 1;
466			}
467
468			// Unreserve deposit
469			let _remaining = T::Currency::unreserve(&who, deposit);
470			debug_assert!(_remaining.is_zero());
471			weight = weight.saturating_add(T::DbWeight::get().reads_writes(1, 2));
472		}
473
474		debug_assert!(!SignedSubmissionIndices::<T>::exists());
475		debug_assert!(!SignedSubmissionNextIndex::<T>::exists());
476		debug_assert!(SignedSubmissionsMap::<T>::iter().next().is_none());
477
478		log!(
479			debug,
480			"closed signed phase, found solution? {}, discarded {}",
481			found_solution,
482			discarded
483		);
484
485		(weight, found_solution)
486	}
487	/// Helper function for the case where a solution is accepted in the signed phase.
488	///
489	/// Extracted to facilitate with weight calculation.
490	///
491	/// Infallible
492	pub fn finalize_signed_phase_accept_solution(
493		ready_solution: ReadySolutionOf<T::MinerConfig>,
494		who: &T::AccountId,
495		deposit: BalanceOf<T>,
496		call_fee: BalanceOf<T>,
497	) {
498		// write this ready solution.
499		QueuedSolution::<T>::put(ready_solution);
500
501		let reward = T::SignedRewardBase::get();
502		// emit reward event
503		Self::deposit_event(crate::Event::Rewarded { account: who.clone(), value: reward });
504
505		// Unreserve deposit.
506		let _remaining = T::Currency::unreserve(who, deposit);
507		debug_assert!(_remaining.is_zero());
508
509		// Reward and refund the call fee.
510		let positive_imbalance =
511			T::Currency::deposit_creating(who, reward.saturating_add(call_fee));
512		T::RewardHandler::on_unbalanced(positive_imbalance);
513	}
514
515	/// Helper function for the case where a solution is accepted in the rejected phase.
516	///
517	/// Extracted to facilitate with weight calculation.
518	///
519	/// Infallible
520	pub fn finalize_signed_phase_reject_solution(who: &T::AccountId, deposit: BalanceOf<T>) {
521		Self::deposit_event(crate::Event::Slashed { account: who.clone(), value: deposit });
522		let (negative_imbalance, _remaining) = T::Currency::slash_reserved(who, deposit);
523		debug_assert!(_remaining.is_zero());
524		T::SlashHandler::on_unbalanced(negative_imbalance);
525	}
526
527	/// The weight of the given raw solution.
528	pub fn solution_weight_of(
529		raw_solution: &RawSolution<SolutionOf<T::MinerConfig>>,
530		size: SolutionOrSnapshotSize,
531	) -> Weight {
532		T::MinerConfig::solution_weight(
533			size.voters,
534			size.targets,
535			raw_solution.solution.voter_count() as u32,
536			raw_solution.solution.unique_targets().len() as u32,
537		)
538	}
539
540	/// Collect a sufficient deposit to store this solution.
541	///
542	/// The deposit is composed of 3 main elements:
543	///
544	/// 1. base deposit, fixed for all submissions.
545	/// 2. a per-byte deposit, for renting the state usage.
546	/// 3. a per-weight deposit, for the potential weight usage in an upcoming on_initialize
547	pub fn deposit_for(
548		raw_solution: &RawSolution<SolutionOf<T::MinerConfig>>,
549		size: SolutionOrSnapshotSize,
550	) -> BalanceOf<T> {
551		let encoded_len: u32 = raw_solution.encoded_size().saturated_into();
552		let encoded_len_balance: BalanceOf<T> = encoded_len.into();
553		let feasibility_weight = Self::solution_weight_of(raw_solution, size);
554
555		let len_deposit = T::SignedDepositByte::get().saturating_mul(encoded_len_balance);
556		let weight_deposit = T::SignedDepositWeight::get()
557			.saturating_mul(feasibility_weight.ref_time().saturated_into());
558
559		T::SignedDepositBase::convert(Self::signed_submissions().len())
560			.saturating_add(len_deposit)
561			.saturating_add(weight_deposit)
562	}
563}
564
565#[cfg(test)]
566mod tests {
567	use super::*;
568	use crate::{
569		mock::*, CurrentPhase, ElectionCompute, ElectionError, Error, Event, Perbill, Phase, Round,
570	};
571	use frame_election_provider_support::bounds::ElectionBoundsBuilder;
572	use frame_support::{assert_noop, assert_ok, assert_storage_noop};
573	use sp_runtime::Percent;
574
575	#[test]
576	fn cannot_submit_on_different_round() {
577		ExtBuilder::default().build_and_execute(|| {
578			// roll to a few rounds ahead.
579			roll_to_round(5);
580			assert_eq!(Round::<Runtime>::get(), 5);
581
582			roll_to_signed();
583			assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
584
585			// create a temp snapshot only for this test.
586			MultiPhase::create_snapshot().unwrap();
587			let mut solution = raw_solution();
588
589			// try a solution prepared in a previous round.
590			solution.round = Round::<Runtime>::get() - 1;
591
592			assert_noop!(
593				MultiPhase::submit(RuntimeOrigin::signed(10), Box::new(solution)),
594				Error::<Runtime>::PreDispatchDifferentRound,
595			);
596
597			// try a solution prepared in a later round (not expected to happen, but in any case).
598			MultiPhase::create_snapshot().unwrap();
599			let mut solution = raw_solution();
600			solution.round = Round::<Runtime>::get() + 1;
601
602			assert_noop!(
603				MultiPhase::submit(RuntimeOrigin::signed(10), Box::new(solution)),
604				Error::<Runtime>::PreDispatchDifferentRound,
605			);
606		})
607	}
608
609	#[test]
610	fn cannot_submit_too_early() {
611		ExtBuilder::default().build_and_execute(|| {
612			roll_to(2);
613			assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
614
615			// create a temp snapshot only for this test.
616			MultiPhase::create_snapshot().unwrap();
617			let solution = raw_solution();
618
619			assert_noop!(
620				MultiPhase::submit(RuntimeOrigin::signed(10), Box::new(solution)),
621				Error::<Runtime>::PreDispatchEarlySubmission,
622			);
623
624			// make sure invariants hold true and post-test try state checks to pass.
625			crate::Snapshot::<Runtime>::kill();
626			crate::SnapshotMetadata::<Runtime>::kill();
627			crate::DesiredTargets::<Runtime>::kill();
628		})
629	}
630
631	#[test]
632	fn data_provider_should_respect_target_limits() {
633		ExtBuilder::default().build_and_execute(|| {
634			// given a reduced expectation of maximum electable targets
635			let new_bounds = ElectionBoundsBuilder::default().targets_count(2.into()).build();
636			ElectionsBounds::set(new_bounds);
637			// and a data provider that does not respect limits
638			DataProviderAllowBadData::set(true);
639
640			assert_noop!(
641				MultiPhase::create_snapshot(),
642				ElectionError::DataProvider("Ensure targets bounds: bounds exceeded."),
643			);
644		})
645	}
646
647	#[test]
648	fn data_provider_should_respect_voter_limits() {
649		ExtBuilder::default().build_and_execute(|| {
650			// given a reduced expectation of maximum electing voters
651			let new_bounds = ElectionBoundsBuilder::default().voters_count(2.into()).build();
652			ElectionsBounds::set(new_bounds);
653			// and a data provider that does not respect limits
654			DataProviderAllowBadData::set(true);
655
656			assert_noop!(
657				MultiPhase::create_snapshot(),
658				ElectionError::DataProvider("Ensure voters bounds: bounds exceeded."),
659			);
660		})
661	}
662
663	#[test]
664	fn desired_targets_greater_than_max_winners() {
665		ExtBuilder::default().build_and_execute(|| {
666			// given desired_targets bigger than MaxWinners
667			DesiredTargets::set(4);
668			MaxWinners::set(3);
669
670			// snapshot not created because data provider returned an unexpected number of
671			// desired_targets
672			assert_noop!(
673				MultiPhase::create_snapshot_external(),
674				ElectionError::DataProvider("desired_targets must not be greater than MaxWinners."),
675			);
676		})
677	}
678
679	#[test]
680	fn should_pay_deposit() {
681		ExtBuilder::default().build_and_execute(|| {
682			roll_to_signed();
683			assert!(CurrentPhase::<Runtime>::get().is_signed());
684
685			let solution = raw_solution();
686			assert_eq!(balances(&99), (100, 0));
687
688			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
689
690			assert_eq!(balances(&99), (95, 5));
691			assert_eq!(MultiPhase::signed_submissions().iter().next().unwrap().deposit, 5);
692
693			assert_eq!(
694				multi_phase_events(),
695				vec![
696					Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
697					Event::SolutionStored {
698						compute: ElectionCompute::Signed,
699						origin: Some(99),
700						prev_ejected: false
701					}
702				]
703			);
704		})
705	}
706
707	#[test]
708	fn good_solution_is_rewarded() {
709		ExtBuilder::default().build_and_execute(|| {
710			roll_to_signed();
711			assert!(CurrentPhase::<Runtime>::get().is_signed());
712
713			let solution = raw_solution();
714			assert_eq!(balances(&99), (100, 0));
715
716			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
717			assert_eq!(balances(&99), (95, 5));
718
719			assert!(MultiPhase::finalize_signed_phase());
720			assert_eq!(balances(&99), (100 + 7 + 8, 0));
721
722			assert_eq!(
723				multi_phase_events(),
724				vec![
725					Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
726					Event::SolutionStored {
727						compute: ElectionCompute::Signed,
728						origin: Some(99),
729						prev_ejected: false
730					},
731					Event::Rewarded { account: 99, value: 7 }
732				]
733			);
734		})
735	}
736
737	#[test]
738	fn bad_solution_is_slashed() {
739		ExtBuilder::default().build_and_execute(|| {
740			roll_to_signed();
741			assert!(CurrentPhase::<Runtime>::get().is_signed());
742
743			let mut solution = raw_solution();
744			assert_eq!(balances(&99), (100, 0));
745
746			// make the solution invalid.
747			solution.score.minimal_stake += 1;
748
749			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
750			assert_eq!(balances(&99), (95, 5));
751
752			// no good solution was stored.
753			assert!(!MultiPhase::finalize_signed_phase());
754			// and the bond is gone.
755			assert_eq!(balances(&99), (95, 0));
756
757			assert_eq!(
758				multi_phase_events(),
759				vec![
760					Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
761					Event::SolutionStored {
762						compute: ElectionCompute::Signed,
763						origin: Some(99),
764						prev_ejected: false
765					},
766					Event::Slashed { account: 99, value: 5 }
767				]
768			);
769		})
770	}
771
772	#[test]
773	fn suppressed_solution_gets_bond_back() {
774		ExtBuilder::default().build_and_execute(|| {
775			roll_to_signed();
776			assert!(CurrentPhase::<Runtime>::get().is_signed());
777
778			let mut solution = raw_solution();
779			assert_eq!(balances(&99), (100, 0));
780			assert_eq!(balances(&999), (100, 0));
781
782			// submit as correct.
783			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution.clone())));
784
785			// make the solution invalid and weaker.
786			solution.score.minimal_stake -= 1;
787			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(999), Box::new(solution)));
788			assert_eq!(balances(&99), (95, 5));
789			assert_eq!(balances(&999), (95, 5));
790
791			// _some_ good solution was stored.
792			assert!(MultiPhase::finalize_signed_phase());
793
794			// 99 is rewarded.
795			assert_eq!(balances(&99), (100 + 7 + 8, 0));
796			// 999 gets everything back, including the call fee.
797			assert_eq!(balances(&999), (100 + 8, 0));
798			assert_eq!(
799				multi_phase_events(),
800				vec![
801					Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
802					Event::SolutionStored {
803						compute: ElectionCompute::Signed,
804						origin: Some(99),
805						prev_ejected: false
806					},
807					Event::SolutionStored {
808						compute: ElectionCompute::Signed,
809						origin: Some(999),
810						prev_ejected: false
811					},
812					Event::Rewarded { account: 99, value: 7 }
813				]
814			);
815		})
816	}
817
818	#[test]
819	fn cannot_submit_worse_with_full_queue() {
820		ExtBuilder::default().build_and_execute(|| {
821			roll_to_signed();
822			assert!(CurrentPhase::<Runtime>::get().is_signed());
823
824			for s in 0..SignedMaxSubmissions::get() {
825				// score is always getting better
826				let solution = RawSolution {
827					score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
828					..Default::default()
829				};
830				assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
831			}
832
833			// weaker.
834			let solution = RawSolution {
835				score: ElectionScore { minimal_stake: 4, ..Default::default() },
836				..Default::default()
837			};
838
839			assert_noop!(
840				MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)),
841				Error::<Runtime>::SignedQueueFull,
842			);
843		})
844	}
845
846	#[test]
847	fn geometric_deposit_queue_size_works() {
848		let constant = vec![1000; 10];
849		// geometric progression with 10% increase in each iteration for 10 terms.
850		let progression_10 = vec![1000, 1100, 1210, 1331, 1464, 1610, 1771, 1948, 2143, 2357];
851		let progression_40 = vec![1000, 1400, 1960, 2744, 3841, 5378, 7529, 10541, 14757, 20661];
852
853		let check_progressive_base_fee = |expected: &Vec<u64>| {
854			for s in 0..SignedMaxSubmissions::get() {
855				let account = 99 + s as u64;
856				Balances::make_free_balance_be(&account, 10000000);
857				let mut solution = raw_solution();
858				solution.score.minimal_stake -= s as u128;
859
860				assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(account), Box::new(solution)));
861				assert_eq!(balances(&account).1, expected[s as usize])
862			}
863		};
864
865		ExtBuilder::default()
866			.signed_max_submission(10)
867			.signed_base_deposit(1000, true, Percent::from_percent(0))
868			.build_and_execute(|| {
869				roll_to_signed();
870				assert!(CurrentPhase::<Runtime>::get().is_signed());
871
872				check_progressive_base_fee(&constant);
873			});
874
875		ExtBuilder::default()
876			.signed_max_submission(10)
877			.signed_base_deposit(1000, true, Percent::from_percent(10))
878			.build_and_execute(|| {
879				roll_to_signed();
880				assert!(CurrentPhase::<Runtime>::get().is_signed());
881
882				check_progressive_base_fee(&progression_10);
883			});
884
885		ExtBuilder::default()
886			.signed_max_submission(10)
887			.signed_base_deposit(1000, true, Percent::from_percent(40))
888			.build_and_execute(|| {
889				roll_to_signed();
890				assert!(CurrentPhase::<Runtime>::get().is_signed());
891
892				check_progressive_base_fee(&progression_40);
893			});
894	}
895
896	#[test]
897	fn call_fee_refund_is_limited_by_signed_max_refunds() {
898		ExtBuilder::default().build_and_execute(|| {
899			roll_to_signed();
900			assert!(CurrentPhase::<Runtime>::get().is_signed());
901			assert_eq!(SignedMaxRefunds::get(), 1);
902			assert!(SignedMaxSubmissions::get() > 2);
903
904			for s in 0..SignedMaxSubmissions::get() {
905				let account = 99 + s as u64;
906				Balances::make_free_balance_be(&account, 100);
907				// score is always decreasing
908				let mut solution = raw_solution();
909				solution.score.minimal_stake -= s as u128;
910
911				assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(account), Box::new(solution)));
912				assert_eq!(balances(&account), (95, 5));
913			}
914
915			assert_ok!(MultiPhase::do_elect());
916
917			for s in 0..SignedMaxSubmissions::get() {
918				let account = 99 + s as u64;
919				// lower accounts have higher scores
920				if s == 0 {
921					// winning solution always gets call fee + reward
922					assert_eq!(balances(&account), (100 + 8 + 7, 0))
923				} else if s == 1 {
924					// 1 runner up gets their call fee refunded
925					assert_eq!(balances(&account), (100 + 8, 0))
926				} else {
927					// all other solutions don't get a call fee refund
928					assert_eq!(balances(&account), (100, 0));
929				}
930			}
931			assert_eq!(
932				multi_phase_events(),
933				vec![
934					Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
935					Event::SolutionStored {
936						compute: ElectionCompute::Signed,
937						origin: Some(99),
938						prev_ejected: false
939					},
940					Event::SolutionStored {
941						compute: ElectionCompute::Signed,
942						origin: Some(100),
943						prev_ejected: false
944					},
945					Event::SolutionStored {
946						compute: ElectionCompute::Signed,
947						origin: Some(101),
948						prev_ejected: false
949					},
950					Event::SolutionStored {
951						compute: ElectionCompute::Signed,
952						origin: Some(102),
953						prev_ejected: false
954					},
955					Event::SolutionStored {
956						compute: ElectionCompute::Signed,
957						origin: Some(103),
958						prev_ejected: false
959					},
960					Event::Rewarded { account: 99, value: 7 },
961					Event::ElectionFinalized {
962						compute: ElectionCompute::Signed,
963						score: ElectionScore {
964							minimal_stake: 40,
965							sum_stake: 100,
966							sum_stake_squared: 5200
967						}
968					}
969				]
970			);
971		});
972	}
973
974	#[test]
975	fn cannot_submit_worse_with_full_queue_depends_on_threshold() {
976		ExtBuilder::default()
977			.signed_max_submission(1)
978			.better_signed_threshold(Perbill::from_percent(20))
979			.build_and_execute(|| {
980				roll_to_signed();
981				assert!(CurrentPhase::<Runtime>::get().is_signed());
982
983				let mut solution = RawSolution {
984					score: ElectionScore {
985						minimal_stake: 5u128,
986						sum_stake: 0u128,
987						sum_stake_squared: 10u128,
988					},
989					..Default::default()
990				};
991				assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
992
993				// This is 10% better, so does not meet the 20% threshold and is therefore rejected.
994				solution = RawSolution {
995					score: ElectionScore {
996						minimal_stake: 5u128,
997						sum_stake: 0u128,
998						sum_stake_squared: 9u128,
999					},
1000					..Default::default()
1001				};
1002
1003				assert_noop!(
1004					MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)),
1005					Error::<Runtime>::SignedQueueFull,
1006				);
1007
1008				// This is however 30% better and should therefore be accepted.
1009				solution = RawSolution {
1010					score: ElectionScore {
1011						minimal_stake: 5u128,
1012						sum_stake: 0u128,
1013						sum_stake_squared: 7u128,
1014					},
1015					..Default::default()
1016				};
1017
1018				assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1019				assert_eq!(
1020					multi_phase_events(),
1021					vec![
1022						Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1023						Event::SolutionStored {
1024							compute: ElectionCompute::Signed,
1025							origin: Some(99),
1026							prev_ejected: false
1027						},
1028						Event::SolutionStored {
1029							compute: ElectionCompute::Signed,
1030							origin: Some(99),
1031							prev_ejected: true
1032						}
1033					]
1034				);
1035			})
1036	}
1037
1038	#[test]
1039	fn weakest_is_removed_if_better_provided() {
1040		ExtBuilder::default().build_and_execute(|| {
1041			roll_to_signed();
1042			assert!(CurrentPhase::<Runtime>::get().is_signed());
1043
1044			for s in 0..SignedMaxSubmissions::get() {
1045				let account = 99 + s as u64;
1046				Balances::make_free_balance_be(&account, 100);
1047				// score is always getting better
1048				let solution = RawSolution {
1049					score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
1050					..Default::default()
1051				};
1052				assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(account), Box::new(solution)));
1053				assert_eq!(balances(&account), (95, 5));
1054			}
1055
1056			assert_eq!(
1057				MultiPhase::signed_submissions()
1058					.iter()
1059					.map(|s| s.raw_solution.score.minimal_stake)
1060					.collect::<Vec<_>>(),
1061				vec![5, 6, 7, 8, 9]
1062			);
1063
1064			// better.
1065			let solution = RawSolution {
1066				score: ElectionScore { minimal_stake: 20, ..Default::default() },
1067				..Default::default()
1068			};
1069			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(999), Box::new(solution)));
1070
1071			// the one with score 5 was rejected, the new one inserted.
1072			assert_eq!(
1073				MultiPhase::signed_submissions()
1074					.iter()
1075					.map(|s| s.raw_solution.score.minimal_stake)
1076					.collect::<Vec<_>>(),
1077				vec![6, 7, 8, 9, 20]
1078			);
1079
1080			// the submitter of the ejected solution does *not* get a call fee refund
1081			assert_eq!(balances(&(99 + 0)), (100, 0));
1082		})
1083	}
1084
1085	#[test]
1086	fn replace_weakest_by_score_works() {
1087		ExtBuilder::default().signed_max_submission(3).build_and_execute(|| {
1088			roll_to_signed();
1089			assert!(CurrentPhase::<Runtime>::get().is_signed());
1090
1091			for s in 1..SignedMaxSubmissions::get() {
1092				// score is always getting better
1093				let solution = RawSolution {
1094					score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
1095					..Default::default()
1096				};
1097				assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1098			}
1099
1100			let solution = RawSolution {
1101				score: ElectionScore { minimal_stake: 4, ..Default::default() },
1102				..Default::default()
1103			};
1104			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1105
1106			assert_eq!(
1107				MultiPhase::signed_submissions()
1108					.iter()
1109					.map(|s| s.raw_solution.score.minimal_stake)
1110					.collect::<Vec<_>>(),
1111				vec![4, 6, 7],
1112			);
1113
1114			// better.
1115			let solution = RawSolution {
1116				score: ElectionScore { minimal_stake: 5, ..Default::default() },
1117				..Default::default()
1118			};
1119			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1120
1121			// the one with score 5 was rejected, the new one inserted.
1122			assert_eq!(
1123				MultiPhase::signed_submissions()
1124					.iter()
1125					.map(|s| s.raw_solution.score.minimal_stake)
1126					.collect::<Vec<_>>(),
1127				vec![5, 6, 7],
1128			);
1129		})
1130	}
1131
1132	#[test]
1133	fn early_ejected_solution_gets_bond_back() {
1134		ExtBuilder::default().signed_deposit(2, 0, 0).build_and_execute(|| {
1135			roll_to_signed();
1136			assert!(CurrentPhase::<Runtime>::get().is_signed());
1137
1138			for s in 0..SignedMaxSubmissions::get() {
1139				// score is always getting better
1140				let solution = RawSolution {
1141					score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
1142					..Default::default()
1143				};
1144				assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1145			}
1146
1147			assert_eq!(balances(&99).1, 2 * 5);
1148			assert_eq!(balances(&999).1, 0);
1149
1150			// better.
1151			let solution = RawSolution {
1152				score: ElectionScore { minimal_stake: 20, ..Default::default() },
1153				..Default::default()
1154			};
1155			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(999), Box::new(solution)));
1156
1157			// got one bond back.
1158			assert_eq!(balances(&99).1, 2 * 4);
1159			assert_eq!(balances(&999).1, 2);
1160		})
1161	}
1162
1163	#[test]
1164	fn equally_good_solution_is_not_accepted_when_queue_full() {
1165		// because in ordering of solutions, an older solution has higher priority and should stay.
1166		ExtBuilder::default().signed_max_submission(3).build_and_execute(|| {
1167			roll_to_signed();
1168			assert!(CurrentPhase::<Runtime>::get().is_signed());
1169
1170			for i in 0..SignedMaxSubmissions::get() {
1171				let solution = RawSolution {
1172					score: ElectionScore { minimal_stake: (5 + i).into(), ..Default::default() },
1173					..Default::default()
1174				};
1175				assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1176			}
1177
1178			assert_eq!(
1179				MultiPhase::signed_submissions()
1180					.iter()
1181					.map(|s| s.raw_solution.score.minimal_stake)
1182					.collect::<Vec<_>>(),
1183				vec![5, 6, 7]
1184			);
1185
1186			// 5 is not accepted. This will only cause processing with no benefit.
1187			let solution = RawSolution {
1188				score: ElectionScore { minimal_stake: 5, ..Default::default() },
1189				..Default::default()
1190			};
1191			assert_noop!(
1192				MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)),
1193				Error::<Runtime>::SignedQueueFull,
1194			);
1195		})
1196	}
1197
1198	#[test]
1199	fn equally_good_solution_is_accepted_when_queue_not_full() {
1200		// because in ordering of solutions, an older solution has higher priority and should stay.
1201		ExtBuilder::default().signed_max_submission(3).build_and_execute(|| {
1202			roll_to(15);
1203			assert!(CurrentPhase::<Runtime>::get().is_signed());
1204
1205			let solution = RawSolution {
1206				score: ElectionScore { minimal_stake: 5, ..Default::default() },
1207				..Default::default()
1208			};
1209			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1210
1211			assert_eq!(
1212				MultiPhase::signed_submissions()
1213					.iter()
1214					.map(|s| (s.who, s.raw_solution.score.minimal_stake,))
1215					.collect::<Vec<_>>(),
1216				vec![(99, 5)]
1217			);
1218
1219			roll_to(16);
1220			let solution = RawSolution {
1221				score: ElectionScore { minimal_stake: 5, ..Default::default() },
1222				..Default::default()
1223			};
1224			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(999), Box::new(solution)));
1225
1226			assert_eq!(
1227				MultiPhase::signed_submissions()
1228					.iter()
1229					.map(|s| (s.who, s.raw_solution.score.minimal_stake,))
1230					.collect::<Vec<_>>(),
1231				vec![(999, 5), (99, 5)]
1232			);
1233
1234			let solution = RawSolution {
1235				score: ElectionScore { minimal_stake: 6, ..Default::default() },
1236				..Default::default()
1237			};
1238			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(9999), Box::new(solution)));
1239
1240			assert_eq!(
1241				MultiPhase::signed_submissions()
1242					.iter()
1243					.map(|s| (s.who, s.raw_solution.score.minimal_stake,))
1244					.collect::<Vec<_>>(),
1245				vec![(999, 5), (99, 5), (9999, 6)]
1246			);
1247		})
1248	}
1249
1250	#[test]
1251	fn all_equal_score() {
1252		// because in ordering of solutions, an older solution has higher priority and should stay.
1253		ExtBuilder::default().signed_max_submission(3).build_and_execute(|| {
1254			roll_to(15);
1255			assert!(CurrentPhase::<Runtime>::get().is_signed());
1256
1257			for i in 0..SignedMaxSubmissions::get() {
1258				roll_to((15 + i).into());
1259				let solution = raw_solution();
1260				assert_ok!(MultiPhase::submit(
1261					RuntimeOrigin::signed(100 + i as AccountId),
1262					Box::new(solution)
1263				));
1264			}
1265
1266			assert_eq!(
1267				MultiPhase::signed_submissions()
1268					.iter()
1269					.map(|s| (s.who, s.raw_solution.score.minimal_stake))
1270					.collect::<Vec<_>>(),
1271				vec![(102, 40), (101, 40), (100, 40)]
1272			);
1273
1274			roll_to(25);
1275
1276			// The first one that will actually get verified is the last one.
1277			assert_eq!(
1278				multi_phase_events(),
1279				vec![
1280					Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1281					Event::SolutionStored {
1282						compute: ElectionCompute::Signed,
1283						origin: Some(100),
1284						prev_ejected: false
1285					},
1286					Event::SolutionStored {
1287						compute: ElectionCompute::Signed,
1288						origin: Some(101),
1289						prev_ejected: false
1290					},
1291					Event::SolutionStored {
1292						compute: ElectionCompute::Signed,
1293						origin: Some(102),
1294						prev_ejected: false
1295					},
1296					Event::Rewarded { account: 100, value: 7 },
1297					Event::PhaseTransitioned {
1298						from: Phase::Signed,
1299						to: Phase::Unsigned((true, 25)),
1300						round: 1
1301					},
1302				]
1303			);
1304		})
1305	}
1306
1307	#[test]
1308	fn all_in_one_signed_submission_scenario() {
1309		// a combination of:
1310		// - good_solution_is_rewarded
1311		// - bad_solution_is_slashed
1312		// - suppressed_solution_gets_bond_back
1313		ExtBuilder::default().build_and_execute(|| {
1314			roll_to_signed();
1315			assert!(CurrentPhase::<Runtime>::get().is_signed());
1316
1317			assert_eq!(balances(&99), (100, 0));
1318			assert_eq!(balances(&999), (100, 0));
1319			assert_eq!(balances(&9999), (100, 0));
1320
1321			let solution = raw_solution();
1322
1323			// submit a correct one.
1324			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution.clone())));
1325
1326			// make the solution invalidly better and submit. This ought to be slashed.
1327			let mut solution_999 = solution.clone();
1328			solution_999.score.minimal_stake += 1;
1329			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(999), Box::new(solution_999)));
1330
1331			// make the solution invalidly worse and submit. This ought to be suppressed and
1332			// returned.
1333			let mut solution_9999 = solution.clone();
1334			solution_9999.score.minimal_stake -= 1;
1335			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(9999), Box::new(solution_9999)));
1336
1337			assert_eq!(
1338				MultiPhase::signed_submissions().iter().map(|x| x.who).collect::<Vec<_>>(),
1339				vec![9999, 99, 999]
1340			);
1341
1342			// _some_ good solution was stored.
1343			assert!(MultiPhase::finalize_signed_phase());
1344
1345			// 99 is rewarded.
1346			assert_eq!(balances(&99), (100 + 7 + 8, 0));
1347			// 999 is slashed.
1348			assert_eq!(balances(&999), (95, 0));
1349			// 9999 gets everything back, including the call fee.
1350			assert_eq!(balances(&9999), (100 + 8, 0));
1351			assert_eq!(
1352				multi_phase_events(),
1353				vec![
1354					Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1355					Event::SolutionStored {
1356						compute: ElectionCompute::Signed,
1357						origin: Some(99),
1358						prev_ejected: false
1359					},
1360					Event::SolutionStored {
1361						compute: ElectionCompute::Signed,
1362						origin: Some(999),
1363						prev_ejected: false
1364					},
1365					Event::SolutionStored {
1366						compute: ElectionCompute::Signed,
1367						origin: Some(9999),
1368						prev_ejected: false
1369					},
1370					Event::Slashed { account: 999, value: 5 },
1371					Event::Rewarded { account: 99, value: 7 }
1372				]
1373			);
1374		})
1375	}
1376
1377	#[test]
1378	fn cannot_consume_too_much_future_weight() {
1379		ExtBuilder::default()
1380			.signed_weight(Weight::from_parts(40, u64::MAX))
1381			.mock_weight_info(MockedWeightInfo::Basic)
1382			.build_and_execute(|| {
1383				roll_to_signed();
1384				assert!(CurrentPhase::<Runtime>::get().is_signed());
1385
1386				let (raw, witness, _) = MultiPhase::mine_solution().unwrap();
1387				let solution_weight = <Runtime as MinerConfig>::solution_weight(
1388					witness.voters,
1389					witness.targets,
1390					raw.solution.voter_count() as u32,
1391					raw.solution.unique_targets().len() as u32,
1392				);
1393				// default solution will have 5 edges (5 * 5 + 10)
1394				assert_eq!(solution_weight, Weight::from_parts(35, 0));
1395				assert_eq!(raw.solution.voter_count(), 5);
1396				assert_eq!(
1397					<Runtime as Config>::SignedMaxWeight::get(),
1398					Weight::from_parts(40, u64::MAX)
1399				);
1400
1401				assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(raw.clone())));
1402
1403				<SignedMaxWeight>::set(Weight::from_parts(30, u64::MAX));
1404
1405				// note: resubmitting the same solution is technically okay as long as the queue has
1406				// space.
1407				assert_noop!(
1408					MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(raw)),
1409					Error::<Runtime>::SignedTooMuchWeight,
1410				);
1411			})
1412	}
1413
1414	#[test]
1415	fn insufficient_deposit_does_not_store_submission() {
1416		ExtBuilder::default().build_and_execute(|| {
1417			roll_to_signed();
1418			assert!(CurrentPhase::<Runtime>::get().is_signed());
1419
1420			let solution = raw_solution();
1421
1422			assert_eq!(balances(&123), (0, 0));
1423			assert_noop!(
1424				MultiPhase::submit(RuntimeOrigin::signed(123), Box::new(solution)),
1425				Error::<Runtime>::SignedCannotPayDeposit,
1426			);
1427
1428			assert_eq!(balances(&123), (0, 0));
1429		})
1430	}
1431
1432	// given a full queue, and a solution which _should_ be allowed in, but the proposer of this
1433	// new solution has insufficient deposit, we should not modify storage at all
1434	#[test]
1435	fn insufficient_deposit_with_full_queue_works_properly() {
1436		ExtBuilder::default().build_and_execute(|| {
1437			roll_to_signed();
1438			assert!(CurrentPhase::<Runtime>::get().is_signed());
1439
1440			for s in 0..SignedMaxSubmissions::get() {
1441				// score is always getting better
1442				let solution = RawSolution {
1443					score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
1444					..Default::default()
1445				};
1446				assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1447			}
1448
1449			// this solution has a higher score than any in the queue
1450			let solution = RawSolution {
1451				score: ElectionScore {
1452					minimal_stake: (5 + SignedMaxSubmissions::get()).into(),
1453					..Default::default()
1454				},
1455				..Default::default()
1456			};
1457
1458			assert_eq!(balances(&123), (0, 0));
1459			assert_noop!(
1460				MultiPhase::submit(RuntimeOrigin::signed(123), Box::new(solution)),
1461				Error::<Runtime>::SignedCannotPayDeposit,
1462			);
1463
1464			assert_eq!(balances(&123), (0, 0));
1465		})
1466	}
1467
1468	#[test]
1469	fn finalize_signed_phase_is_idempotent_given_no_submissions() {
1470		ExtBuilder::default().build_and_execute(|| {
1471			for block_number in 0..25 {
1472				roll_to(block_number);
1473
1474				assert_eq!(SignedSubmissions::<Runtime>::decode_len().unwrap_or_default(), 0);
1475				assert_storage_noop!(MultiPhase::finalize_signed_phase_internal());
1476			}
1477		})
1478	}
1479
1480	#[test]
1481	fn finalize_signed_phase_is_idempotent_given_submissions() {
1482		ExtBuilder::default().build_and_execute(|| {
1483			roll_to_signed();
1484			assert!(CurrentPhase::<Runtime>::get().is_signed());
1485
1486			let solution = raw_solution();
1487
1488			// submit a correct one.
1489			assert_ok!(MultiPhase::submit(RuntimeOrigin::signed(99), Box::new(solution)));
1490
1491			// _some_ good solution was stored.
1492			assert!(MultiPhase::finalize_signed_phase());
1493
1494			// calling it again doesn't change anything
1495			assert_storage_noop!(MultiPhase::finalize_signed_phase());
1496
1497			assert_eq!(
1498				multi_phase_events(),
1499				vec![
1500					Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
1501					Event::SolutionStored {
1502						compute: ElectionCompute::Signed,
1503						origin: Some(99),
1504						prev_ejected: false
1505					},
1506					Event::Rewarded { account: 99, value: 7 }
1507				]
1508			);
1509		})
1510	}
1511}