referrerpolicy=no-referrer-when-downgrade

pallet_election_provider_multi_block/signed/
mod.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 of the multi-block election system.
19//!
20//! Signed submissions work on the basis of keeping a queue of submissions from unknown signed
21//! accounts, and sorting them based on the best claimed score to the worst.
22//!
23//! Each submission must put a deposit down. This is parameterize-able by the runtime, and might be
24//! a constant, linear or exponential value. See [`signed::Config::DepositPerPage`] and
25//! [`signed::Config::DepositBase`].
26//!
27//! During the queuing time, if the queue is full, and a better solution comes in, the weakest
28//! deposit is said to be **Ejected**. Ejected solutions get [`signed::Config::EjectGraceRatio`] of
29//! their deposit back. This is because we have to delete any submitted pages from them on the spot.
30//! They don't get any refund of whatever tx-fee they have paid.
31//!
32//! Once the time to evaluate the signed phase comes (`Phase::SignedValidation`), the solutions are
33//! checked from best-to-worst claim, and they end up in either of the 3 buckets:
34//!
35//! 1. **Rewarded**: If they are the first correct solution (and consequently the best one, since we
36//!    start evaluating from the best claim), they are rewarded. Rewarded solutions always get both
37//!    their deposit and transaction fee back, the latter up to [`signed::Config::MaxFeeRefund`].
38//! 2. **Slashed**: Any invalid solution that wasted valuable blockchain time gets slashed for their
39//!    deposit.
40//! 3. **Discarded**: Any solution after the first correct solution is eligible to be peacefully
41//!    discarded. But, to delete their data, they have to call
42//!    [`signed::Call::clear_old_round_data`]. Once done, they get their full deposit back. Accounts
43//!    in [`signed::Invulnerables`] also get their tx-fee back, up to
44//!    [`signed::Config::MaxFeeRefund`]. Other submitters' tx-fees are not refunded.
45//!
46//! ## Future Plans:
47//!
48//! **Lazy Deletion In Eject**: While most deletion ops of the signed phase are now lazy, if someone
49//! is ejected from the list, we still remove their data in sync.
50//!
51//! **Metadata update**: imagine you mis-computed your score.
52//!
53//! **Permissionless `clear_old_round_data`**: Anyone can clean anyone else's data, and get a part
54//! of their deposit.
55
56use crate::{
57	types::SolutionOf,
58	verifier::{AsynchronousVerifier, SolutionDataProvider, Status, VerificationResult},
59};
60use codec::{Decode, Encode, MaxEncodedLen};
61use frame_election_provider_support::PageIndex;
62use frame_support::{
63	dispatch::{DispatchResultWithPostInfo, GetDispatchInfo},
64	pallet_prelude::{StorageDoubleMap, ValueQuery, *},
65	traits::{
66		tokens::{
67			fungible::{
68				BalancedHold, Credit as FungibleCredit, Inspect, Mutate, MutateHold, Unbalanced,
69			},
70			Imbalance, Precision, Preservation,
71		},
72		Defensive, DefensiveSaturating, EstimateCallFee, EstimateFee, OnUnbalanced,
73	},
74	BoundedVec, Twox64Concat,
75};
76use frame_system::{ensure_signed, pallet_prelude::*};
77use scale_info::TypeInfo;
78use sp_io::MultiRemovalResults;
79use sp_npos_elections::ElectionScore;
80use sp_runtime::{
81	traits::{Saturating, Zero},
82	Perbill,
83};
84use sp_std::prelude::*;
85
86/// Explore all weights
87pub use crate::weights::traits::pallet_election_provider_multi_block_signed::*;
88/// Exports of this pallet
89pub use pallet::*;
90
91#[cfg(feature = "runtime-benchmarks")]
92mod benchmarking;
93
94pub(crate) type SignedWeightsOf<T> = <T as crate::signed::Config>::WeightInfo;
95
96#[cfg(test)]
97mod tests;
98
99type BalanceOf<T> =
100	<<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
101
102/// All of the (meta) data around a signed submission
103#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Default, DebugNoBound)]
104#[cfg_attr(test, derive(frame_support::PartialEqNoBound, frame_support::EqNoBound))]
105#[codec(mel_bound(T: Config))]
106#[scale_info(skip_type_params(T))]
107pub struct SubmissionMetadata<T: Config> {
108	/// The amount of deposit that has been held in reserve.
109	deposit: BalanceOf<T>,
110	/// The amount of transaction fee that this submission has cost for its submitter so far.
111	fee: BalanceOf<T>,
112	/// The amount of rewards that we expect to give to this submission, if deemed worthy.
113	reward: BalanceOf<T>,
114	/// The score that this submission is claiming to achieve.
115	claimed_score: ElectionScore,
116	/// A bounded-bool-vec of pages that have been submitted so far.
117	pages: BoundedVec<bool, T::Pages>,
118}
119
120/// Maximum number of entries in [`UnpaidRewards`].
121///
122/// Deferrals require a depleted pot and at most one entry is created per round, so a small fixed
123/// bound suffices for every runtime. When full, the oldest entry is evicted.
124pub const MAX_UNPAID_REWARDS: u32 = 16;
125
126/// A round-winner reward that failed to pay, pending a permissionless claim via
127/// [`Pallet::claim_unpaid_reward`]. Rewards only, not fee refunds, so `round` is a unique key.
128#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, DebugNoBound)]
129#[cfg_attr(test, derive(frame_support::PartialEqNoBound, frame_support::EqNoBound))]
130#[codec(mel_bound(T: Config))]
131#[scale_info(skip_type_params(T))]
132pub struct UnpaidReward<T: Config> {
133	/// The round the reward was owed for; unique among [`UnpaidRewards`] entries.
134	round: u32,
135	/// The winner the reward is owed to.
136	who: T::AccountId,
137	/// The amount owed.
138	amount: BalanceOf<T>,
139}
140
141impl<T: Config> crate::types::SignedInterface for Pallet<T> {
142	fn has_leader(round: u32) -> bool {
143		Submissions::<T>::has_leader(round)
144	}
145}
146
147impl<T: Config> SolutionDataProvider for Pallet<T> {
148	type Solution = SolutionOf<T::MinerConfig>;
149
150	// `get_page` should only be called when a leader exists.
151	// The verifier only transitions to `Status::Ongoing` when a leader is confirmed to exist.
152	// During verification, the leader should remain unchanged - it's only removed when
153	// verification fails (which immediately stops the verifier) or completes successfully.
154	fn get_page(page: PageIndex) -> Self::Solution {
155		let current_round = Self::current_round();
156		Submissions::<T>::leader(current_round)
157			.defensive()
158			.and_then(|(who, _score)| {
159				sublog!(
160					debug,
161					"signed",
162					"returning page {} of {:?}'s submission as leader.",
163					page,
164					who
165				);
166				Submissions::<T>::get_page_of(current_round, &who, page)
167			})
168			.unwrap_or_default()
169	}
170
171	// `get_score` should only be called when a leader exists.
172	fn get_score() -> ElectionScore {
173		let current_round = Self::current_round();
174		Submissions::<T>::leader(current_round)
175			.defensive()
176			.inspect(|(_who, score)| {
177				sublog!(
178					debug,
179					"signed",
180					"returning score {:?} of current leader for round {}.",
181					score,
182					current_round
183				);
184			})
185			.map(|(_who, score)| score)
186			.unwrap_or_default()
187	}
188
189	fn report_result(result: crate::verifier::VerificationResult) {
190		// assumption of the trait.
191		debug_assert!(matches!(<T::Verifier as AsynchronousVerifier>::status(), Status::Nothing));
192		let current_round = Self::current_round();
193
194		match result {
195			VerificationResult::Queued => {
196				// defensive: if there is a result to be reported, then we must have had some
197				// leader.
198				if let Some((winner, metadata)) =
199					Submissions::<T>::take_leader_with_data(Self::current_round()).defensive()
200				{
201					// first, let's give them their reward.
202					let reward =
203						metadata.reward.saturating_add(metadata.fee.min(T::MaxFeeRefund::get()));
204					Self::pay_reward(current_round, &winner, reward);
205
206					// then, unreserve their deposit
207					let _res = T::Currency::release(
208						&HoldReason::SignedSubmission.into(),
209						&winner,
210						metadata.deposit,
211						Precision::BestEffort,
212					);
213					debug_assert!(_res.is_ok());
214				}
215			},
216			VerificationResult::Rejected => {
217				Self::handle_solution_rejection(current_round);
218			},
219		}
220	}
221}
222
223/// Something that can compute the base deposit that is collected upon `register`.
224///
225/// A blanket impl allows for any `Get` to be used as-is, which will always return the said balance
226/// as deposit.
227pub trait CalculateBaseDeposit<Balance> {
228	fn calculate_base_deposit(existing_submitters: usize) -> Balance;
229}
230
231impl<Balance, G: Get<Balance>> CalculateBaseDeposit<Balance> for G {
232	fn calculate_base_deposit(_existing_submitters: usize) -> Balance {
233		G::get()
234	}
235}
236
237/// Something that can calculate the deposit per-page upon `submit`.
238///
239/// A blanket impl allows for any `Get` to be used as-is, which will always return the said balance
240/// as deposit **per page**.
241pub trait CalculatePageDeposit<Balance> {
242	fn calculate_page_deposit(existing_submitters: usize, page_size: usize) -> Balance;
243}
244
245impl<Balance: From<u32> + Saturating, G: Get<Balance>> CalculatePageDeposit<Balance> for G {
246	fn calculate_page_deposit(_existing_submitters: usize, page_size: usize) -> Balance {
247		let page_size: Balance = (page_size as u32).into();
248		G::get().saturating_mul(page_size)
249	}
250}
251
252/// Provides the account that reward payments and invulnerable fee refunds are drawn from.
253///
254/// `None` mints directly. `Some(account)` transfers from `account`, followed by a call to
255/// [`Self::paid`] for any bookkeeping the source needs to perform after a successful payout.
256pub trait RewardSource<AccountId, Balance> {
257	/// The pot account to draw the reward from, or `None` to mint directly.
258	fn account() -> Option<AccountId>;
259
260	/// Called after `amount` has been successfully transferred out of the pot.
261	fn paid(amount: Balance);
262}
263
264/// A [`RewardSource`] drawing from the account in `P`, with no issuance reactivation.
265///
266/// Correct only for pots holding *active* funds. A pot holding deactivated issuance, such as a
267/// DAP buffer, must use a source that reactivates in [`RewardSource::paid`].
268pub struct ActivePot<P>(sp_std::marker::PhantomData<P>);
269
270impl<AccountId, Balance, P: Get<Option<AccountId>>> RewardSource<AccountId, Balance>
271	for ActivePot<P>
272{
273	fn account() -> Option<AccountId> {
274		P::get()
275	}
276
277	fn paid(_amount: Balance) {}
278}
279
280/// A [`RewardSource`] drawing from the account in `P`, reactivating the paid amount in
281/// `Currency` afterwards.
282///
283/// For pots holding previously-deactivated issuance, such as a DAP buffer.
284pub struct ReactivatingPot<P, Currency>(sp_std::marker::PhantomData<(P, Currency)>);
285
286impl<AccountId, Balance, P, Currency> RewardSource<AccountId, Balance>
287	for ReactivatingPot<P, Currency>
288where
289	P: Get<Option<AccountId>>,
290	Currency: Unbalanced<AccountId, Balance = Balance>,
291{
292	fn account() -> Option<AccountId> {
293		P::get()
294	}
295
296	fn paid(amount: Balance) {
297		Currency::reactivate(amount);
298	}
299}
300
301/// A [`Config::MaxFeeRefund`] that is the fee of a full submission: one [`Pallet::register`], plus
302/// [`crate::Config::Pages`] [`Pallet::submit_page`] calls, each carrying a maximum-size solution
303/// page.
304///
305/// `F` is the fee model of the runtime, typically `pallet_transaction_payment::Pallet`. This is
306/// derived from the same inputs the pallet accrues from, so it follows any change to the page
307/// count, the solution type, or the fee model.
308pub struct FullSubmissionFee<T, F>(PhantomData<(T, F)>);
309
310impl<T: Config, F: EstimateFee<BalanceOf<T>>> Get<BalanceOf<T>> for FullSubmissionFee<T, F> {
311	fn get() -> BalanceOf<T> {
312		// top the length up with the largest page that `maybe_solution` could have carried.
313		let page_call = Call::<T>::submit_page { page: 0, maybe_solution: None };
314		let page_len = page_call
315			.encoded_size()
316			.saturating_add(SolutionOf::<T::MinerConfig>::max_encoded_len());
317		let per_page = F::estimate_fee(page_len as u32, &page_call.get_dispatch_info());
318
319		let register_call = Call::<T>::register { claimed_score: Default::default() };
320		let register = F::estimate_fee(
321			register_call.encoded_size() as u32,
322			&register_call.get_dispatch_info(),
323		);
324
325		register.saturating_add(per_page.saturating_mul(T::Pages::get().into()))
326	}
327}
328
329#[frame_support::pallet]
330pub mod pallet {
331	use super::*;
332
333	#[pallet::config]
334	#[pallet::disable_frame_system_supertrait_check]
335	pub trait Config: crate::Config {
336		/// Handler to the currency.
337		type Currency: Inspect<Self::AccountId>
338			+ Mutate<Self::AccountId>
339			+ MutateHold<Self::AccountId, Reason: From<HoldReason>>
340			+ BalancedHold<Self::AccountId>;
341
342		/// Base deposit amount for a submission.
343		type DepositBase: CalculateBaseDeposit<BalanceOf<Self>>;
344
345		/// Extra deposit per-page.
346		type DepositPerPage: CalculatePageDeposit<BalanceOf<Self>>;
347
348		/// The fixed deposit charged upon [`Pallet::register`] from [`Invulnerables`].
349		type InvulnerableDeposit: Get<BalanceOf<Self>>;
350
351		/// Base reward that is given to the winner.
352		type RewardBase: Get<BalanceOf<Self>>;
353
354		/// Maximum number of submissions. This, combined with `SignedValidationPhase` and `Pages`
355		/// dictates how many signed solutions we can verify.
356		type MaxSubmissions: Get<u32>;
357
358		/// The ratio of the deposit to return in case a signed account submits a solution via
359		/// [`Pallet::register`], but later calls [`Pallet::bail`].
360		///
361		/// This should be large enough to cover for the deletion cost of possible all pages. To be
362		/// safe, you can put it to 100% to begin with to fully dis-incentivize bailing.
363		type BailoutGraceRatio: Get<Perbill>;
364
365		/// The ratio of the deposit to return in case a signed account is ejected from the queue.
366		///
367		/// This value is assumed to be 100% for accounts that are in the invulnerable list,
368		/// which can only be set by governance.
369		type EjectGraceRatio: Get<Perbill>;
370
371		/// Handler to estimate the fee of a call. Useful to refund the transaction fee of the
372		/// submitter for the winner.
373		type EstimateCallFee: EstimateCallFee<Call<Self>, BalanceOf<Self>>;
374
375		/// Handler for slashed deposits. Use `()` to burn them, or redirect them elsewhere.
376		type Slash: OnUnbalanced<FungibleCredit<Self::AccountId, Self::Currency>>;
377
378		/// Source account for reward payments. `Some(pot)` transfers from that account; `None`
379		/// mints directly into the winner's account.
380		type RewardSource: RewardSource<Self::AccountId, BalanceOf<Self>>;
381
382		/// Ceiling on the transaction fee that is refunded to a submitter.
383		///
384		/// Each call that stores part of a submission accrues its transaction fee. That total is
385		/// refunded to the winner on top of [`Config::RewardBase`], and to an invulnerable whose
386		/// submission is discarded. This ceiling keeps that refund bounded by config.
387		///
388		/// Should cover one [`Pallet::register`] plus [`crate::Config::Pages`]
389		/// [`Pallet::submit_page`] calls at the maximum encoded page size. A smaller value
390		/// under-refunds a submitter that stores all of their pages.
391		type MaxFeeRefund: Get<BalanceOf<Self>>;
392
393		/// Provided weights of this pallet.
394		type WeightInfo: WeightInfo;
395	}
396
397	/// The hold reason of this palelt.
398	#[pallet::composite_enum]
399	pub enum HoldReason {
400		/// Because of submitting a signed solution.
401		#[codec(index = 0)]
402		SignedSubmission,
403	}
404
405	/// Accounts whitelisted by governance to always submit their solutions.
406	///
407	/// They are different in that:
408	///
409	/// * They always pay a fixed deposit for submission, specified by
410	///   [`Config::InvulnerableDeposit`]. They pay no page deposit.
411	/// * If _ejected_ by better solution from [`SortedScores`], they will get their full deposit
412	///   back.
413	/// * They always get their tx-fee back even if they are _discarded_, up to
414	///   [`Config::MaxFeeRefund`].
415	#[pallet::storage]
416	pub type Invulnerables<T: Config> =
417		StorageValue<_, BoundedVec<T::AccountId, ConstU32<16>>, ValueQuery>;
418
419	/// Round-winner rewards that failed to pay out of [`Config::RewardSource`], pending a
420	/// permissionless claim via [`Pallet::claim_unpaid_reward`].
421	///
422	/// Expected to stay empty in normal operation; only grows when the pot is depleted. Bounded
423	/// to [`MAX_UNPAID_REWARDS`] entries, pushed in round order, so if full, the oldest one is
424	/// evicted (see [`Pallet::pay_reward`]).
425	#[pallet::storage]
426	pub type UnpaidRewards<T: Config> =
427		StorageValue<_, BoundedVec<UnpaidReward<T>, ConstU32<MAX_UNPAID_REWARDS>>, ValueQuery>;
428
429	/// Wrapper type for signed submissions.
430	///
431	/// It handles 3 storage items:
432	///
433	/// 1. [`SortedScores`]: A flat vector of all submissions' `(submitter_id, claimed_score)`.
434	/// 2. [`SubmissionStorage`]: Paginated map of of all submissions, keyed by submitter and page.
435	/// 3. [`SubmissionMetadataStorage`]: Map from submitter to the metadata of their submission.
436	///
437	/// All storage items in this group are mapped, and their first key is the `round` to which they
438	/// belong to. In essence, we are storing multiple versions of each group.
439	///
440	/// ### Invariants:
441	///
442	/// This storage group is sane, clean, and consistent if the following invariants are held:
443	///
444	/// Among the submissions of each round:
445	/// - `SortedScores` should never contain duplicate account ids.
446	/// - For any account id in `SortedScores`, a corresponding value should exist in
447	/// `SubmissionMetadataStorage` under that account id's key.
448	///       - And the value of `metadata.score` must be equal to the score stored in
449	///         `SortedScores`.
450	/// - And visa versa: for any key existing in `SubmissionMetadataStorage`, an item must exist in
451	///   `SortedScores`.
452	/// - For any first key existing in `SubmissionStorage`, a key must exist in
453	///   `SubmissionMetadataStorage`.
454	/// - For any first key in `SubmissionStorage`, the number of second keys existing should be the
455	///   same as the `true` count of `pages` in [`SubmissionMetadata`] (this already implies the
456	///   former, since it uses the metadata).
457	///
458	/// All mutating functions are only allowed to transition into states where all of the above
459	/// conditions are met.
460	///
461	/// No particular invariant exists between data that related to different rounds. They are
462	/// purely independent.
463	pub(crate) struct Submissions<T: Config>(sp_std::marker::PhantomData<T>);
464
465	#[pallet::storage]
466	pub type SortedScores<T: Config> = StorageMap<
467		_,
468		Twox64Concat,
469		u32,
470		BoundedVec<(T::AccountId, ElectionScore), T::MaxSubmissions>,
471		ValueQuery,
472	>;
473
474	/// Triple map from (round, account, page) to a solution page.
475	#[pallet::storage]
476	type SubmissionStorage<T: Config> = StorageNMap<
477		_,
478		(
479			NMapKey<Twox64Concat, u32>,
480			NMapKey<Twox64Concat, T::AccountId>,
481			NMapKey<Twox64Concat, PageIndex>,
482		),
483		SolutionOf<T::MinerConfig>,
484		OptionQuery,
485	>;
486
487	/// Map from account to the metadata of their submission.
488	///
489	/// invariant: for any Key1 of type `AccountId` in [`Submissions`], this storage map also has a
490	/// value.
491	#[pallet::storage]
492	type SubmissionMetadataStorage<T: Config> =
493		StorageDoubleMap<_, Twox64Concat, u32, Twox64Concat, T::AccountId, SubmissionMetadata<T>>;
494
495	impl<T: Config> Submissions<T> {
496		// -- mutating functions
497
498		/// Generic checked mutation helper.
499		///
500		/// All mutating functions must be fulled through this bad boy. The round at which the
501		/// mutation happens must be provided
502		fn mutate_checked<R, F: FnOnce() -> R>(_round: u32, mutate: F) -> R {
503			let result = mutate();
504
505			#[cfg(debug_assertions)]
506			{
507				assert!(Self::sanity_check_round(_round).is_ok());
508				assert!(Self::sanity_check_round(_round + 1).is_ok());
509				assert!(Self::sanity_check_round(_round.saturating_sub(1)).is_ok());
510			}
511
512			result
513		}
514
515		/// *Fully* **TAKE** (i.e. get and remove) the leader from storage, with all of its
516		/// associated data.
517		///
518		/// This removes all associated data of the leader from storage, discarding the submission
519		/// data and score, returning the rest.
520		pub(crate) fn take_leader_with_data(
521			round: u32,
522		) -> Option<(T::AccountId, SubmissionMetadata<T>)> {
523			Self::mutate_checked(round, || {
524				SortedScores::<T>::mutate(round, |sorted| sorted.pop()).and_then(
525					|(submitter, _score)| {
526						// NOTE: safe to remove unbounded, as at most `Pages` pages are stored.
527						let r: MultiRemovalResults = SubmissionStorage::<T>::clear_prefix(
528							(round, &submitter),
529							u32::MAX,
530							None,
531						);
532						debug_assert!(r.unique <= T::Pages::get());
533
534						SubmissionMetadataStorage::<T>::take(round, &submitter)
535							.map(|metadata| (submitter, metadata))
536					},
537				)
538			})
539		}
540
541		/// *Fully* **TAKE** (i.e. get and remove) a submission from storage, with all of its
542		/// associated data.
543		///
544		/// This removes all associated data of the submitter from storage, discarding the
545		/// submission data and score, returning the metadata.
546		pub(crate) fn take_submission_with_data(
547			round: u32,
548			who: &T::AccountId,
549		) -> Option<SubmissionMetadata<T>> {
550			Self::mutate_checked(round, || {
551				let mut sorted_scores = SortedScores::<T>::get(round);
552				if let Some(index) = sorted_scores.iter().position(|(x, _)| x == who) {
553					sorted_scores.remove(index);
554				}
555				if sorted_scores.is_empty() {
556					SortedScores::<T>::remove(round);
557				} else {
558					SortedScores::<T>::insert(round, sorted_scores);
559				}
560
561				// Note: safe to remove unbounded, as at most `Pages` pages are stored.
562				let r = SubmissionStorage::<T>::clear_prefix((round, who), u32::MAX, None);
563				debug_assert!(r.unique <= T::Pages::get());
564
565				SubmissionMetadataStorage::<T>::take(round, who)
566			})
567		}
568
569		/// Try and register a new solution.
570		///
571		/// Registration can only happen for the current round.
572		///
573		/// registration might fail if the queue is already full, and the solution is not good
574		/// enough to eject the weakest.
575		fn try_register(
576			round: u32,
577			who: &T::AccountId,
578			metadata: SubmissionMetadata<T>,
579		) -> Result<bool, DispatchError> {
580			Self::mutate_checked(round, || Self::try_register_inner(round, who, metadata))
581		}
582
583		fn try_register_inner(
584			round: u32,
585			who: &T::AccountId,
586			metadata: SubmissionMetadata<T>,
587		) -> Result<bool, DispatchError> {
588			let mut sorted_scores = SortedScores::<T>::get(round);
589
590			let did_eject = if let Some(_) = sorted_scores.iter().position(|(x, _)| x == who) {
591				return Err(Error::<T>::Duplicate.into());
592			} else {
593				// must be new.
594				debug_assert!(!SubmissionMetadataStorage::<T>::contains_key(round, who));
595
596				let insert_idx = match sorted_scores
597					.binary_search_by_key(&metadata.claimed_score, |(_, y)| *y)
598				{
599					// an equal score exists, unlikely, but could very well happen. We just put them
600					// next to each other.
601					Ok(pos) => pos,
602					// new score, should be inserted in this pos.
603					Err(pos) => pos,
604				};
605
606				let mut record = (who.clone(), metadata.claimed_score);
607				if sorted_scores.is_full() {
608					let remove_idx = sorted_scores
609						.iter()
610						.position(|(x, _)| !Pallet::<T>::is_invulnerable(x))
611						.ok_or(Error::<T>::QueueFull)?;
612					if insert_idx > remove_idx {
613						// we have a better solution
614						sp_std::mem::swap(&mut sorted_scores[remove_idx], &mut record);
615						// slicing safety note:
616						// - `insert_idx` is at most `sorted_scores.len()`, obtained from
617						//   `binary_search_by_key`, valid for the upper bound of slicing.
618						// - `remove_idx` is a valid index, less then `insert_idx`, obtained from
619						//   `.iter().position()`
620						sorted_scores[remove_idx..insert_idx].rotate_left(1);
621
622						let discarded = record.0;
623						let maybe_metadata =
624							SubmissionMetadataStorage::<T>::take(round, &discarded).defensive();
625						// Note: safe to remove unbounded, as at most `Pages` pages are stored.
626						let _r = SubmissionStorage::<T>::clear_prefix(
627							(round, &discarded),
628							u32::MAX,
629							None,
630						);
631						debug_assert!(_r.unique <= T::Pages::get());
632
633						if let Some(metadata) = maybe_metadata {
634							Pallet::<T>::settle_deposit(
635								round,
636								&discarded,
637								metadata.deposit,
638								T::EjectGraceRatio::get(),
639							);
640						}
641
642						Pallet::<T>::deposit_event(Event::<T>::Ejected(round, discarded));
643						true
644					} else {
645						// we don't have a better solution
646						return Err(Error::<T>::QueueFull.into());
647					}
648				} else {
649					sorted_scores
650						.try_insert(insert_idx, record)
651						.expect("length checked above; qed");
652					false
653				}
654			};
655
656			SortedScores::<T>::insert(round, sorted_scores);
657			SubmissionMetadataStorage::<T>::insert(round, who, metadata);
658			Ok(did_eject)
659		}
660
661		/// Submit a page of `solution` to the `page` index of `who`'s submission.
662		///
663		/// Updates the deposit in the metadata accordingly.
664		///
665		/// - If `maybe_solution` is `None`, then the given page is deleted.
666		/// - `who` must have already registered their submission.
667		/// - If the page is duplicate, it will replaced.
668		pub(crate) fn try_mutate_page(
669			round: u32,
670			who: &T::AccountId,
671			page: PageIndex,
672			maybe_solution: Option<Box<SolutionOf<T::MinerConfig>>>,
673		) -> DispatchResultWithPostInfo {
674			Self::mutate_checked(round, || {
675				Self::try_mutate_page_inner(round, who, page, maybe_solution)
676			})
677		}
678
679		/// Get the deposit of a registration with the given number of pages.
680		fn deposit_for(who: &T::AccountId, pages: usize) -> BalanceOf<T> {
681			if Pallet::<T>::is_invulnerable(who) {
682				T::InvulnerableDeposit::get()
683			} else {
684				let round = Pallet::<T>::current_round();
685				let queue_size = Self::submitters_count(round);
686				let base = T::DepositBase::calculate_base_deposit(queue_size);
687				let pages = T::DepositPerPage::calculate_page_deposit(queue_size, pages);
688				base.saturating_add(pages)
689			}
690		}
691
692		fn try_mutate_page_inner(
693			round: u32,
694			who: &T::AccountId,
695			page: PageIndex,
696			maybe_solution: Option<Box<SolutionOf<T::MinerConfig>>>,
697		) -> DispatchResultWithPostInfo {
698			let mut metadata =
699				SubmissionMetadataStorage::<T>::get(round, who).ok_or(Error::<T>::NotRegistered)?;
700			ensure!(page < T::Pages::get(), Error::<T>::BadPageIndex);
701
702			let was_set = metadata.pages.get(page as usize).copied().unwrap_or_default();
703
704			// defensive only: we resize `meta.pages` once to be `T::Pages` elements once, and never
705			// resize it again; `page` is checked here to be in bound; element must exist; qed.
706			if let Some(page_bit) = metadata.pages.get_mut(page as usize).defensive() {
707				*page_bit = maybe_solution.is_some();
708			}
709
710			// update deposit.
711			let new_pages = metadata.pages.iter().filter(|x| **x).count();
712			let new_deposit = Self::deposit_for(&who, new_pages);
713			let old_deposit = metadata.deposit;
714			if new_deposit > old_deposit {
715				let to_reserve = new_deposit - old_deposit;
716				T::Currency::hold(&HoldReason::SignedSubmission.into(), who, to_reserve)?;
717			} else {
718				let to_unreserve = old_deposit - new_deposit;
719				let _res = T::Currency::release(
720					&HoldReason::SignedSubmission.into(),
721					who,
722					to_unreserve,
723					Precision::BestEffort,
724				);
725				debug_assert_eq!(_res, Ok(to_unreserve));
726			};
727			metadata.deposit = new_deposit;
728
729			// If a page is being added, we record the fee as well. For removals, we ignore the fee
730			// as it is negligible, and we don't want to encourage anyone to submit and remove
731			// anyways. For pages that are already stored, we ignore it too: the refund covers the
732			// cost of storing a submission once. Note that fee is only refunded for the winner
733			// anyways.
734			if maybe_solution.is_some() && !was_set {
735				let fee = T::EstimateCallFee::estimate_call_fee(
736					&Call::submit_page { page, maybe_solution: maybe_solution.clone() },
737					None.into(),
738				);
739				metadata.fee.saturating_accrue(fee);
740			}
741
742			SubmissionStorage::<T>::mutate_exists((round, who, page), |maybe_old_solution| {
743				*maybe_old_solution = maybe_solution.map(|s| *s)
744			});
745			SubmissionMetadataStorage::<T>::insert(round, who, metadata);
746			Ok(().into())
747		}
748
749		// -- getter functions
750		pub(crate) fn has_leader(round: u32) -> bool {
751			!SortedScores::<T>::get(round).is_empty()
752		}
753
754		pub(crate) fn leader(round: u32) -> Option<(T::AccountId, ElectionScore)> {
755			SortedScores::<T>::get(round).last().cloned()
756		}
757
758		pub(crate) fn submitters_count(round: u32) -> usize {
759			SortedScores::<T>::get(round).len()
760		}
761
762		pub(crate) fn get_page_of(
763			round: u32,
764			who: &T::AccountId,
765			page: PageIndex,
766		) -> Option<SolutionOf<T::MinerConfig>> {
767			SubmissionStorage::<T>::get((round, who, &page))
768		}
769	}
770
771	#[allow(unused)]
772	#[cfg(any(feature = "try-runtime", test, feature = "runtime-benchmarks", debug_assertions))]
773	impl<T: Config> Submissions<T> {
774		pub(crate) fn sorted_submitters(round: u32) -> BoundedVec<T::AccountId, T::MaxSubmissions> {
775			use frame_support::traits::TryCollect;
776			SortedScores::<T>::get(round).into_iter().map(|(x, _)| x).try_collect().unwrap()
777		}
778
779		pub fn submissions_iter(
780			round: u32,
781		) -> impl Iterator<Item = (T::AccountId, PageIndex, SolutionOf<T::MinerConfig>)> {
782			SubmissionStorage::<T>::iter_prefix((round,)).map(|((x, y), z)| (x, y, z))
783		}
784
785		pub fn metadata_iter(
786			round: u32,
787		) -> impl Iterator<Item = (T::AccountId, SubmissionMetadata<T>)> {
788			SubmissionMetadataStorage::<T>::iter_prefix(round)
789		}
790
791		pub fn metadata_of(round: u32, who: T::AccountId) -> Option<SubmissionMetadata<T>> {
792			SubmissionMetadataStorage::<T>::get(round, who)
793		}
794
795		pub fn pages_of(
796			round: u32,
797			who: T::AccountId,
798		) -> impl Iterator<Item = (PageIndex, SolutionOf<T::MinerConfig>)> {
799			SubmissionStorage::<T>::iter_prefix((round, who))
800		}
801
802		pub fn leaderboard(
803			round: u32,
804		) -> BoundedVec<(T::AccountId, ElectionScore), T::MaxSubmissions> {
805			SortedScores::<T>::get(round)
806		}
807
808		/// Ensure that all the storage items associated with the given round are in `killed` state,
809		/// meaning that in the expect state after an election is OVER.
810		pub(crate) fn ensure_killed(round: u32) -> DispatchResult {
811			ensure!(Self::metadata_iter(round).count() == 0, "metadata_iter not cleared.");
812			ensure!(Self::submissions_iter(round).count() == 0, "submissions_iter not cleared.");
813			ensure!(Self::sorted_submitters(round).len() == 0, "sorted_submitters not cleared.");
814
815			Ok(())
816		}
817
818		/// Ensure that no data associated with `who` exists for `round`.
819		pub(crate) fn ensure_killed_with(who: &T::AccountId, round: u32) -> DispatchResult {
820			ensure!(
821				SubmissionMetadataStorage::<T>::get(round, who).is_none(),
822				"metadata not cleared."
823			);
824			ensure!(
825				SubmissionStorage::<T>::iter_prefix((round, who)).count() == 0,
826				"submissions not cleared."
827			);
828			ensure!(
829				SortedScores::<T>::get(round).iter().all(|(x, _)| x != who),
830				"sorted_submitters not cleared."
831			);
832
833			Ok(())
834		}
835
836		/// Perform all the sanity checks of this storage item group at the given round.
837		pub(crate) fn sanity_check_round(round: u32) -> DispatchResult {
838			use sp_std::collections::btree_set::BTreeSet;
839			let sorted_scores = SortedScores::<T>::get(round);
840			assert_eq!(
841				sorted_scores.clone().into_iter().map(|(x, _)| x).collect::<BTreeSet<_>>().len(),
842				sorted_scores.len()
843			);
844
845			let _ = SubmissionMetadataStorage::<T>::iter_prefix(round)
846				.map(|(submitter, meta)| {
847					let mut matches = SortedScores::<T>::get(round)
848						.into_iter()
849						.filter(|(who, _score)| who == &submitter)
850						.collect::<Vec<_>>();
851
852					ensure!(
853						matches.len() == 1,
854						"item existing in metadata but missing in sorted list.",
855					);
856
857					let (_, score) = matches.pop().expect("checked; qed");
858					ensure!(score == meta.claimed_score, "score mismatch");
859					Ok(())
860				})
861				.collect::<Result<Vec<_>, &'static str>>()?;
862
863			ensure!(
864				SubmissionStorage::<T>::iter_key_prefix((round,)).map(|(k1, _k2)| k1).all(
865					|submitter| SubmissionMetadataStorage::<T>::contains_key(round, submitter)
866				),
867				"missing metadata of submitter"
868			);
869
870			for submitter in SubmissionStorage::<T>::iter_key_prefix((round,)).map(|(k1, _k2)| k1) {
871				let pages_count =
872					SubmissionStorage::<T>::iter_key_prefix((round, &submitter)).count();
873				let metadata = SubmissionMetadataStorage::<T>::get(round, submitter)
874					.expect("metadata checked to exist for all keys; qed");
875				let assumed_pages_count = metadata.pages.iter().filter(|x| **x).count();
876				ensure!(pages_count == assumed_pages_count, "wrong page count");
877			}
878
879			Ok(())
880		}
881	}
882
883	#[pallet::pallet]
884	pub struct Pallet<T>(PhantomData<T>);
885
886	#[pallet::event]
887	#[pallet::generate_deposit(pub(super) fn deposit_event)]
888	pub enum Event<T: Config> {
889		/// Upcoming submission has been registered for the given account, with the given score.
890		Registered(u32, T::AccountId, ElectionScore),
891		/// A page of solution solution with the given index has been stored for the given account.
892		Stored(u32, T::AccountId, PageIndex),
893		/// The given account has been rewarded with the given amount.
894		Rewarded(u32, T::AccountId, BalanceOf<T>),
895		/// A reward payout failed and has been queued in [`UnpaidRewards`]; claimable via
896		/// [`Pallet::claim_unpaid_reward`] once the pot is refilled.
897		RewardPaymentDeferred(u32, T::AccountId, BalanceOf<T>),
898		/// [`UnpaidRewards`] was full, so this (the oldest) entry was evicted to make room for a
899		/// new deferral; no funds moved, the reward is now unrecoverable.
900		UnpaidRewardEvicted(u32, T::AccountId, BalanceOf<T>),
901		/// An invulnerable's transaction fee refund failed; no funds moved, no recovery.
902		FeeRefundFailed(u32, T::AccountId, BalanceOf<T>),
903		/// The given account has been slashed with the given amount.
904		Slashed(u32, T::AccountId, BalanceOf<T>),
905		/// The given solution, for the given round, was ejected.
906		Ejected(u32, T::AccountId),
907		/// The given account has been discarded.
908		Discarded(u32, T::AccountId),
909		/// The given account has bailed.
910		Bailed(u32, T::AccountId),
911	}
912
913	#[pallet::error]
914	pub enum Error<T> {
915		/// The phase is not signed.
916		PhaseNotSigned,
917		/// The submission is a duplicate.
918		Duplicate,
919		/// The queue is full.
920		QueueFull,
921		/// The page index is out of bounds.
922		BadPageIndex,
923		/// The account is not registered.
924		NotRegistered,
925		/// No submission found.
926		NoSubmission,
927		/// Round is not yet over.
928		RoundNotOver,
929		/// Bad witness data provided.
930		BadWitnessData,
931		/// Too many invulnerable accounts are provided,
932		TooManyInvulnerables,
933		/// No [`UnpaidRewards`] entry exists for the caller in the given round.
934		NoUnpaidReward,
935		/// The [`Config::RewardSource`] pot is still insufficient to pay the unpaid reward.
936		PotStillDepleted,
937	}
938
939	#[pallet::call]
940	impl<T: Config> Pallet<T> {
941		/// Register oneself for an upcoming signed election.
942		#[pallet::weight(SignedWeightsOf::<T>::register_eject())]
943		#[pallet::call_index(0)]
944		pub fn register(
945			origin: OriginFor<T>,
946			claimed_score: ElectionScore,
947		) -> DispatchResultWithPostInfo {
948			let who = ensure_signed(origin)?;
949			ensure!(crate::Pallet::<T>::current_phase().is_signed(), Error::<T>::PhaseNotSigned);
950
951			// note: we could already check if this is a duplicate here, but prefer keeping the code
952			// simple for now.
953
954			let deposit = Submissions::<T>::deposit_for(&who, 0);
955			let reward = T::RewardBase::get();
956			let fee = T::EstimateCallFee::estimate_call_fee(
957				&Call::register { claimed_score },
958				None.into(),
959			);
960			let mut pages = BoundedVec::<_, _>::with_bounded_capacity(T::Pages::get() as usize);
961			pages.bounded_resize(T::Pages::get() as usize, false);
962
963			let new_metadata = SubmissionMetadata { claimed_score, deposit, reward, fee, pages };
964
965			T::Currency::hold(&HoldReason::SignedSubmission.into(), &who, deposit)?;
966			let round = Self::current_round();
967			let discarded = Submissions::<T>::try_register(round, &who, new_metadata)?;
968			Self::deposit_event(Event::<T>::Registered(round, who, claimed_score));
969
970			// maybe refund.
971			if discarded {
972				Ok(().into())
973			} else {
974				Ok(Some(SignedWeightsOf::<T>::register_not_full()).into())
975			}
976		}
977
978		/// Submit a single page of a solution.
979		///
980		/// Must always come after [`Pallet::register`].
981		///
982		/// `maybe_solution` can be set to `None` to erase the page.
983		///
984		/// Collects deposits from the signed origin based on [`Config::DepositBase`] and
985		/// [`Config::DepositPerPage`].
986		#[pallet::weight(SignedWeightsOf::<T>::submit_page())]
987		#[pallet::call_index(1)]
988		pub fn submit_page(
989			origin: OriginFor<T>,
990			page: PageIndex,
991			maybe_solution: Option<Box<SolutionOf<T::MinerConfig>>>,
992		) -> DispatchResultWithPostInfo {
993			let who = ensure_signed(origin)?;
994			ensure!(crate::Pallet::<T>::current_phase().is_signed(), Error::<T>::PhaseNotSigned);
995			let is_set = maybe_solution.is_some();
996
997			let round = Self::current_round();
998			Submissions::<T>::try_mutate_page(round, &who, page, maybe_solution)?;
999			Self::deposit_event(Event::<T>::Stored(round, who, page));
1000
1001			// maybe refund.
1002			if is_set {
1003				Ok(().into())
1004			} else {
1005				Ok(Some(SignedWeightsOf::<T>::unset_page()).into())
1006			}
1007		}
1008
1009		/// Retract a submission.
1010		///
1011		/// A portion of the deposit may be returned, based on the [`Config::EjectGraceRatio`].
1012		///
1013		/// This will fully remove the solution from storage.
1014		#[pallet::weight(SignedWeightsOf::<T>::bail())]
1015		#[pallet::call_index(2)]
1016		pub fn bail(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
1017			let who = ensure_signed(origin)?;
1018			ensure!(crate::Pallet::<T>::current_phase().is_signed(), Error::<T>::PhaseNotSigned);
1019			let round = Self::current_round();
1020			let metadata = Submissions::<T>::take_submission_with_data(round, &who)
1021				.ok_or(Error::<T>::NoSubmission)?;
1022
1023			let deposit = metadata.deposit;
1024			Self::settle_deposit(round, &who, deposit, T::BailoutGraceRatio::get());
1025			Self::deposit_event(Event::<T>::Bailed(round, who));
1026
1027			Ok(None.into())
1028		}
1029
1030		/// Clear the data of a submitter form an old round.
1031		///
1032		/// The dispatch origin of this call must be signed, and the original submitter.
1033		///
1034		/// This can only be called for submissions that end up being discarded, as in they are not
1035		/// processed and they end up lingering in the queue.
1036		#[pallet::call_index(3)]
1037		#[pallet::weight(SignedWeightsOf::<T>::clear_old_round_data(*witness_pages))]
1038		pub fn clear_old_round_data(
1039			origin: OriginFor<T>,
1040			round: u32,
1041			witness_pages: u32,
1042		) -> DispatchResultWithPostInfo {
1043			let discarded = ensure_signed(origin)?;
1044
1045			let current_round = Self::current_round();
1046			// we can only operate on old rounds.
1047			ensure!(round < current_round, Error::<T>::RoundNotOver);
1048
1049			let metadata = Submissions::<T>::take_submission_with_data(round, &discarded)
1050				.ok_or(Error::<T>::NoSubmission)?;
1051			ensure!(
1052				metadata.pages.iter().filter(|p| **p).count() as u32 <= witness_pages,
1053				Error::<T>::BadWitnessData
1054			);
1055
1056			// give back their deposit.
1057			let _res = T::Currency::release(
1058				&HoldReason::SignedSubmission.into(),
1059				&discarded,
1060				metadata.deposit,
1061				Precision::BestEffort,
1062			);
1063			debug_assert_eq!(_res, Ok(metadata.deposit));
1064
1065			// maybe give back their fees
1066			if Self::is_invulnerable(&discarded) {
1067				Self::refund_fee(round, &discarded, metadata.fee.min(T::MaxFeeRefund::get()));
1068			}
1069
1070			Self::deposit_event(Event::<T>::Discarded(round, discarded));
1071
1072			// IFF all good, this is free of charge.
1073			Ok(None.into())
1074		}
1075
1076		/// Set the invulnerable list.
1077		///
1078		/// Dispatch origin must the the same as [`crate::Config::AdminOrigin`].
1079		#[pallet::call_index(4)]
1080		#[pallet::weight(T::DbWeight::get().writes(1))]
1081		pub fn set_invulnerables(origin: OriginFor<T>, inv: Vec<T::AccountId>) -> DispatchResult {
1082			<T as crate::Config>::AdminOrigin::ensure_origin(origin)?;
1083			let bounded: BoundedVec<_, ConstU32<16>> =
1084				inv.try_into().map_err(|_| Error::<T>::TooManyInvulnerables)?;
1085			Invulnerables::<T>::set(bounded);
1086			Ok(())
1087		}
1088
1089		/// Pay out a round's [`UnpaidRewards`] entry to its winner. Permissionless: anyone may
1090		/// call it for any round. Free on success, normal fee on failure to discourage spam.
1091		#[pallet::call_index(5)]
1092		#[pallet::weight(SignedWeightsOf::<T>::claim_unpaid_reward())]
1093		pub fn claim_unpaid_reward(origin: OriginFor<T>, round: u32) -> DispatchResultWithPostInfo {
1094			let _ = ensure_signed(origin)?;
1095			let mut unpaid = UnpaidRewards::<T>::get();
1096			let idx = unpaid
1097				.iter()
1098				.position(|entry| entry.round == round)
1099				.ok_or(Error::<T>::NoUnpaidReward)?;
1100			let entry = unpaid[idx].clone();
1101
1102			Self::transfer_or_mint(&entry.who, entry.amount)
1103				.map_err(|_| Error::<T>::PotStillDepleted)?;
1104
1105			unpaid.remove(idx);
1106			UnpaidRewards::<T>::put(unpaid);
1107			Self::deposit_event(Event::<T>::Rewarded(entry.round, entry.who, entry.amount));
1108
1109			Ok(Pays::No.into())
1110		}
1111	}
1112
1113	#[pallet::view_functions]
1114	impl<T: Config> Pallet<T> {
1115		/// Get the deposit amount that will be held for a solution of `pages`.
1116		///
1117		/// This allows an offchain application to know what [`Config::DepositPerPage`] and
1118		/// [`Config::DepositBase`] are doing under the hood. It also takes into account if `who` is
1119		/// [`Invulnerables`] or not.
1120		pub fn deposit_for(who: T::AccountId, pages: u32) -> BalanceOf<T> {
1121			Submissions::<T>::deposit_for(&who, pages as usize)
1122		}
1123	}
1124
1125	#[pallet::hooks]
1126	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
1127		#[cfg(feature = "try-runtime")]
1128		fn try_state(n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
1129			Self::do_try_state(n)
1130		}
1131	}
1132}
1133
1134impl<T: Config> Pallet<T> {
1135	#[cfg(any(feature = "try-runtime", test, feature = "runtime-benchmarks"))]
1136	pub(crate) fn do_try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
1137		Submissions::<T>::sanity_check_round(Self::current_round())
1138	}
1139
1140	fn current_round() -> u32 {
1141		crate::Pallet::<T>::round()
1142	}
1143
1144	fn is_invulnerable(who: &T::AccountId) -> bool {
1145		Invulnerables::<T>::get().contains(who)
1146	}
1147
1148	/// Transfer `amount` from [`Config::RewardSource`] pot to `to`, or mint if `None`.
1149	fn transfer_or_mint(to: &T::AccountId, amount: BalanceOf<T>) -> Result<(), ()> {
1150		if let Some(source) = T::RewardSource::account() {
1151			T::Currency::transfer(&source, to, amount, Preservation::Preserve).map_err(|_| ())?;
1152			T::RewardSource::paid(amount);
1153		} else {
1154			let _r = T::Currency::mint_into(to, amount);
1155			debug_assert!(_r.is_ok());
1156		}
1157		Ok(())
1158	}
1159
1160	/// Pay the round's winner. On success emits `Rewarded`. On failure, always defers into
1161	/// [`UnpaidRewards`] (`RewardPaymentDeferred`), evicting the oldest entry first
1162	/// (`UnpaidRewardEvicted`) if it's already full.
1163	fn pay_reward(round: u32, to: &T::AccountId, amount: BalanceOf<T>) {
1164		if Self::transfer_or_mint(to, amount).is_ok() {
1165			Self::deposit_event(Event::<T>::Rewarded(round, to.clone(), amount));
1166			return;
1167		}
1168
1169		sublog!(
1170			warn,
1171			"signed",
1172			"reward pot insufficient; deferring {:?} to {:?} for round {}",
1173			amount,
1174			to,
1175			round
1176		);
1177		let entry = UnpaidReward { round, who: to.clone(), amount };
1178		UnpaidRewards::<T>::mutate(|unpaid| {
1179			if unpaid.is_full() {
1180				// Entries are pushed in round order, so index 0 is the oldest.
1181				let evicted = unpaid.remove(0);
1182				Self::deposit_event(Event::<T>::UnpaidRewardEvicted(
1183					evicted.round,
1184					evicted.who,
1185					evicted.amount,
1186				));
1187			}
1188			let _ = unpaid.try_push(entry).defensive_proof("an element was just evicted; qed");
1189		});
1190		Self::deposit_event(Event::<T>::RewardPaymentDeferred(round, to.clone(), amount));
1191	}
1192
1193	/// Refund an invulnerable's tx fee on discard. Unlike `pay_reward`, a failure is not
1194	/// deferred: it is out of scope for [`UnpaidRewards`] (see its doc for why) and simply emits
1195	/// `FeeRefundFailed`, never `Rewarded`.
1196	fn refund_fee(round: u32, to: &T::AccountId, amount: BalanceOf<T>) {
1197		if Self::transfer_or_mint(to, amount).is_err() {
1198			sublog!(
1199				warn,
1200				"signed",
1201				"reward pot insufficient; fee refund of {:?} to {:?} not paid",
1202				amount,
1203				to
1204			);
1205			Self::deposit_event(Event::<T>::FeeRefundFailed(round, to.clone(), amount));
1206		}
1207	}
1208
1209	fn settle_deposit(round: u32, who: &T::AccountId, deposit: BalanceOf<T>, grace: Perbill) {
1210		let to_refund = grace * deposit;
1211		let to_slash = deposit.defensive_saturating_sub(to_refund);
1212
1213		let _res = T::Currency::release(
1214			&HoldReason::SignedSubmission.into(),
1215			who,
1216			to_refund,
1217			Precision::BestEffort,
1218		)
1219		.defensive();
1220		debug_assert_eq!(_res, Ok(to_refund));
1221
1222		let (credit, remainder) =
1223			T::Currency::slash(&HoldReason::SignedSubmission.into(), who, to_slash);
1224		debug_assert!(remainder.is_zero(), "the full deposit was held; slash must not be partial");
1225		let slashed = credit.peek();
1226		T::Slash::on_unbalanced(credit);
1227		if !slashed.is_zero() {
1228			Self::deposit_event(Event::<T>::Slashed(round, who.clone(), slashed));
1229		}
1230	}
1231
1232	/// Common logic for handling solution rejection - slash the submitter and try next solution
1233	fn handle_solution_rejection(current_round: u32) {
1234		if let Some((loser, metadata)) =
1235			Submissions::<T>::take_leader_with_data(current_round).defensive()
1236		{
1237			// Slash the deposit.
1238			// Note that an invulnerable is not expelled from the list despite the slashing.
1239			// Removal should occur only through governance, not automatically. An operational or
1240			// network issue that leads to an incomplete submission is much more likely than a bad
1241			// faith action from an invulnerable.
1242			let slash = metadata.deposit;
1243			let (credit, remainder) =
1244				T::Currency::slash(&HoldReason::SignedSubmission.into(), &loser, slash);
1245			debug_assert!(
1246				remainder.is_zero(),
1247				"the full deposit was held; slash must not be partial"
1248			);
1249			let slashed = credit.peek();
1250			T::Slash::on_unbalanced(credit);
1251			if !slashed.is_zero() {
1252				Self::deposit_event(Event::<T>::Slashed(current_round, loser.clone(), slashed));
1253			}
1254
1255			// Try to start verification again if we still have submissions
1256			if let crate::types::Phase::SignedValidation(remaining_blocks) =
1257				crate::Pallet::<T>::current_phase()
1258			{
1259				// Only start verification if there are sufficient blocks remaining
1260				// Note: SignedValidation(N) means N+1 blocks remaining in the phase
1261				if remaining_blocks >= T::Pages::get().into() {
1262					if Submissions::<T>::has_leader(current_round) {
1263						// defensive: verifier just reported back a result, it must be in clear
1264						// state.
1265						let _ = <T::Verifier as AsynchronousVerifier>::start().defensive();
1266					}
1267				} else {
1268					sublog!(
1269						warn,
1270						"signed",
1271						"SignedValidation phase has {:?} blocks remaining, which are insufficient for {} pages",
1272						remaining_blocks,
1273						T::Pages::get()
1274					);
1275				}
1276			}
1277		} else {
1278			// No leader to slash; nothing to do.
1279			sublog!(
1280				warn,
1281				"signed",
1282				"Tried to slash but no leader was present for round {}",
1283				current_round
1284			);
1285		}
1286	}
1287}