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