referrerpolicy=no-referrer-when-downgrade

pallet_staking_async_points_converter/
lib.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// SPDX-License-Identifier: Apache-2.0
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// 	http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Converts reported units of a validator service into era points, intended to be used in Asset
17//! Hub.
18//!
19//! Receives the blocks validators author on system chains and on the chain that runs this pallet.
20//!
21//! ## Overview
22//!
23//! Each system chain counts the blocks its authors produce in `pallet-validator-collators` and
24//! reports the counts of the validators. Other chains call [`Pallet::report_authored_blocks`]
25//! from [`Config::ReportOrigin`], which names the reporting chain as a [`Config::Source`]. The
26//! chain that runs this pallet credits its own counts through [`Pallet::credit_authored_blocks`].
27//! A count becomes `min(count, MaxBlocksPerEntry) × PointsPerService[(source, Collation)]` era
28//! points, handed to [`Config::RewardPoints`] as [`sp_staking::ServiceKind::Collation`]
29//! points. A source without an entry for [`sp_staking::ServiceKind::Collation`] in
30//! [`PointsPerService`] earns nothing. Governance sets the entries through
31//! [`Config::PointsOrigin`]. Relay chain consensus points are not converted here.
32//!
33//! For example, with `PointsPerService[(source, Collation)] = 10` for a reporting chain, a report
34//! from that chain of 40 units (blocks authored, for
35//! [`sp_staking::ServiceKind::Collation`]) for an account credits it 400 era points.
36
37#![cfg_attr(not(feature = "std"), no_std)]
38
39extern crate alloc;
40
41pub use pallet::*;
42
43#[cfg(test)]
44mod mock;
45
46#[cfg(test)]
47mod tests;
48
49#[cfg(feature = "runtime-benchmarks")]
50mod benchmarking;
51pub mod weights;
52
53#[frame_support::pallet]
54pub mod pallet {
55	pub use crate::weights::WeightInfo;
56	use alloc::vec::Vec;
57	use frame_support::{pallet_prelude::*, DefaultNoBound};
58	use frame_system::pallet_prelude::*;
59	use sp_runtime::traits::{MaybeSerializeDeserialize, Saturating};
60	use sp_staking::{RewardPoints, ServiceKind};
61
62	#[pallet::pallet]
63	pub struct Pallet<T>(_);
64
65	/// Configuration trait of this pallet.
66	#[pallet::config]
67	pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
68		/// Identifies a chain whose blocks earn era points.
69		type Source: Parameter + MaxEncodedLen + Ord + Copy + MaybeSerializeDeserialize;
70
71		/// Origin of a chain allowed to report the blocks authored on it. Returns that chain.
72		type ReportOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::Source>;
73
74		/// Origin allowed to set the points per service of a source.
75		type PointsOrigin: EnsureOrigin<Self::RuntimeOrigin>;
76
77		/// Credits era points to the validators.
78		type RewardPoints: RewardPoints<Self::AccountId>;
79
80		/// Largest points per service [`Pallet::set_points_per_service`] and the genesis config
81		/// accept, for every service.
82		///
83		/// Crediting also bounds a stored value by it.
84		#[pallet::constant]
85		type MaxPointsPerService: Get<u32>;
86
87		/// Largest count of blocks credited for one entry of a report. A larger count is credited
88		/// as this many blocks.
89		#[pallet::constant]
90		type MaxBlocksPerEntry: Get<u32>;
91
92		/// Weight information for this pallet.
93		type WeightInfo: WeightInfo;
94	}
95
96	/// Era points a validator earns for each unit of a service on a source, bounded by
97	/// [`Config::MaxPointsPerService`] when crediting. Absent means zero.
98	///
99	/// A unit is specific to the service. For [`ServiceKind::Collation`] it is one block
100	/// authored. The result is credited to staking as era points.
101	#[pallet::storage]
102	pub type PointsPerService<T: Config> =
103		StorageDoubleMap<_, Twox64Concat, T::Source, Twox64Concat, ServiceKind, u32, ValueQuery>;
104
105	#[pallet::genesis_config]
106	#[derive(DefaultNoBound)]
107	pub struct GenesisConfig<T: Config> {
108		/// Initial points per service, per source.
109		pub points_per_service: Vec<(T::Source, ServiceKind, u32)>,
110	}
111
112	#[pallet::genesis_build]
113	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
114		fn build(&self) {
115			for (source, service, points) in
116				self.points_per_service.iter().filter(|(_, _, points)| *points > 0)
117			{
118				assert!(
119					*service != ServiceKind::RelayConsensus,
120					"the genesis config weights points that come from the relay chain"
121				);
122				assert!(
123					*points <= T::MaxPointsPerService::get(),
124					"points per service in the genesis config exceed `MaxPointsPerService`"
125				);
126				PointsPerService::<T>::insert(source, service, points);
127			}
128		}
129	}
130
131	#[pallet::event]
132	#[pallet::generate_deposit(pub(super) fn deposit_event)]
133	pub enum Event<T: Config> {
134		/// Validators earn `points` era points for each unit of `service` on `source`.
135		PointsPerServiceSet { source: T::Source, service: ServiceKind, points: u32 },
136		/// Era points for `blocks` blocks authored on `source` were handed to
137		/// [`Config::RewardPoints`].
138		BlocksCredited { source: T::Source, blocks: u32 },
139		/// `blocks` blocks authored on `source` earned no points and were not credited.
140		BlocksIgnored { source: T::Source, blocks: u32 },
141	}
142
143	#[pallet::error]
144	pub enum Error<T> {
145		/// The points per service exceed [`Config::MaxPointsPerService`].
146		PointsPerServiceTooHigh,
147		/// Relay consensus points are weighted by the relay chain, not by this pallet.
148		///
149		/// Note: This is because relay-chain issues its own points, and never sends its units of
150		/// work to this pallet to be converted to points. This is for legacy reasons, and while
151		/// unifying the interface would be more ergonomic, we keep it as-is for now due to JAM
152		/// being on the horizon, and the effort of altering relay chain not being considered
153		/// worthwhile.
154		RelayConsensusNotWeighted,
155	}
156
157	#[pallet::hooks]
158	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
159		fn integrity_test() {
160			assert!(T::MaxPointsPerService::get() > 0, "`MaxPointsPerService` must not be zero");
161			assert!(T::MaxBlocksPerEntry::get() > 0, "`MaxBlocksPerEntry` must not be zero");
162		}
163	}
164
165	#[pallet::call]
166	impl<T: Config> Pallet<T> {
167		/// Set the era points a validator earns for each unit of `service` on `source`.
168		///
169		/// Zero removes the entry, so `source` earns nothing for `service`. Fails with
170		/// [`Error::PointsPerServiceTooHigh`] above [`Config::MaxPointsPerService`], and with
171		/// [`Error::RelayConsensusNotWeighted`] for [`ServiceKind::RelayConsensus`].
172		#[pallet::call_index(0)]
173		#[pallet::weight(T::WeightInfo::set_points_per_service())]
174		pub fn set_points_per_service(
175			origin: OriginFor<T>,
176			source: T::Source,
177			service: ServiceKind,
178			points: u32,
179		) -> DispatchResult {
180			T::PointsOrigin::ensure_origin(origin)?;
181			ensure!(service != ServiceKind::RelayConsensus, Error::<T>::RelayConsensusNotWeighted);
182			ensure!(points <= T::MaxPointsPerService::get(), Error::<T>::PointsPerServiceTooHigh);
183			match points {
184				0 => PointsPerService::<T>::remove(source, service),
185				points => PointsPerService::<T>::insert(source, service, points),
186			}
187			Self::deposit_event(Event::PointsPerServiceSet { source, service, points });
188			Ok(())
189		}
190
191		/// Credit the blocks the validators authored on the calling chain.
192		///
193		/// The weight of crediting is refunded when the calling chain earns no points.
194		#[pallet::call_index(1)]
195		#[pallet::weight(Pallet::<T>::report_authored_blocks_weight(blocks.len() as u32))]
196		pub fn report_authored_blocks(
197			origin: OriginFor<T>,
198			blocks: Vec<(T::AccountId, u32)>,
199		) -> DispatchResultWithPostInfo {
200			let source = T::ReportOrigin::ensure_origin(origin)?;
201			let credited = Self::credit_authored_blocks(source, blocks);
202			Ok(Some(Self::report_authored_blocks_weight(credited)).into())
203		}
204	}
205
206	impl<T: Config> Pallet<T> {
207		/// Credit `blocks` authored on `source` as era points, with the points per service of
208		/// `source` for [`ServiceKind::Collation`]. Each count is credited up to
209		/// [`Config::MaxBlocksPerEntry`]. Nothing is credited when `blocks` is empty or `source`
210		/// earns no points, and an empty `blocks` deposits no event.
211		///
212		/// Returns the number of entries handed to [`Config::RewardPoints`].
213		pub fn credit_authored_blocks(
214			source: T::Source,
215			blocks: impl IntoIterator<Item = (T::AccountId, u32)>,
216		) -> u32 {
217			let points_per_service = PointsPerService::<T>::get(source, ServiceKind::Collation)
218				.min(T::MaxPointsPerService::get());
219			let mut blocks = blocks.into_iter().peekable();
220			if blocks.peek().is_none() {
221				return 0;
222			}
223			if points_per_service == 0 {
224				let blocks = blocks.fold(0u32, |total, (_, blocks)| total.saturating_add(blocks));
225				Self::deposit_event(Event::BlocksIgnored { source, blocks });
226				return 0;
227			}
228			let max_blocks = T::MaxBlocksPerEntry::get();
229			let mut credited_blocks = 0u32;
230			let points: Vec<_> = blocks
231				.map(|(who, blocks)| {
232					let blocks = blocks.min(max_blocks);
233					credited_blocks.saturating_accrue(blocks);
234					(who, blocks.saturating_mul(points_per_service))
235				})
236				.collect();
237			let entries = points.len() as u32;
238			T::RewardPoints::reward(ServiceKind::Collation, points);
239			Self::deposit_event(Event::BlocksCredited { source, blocks: credited_blocks });
240			entries
241		}
242
243		/// Weight of [`Self::report_authored_blocks`] when `credited` entries are handed to
244		/// [`Config::RewardPoints`].
245		pub fn report_authored_blocks_weight(credited: u32) -> Weight {
246			let reward = match credited {
247				0 => Weight::zero(),
248				credited => T::RewardPoints::weight(credited),
249			};
250			T::WeightInfo::report_authored_blocks().saturating_add(reward)
251		}
252	}
253}