pallet_chain_manager/session.rs
1// SPDX-License-Identifier: MPL-2.0
2//
3// Part of Auguth Labs open-source softwares.
4// Built for the Substrate framework.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at https://mozilla.org/MPL/2.0/.
9//
10// Copyright (c) 2026 Auguth Labs (OPC) Pvt Ltd, India
11
12// ===============================================================================
13// ````````````````````````````` SESSION MANAGEMENT ``````````````````````````````
14// ===============================================================================
15
16//! Implements [`SessionManager`] for [`Pallet`].
17//!
18//! Session management logic for author rotation, reward settlement,
19//! and session boundary coordination.
20
21// ===============================================================================
22// ``````````````````````````````````` IMPORTS ```````````````````````````````````
23// ===============================================================================
24
25// --- Local crate imports ---
26use crate::{
27 types::*, Config, CurrentSession, ElectionRunnerPoints, ElectionRunnerPointsUpgrade,
28 ElectsPreparedBy, Internals, Pallet, SessionStartAt,
29};
30
31// --- FRAME Suite ---
32use frame_suite::{blockchain::*, roles::RoleManager};
33
34// --- External pallets ---
35use pallet_session::SessionManager;
36
37// --- Substrate primitives ---
38use sp_runtime::{
39 traits::{Convert, Saturating, Zero},
40 Vec,
41};
42
43// ===============================================================================
44// ```````````````````````````` SESSION-ID CONVERSION ````````````````````````````
45// ===============================================================================
46
47impl<T: Config> Convert<AuthorOf<T>, Option<SessionId<T>>> for Pallet<T> {
48 /// Converts a valid `Author` to an `Result<SessionId, DispatchError>`
49 ///
50 /// None if no SessionId found
51 fn convert(a: AuthorOf<T>) -> Option<SessionId<T>> {
52 // Verify that the author has an existing role
53 let Ok(_) = <T::RoleAdapter as RoleManager<AuthorOf<T>>>::role_exists(&a) else {
54 return None;
55 };
56 let Ok(id) = a.try_into() else { return None };
57 Some(id)
58 }
59}
60
61// ===============================================================================
62// ``````````````````````````````` SESSION MANAGER ```````````````````````````````
63// ===============================================================================
64
65/// Implementation of [`SessionManager`] for the pallet.
66///
67/// This implementation integrates **author election**, **reward settlement**,
68/// and **session boundary tracking** into Substrate's session lifecycle.
69///
70/// It acts as the coordination layer between:
71/// - Election resolution ([`ElectAuthors`])
72/// - Reward scheduling ([`RewardAuthors`])
73/// - Session metadata management ([`CurrentSession`], [`SessionStartAt`])
74///
75/// ## Design Notes
76/// - Elections always target the *next* session.
77/// - Rewards are settled for the *ending* session.
78/// - Session state is updated deterministically at boundaries.
79/// - No election logic or reward computation is performed here.
80///
81/// ## Implementation Notes
82/// - This implementation assumes election results are already finalized
83/// before `new_session` is invoked.
84/// - All side effects are **session-boundary safe** and audit-friendly.
85impl<T: Config> SessionManager<AuthorOf<T>> for Pallet<T> {
86 /// Prepares the author set for the upcoming session.
87 ///
88 /// ## Workflow
89 /// - Reveal elected authors via [`ElectAuthors::reveal`].
90 /// - Reward the election runner with additional block points
91 /// for the *previous* session.
92 ///
93 /// ## Semantics
94 /// - Returns `None` if:
95 /// - No election was executed, or
96 /// - The elected author set is empty.
97 /// - Election runner rewards are credited to the session
98 /// that is ending (`new_index - 1`).
99 fn new_session(new_index: SessionIndex) -> Option<Vec<AuthorOf<T>>> {
100 // Reveal the elected authors for the upcoming session.
101 let Some(authors) = <Internals<T> as ElectAuthors<AuthorOf<T>, ElectionVia<T>>>::reveal()
102 else {
103 return None;
104 };
105
106 // Materialize the elected set into a concrete vector.
107 let mut elected: Vec<AuthorOf<T>> = Vec::new();
108 for author in authors.into_iter() {
109 elected.push(author);
110 }
111
112 // Abort if the election yielded no authors.
113 if elected.is_empty() {
114 return None;
115 }
116
117 // Reward the election runner in the session that is going to end.
118 if let Some((ref runner, _block)) = ElectsPreparedBy::<T>::get(new_index) {
119 let runner_points = ElectionRunnerPoints::<T>::get();
120 let points = T::PointsAdapter::points_of(runner).unwrap_or(Zero::zero());
121 let _ = T::PointsAdapter::set_points(runner, points.saturating_add(runner_points));
122 }
123
124 Some(elected)
125 }
126
127 /// Finalizes the ending session.
128 ///
129 /// ## Workflow
130 /// - Schedule rewards for authors based on accumulated points.
131 /// - Apply any pending configuration upgrades related to
132 /// election runner incentives.
133 ///
134 /// ## Notes
135 /// - Reward scheduling is deferred; no immediate transfers occur.
136 /// - Configuration upgrades take effect atomically at session end.
137 fn end_session(_end_index: SessionIndex) {
138 // Schedule rewards for authors of the ending session.
139 <Internals<T> as RewardAuthors<AuthorOf<T>, AssetOf<T>, T::Points>>::reward_authors();
140
141 // Apply pending election runner point upgrades, if any.
142 if let Some(update) = ElectionRunnerPointsUpgrade::<T>::get() {
143 ElectionRunnerPoints::<T>::set(update);
144 ElectionRunnerPointsUpgrade::<T>::set(None);
145 }
146 }
147
148 /// Initializes metadata for the newly started session.
149 ///
150 /// ## Workflow
151 /// - Record the new session index.
152 /// - Capture the block number at which the session begins.
153 ///
154 /// ## Invariants
155 /// - Must be called exactly once per session start.
156 /// - Session metadata is monotonic and never rewritten.
157 fn start_session(start_index: SessionIndex) {
158 CurrentSession::<T>::set(start_index);
159 SessionStartAt::<T>::set(frame_system::Pallet::<T>::block_number());
160 }
161}
162
163// ===============================================================================
164// ```````````````````````````````` SESSION TESTS ````````````````````````````````
165// ===============================================================================
166
167#[cfg(test)]
168mod tests {
169
170 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
171 // ```````````````````````````````````` IMPORTS ``````````````````````````````````
172 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
173
174 // --- Local crate imports ---
175 use crate::mock::*;
176
177 // --- FRAME Suite ---
178 use frame_suite::{blockchain::*, roles::*};
179
180 // --- FRAME Support ---
181 use frame_support::traits::tokens::{Fortitude, Precision};
182
183 // --- External pallets ---
184 use pallet_session::SessionManager;
185
186 // --- Substrate primitives ---
187 use sp_runtime::traits::Convert;
188
189 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
190 // ```````````````````````````````````` CONVERT ``````````````````````````````````
191 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
192
193 #[test]
194 fn convert_returns_some() {
195 chain_manager_test_ext().execute_with(|| {
196 set_user_balance_and_hold(ALICE, 250, 200).unwrap();
197 RoleAdapter::enroll(&ALICE, 150, Fortitude::Force).unwrap();
198 let session_id = Pallet::convert(ALICE);
199 assert!(session_id.is_some());
200 })
201 }
202
203 #[test]
204 fn convert_returns_none() {
205 chain_manager_test_ext().execute_with(|| {
206 set_user_balance_and_hold(ALICE, 250, 200).unwrap();
207 RoleAdapter::enroll(&ALICE, 150, Fortitude::Force).unwrap();
208 let session_id = Pallet::convert(BOB);
209 assert!(session_id.is_none());
210 dbg!(session_id);
211 })
212 }
213
214 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
215 // ``````````````````````````````` SESSION-MANAGER ```````````````````````````````
216 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
217
218 #[test]
219 fn new_session_returns_authors_and_awards_election_runner_points() {
220 chain_manager_test_ext().execute_with(|| {
221 CurrentSession::put(0);
222 set_user_balance_and_hold(ALICE, 250, 250).unwrap();
223 set_user_balance_and_hold(CHARLIE, 250, 250).unwrap();
224 set_user_balance_and_hold(ALAN, 250, 250).unwrap();
225 set_user_balance_and_hold(MIKE, 250, 250).unwrap();
226 set_user_balance_and_hold(BOB, 250, 250).unwrap();
227 set_user_balance_and_hold(NIX, 250, 250).unwrap();
228
229 RoleAdapter::enroll(&ALICE, 200, Fortitude::Force).unwrap();
230 RoleAdapter::enroll(&BOB, 200, Fortitude::Force).unwrap();
231 RoleAdapter::enroll(&MIKE, 200, Fortitude::Force).unwrap();
232
233 RoleAdapter::fund(
234 &ALICE,
235 &Funder::Direct(CHARLIE),
236 100,
237 Precision::Exact,
238 Fortitude::Force,
239 )
240 .unwrap();
241 RoleAdapter::fund(
242 &BOB,
243 &Funder::Direct(ALAN),
244 150,
245 Precision::Exact,
246 Fortitude::Force,
247 )
248 .unwrap();
249 RoleAdapter::fund(
250 &MIKE,
251 &Funder::Direct(NIX),
252 125,
253 Precision::Exact,
254 Fortitude::Force,
255 )
256 .unwrap();
257
258 AffidavitKeys::insert((1, AFFIDAVIT_KEY_A), ALICE);
259 AffidavitKeys::insert((1, AFFIDAVIT_KEY_B), BOB);
260 AffidavitKeys::insert((1, AFFIDAVIT_KEY_C), MIKE);
261
262 let affidavit_alice = Pallet::gen_affidavit(&AFFIDAVIT_KEY_A).unwrap();
263 Pallet::submit_affidavit(&AFFIDAVIT_KEY_A, &affidavit_alice).unwrap();
264 let affidavit_bob = Pallet::gen_affidavit(&AFFIDAVIT_KEY_B).unwrap();
265 Pallet::submit_affidavit(&AFFIDAVIT_KEY_B, &affidavit_bob).unwrap();
266 let affidavit_mike = Pallet::gen_affidavit(&AFFIDAVIT_KEY_C).unwrap();
267 Pallet::submit_affidavit(&AFFIDAVIT_KEY_C, &affidavit_mike).unwrap();
268
269 let candidates = Internals::prepare_candidates().unwrap();
270 Internals::prepare_authors(candidates).unwrap();
271
272 // Set up election runner for session 1
273 ElectsPreparedBy::insert(1, (ALICE, 100));
274 ElectionRunnerPoints::set(50);
275
276 // Simulate new_session call
277 let result = Pallet::new_session(1);
278
279 // Verify that authors were returned
280 assert!(result.is_some());
281
282 // Election runner received bonus points for session 0
283 let alice_points = PointsAdapter::points_of(&ALICE).unwrap();
284 assert_eq!(alice_points, 50);
285 })
286 }
287
288 #[test]
289 fn new_session_returns_none_when_reveal_fails() {
290 chain_manager_test_ext().execute_with(|| {
291 // No election setup, so reveal will fail
292 let result = Pallet::new_session(1);
293 assert!(result.is_none());
294 })
295 }
296
297 #[test]
298 fn end_session_rewards_authors() {
299 chain_manager_test_ext().execute_with(|| {
300 CurrentSession::put(1);
301 set_user_balance_and_hold(ALICE, 250, 250).unwrap();
302 set_user_balance_and_hold(BOB, 250, 250).unwrap();
303 set_user_balance_and_hold(ALAN, 250, 250).unwrap();
304 set_user_balance_and_hold(MIKE, 250, 250).unwrap();
305
306 System::set_block_number(5);
307 RoleAdapter::enroll(&ALICE, 200, Fortitude::Force).unwrap();
308 RoleAdapter::enroll(&BOB, 150, Fortitude::Force).unwrap();
309 RoleAdapter::fund(
310 &ALICE,
311 &Funder::Direct(ALAN),
312 150,
313 Precision::Exact,
314 Fortitude::Force,
315 )
316 .unwrap();
317 RoleAdapter::fund(
318 &BOB,
319 &Funder::Direct(MIKE),
320 125,
321 Precision::Exact,
322 Fortitude::Force,
323 )
324 .unwrap();
325
326 Pallet::add_point(&ALICE).unwrap();
327 Pallet::add_point(&ALICE).unwrap();
328 Pallet::add_point(&BOB).unwrap();
329 Pallet::add_point(&BOB).unwrap();
330 Pallet::add_point(&BOB).unwrap();
331 Pallet::add_point(&ALICE).unwrap();
332 Pallet::add_point(&ALICE).unwrap();
333 Pallet::add_point(&ALICE).unwrap();
334
335 ElectionRunnerPointsUpgrade::put(Some(50));
336 let election_runner_points = ElectionRunnerPoints::get();
337 assert_eq!(election_runner_points, 10);
338
339 System::set_block_number(590);
340 Pallet::end_session(1);
341
342 let election_runner_points = ElectionRunnerPoints::get();
343 assert_eq!(election_runner_points, 50);
344 assert!(ElectionRunnerPointsUpgrade::get().is_none());
345
346 let rewards_of_alice = RoleAdapter::get_rewards_of(&ALICE).unwrap();
347 let rewards_of_bob = RoleAdapter::get_rewards_of(&BOB).unwrap();
348
349 let expected_alice_rewards = vec![(592, 62)];
350 let expected_bob_rewards = vec![(592, 38)];
351
352 assert_eq!(rewards_of_alice, expected_alice_rewards);
353 assert_eq!(rewards_of_bob, expected_bob_rewards);
354 })
355 }
356
357 #[test]
358 fn end_session_does_not_upgrade_when_none() {
359 chain_manager_test_ext().execute_with(|| {
360 ElectionRunnerPoints::set(50);
361 ElectionRunnerPointsUpgrade::set(None);
362
363 Pallet::end_session(1);
364
365 assert_eq!(ElectionRunnerPoints::get(), 50);
366 assert_eq!(ElectionRunnerPointsUpgrade::get(), None);
367 })
368 }
369
370 #[test]
371 fn start_session_updates_current_session_and_block_number() {
372 chain_manager_test_ext().execute_with(|| {
373 System::set_block_number(500);
374 CurrentSession::put(0);
375 SessionStartsAt::put(0);
376
377 Pallet::start_session(5);
378
379 assert_eq!(CurrentSession::get(), 5);
380 assert_eq!(SessionStartsAt::get(), 500);
381 })
382 }
383}