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::{CommitteeMember, 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 = 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 and keys set up already
58				if !old_committee_accounts.contains(&account) {
59					setup_block_producer::<T>(account, member.authority_keys());
60				}
61			}
62		}
63
64		for account in old_committee_accounts.difference(&new_committee_accounts) {
65			teardown_block_producer::<T>(account)
66		}
67
68		crate::ProvidedAccounts::<T>::set(new_committee_accounts.clone().try_into().unwrap());
69
70		Some(new_committee.into_iter().map(|member| member.authority_id().into()).collect())
71	}
72
73	fn end_session(end_index: SessionIndex) {
74		debug!("Session manager: End session {end_index}");
75	}
76
77	// Session is expected to be at least 1 block behind sidechain epoch.
78	fn start_session(start_index: SessionIndex) {
79		let epoch_number = T::current_epoch_number();
80		debug!("Session manager: Start session {start_index}, epoch {epoch_number}");
81	}
82}
83
84/// Provides accounts and registers keys for new committee members
85fn setup_block_producer<T: crate::Config + pallet_session::Config>(
86	account: T::AccountId,
87	keys: T::AuthorityKeys,
88) where
89	<T as pallet_session::Config>::Keys: From<T::AuthorityKeys>,
90{
91	log::debug!(
92		"➕💼 Incrementing provider count and registering keys for block producer {account:?}"
93	);
94
95	frame_system::Pallet::<T>::inc_providers(&account);
96
97	let set_keys_result = pallet_session::Call::<T>::set_keys { keys: keys.into(), proof: vec![] }
98		.dispatch_bypass_filter(RawOrigin::Signed(account.clone()).into());
99
100	match set_keys_result {
101		Ok(_) => debug!("set_keys for {account:?}"),
102		Err(e) => {
103			info!("Could not set_keys for {account:?}, error: {:?}", e.error)
104		},
105	}
106}
107
108/// Removes account provisions and purges keys for outgoing old committee members
109fn teardown_block_producer<T: crate::Config + pallet_session::Config>(account: &T::AccountId)
110where
111	<T as pallet_session::Config>::Keys: From<T::AuthorityKeys>,
112{
113	let purge_keys_result = pallet_session::Call::<T>::purge_keys {}
114		.dispatch_bypass_filter(RawOrigin::Signed(account.clone()).into());
115	match purge_keys_result {
116		Ok(_) => debug!("purge_keys for {account:?}"),
117		Err(e) => info!("Could not purge_keys for {account:?}, error: {:?}", e.error),
118	}
119	log::info!(
120		"➖💼 Decrementing provider count and deregisteringkeys for block producer {account:?}"
121	);
122	frame_system::Pallet::<T>::dec_providers(&account).expect(
123		"We always match dec_providers with corresponding inc_providers, thus it cannot fail",
124	);
125}
126
127/// Tries to end each session in the first block of each partner chains epoch in which the committee for the epoch is defined.
128impl<T, EpochNumber> pallet_session::ShouldEndSession<BlockNumberFor<T>> for crate::Pallet<T>
129where
130	T: crate::Config<ScEpochNumber = EpochNumber>,
131	EpochNumber: Clone + PartialOrd,
132{
133	fn should_end_session(n: BlockNumberFor<T>) -> bool {
134		let current_epoch_number = T::current_epoch_number();
135		let current_committee_epoch = crate::Pallet::<T>::current_committee_storage().epoch;
136		let next_committee_is_defined = crate::Pallet::<T>::next_committee().is_some();
137		if current_epoch_number > current_committee_epoch {
138			if next_committee_is_defined {
139				info!("Session manager: should_end_session({n:?}) = true");
140				CommitteeRotationStage::<T>::put(CommitteeRotationStages::NewSessionDueEpochChange);
141				true
142			} else {
143				warn!(
144					"Session manager: should_end_session({n:?}) 'current epoch' > 'committee epoch' but the next committee is not defined"
145				);
146				false
147			}
148		} else {
149			let stage = CommitteeRotationStage::<T>::get();
150			if stage == CommitteeRotationStages::NewSessionDueEpochChange {
151				CommitteeRotationStage::<T>::put(CommitteeRotationStages::AdditionalSession);
152				info!("Session manager: should_end_session({n:?}) to force the new committee");
153				true
154			} else {
155				false
156			}
157		}
158	}
159}
160
161#[cfg(test)]
162mod tests {
163	use crate::{
164		CommitteeInfo, CurrentCommittee, NextCommittee,
165		mock::{mock_pallet::CurrentEpoch, *},
166		tests::increment_epoch,
167	};
168	use pallet_session::ShouldEndSession;
169	pub const IRRELEVANT: u64 = 2;
170	use sp_runtime::testing::UintAuthorityId;
171
172	type Manager = crate::Pallet<Test>;
173
174	#[test]
175	fn should_end_session_if_last_one_ended_late_and_new_committee_is_defined() {
176		let current_committee_epoch = 100;
177		let current_committee = ids_and_keys_fn(&[ALICE]);
178		let next_committee_epoch = 102;
179		let next_committee = ids_and_keys_fn(&[BOB]);
180
181		new_test_ext().execute_with(|| {
182			CurrentCommittee::<Test>::put(CommitteeInfo {
183				epoch: current_committee_epoch,
184				committee: current_committee,
185			});
186			CurrentEpoch::<Test>::set(current_committee_epoch + 2);
187			assert!(!Manager::should_end_session(IRRELEVANT));
188			NextCommittee::<Test>::put(CommitteeInfo {
189				epoch: next_committee_epoch,
190				committee: next_committee,
191			});
192			assert!(Manager::should_end_session(IRRELEVANT));
193		});
194	}
195
196	#[test]
197	fn register_session_keys_for_provided_authorities() {
198		new_test_ext().execute_with(|| {
199			set_validators_directly(&[DAVE, EVE], 1).unwrap();
200			// By default, the session keys are not set for the account.
201			assert_eq!(Session::load_keys(&DAVE.authority_id), None);
202			assert_eq!(Session::load_keys(&EVE.authority_id), None);
203			increment_epoch();
204
205			start_session(1);
206
207			// After setting the keys, they should be stored in the session.
208			assert_eq!(
209				Session::load_keys(&DAVE.authority_id),
210				Some(SessionKeys { foo: UintAuthorityId(DAVE.authority_keys) })
211			);
212			assert_eq!(
213				Session::load_keys(&EVE.authority_id),
214				Some(SessionKeys { foo: UintAuthorityId(EVE.authority_keys) })
215			);
216		});
217	}
218
219	#[test]
220	fn ends_two_sessions_and_rotates_once_when_committee_changes() {
221		new_test_ext().execute_with(|| {
222			assert_eq!(Session::current_index(), 0);
223			assert_eq!(SessionCommitteeManagement::current_committee_storage().epoch, 0);
224			increment_epoch();
225			set_validators_directly(&[CHARLIE, DAVE], 1).unwrap();
226
227			advance_one_block();
228			assert_eq!(Session::current_index(), 1);
229			// pallet_session needs additional session to apply CHARLIE and DAVE as validators
230			assert_eq!(Session::validators(), Vec::<u64>::new());
231			assert_eq!(SessionCommitteeManagement::current_committee_storage().epoch, 1);
232
233			advance_one_block();
234			assert_eq!(Session::current_index(), 2);
235			assert_eq!(Session::validators(), vec![CHARLIE.authority_id, DAVE.authority_id]);
236			assert_eq!(SessionCommitteeManagement::current_committee_storage().epoch, 1);
237
238			for _i in 0..10 {
239				advance_one_block();
240				assert_eq!(Session::current_index(), 2);
241				assert_eq!(Session::validators(), vec![CHARLIE.authority_id, DAVE.authority_id]);
242				assert_eq!(SessionCommitteeManagement::current_committee_storage().epoch, 1);
243			}
244		});
245	}
246}