pallet_session_validator_management/
pallet_session_support.rs

1//! Implements [pallet_session::SessionManager] and [pallet_session::ShouldEndSession] for [crate::Pallet].
2//!
3//! This implementation has lag of one additional PC epoch when applying committees to sessions.
4//!
5//! To use it, wire [crate::Pallet] in runtime configuration of [`pallet_session`].
6use crate::{CommitteeRotationStage, CommitteeRotationStages};
7use frame_support::traits::UnfilteredDispatchable;
8use frame_system::RawOrigin;
9use frame_system::pallet_prelude::BlockNumberFor;
10use log::{debug, info, warn};
11use sp_staking::SessionIndex;
12use sp_std::collections::btree_set::BTreeSet;
13use sp_std::{vec, vec::Vec};
14
15impl<T: crate::Config + pallet_session::Config> pallet_session::SessionManager<T::AccountId>
16	for crate::Pallet<T>
17where
18	<T as pallet_session::Config>::Keys: From<T::AuthorityKeys>,
19{
20	/// Sets the first validator-set by mapping the current committee from [crate::Pallet]
21	fn new_session_genesis(_new_index: SessionIndex) -> Option<Vec<T::AccountId>> {
22		Some(
23			crate::Pallet::<T>::current_committee_storage()
24				.committee
25				.into_iter()
26				.map(|member| member.authority_id().into())
27				.collect::<Vec<_>>(),
28		)
29	}
30
31	/// Rotates the committee in [crate::Pallet] and plans this new committee as upcoming validator-set.
32	/// Updates the session index of [`pallet_session`].
33	// Instead of Some((*).expect) we could just use (*). However, we rather panic in presence of important programming errors.
34	fn new_session(new_index: SessionIndex) -> Option<Vec<T::AccountId>> {
35		if CommitteeRotationStage::<T>::get() == CommitteeRotationStages::AdditionalSession {
36			info!("💼 Session manager: new additional session {new_index}");
37			CommitteeRotationStage::<T>::put(CommitteeRotationStages::AwaitEpochChange);
38			let committee = crate::Pallet::<T>::current_committee_storage().committee;
39			return Some(committee.iter().map(|member| member.authority_id().into()).collect());
40		}
41
42		info!("💼 Session manager: new_session {new_index}, rotating the committee");
43		let new_committee = crate::Pallet::<T>::rotate_committee_to_next_epoch().expect(
44			"Session should never end without current epoch validators defined. \
45				Check ShouldEndSession implementation or if it is used before starting new session",
46		);
47
48		let old_committee_accounts = crate::ProvidedAccounts::<T>::take();
49		let mut new_committee_accounts: BTreeSet<T::AccountId> = BTreeSet::new();
50
51		for member in new_committee.iter() {
52			let account: T::AccountId = member.authority_id().into();
53
54			if !new_committee_accounts.contains(&account) {
55				new_committee_accounts.insert(account.clone());
56
57				// Members that were already in the old committee have their accounts provided
58				if !old_committee_accounts.contains(&account) {
59					provide_account::<T>(&account);
60				}
61
62				let new_keys = member.authority_keys();
63				let current_keys = load_keys::<T>(&account);
64
65				if current_keys != Some(new_keys.clone().into()) {
66					purge_keys::<T>(&account);
67					set_keys::<T>(&account, new_keys.clone());
68				}
69			}
70		}
71
72		for account in old_committee_accounts.difference(&new_committee_accounts) {
73			purge_keys::<T>(account);
74			unprovide_account::<T>(account);
75		}
76
77		crate::ProvidedAccounts::<T>::set(new_committee_accounts.clone().try_into().unwrap());
78
79		Some(new_committee.into_iter().map(|member| member.authority_id().into()).collect())
80	}
81
82	fn end_session(end_index: SessionIndex) {
83		debug!("Session manager: End session {end_index}");
84	}
85
86	// Session is expected to be at least 1 block behind sidechain epoch.
87	fn start_session(start_index: SessionIndex) {
88		let epoch_number = T::current_epoch_number();
89		debug!("Session manager: Start session {start_index}, epoch {epoch_number}");
90	}
91}
92
93fn load_keys<T: pallet_session::Config>(account: &T::AccountId) -> Option<T::Keys> {
94	<T as pallet_session::Config>::ValidatorId::try_from(account.clone())
95		.ok()
96		.as_ref()
97		.and_then(pallet_session::Pallet::<T>::load_keys)
98}
99
100fn set_keys<T: crate::Config + pallet_session::Config>(
101	account: &T::AccountId,
102	keys: T::AuthorityKeys,
103) where
104	<T as pallet_session::Config>::Keys: From<T::AuthorityKeys>,
105{
106	let set_keys_result = pallet_session::Call::<T>::set_keys { keys: keys.into(), proof: vec![] }
107		.dispatch_bypass_filter(RawOrigin::Signed(account.clone()).into());
108
109	match set_keys_result {
110		Ok(_) => debug!("set_keys for {account:?}"),
111		Err(e) => {
112			info!("Could not set_keys for {account:?}, error: {:?}", e.error)
113		},
114	}
115}
116
117fn purge_keys<T: crate::Config + pallet_session::Config>(account: &T::AccountId)
118where
119	<T as pallet_session::Config>::Keys: From<T::AuthorityKeys>,
120{
121	let purge_keys_result = pallet_session::Call::<T>::purge_keys {}
122		.dispatch_bypass_filter(RawOrigin::Signed(account.clone()).into());
123	match purge_keys_result {
124		Ok(_) => debug!("purge_keys for {account:?}"),
125		Err(e) => info!("Could not purge_keys for {account:?}, error: {:?}", e.error),
126	}
127}
128
129fn provide_account<T: crate::Config + pallet_session::Config>(account: &T::AccountId) {
130	log::debug!(
131		"➕💼 Incrementing provider count and registering keys for block producer {account:?}"
132	);
133
134	frame_system::Pallet::<T>::inc_providers(&account);
135}
136
137fn unprovide_account<T: crate::Config + pallet_session::Config>(account: &T::AccountId) {
138	log::info!(
139		"➖💼 Decrementing provider count and deregistering keys for block producer {account:?}"
140	);
141	frame_system::Pallet::<T>::dec_providers(&account).expect(
142		"We always match dec_providers with corresponding inc_providers, thus it cannot fail",
143	);
144}
145
146/// Tries to end each session in the first block of each partner chains epoch in which the committee for the epoch is defined.
147impl<T> pallet_session::ShouldEndSession<BlockNumberFor<T>> for crate::Pallet<T>
148where
149	T: crate::Config,
150{
151	fn should_end_session(n: BlockNumberFor<T>) -> bool {
152		let current_epoch_number = T::current_epoch_number();
153		let current_committee_epoch = crate::Pallet::<T>::current_committee_storage().epoch;
154		let next_committee_is_defined = crate::Pallet::<T>::next_committee().is_some();
155		if current_epoch_number > current_committee_epoch {
156			if next_committee_is_defined {
157				info!("Session manager: should_end_session({n:?}) = true");
158				CommitteeRotationStage::<T>::put(CommitteeRotationStages::NewSessionDueEpochChange);
159				true
160			} else {
161				warn!(
162					"Session manager: should_end_session({n:?}) 'current epoch' > 'committee epoch' but the next committee is not defined"
163				);
164				false
165			}
166		} else {
167			let stage = CommitteeRotationStage::<T>::get();
168			if stage == CommitteeRotationStages::NewSessionDueEpochChange {
169				CommitteeRotationStage::<T>::put(CommitteeRotationStages::AdditionalSession);
170				info!("Session manager: should_end_session({n:?}) to force the new committee");
171				true
172			} else {
173				false
174			}
175		}
176	}
177}
178
179#[cfg(test)]
180mod tests {
181	use crate::{
182		CommitteeInfo, CurrentCommittee, NextCommittee,
183		mock::{mock_pallet::CurrentEpoch, *},
184		tests::increment_epoch,
185	};
186	use pallet_session::ShouldEndSession;
187	pub const IRRELEVANT: u64 = 2;
188	use sidechain_domain::ScEpochNumber;
189	use sp_runtime::testing::UintAuthorityId;
190
191	type Manager = crate::Pallet<Test>;
192
193	#[test]
194	fn should_end_session_if_last_one_ended_late_and_new_committee_is_defined() {
195		let current_committee_epoch = ScEpochNumber(100);
196		let current_committee = as_permissioned_members(&[alice()]);
197		let next_committee_epoch = 102;
198		let next_committee = as_permissioned_members(&[bob()]);
199
200		new_test_ext().execute_with(|| {
201			CurrentCommittee::<Test>::put(CommitteeInfo {
202				epoch: current_committee_epoch.into(),
203				committee: current_committee,
204			});
205			CurrentEpoch::<Test>::set(current_committee_epoch.next().next());
206			assert!(!Manager::should_end_session(IRRELEVANT));
207			NextCommittee::<Test>::put(CommitteeInfo {
208				epoch: next_committee_epoch.into(),
209				committee: next_committee,
210			});
211			assert!(Manager::should_end_session(IRRELEVANT));
212		});
213	}
214
215	#[test]
216	fn register_session_keys_for_provided_authorities() {
217		new_test_ext().execute_with(|| {
218			set_validators_directly(&[dave(), eve()], 1).unwrap();
219			// By default, the session keys are not set for the account.
220			assert_eq!(Session::load_keys(&dave().account_id()), None);
221			assert_eq!(Session::load_keys(&eve().account_id()), None);
222			increment_epoch();
223
224			start_session(1);
225
226			// After setting the keys, they should be stored in the session.
227			assert_eq!(Session::load_keys(&dave().account_id()), Some(dave().authority_keys));
228			assert_eq!(Session::load_keys(&eve().account_id()), Some(eve().authority_keys));
229		});
230	}
231
232	#[test]
233	fn reregister_changed_session_keys_for_sitting_authority() {
234		new_test_ext().execute_with(|| {
235			set_validators_directly(&[dave(), eve()], 1).unwrap();
236			// By default, the session keys are not set for the account.
237			assert_eq!(Session::load_keys(&dave().account_id()), None);
238			assert_eq!(Session::load_keys(&eve().account_id()), None);
239			increment_epoch();
240
241			start_session(1);
242
243			// After setting the keys, they should be stored in the session.
244			assert_eq!(Session::load_keys(&dave().account_id()), Some(dave().authority_keys));
245			assert_eq!(Session::load_keys(&eve().account_id()), Some(eve().authority_keys));
246
247			let eve_with_different_keys = MockValidator {
248				name: "Eve with different keys",
249				authority_keys: SessionKeys { foo: UintAuthorityId(44) },
250				..eve()
251			};
252
253			// Eve is re-elected to the committee, but is using different keys now
254			set_validators_directly(&[dave(), eve_with_different_keys.clone()], 2).unwrap();
255			increment_epoch();
256			start_session(2);
257
258			// Eve's keys were updated
259			assert_eq!(
260				Session::load_keys(&eve().account_id()),
261				Some(eve_with_different_keys.authority_keys)
262			);
263		});
264	}
265
266	#[test]
267	fn ends_two_sessions_and_rotates_once_when_committee_changes() {
268		new_test_ext().execute_with(|| {
269			assert_eq!(Session::current_index(), 0);
270			assert_eq!(SessionCommitteeManagement::current_committee_storage().epoch.0, 0);
271			increment_epoch();
272			set_validators_directly(&[charlie(), dave()], 1).unwrap();
273
274			advance_one_block();
275			assert_eq!(Session::current_index(), 1);
276			// pallet_session needs additional session to apply charlie() and dave() as validators
277			assert_eq!(Session::validators(), vec![]);
278			assert_eq!(SessionCommitteeManagement::current_committee_storage().epoch.0, 1);
279
280			advance_one_block();
281			assert_eq!(Session::current_index(), 2);
282			assert_eq!(Session::validators(), vec![charlie().account_id(), dave().account_id()]);
283			assert_eq!(SessionCommitteeManagement::current_committee_storage().epoch.0, 1);
284
285			for _i in 0..10 {
286				advance_one_block();
287				assert_eq!(Session::current_index(), 2);
288				assert_eq!(
289					Session::validators(),
290					vec![charlie().account_id(), dave().account_id()]
291				);
292				assert_eq!(SessionCommitteeManagement::current_committee_storage().epoch.0, 1);
293			}
294		});
295	}
296}