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`]. The runtime combines this pallet with
33//! `pallet-collator-selection` through [`pallet_session::UnionSessionManager`], so invulnerables
34//! and candidates keep collating next to the validators.
35//!
36//! [`MaxCollators`] optionally caps how many validators are returned. Governance can start with a
37//! low cap and raise it in steps, or lower it again, to roll out validator collators gradually.
38//! Under a cap the pallet draws the validators at random at the first forced rotation after a set
39//! arrives or the cap changes, stores them in [`Collators`] and returns those with registered keys
40//! until the next draw. The draw shuffles the whole set with a seed made of the genesis hash, the
41//! era and the relay chain epoch randomness cached in [`EpochRandomness`], then keeps the first
42//! validators with registered keys up to the cap. Every validator with keys has the same chance to
43//! be drawn in each era. A cap change draws again from the stored set, so the drawn validators
44//! can change. Every draw takes its seed from the randomness cached at the time of the draw. The
45//! seed includes the genesis hash, so each chain draws independently from the same set. A
46//! validator that registers keys after the draw waits for the next set or for a cap change. Each
47//! set, so each era, gets a new draw, which is enacted at the second forced rotation with no
48//! notice, so a validator with keys must already run a synced collator node to collate when drawn.
49//!
50//! The pallet implements [`cumulus_pallet_parachain_system::OnSystemEvent`]. The runtime must
51//! include it in the `OnSystemEvent` of `cumulus-pallet-parachain-system`, alone or in a tuple
52//! with other handlers, to keep [`EpochRandomness`] up to date from the relay chain state proof.
53//! The runtime should also return the keys of [`Pallet::relay_keys_to_prove`] from
54//! [`KeyToIncludeInRelayProof`](cumulus_primitives_core::KeyToIncludeInRelayProof), so collators
55//! include the randomness key in the proof.
56//! The draw runs in the session rotation, before the block's own proof is processed, so it uses
57//! the value cached by the previous block.
58//! A block whose proof lacks the trie nodes of the randomness key, or holds a value that does not
59//! decode, is invalid. A proof that shows the key absent, as before the relay chain's first epoch
60//! change, caches the zero value a draw without randomness uses.
61//! Without the `OnSystemEvent` wiring, every draw uses a zero randomness value and logs an error.
62//!
63//! The stored set is the one staking elected for the era. The relay chain enacts only the elected
64//! validators with relay-chain session keys, and each system chain returns only those with keys
65//! registered there, so a returned collator is not necessarily an active relay-chain validator.
66//!
67//! The pallet is also a [`pallet_session::ShouldEndSession`]. Pallet-session queues a new set at
68//! one rotation and enacts it at the next. When a set arrives, or the cap changes while a set is
69//! stored, the pallet forces two rotations in the following two blocks, so the change is in force
70//! without waiting for the regular period. The regular rotations given by
71//! [`Config::PeriodicSession`] continue as before.
72//!
73//! The returned validators author blocks like any collator, so a `pallet-collator-selection` event
74//! handler pays them from its pot and records them in `LastAuthoredBlock`.
75//!
76//! A runtime must configure `pallet-collator-selection`'s `KickThreshold` to exceed a full Aura
77//! round of the merged list, [`Config::MaxValidators`] plus the invulnerables and candidates, times
78//! the blocks the chain produces per Aura slot. Otherwise bonded candidates are kicked as stale
79//! between their slots. [`Config::MaxValidators`] rather than the cap is the bound, since the cap
80//! can be raised or removed without a runtime upgrade.
81//!
82//! ## TODO
83//!
84//! - Counting the blocks each validator authors and reporting era points to the chain where
85//! `pallet-staking-async` runs, typically Asset Hub.
86//! - Dropping validators that author no blocks for a session, judged among the drawn validators
87//! under a cap.
88//! - (Only if a need is established) Propagating relay-chain offences to the collator set.
89//! - Under a cap, revisit the redraw period and notice once operators have run collators. A draw
90//! per era without notice is the first step, with operators expected to run a synced collator
91//! node all the time. Options: redraw every few eras or sessions, keep a drawn validator for
92//! several eras while it stays in the active set, and announce the drawn validators before they
93//! collate so they can sync a node. Open points: the time a node needs to sync, and refilling the
94//! place of a drawn validator that leaves the active set, which needs notice too.
95
96#![cfg_attr(not(feature = "std"), no_std)]
97
98extern crate alloc;
99
100pub use pallet::*;
101
102#[cfg(test)]
103mod mock;
104
105#[cfg(test)]
106mod tests;
107
108#[cfg(feature = "runtime-benchmarks")]
109mod benchmarking;
110#[cfg(any(test, feature = "runtime-benchmarks"))]
111mod relay_proof;
112pub mod weights;
113
114const LOG_TARGET: &str = "runtime::validator-collators";
115
116#[frame_support::pallet]
117pub mod pallet {
118 pub use crate::weights::WeightInfo;
119 use alloc::{collections::BTreeSet, vec, vec::Vec};
120 use cumulus_pallet_parachain_system::{
121 relay_state_snapshot::{Error as RelayProofError, ReadEntryErr},
122 OnSystemEvent, RelayChainStateProof,
123 };
124 use cumulus_primitives_core::{
125 relay_chain::well_known_keys, PersistedValidationData, RelayStorageKey,
126 };
127 use frame_support::{
128 pallet_prelude::*,
129 traits::{EnsureOrigin, ValidatorRegistration},
130 BoundedBTreeSet,
131 };
132 use frame_system::pallet_prelude::*;
133 use pallet_session::{SessionManager, ShouldEndSession};
134 use rand::{seq::SliceRandom, SeedableRng};
135 use rand_chacha::ChaCha20Rng;
136 use sp_runtime::traits::{BlakeTwo256, Hash, Zero};
137 use sp_staking::{EraIndex, SessionIndex};
138
139 /// A validator set received for an era.
140 #[frame_support::stored]
141 pub struct EraValidatorSet<AccountId, MaxValidators: Get<u32>> {
142 /// The era the set belongs to.
143 pub era: EraIndex,
144 /// The validator stashes of that era.
145 pub validators: BoundedBTreeSet<AccountId, MaxValidators>,
146 }
147
148 /// The validators drawn under [`MaxCollators`], with the era of the set they were drawn from.
149 #[frame_support::stored]
150 pub struct DrawnCollators<AccountId, MaxValidators: Get<u32>> {
151 /// The era of the set the validators were drawn from.
152 pub era: EraIndex,
153 /// The drawn validators, in drawn order.
154 pub validators: BoundedVec<AccountId, MaxValidators>,
155 }
156
157 /// Progress of the two rotations that bring a received set into force.
158 #[derive(
159 Clone, Copy, Eq, PartialEq, Default, Encode, Decode, Debug, TypeInfo, MaxEncodedLen,
160 )]
161 pub enum RotationState {
162 /// No forced rotation is pending.
163 #[default]
164 Idle,
165 /// A set was received and the next rotation queues it.
166 AwaitingQueue,
167 /// The set is queued and the next rotation enacts it.
168 AwaitingEnactment,
169 }
170
171 #[pallet::pallet]
172 pub struct Pallet<T>(_);
173
174 /// Configuration trait of this pallet.
175 #[pallet::config]
176 pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
177 /// Origin allowed to submit a validator set.
178 type SetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
179
180 /// Origin allowed to change [`MaxCollators`].
181 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
182
183 /// Lookup of registered session keys.
184 type ValidatorRegistration: ValidatorRegistration<Self::AccountId>;
185
186 /// Maximum number of validators in a received set.
187 #[pallet::constant]
188 type MaxValidators: Get<u32>;
189
190 /// The regular session rotation rule kept next to the forced rotations.
191 type PeriodicSession: ShouldEndSession<BlockNumberFor<Self>>;
192
193 /// Weight information for extrinsics in this pallet.
194 type WeightInfo: WeightInfo;
195 }
196
197 /// The latest received validator set.
198 #[pallet::storage]
199 pub type ValidatorSet<T: Config> =
200 StorageValue<_, EraValidatorSet<T::AccountId, T::MaxValidators>, OptionQuery>;
201
202 /// Progress of the forced rotations for the latest received set.
203 #[pallet::storage]
204 pub type PendingRotation<T: Config> = StorageValue<_, RotationState, ValueQuery>;
205
206 /// Maximum number of validators returned as collators, `None` returns every validator with
207 /// registered keys.
208 ///
209 /// The cap applies before the union with the other session manager. A validator that the
210 /// other session manager also returns takes a place under the cap without adding a collator.
211 ///
212 /// The merged list becomes Aura's authority list, which Aura writes into the block header
213 /// whenever it changes, and the relay chain rejects a header above its head-data limit. Size
214 /// the cap so that the merged list, invulnerables and candidates included, keeps a
215 /// session-change header within that limit.
216 ///
217 /// Under a cap the validators are drawn at random, see [`Collators`].
218 ///
219 /// A change with a stored set forces two rotations. The first draws [`Collators`] again from
220 /// the stored set, so the drawn validators can change. The change is in force two blocks
221 /// later. Removing the cap returns every validator with registered keys.
222 #[pallet::storage]
223 pub type MaxCollators<T: Config> = StorageValue<_, u32, OptionQuery>;
224
225 /// The validators drawn from the stored set under [`MaxCollators`].
226 ///
227 /// Drawn at the first forced rotation after a set arrives or the cap changes, with a seed made
228 /// of the genesis hash, the era of the stored set and the cached [`EpochRandomness`]. Removed
229 /// when the cap is removed. At every rotation the pallet returns the drawn validators that
230 /// still have registered keys.
231 #[pallet::storage]
232 pub type Collators<T: Config> =
233 StorageValue<_, DrawnCollators<T::AccountId, T::MaxValidators>, OptionQuery>;
234
235 /// The relay chain randomness of one epoch ago, read from the relay chain state proof of every
236 /// block and written when it changes.
237 #[pallet::storage]
238 pub type EpochRandomness<T: Config> = StorageValue<_, [u8; 32], OptionQuery>;
239
240 #[pallet::event]
241 #[pallet::generate_deposit(pub(super) fn deposit_event)]
242 pub enum Event<T: Config> {
243 /// A validator set was stored for an era.
244 ValidatorSetReceived { era: EraIndex, count: u32 },
245 /// The maximum number of validator collators was changed.
246 MaxCollatorsSet { max: Option<u32> },
247 /// Validators were drawn from the set of `era` under the cap.
248 CollatorsDrawn { era: EraIndex, validators: Vec<T::AccountId> },
249 /// The stored validator set does not decode, so no validators were returned for the
250 /// session.
251 StoredSetUndecodable,
252 }
253
254 #[pallet::error]
255 pub enum Error<T> {
256 /// The era of the set is not newer than the era of the stored set.
257 StaleEra,
258 /// The set has more validators than [`Config::MaxValidators`].
259 TooManyValidators,
260 }
261
262 #[pallet::hooks]
263 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
264 /// Charges the read of [`PendingRotation`] by [`ShouldEndSession::should_end_session`],
265 /// which pallet-session makes in every block without charging it.
266 fn on_initialize(_: BlockNumberFor<T>) -> Weight {
267 T::DbWeight::get()
268 .reads(1)
269 .saturating_add(Weight::from_parts(0, RotationState::max_encoded_len() as u64))
270 }
271
272 #[cfg(feature = "try-runtime")]
273 fn try_state(_: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
274 Self::do_try_state()
275 }
276 }
277
278 #[pallet::call]
279 impl<T: Config> Pallet<T> {
280 /// Store the validator set of `era`.
281 #[pallet::call_index(0)]
282 #[pallet::weight(T::WeightInfo::set_validators(validators.len() as u32))]
283 pub fn set_validators(
284 origin: OriginFor<T>,
285 era: EraIndex,
286 validators: BoundedBTreeSet<T::AccountId, T::MaxValidators>,
287 ) -> DispatchResult {
288 T::SetOrigin::ensure_origin(origin)?;
289 Self::do_receive_validator_set(era, validators)
290 }
291
292 /// Set the maximum number of validators returned as collators.
293 ///
294 /// With a stored set, the change forces two rotations and the first draws [`Collators`]
295 /// again, so the drawn validators can change. `None` clears [`Collators`].
296 #[pallet::call_index(1)]
297 #[pallet::weight(T::WeightInfo::set_max_collators())]
298 pub fn set_max_collators(origin: OriginFor<T>, max: Option<u32>) -> DispatchResult {
299 T::UpdateOrigin::ensure_origin(origin)?;
300 MaxCollators::<T>::set(max);
301 if max.is_none() {
302 Collators::<T>::kill();
303 }
304 if ValidatorSet::<T>::exists() {
305 PendingRotation::<T>::put(RotationState::AwaitingQueue);
306 }
307 Self::deposit_event(Event::MaxCollatorsSet { max });
308 Ok(())
309 }
310 }
311
312 impl<T: Config> Pallet<T> {
313 /// Store the validator set of `era` and schedule the rotations that bring it into force.
314 ///
315 /// An account listed more than once is kept once, and the set is checked against
316 /// [`Config::MaxValidators`] after that.
317 pub fn receive_validator_set(
318 era: EraIndex,
319 validators: impl IntoIterator<Item = T::AccountId>,
320 ) -> DispatchResult {
321 let validators =
322 BoundedBTreeSet::try_from(validators.into_iter().collect::<BTreeSet<_>>())
323 .map_err(|_| Error::<T>::TooManyValidators)?;
324 Self::do_receive_validator_set(era, validators)
325 }
326
327 pub(crate) fn do_receive_validator_set(
328 era: EraIndex,
329 validators: BoundedBTreeSet<T::AccountId, T::MaxValidators>,
330 ) -> DispatchResult {
331 ensure!(
332 ValidatorSet::<T>::get().is_none_or(|stored| era > stored.era),
333 Error::<T>::StaleEra
334 );
335 let count = validators.len() as u32;
336 ValidatorSet::<T>::put(EraValidatorSet { era, validators });
337 PendingRotation::<T>::put(RotationState::AwaitingQueue);
338 Self::deposit_event(Event::ValidatorSetReceived { era, count });
339 Ok(())
340 }
341
342 /// The relay chain storage keys this pallet reads from the relay chain state proof.
343 ///
344 /// A runtime returns them, next to the keys of other pallets, from
345 /// [`KeyToIncludeInRelayProof`](cumulus_primitives_core::KeyToIncludeInRelayProof).
346 pub fn relay_keys_to_prove() -> Vec<RelayStorageKey> {
347 vec![RelayStorageKey::Top(well_known_keys::ONE_EPOCH_AGO_RANDOMNESS.to_vec())]
348 }
349
350 /// The seed of a draw from the set of `era`, made of the genesis hash, the era and
351 /// [`EpochRandomness`], or a zero value when no randomness is cached.
352 pub(crate) fn draw_seed(era: EraIndex) -> [u8; 32] {
353 let randomness = EpochRandomness::<T>::get().unwrap_or_else(|| {
354 log::error!(
355 target: crate::LOG_TARGET,
356 "no relay chain epoch randomness is cached, drawing with a zero value"
357 );
358 [0; 32]
359 });
360 let genesis = frame_system::Pallet::<T>::block_hash(BlockNumberFor::<T>::zero());
361 BlakeTwo256::hash_of(&(b"validator-collators", genesis, era, randomness)).into()
362 }
363
364 /// Draw up to `cap` validators with registered keys from `set` into [`Collators`] and
365 /// emit [`Event::CollatorsDrawn`].
366 ///
367 /// The whole set is shuffled before the key filter, so the order depends only on the seed
368 /// and the set.
369 fn draw_collators(
370 set: EraValidatorSet<T::AccountId, T::MaxValidators>,
371 cap: u32,
372 ) -> BoundedVec<T::AccountId, T::MaxValidators> {
373 let EraValidatorSet { era, validators } = set;
374 let mut order: Vec<_> = validators.into_iter().collect();
375 order.shuffle(&mut ChaCha20Rng::from_seed(Self::draw_seed(era)));
376 let drawn: Vec<_> = order
377 .into_iter()
378 .filter(T::ValidatorRegistration::is_registered)
379 .take(cap as usize)
380 .collect();
381 Self::deposit_event(Event::CollatorsDrawn { era, validators: drawn.clone() });
382 // At most the length of the set, which is bounded by `MaxValidators`.
383 let validators = BoundedVec::truncate_from(drawn);
384 Collators::<T>::put(DrawnCollators { era, validators: validators.clone() });
385 validators
386 }
387
388 /// Check the pallet invariants.
389 #[cfg(any(test, feature = "try-runtime"))]
390 pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
391 ensure!(
392 ValidatorSet::<T>::exists() || PendingRotation::<T>::get() == RotationState::Idle,
393 "a rotation is pending without a stored validator set"
394 );
395 ensure!(
396 !ValidatorSet::<T>::exists() || ValidatorSet::<T>::get().is_some(),
397 "the stored validator set does not decode, it may exceed `MaxValidators`"
398 );
399 ensure!(
400 !Collators::<T>::exists() ||
401 (MaxCollators::<T>::exists() && ValidatorSet::<T>::exists()),
402 "drawn collators are stored without both a cap and a validator set"
403 );
404 ensure!(
405 !Collators::<T>::exists() || Collators::<T>::get().is_some(),
406 "the drawn collators do not decode"
407 );
408 // With the pallet wired as `OnSystemEvent`, `on_relay_state_proof` caches a value in
409 // every block's inherent and the cache is never removed. So this holds for every draw
410 // after the first block that runs the pallet, and flags a runtime without that wiring.
411 ensure!(
412 !Collators::<T>::exists() || EpochRandomness::<T>::exists(),
413 "collators are drawn without a cached relay chain epoch randomness"
414 );
415 // Until the first forced rotation the drawn collators may be missing or belong to a
416 // previous set or cap.
417 if PendingRotation::<T>::get() == RotationState::AwaitingQueue {
418 return Ok(());
419 }
420 if let (Some(set), Some(cap)) = (ValidatorSet::<T>::get(), MaxCollators::<T>::get()) {
421 let drawn = Collators::<T>::get().ok_or("no collators are drawn under a cap")?;
422 ensure!(drawn.era == set.era, "the drawn collators belong to another era");
423 let mut seen = BTreeSet::new();
424 ensure!(
425 drawn.validators.iter().all(|v| seen.insert(v)),
426 "the drawn collators contain duplicates"
427 );
428 ensure!(
429 drawn.validators.iter().all(|v| set.validators.contains(v)),
430 "a drawn collator is not in the stored validator set"
431 );
432 ensure!(
433 drawn.validators.len() <= cap as usize,
434 "more collators are drawn than the cap allows"
435 );
436 }
437 Ok(())
438 }
439 }
440
441 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {
442 fn new_session(_: SessionIndex) -> Option<Vec<T::AccountId>> {
443 let pending = PendingRotation::<T>::get();
444 match pending {
445 RotationState::AwaitingQueue => {
446 PendingRotation::<T>::put(RotationState::AwaitingEnactment)
447 },
448 RotationState::AwaitingEnactment => PendingRotation::<T>::kill(),
449 RotationState::Idle => {},
450 }
451 let Some(set) = ValidatorSet::<T>::get() else {
452 if ValidatorSet::<T>::exists() {
453 log::error!(
454 target: crate::LOG_TARGET,
455 "the stored validator set does not decode"
456 );
457 Self::deposit_event(Event::StoredSetUndecodable);
458 }
459 return None;
460 };
461 // Drawn at the rotation, which pallet-session weighs as the whole block, since the key
462 // reads may not fit the weight of the message or report that delivers the set.
463 let validators = match MaxCollators::<T>::get() {
464 Some(cap) if pending == RotationState::AwaitingQueue => {
465 Self::draw_collators(set, cap).into_inner()
466 },
467 Some(_) => Collators::<T>::get()
468 .map(|drawn| drawn.validators.into_inner())
469 .unwrap_or_default(),
470 None => set.validators.into_iter().collect(),
471 };
472 Some(validators.into_iter().filter(T::ValidatorRegistration::is_registered).collect())
473 }
474
475 fn start_session(_: SessionIndex) {}
476
477 fn end_session(_: SessionIndex) {}
478 }
479
480 impl<T: Config> ShouldEndSession<BlockNumberFor<T>> for Pallet<T> {
481 fn should_end_session(now: BlockNumberFor<T>) -> bool {
482 PendingRotation::<T>::get() != RotationState::Idle ||
483 T::PeriodicSession::should_end_session(now)
484 }
485 }
486
487 impl<T: Config> OnSystemEvent for Pallet<T> {
488 fn on_validation_data(_: &PersistedValidationData) {}
489
490 fn on_validation_code_applied() {}
491
492 /// Caches the relay chain randomness of one epoch ago in [`EpochRandomness`].
493 ///
494 /// A proof that shows the key absent caches a zero value. A proof that lacks the trie nodes
495 /// of the key, or holds a value that does not decode, panics and so makes the block
496 /// invalid.
497 fn on_relay_state_proof(relay_state_proof: &RelayChainStateProof) -> Weight {
498 let randomness = match relay_state_proof
499 .read_entry::<[u8; 32]>(well_known_keys::ONE_EPOCH_AGO_RANDOMNESS, None)
500 {
501 Ok(randomness) => randomness,
502 // No randomness on the relay chain, as before its first epoch change, is cached as
503 // the zero value a draw without randomness uses.
504 Err(RelayProofError::ReadEntry(ReadEntryErr::Absent)) => {
505 log::debug!(
506 target: crate::LOG_TARGET,
507 "the relay chain state proof shows no epoch randomness, caching a zero value"
508 );
509 [0; 32]
510 },
511 Err(RelayProofError::ReadEntry(
512 error @ (ReadEntryErr::Proof | ReadEntryErr::Decode),
513 )) => panic!(
514 "Invalid relay chain epoch randomness in the relay chain state proof: {error:?}"
515 ),
516 // `read_entry` returns only `ReadEntry` errors.
517 Err(error) => panic!("Invalid relay chain state proof: {error:?}"),
518 };
519 if EpochRandomness::<T>::get() != Some(randomness) {
520 EpochRandomness::<T>::put(randomness);
521 }
522 T::WeightInfo::on_relay_state_proof()
523 }
524 }
525}