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