referrerpolicy=no-referrer-when-downgrade

pallet_validator_collators/
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//! Validator Collators pallet.
17//!
18//! Stores the validator set announced by the chain that runs `pallet-staking-async`, typically
19//! Asset Hub, and offers it as a session manager, so the relay-chain validators of the current era
20//! collate on a system parachain.
21//!
22//! ## Overview
23//!
24//! The announcing chain sends the active validator set of each era, tagged with the era index of
25//! `pallet-staking-async`.
26//! This pallet stores the latest set, received through `set_validators` from [`Config::SetOrigin`]
27//! or from another pallet through [`Pallet::receive_validator_set`]. It rejects a set whose era
28//! is not newer than the stored one, or that has more validators than [`Config::MaxValidators`].
29//!
30//! The pallet is a [`pallet_session::SessionManager`]. At every session rotation it returns the
31//! stored validators that have registered local session keys, checked with
32//! [`Config::ValidatorRegistration`]. [`MaxCollators`] optionally caps how many of them are
33//! returned. The runtime combines this pallet with `pallet-collator-selection` through
34//! [`pallet_session::UnionSessionManager`], so invulnerables and candidates keep collating next to
35//! the validators.
36//!
37//! The stored set is the one staking elected for the era. The relay chain enacts only the elected
38//! validators with relay-chain session keys, and each system chain returns only those with keys
39//! registered there, so a returned collator is not necessarily an active relay-chain validator.
40//!
41//! The pallet is also a [`pallet_session::ShouldEndSession`]. Pallet-session queues a new set at
42//! one rotation and enacts it at the next. When a set arrives the pallet forces two rotations in
43//! the following two blocks, so the set is in force without waiting for the regular period. The
44//! regular rotations given by [`Config::PeriodicSession`] continue as before.
45//!
46//! The returned validators author blocks like any collator, so a `pallet-collator-selection` event
47//! handler pays them from its pot and records them in `LastAuthoredBlock`.
48//!
49//! A runtime must configure `pallet-collator-selection`'s `KickThreshold` to exceed a full Aura
50//! round of the merged list, [`Config::MaxValidators`] plus the invulnerables and candidates, times
51//! the blocks the chain produces per Aura slot. Otherwise bonded candidates are kicked as stale
52//! between their slots.
53//!
54//! ## TODO
55//!
56//! - A random draw among the opted-in validators when a cap is set. For now the cap keeps a prefix
57//!   of the set in account order.
58//! - Counting the blocks each validator authors and reporting era points to the chain where
59//!   `pallet-staking-async` runs, typically Asset Hub.
60//! - Dropping validators that author no blocks for a session.
61//! - (Only if a need is established) Propagating relay-chain offences to the collator set.
62
63#![cfg_attr(not(feature = "std"), no_std)]
64
65extern crate alloc;
66
67pub use pallet::*;
68
69#[cfg(test)]
70mod mock;
71
72#[cfg(test)]
73mod tests;
74
75#[cfg(feature = "runtime-benchmarks")]
76mod benchmarking;
77pub mod weights;
78
79const LOG_TARGET: &str = "runtime::validator-collators";
80
81#[frame_support::pallet]
82pub mod pallet {
83	pub use crate::weights::WeightInfo;
84	use alloc::{collections::BTreeSet, vec::Vec};
85	use frame_support::{
86		pallet_prelude::*,
87		traits::{EnsureOrigin, ValidatorRegistration},
88		BoundedBTreeSet, CloneNoBound, DebugNoBound, EqNoBound, PartialEqNoBound,
89	};
90	use frame_system::pallet_prelude::*;
91	use pallet_session::{SessionManager, ShouldEndSession};
92	use sp_staking::{EraIndex, SessionIndex};
93
94	/// A validator set received for an era.
95	#[derive(
96		CloneNoBound,
97		EqNoBound,
98		PartialEqNoBound,
99		Encode,
100		Decode,
101		DebugNoBound,
102		TypeInfo,
103		MaxEncodedLen,
104	)]
105	#[scale_info(skip_type_params(MaxValidators))]
106	pub struct EraValidatorSet<AccountId, MaxValidators>
107	where
108		AccountId: Clone + Ord + core::fmt::Debug,
109		MaxValidators: Get<u32>,
110	{
111		/// The era the set belongs to.
112		pub era: EraIndex,
113		/// The validator stashes of that era.
114		pub validators: BoundedBTreeSet<AccountId, MaxValidators>,
115	}
116
117	/// Progress of the two rotations that bring a received set into force.
118	#[derive(
119		Clone, Copy, Eq, PartialEq, Default, Encode, Decode, Debug, TypeInfo, MaxEncodedLen,
120	)]
121	pub enum RotationState {
122		/// No forced rotation is pending.
123		#[default]
124		Idle,
125		/// A set was received and the next rotation queues it.
126		AwaitingQueue,
127		/// The set is queued and the next rotation enacts it.
128		AwaitingEnactment,
129	}
130
131	#[pallet::pallet]
132	pub struct Pallet<T>(_);
133
134	/// Configuration trait of this pallet.
135	#[pallet::config]
136	pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
137		/// Origin allowed to submit a validator set.
138		type SetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
139
140		/// Origin allowed to change [`MaxCollators`].
141		type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
142
143		/// Lookup of registered session keys.
144		type ValidatorRegistration: ValidatorRegistration<Self::AccountId>;
145
146		/// Maximum number of validators in a received set.
147		#[pallet::constant]
148		type MaxValidators: Get<u32>;
149
150		/// The regular session rotation rule kept next to the forced rotations.
151		type PeriodicSession: ShouldEndSession<BlockNumberFor<Self>>;
152
153		/// Weight information for extrinsics in this pallet.
154		type WeightInfo: WeightInfo;
155	}
156
157	/// The latest received validator set.
158	#[pallet::storage]
159	pub type ValidatorSet<T: Config> =
160		StorageValue<_, EraValidatorSet<T::AccountId, T::MaxValidators>, OptionQuery>;
161
162	/// Progress of the forced rotations for the latest received set.
163	#[pallet::storage]
164	pub type PendingRotation<T: Config> = StorageValue<_, RotationState, ValueQuery>;
165
166	/// Maximum number of validators returned as collators, `None` returns every validator with
167	/// registered keys.
168	///
169	/// The cap applies before the union with the other session manager. A validator that the
170	/// other session manager also returns takes a place under the cap without adding a collator.
171	///
172	/// The merged list becomes Aura's authority list, which Aura writes into the block header
173	/// whenever it changes, and the relay chain rejects a header above its head-data limit. Size
174	/// the cap so that the merged list, invulnerables and candidates included, keeps a
175	/// session-change header within that limit.
176	///
177	/// A change is read at the next rotation, which queues the capped list, and is in force from
178	/// the rotation after. Setting the cap does not force rotations.
179	#[pallet::storage]
180	pub type MaxCollators<T: Config> = StorageValue<_, u32, OptionQuery>;
181
182	#[pallet::event]
183	#[pallet::generate_deposit(pub(super) fn deposit_event)]
184	pub enum Event<T: Config> {
185		/// A validator set was stored for an era.
186		ValidatorSetReceived { era: EraIndex, count: u32 },
187		/// The maximum number of validator collators was changed.
188		MaxCollatorsSet { max: Option<u32> },
189		/// The stored validator set does not decode, so no validators were returned for the
190		/// session.
191		StoredSetUndecodable,
192	}
193
194	#[pallet::error]
195	pub enum Error<T> {
196		/// The era of the set is not newer than the era of the stored set.
197		StaleEra,
198		/// The set has more validators than [`Config::MaxValidators`].
199		TooManyValidators,
200	}
201
202	#[pallet::hooks]
203	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
204		/// Charges the read of [`PendingRotation`] by [`ShouldEndSession::should_end_session`],
205		/// which pallet-session makes in every block without charging it.
206		fn on_initialize(_: BlockNumberFor<T>) -> Weight {
207			T::DbWeight::get()
208				.reads(1)
209				.saturating_add(Weight::from_parts(0, RotationState::max_encoded_len() as u64))
210		}
211
212		#[cfg(feature = "try-runtime")]
213		fn try_state(_: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
214			Self::do_try_state()
215		}
216	}
217
218	#[pallet::call]
219	impl<T: Config> Pallet<T> {
220		/// Store the validator set of `era`.
221		#[pallet::call_index(0)]
222		#[pallet::weight(T::WeightInfo::set_validators(validators.len() as u32))]
223		pub fn set_validators(
224			origin: OriginFor<T>,
225			era: EraIndex,
226			validators: BoundedBTreeSet<T::AccountId, T::MaxValidators>,
227		) -> DispatchResult {
228			T::SetOrigin::ensure_origin(origin)?;
229			Self::do_receive_validator_set(era, validators)
230		}
231
232		/// Set the maximum number of validators returned as collators.
233		#[pallet::call_index(1)]
234		#[pallet::weight(T::WeightInfo::set_max_collators())]
235		pub fn set_max_collators(origin: OriginFor<T>, max: Option<u32>) -> DispatchResult {
236			T::UpdateOrigin::ensure_origin(origin)?;
237			MaxCollators::<T>::set(max);
238			Self::deposit_event(Event::MaxCollatorsSet { max });
239			Ok(())
240		}
241	}
242
243	impl<T: Config> Pallet<T> {
244		/// Store the validator set of `era` and schedule the rotations that bring it into force.
245		///
246		/// An account listed more than once is kept once, and the set is checked against
247		/// [`Config::MaxValidators`] after that.
248		pub fn receive_validator_set(
249			era: EraIndex,
250			validators: impl IntoIterator<Item = T::AccountId>,
251		) -> DispatchResult {
252			let validators =
253				BoundedBTreeSet::try_from(validators.into_iter().collect::<BTreeSet<_>>())
254					.map_err(|_| Error::<T>::TooManyValidators)?;
255			Self::do_receive_validator_set(era, validators)
256		}
257
258		pub(crate) fn do_receive_validator_set(
259			era: EraIndex,
260			validators: BoundedBTreeSet<T::AccountId, T::MaxValidators>,
261		) -> DispatchResult {
262			ensure!(
263				ValidatorSet::<T>::get().is_none_or(|stored| era > stored.era),
264				Error::<T>::StaleEra
265			);
266			let count = validators.len() as u32;
267			ValidatorSet::<T>::put(EraValidatorSet { era, validators });
268			PendingRotation::<T>::put(RotationState::AwaitingQueue);
269			Self::deposit_event(Event::ValidatorSetReceived { era, count });
270			Ok(())
271		}
272
273		/// Check the pallet invariants.
274		#[cfg(any(test, feature = "try-runtime"))]
275		pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
276			ensure!(
277				ValidatorSet::<T>::exists() || PendingRotation::<T>::get() == RotationState::Idle,
278				"a rotation is pending without a stored validator set"
279			);
280			ensure!(
281				!ValidatorSet::<T>::exists() || ValidatorSet::<T>::get().is_some(),
282				"the stored validator set does not decode, it may exceed `MaxValidators`"
283			);
284			Ok(())
285		}
286	}
287
288	impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {
289		fn new_session(_: SessionIndex) -> Option<Vec<T::AccountId>> {
290			match PendingRotation::<T>::get() {
291				RotationState::AwaitingQueue => {
292					PendingRotation::<T>::put(RotationState::AwaitingEnactment)
293				},
294				RotationState::AwaitingEnactment => PendingRotation::<T>::kill(),
295				RotationState::Idle => {},
296			}
297			let Some(set) = ValidatorSet::<T>::get() else {
298				if ValidatorSet::<T>::exists() {
299					log::error!(
300						target: crate::LOG_TARGET,
301						"the stored validator set does not decode"
302					);
303					Self::deposit_event(Event::StoredSetUndecodable);
304				}
305				return None;
306			};
307			let registered =
308				set.validators.into_iter().filter(T::ValidatorRegistration::is_registered);
309			// TODO: replace the truncation with a random draw among the validators with registered
310			// keys. Until then the cap keeps the first validators with registered keys in account
311			// order.
312			Some(match MaxCollators::<T>::get() {
313				Some(max) => registered.take(max as usize).collect(),
314				None => registered.collect(),
315			})
316		}
317
318		fn start_session(_: SessionIndex) {}
319
320		fn end_session(_: SessionIndex) {}
321	}
322
323	impl<T: Config> ShouldEndSession<BlockNumberFor<T>> for Pallet<T> {
324		fn should_end_session(now: BlockNumberFor<T>) -> bool {
325			PendingRotation::<T>::get() != RotationState::Idle ||
326				T::PeriodicSession::should_end_session(now)
327		}
328	}
329}