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