pallet_session_validator_management/
lib.rs

1//!  Pallet for setting the Partner Chain validators using inherent data
2
3#![cfg_attr(not(feature = "std"), no_std)]
4#![allow(clippy::type_complexity)]
5#![deny(missing_docs)]
6
7pub mod migrations;
8/// [`pallet_session`] integration.
9#[cfg(feature = "pallet-session-compat")]
10pub mod pallet_session_support;
11#[cfg(feature = "pallet-session-compat")]
12pub mod session_manager;
13
14pub use pallet::*;
15
16#[cfg(test)]
17mod mock;
18
19#[cfg(test)]
20mod tests;
21
22pub mod weights;
23
24pub use sp_session_validator_management::CommitteeMember;
25pub use weights::WeightInfo;
26
27#[frame_support::pallet]
28pub mod pallet {
29	use super::*;
30	use frame_support::pallet_prelude::*;
31	use frame_system::pallet_prelude::*;
32	use log::{info, warn};
33	use sidechain_domain::byte_string::SizedByteString;
34	use sidechain_domain::{MainchainAddress, PolicyId};
35	use sp_core::blake2_256;
36	use sp_runtime::traits::{MaybeSerializeDeserialize, One, Zero};
37	use sp_session_validator_management::*;
38	use sp_std::fmt::Display;
39	use sp_std::{ops::Add, vec, vec::Vec};
40
41	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
42
43	#[pallet::pallet]
44	#[pallet::storage_version(STORAGE_VERSION)]
45	pub struct Pallet<T>(_);
46
47	#[pallet::config]
48	pub trait Config: frame_system::Config {
49		#[pallet::constant]
50		/// Maximum amount of validators.
51		type MaxValidators: Get<u32>;
52		/// Type identifying authorities.
53		type AuthorityId: Member
54			+ Parameter
55			+ MaybeSerializeDeserialize
56			+ MaxEncodedLen
57			+ Ord
58			+ Into<Self::AccountId>;
59		/// Type of authority keys.
60		type AuthorityKeys: Parameter + Member + MaybeSerializeDeserialize + Ord + MaxEncodedLen;
61		/// Type of input data used by `select_authorities` to select a committee.
62		type AuthoritySelectionInputs: Parameter;
63		/// Type of the epoch number used by the partner chain.
64		type ScEpochNumber: Parameter
65			+ MaxEncodedLen
66			+ Zero
67			+ Display
68			+ Add
69			+ One
70			+ Default
71			+ Ord
72			+ Copy
73			+ From<u64>
74			+ Into<u64>;
75
76		/// Type of committee members returned by `select_authorities`.
77		type CommitteeMember: Parameter
78			+ Member
79			+ MaybeSerializeDeserialize
80			+ MaxEncodedLen
81			+ CommitteeMember<AuthorityId = Self::AuthorityId, AuthorityKeys = Self::AuthorityKeys>;
82
83		/// Origin for governance calls
84		type MainChainScriptsOrigin: EnsureOrigin<Self::RuntimeOrigin>;
85
86		/// Should select a committee for `sidechain_epoch` based on selection inputs `input`.
87		/// Should return [None] if selection was impossible for the given input.
88		fn select_authorities(
89			input: Self::AuthoritySelectionInputs,
90			sidechain_epoch: Self::ScEpochNumber,
91		) -> Option<BoundedVec<Self::CommitteeMember, Self::MaxValidators>>;
92
93		/// Should return the current partner chain epoch.
94		fn current_epoch_number() -> Self::ScEpochNumber;
95
96		/// Weight functions needed for pallet_session_validator_management.
97		type WeightInfo: WeightInfo;
98	}
99
100	#[pallet::event]
101	pub enum Event<T: Config> {}
102
103	use frame_support::{BoundedVec, CloneNoBound};
104	use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
105	use scale_info::TypeInfo;
106
107	#[derive(CloneNoBound, Encode, Decode, TypeInfo, MaxEncodedLen)]
108	#[scale_info(skip_type_params(MaxValidators))]
109	/// Committee info type used on-chain.
110	pub struct CommitteeInfo<ScEpochNumber: Clone, CommitteeMember: Clone, MaxValidators> {
111		/// Epoch number the committee is selected for.
112		pub epoch: ScEpochNumber,
113		/// List of committee members.
114		pub committee: BoundedVec<CommitteeMember, MaxValidators>,
115	}
116
117	impl<ScEpochNumber: Clone, CommitteeMember: Clone, MaxValidators>
118		CommitteeInfo<ScEpochNumber, CommitteeMember, MaxValidators>
119	{
120		/// Returns committee info as a pair of epoch number and list of committee members
121		pub fn as_pair(self) -> (ScEpochNumber, Vec<CommitteeMember>) {
122			(self.epoch, self.committee.to_vec())
123		}
124	}
125
126	impl<ScEpochNumber, CommitteeMember, MaxValidators> Default
127		for CommitteeInfo<ScEpochNumber, CommitteeMember, MaxValidators>
128	where
129		CommitteeMember: Clone,
130		ScEpochNumber: Clone + Zero,
131	{
132		fn default() -> Self {
133			Self { epoch: ScEpochNumber::zero(), committee: BoundedVec::new() }
134		}
135	}
136
137	#[pallet::storage]
138	pub type CurrentCommittee<T: Config> = StorageValue<
139		_,
140		CommitteeInfo<T::ScEpochNumber, T::CommitteeMember, T::MaxValidators>,
141		ValueQuery,
142	>;
143
144	#[pallet::storage]
145	pub type NextCommittee<T: Config> = StorageValue<
146		_,
147		CommitteeInfo<T::ScEpochNumber, T::CommitteeMember, T::MaxValidators>,
148		OptionQuery,
149	>;
150
151	#[pallet::storage]
152	pub type MainChainScriptsConfiguration<T: Config> =
153		StorageValue<_, MainChainScripts, ValueQuery>;
154
155	#[pallet::error]
156	pub enum Error<T> {
157		/// [Pallet::set] has been called with epoch number that is not current epoch + 1
158		InvalidEpoch,
159		/// [Pallet::set] has been called a second time for the same next epoch
160		NextCommitteeAlreadySet,
161	}
162
163	#[pallet::genesis_config]
164	#[derive(frame_support::DefaultNoBound)]
165	pub struct GenesisConfig<T: Config> {
166		/// Initial committee members of the partner chain.
167		pub initial_authorities: Vec<T::CommitteeMember>,
168		/// Initial [MainChainScripts] of the partner chain.
169		pub main_chain_scripts: MainChainScripts,
170	}
171
172	#[pallet::genesis_build]
173	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
174		fn build(&self) {
175			let initial_authorities = BoundedVec::truncate_from(self.initial_authorities.clone());
176			let committee_info =
177				CommitteeInfo { epoch: T::ScEpochNumber::zero(), committee: initial_authorities };
178			CurrentCommittee::<T>::put(committee_info);
179			MainChainScriptsConfiguration::<T>::put(self.main_chain_scripts.clone());
180		}
181	}
182
183	#[pallet::hooks]
184	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
185		// Only reason for this hook is to set the genesis committee as the committee for first block's epoch.
186		// If it wouldn't be set, the should_end_session() function would return true at the 2nd block,
187		// thus denying handover phase to genesis committee, which would break the chain. With this hook,
188		// should_end_session() returns true at 1st block and changes committee to the same one, thus allowing
189		// handover phase to happen. After having proper chain initialization procedure this probably won't be needed anymore.
190		// Note: If chain is started during handover phase, it will wait until new epoch to produce the first block.
191		fn on_initialize(block_nr: BlockNumberFor<T>) -> Weight {
192			if block_nr.is_one() {
193				CurrentCommittee::<T>::mutate(|committee| {
194					committee.epoch = T::current_epoch_number();
195				});
196				T::DbWeight::get().reads_writes(2, 1)
197			} else {
198				Weight::zero()
199			}
200		}
201	}
202
203	#[pallet::inherent]
204	impl<T: Config> ProvideInherent for Pallet<T> {
205		type Call = Call<T>;
206		type Error = InherentError;
207		const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;
208
209		/// Responsible for calling [Call::set] on each block by the block author, if the validator list changed
210		fn create_inherent(data: &InherentData) -> Option<Self::Call> {
211			if NextCommittee::<T>::exists() {
212				None
213			} else {
214				let for_epoch_number = CurrentCommittee::<T>::get().epoch + One::one();
215				let (authority_selection_inputs, selection_inputs_hash) =
216					Self::inherent_data_to_authority_selection_inputs(data);
217				if let Some(validators) =
218					T::select_authorities(authority_selection_inputs, for_epoch_number)
219				{
220					Some(Call::set { validators, for_epoch_number, selection_inputs_hash })
221				} else {
222					let current_committee = CurrentCommittee::<T>::get();
223					let current_committee_epoch = current_committee.epoch;
224					warn!(
225						"Committee for epoch {for_epoch_number} is the same as for epoch {current_committee_epoch}"
226					);
227					let validators = current_committee.committee;
228					Some(Call::set { validators, for_epoch_number, selection_inputs_hash })
229				}
230			}
231		}
232
233		// TODO make this call run by every full node, so it can be relied upon for ensuring that the block is correct
234		fn check_inherent(call: &Self::Call, data: &InherentData) -> Result<(), Self::Error> {
235			let (validators_param, for_epoch_number_param, call_selection_inputs_hash) = match call
236			{
237				Call::set { validators, for_epoch_number, selection_inputs_hash } => {
238					(validators, for_epoch_number, selection_inputs_hash)
239				},
240				_ => return Ok(()),
241			};
242
243			let (authority_selection_inputs, computed_selection_inputs_hash) =
244				Self::inherent_data_to_authority_selection_inputs(data);
245			let validators =
246				T::select_authorities(authority_selection_inputs, *for_epoch_number_param)
247					.unwrap_or_else(|| {
248						// Proposed block should keep the same committee if calculation of new one was impossible.
249						// This is code is executed before the committee rotation, so the NextCommittee should be used.
250						let committee_info = NextCommittee::<T>::get()
251							// Needed only for verification of the block no 1, before any `set` call is executed.
252							.unwrap_or_else(CurrentCommittee::<T>::get);
253						committee_info.committee
254					});
255
256			if *validators_param != validators {
257				if *call_selection_inputs_hash == computed_selection_inputs_hash {
258					return Err(InherentError::InvalidValidatorsMatchingHash(
259						computed_selection_inputs_hash,
260					));
261				} else {
262					return Err(InherentError::InvalidValidatorsHashMismatch(
263						computed_selection_inputs_hash,
264						call_selection_inputs_hash.clone(),
265					));
266				}
267			}
268
269			Ok(())
270		}
271
272		fn is_inherent(call: &Self::Call) -> bool {
273			matches!(call, Call::set { .. })
274		}
275
276		fn is_inherent_required(_: &InherentData) -> Result<Option<Self::Error>, Self::Error> {
277			if !NextCommittee::<T>::exists() {
278				Ok(Some(InherentError::CommitteeNeedsToBeStoredOneEpochInAdvance)) // change error
279			} else {
280				Ok(None)
281			}
282		}
283	}
284
285	#[pallet::call]
286	impl<T: Config> Pallet<T> {
287		/// 'for_epoch_number' parameter is needed only for validation purposes, because we need to make sure that
288		/// check_inherent uses the same epoch_number as was used to create inherent data.
289		/// Alternative approach would be to put epoch number inside InherentData. However, sidechain
290		/// epoch number is set in Runtime, thus, inherent data provider doesn't have to know about it.
291		/// On top of that, the latter approach is slightly more complicated to code.
292		#[pallet::call_index(0)]
293		#[pallet::weight((
294		T::WeightInfo::set(validators.len() as u32),
295		DispatchClass::Mandatory
296		))]
297		pub fn set(
298			origin: OriginFor<T>,
299			validators: BoundedVec<T::CommitteeMember, T::MaxValidators>,
300			for_epoch_number: T::ScEpochNumber,
301			selection_inputs_hash: SizedByteString<32>,
302		) -> DispatchResult {
303			ensure_none(origin)?;
304			let expected_epoch_number = CurrentCommittee::<T>::get().epoch + One::one();
305			ensure!(for_epoch_number == expected_epoch_number, Error::<T>::InvalidEpoch);
306			ensure!(!NextCommittee::<T>::exists(), Error::<T>::NextCommitteeAlreadySet);
307			let len = validators.len();
308			info!(
309				"💼 Storing committee of size {len} for epoch {for_epoch_number}, input data hash: {}",
310				selection_inputs_hash.to_hex_string()
311			);
312			NextCommittee::<T>::put(CommitteeInfo {
313				epoch: for_epoch_number,
314				committee: validators,
315			});
316			Ok(())
317		}
318
319		/// Changes the main chain scripts used for committee rotation.
320		///
321		/// This extrinsic must be run either using `sudo` or some other chain governance mechanism.
322		#[pallet::call_index(1)]
323		#[pallet::weight(T::WeightInfo::set(1))]
324		pub fn set_main_chain_scripts(
325			origin: OriginFor<T>,
326			committee_candidate_address: MainchainAddress,
327			d_parameter_policy_id: PolicyId,
328			permissioned_candidates_policy_id: PolicyId,
329		) -> DispatchResult {
330			T::MainChainScriptsOrigin::ensure_origin(origin)?;
331			let new_scripts = MainChainScripts {
332				committee_candidate_address,
333				d_parameter_policy_id,
334				permissioned_candidates_policy_id,
335			};
336			MainChainScriptsConfiguration::<T>::put(new_scripts);
337			Ok(())
338		}
339	}
340
341	impl<T: Config> Pallet<T> {
342		/// Returns epoch number for which next committee hasn't been set yet.
343		pub fn get_next_unset_epoch_number() -> T::ScEpochNumber {
344			NextCommittee::<T>::get()
345				.map(|next_committee| next_committee.epoch + One::one())
346				.unwrap_or(CurrentCommittee::<T>::get().epoch + One::one())
347		}
348
349		/// Returns current committee member for an index, repeating them in a round-robin fashion if needed.
350		pub fn get_current_authority_round_robin(index: usize) -> Option<T::CommitteeMember> {
351			let committee = CurrentCommittee::<T>::get().committee;
352			if committee.is_empty() {
353				return None;
354			}
355
356			committee.get(index % committee.len() as usize).cloned()
357		}
358
359		/// Returns current committee from storage.
360		pub fn current_committee_storage()
361		-> CommitteeInfo<T::ScEpochNumber, T::CommitteeMember, T::MaxValidators> {
362			CurrentCommittee::<T>::get()
363		}
364
365		/// Returns next committee from storage.
366		pub fn next_committee_storage()
367		-> Option<CommitteeInfo<T::ScEpochNumber, T::CommitteeMember, T::MaxValidators>> {
368			NextCommittee::<T>::get()
369		}
370
371		/// Returns the `AuthorityId`s of next committee from storage.
372		///
373		/// This function's result should be always defined after inherent call of 1st block of each epoch
374		pub fn next_committee() -> Option<BoundedVec<T::AuthorityId, T::MaxValidators>> {
375			Some(BoundedVec::truncate_from(
376				NextCommittee::<T>::get()?
377					.committee
378					.into_iter()
379					.map(|member| member.authority_id())
380					.collect::<Vec<T::AuthorityId>>(),
381			))
382		}
383
384		fn inherent_data_to_authority_selection_inputs(
385			data: &InherentData,
386		) -> (T::AuthoritySelectionInputs, SizedByteString<32>) {
387			let decoded_data = data
388				.get_data::<T::AuthoritySelectionInputs>(&INHERENT_IDENTIFIER)
389				.expect("Validator inherent data not correctly encoded")
390				.expect("Validator inherent data must be provided");
391			let data_hash = SizedByteString(blake2_256(&decoded_data.encode()));
392
393			(decoded_data, data_hash)
394		}
395
396		/// Calculates committee using configured `select_authorities` function
397		pub fn calculate_committee(
398			authority_selection_inputs: T::AuthoritySelectionInputs,
399			sidechain_epoch: T::ScEpochNumber,
400		) -> Option<Vec<T::CommitteeMember>> {
401			T::select_authorities(authority_selection_inputs, sidechain_epoch).map(|c| c.to_vec())
402		}
403
404		/// If [NextCommittee] is defined, it moves its value to [CurrentCommittee] storage.
405		/// Returns the value taken from [NextCommittee].
406		pub fn rotate_committee_to_next_epoch() -> Option<Vec<T::CommitteeMember>> {
407			let next_committee = NextCommittee::<T>::take()?;
408
409			CurrentCommittee::<T>::put(next_committee.clone());
410
411			let validators = next_committee.committee.to_vec();
412			let len = validators.len();
413			info!(
414				"Committee rotated: Returning {len} validators, stored in epoch {}",
415				next_committee.epoch
416			);
417			Some(validators)
418		}
419
420		/// Returns main chain scripts.
421		pub fn get_main_chain_scripts() -> MainChainScripts {
422			MainChainScriptsConfiguration::<T>::get()
423		}
424	}
425}