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`]. 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}