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}