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}