pallet_chain_manager/
roles.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// ``````````````````````````````` AUTHOR ACTIVITY ```````````````````````````````
14// ===============================================================================
15
16//! Implements [`RoleActivity`] for [`Pallet`].
17//!
18//! Derives author activity from session state and election lifecycle
19//! to determine whether an author is idle (not validating) or active.
20
21// ===============================================================================
22// ``````````````````````````````````` IMPORTS ```````````````````````````````````
23// ===============================================================================
24
25// --- Core / Std ---
26use core::marker::PhantomData;
27
28// --- Local crate imports ---
29use crate::{
30    types::*, AuthorAffidavits, Config, CurrentSession,
31    Error, Internals, Pallet,
32};
33
34// --- Scale-codec crates ---
35use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
36use scale_info::TypeInfo;
37
38// --- FRAME Suite ---
39use frame_suite::{blockchain::*, roles::RoleActivity};
40
41// --- Substrate primitives ---
42use sp_runtime::{
43    traits::{Convert, One},
44    DispatchError,
45};
46
47// ===============================================================================
48// ``````````````````````````````````` STRUCTS ```````````````````````````````````
49// ===============================================================================
50
51/// Represents the **current blocking duty** being performed by an author.
52///
53/// This enum is used as the activity context for [`RoleActivity`], indicating
54/// why an author is considered *active* and therefore temporarily unable to
55/// perform certain operations (e.g. resigning or withdrawing collateral).
56///
57/// Each variant must map to a **user-facing, actionable [`DispatchError`]**
58/// explaining the ongoing duty and how or when it can be exited.
59///
60/// ## Invariants
61/// - An author may be blocked by **at most one** activity at a time.
62/// - Activity states are **derived**, not persisted.
63///
64/// ## Design Notes
65/// - Activity is inferred from session state, affidavits, and election results.
66/// - No explicit activity storage is maintained.
67/// - This enum is strictly descriptive and has no side effects.
68#[derive(
69    Encode, Decode, Clone, Copy, Eq, PartialEq, TypeInfo, MaxEncodedLen, DecodeWithMemTracking,
70)]
71#[scale_info(skip_type_params(T))]
72pub enum AuthorActivity<T: Config> {
73    /// The author is actively validating in the current session.
74    SessionValidator,
75
76    /// The author has submitted an affidavit and is participating
77    /// in the ongoing election process.
78    ElectionCandidate,
79
80    /// The author has won the election and is waiting to enter
81    /// the next validation session.
82    ElectionWinner,
83
84    /// Internal fallback variant used when activity cannot be
85    /// determined conclusively.
86    Indeterminate(PhantomData<T>),
87}
88
89impl<T: Config> Into<DispatchError> for AuthorActivity<T> {
90    fn into(self) -> DispatchError {
91        match self {
92            AuthorActivity::SessionValidator => Error::<T>::ActivelyValidating.into(),
93            AuthorActivity::ElectionCandidate => Error::<T>::ActivelyContestingElection.into(),
94            AuthorActivity::ElectionWinner => Error::<T>::ActivelyWarmingForValidation.into(),
95            AuthorActivity::Indeterminate(_) => Error::<T>::CannotDetermineAuthorActiveDuty.into(),
96        }
97    }
98}
99
100impl<T: Config> core::fmt::Debug for AuthorActivity<T> {
101    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
102        match self {
103            Self::SessionValidator => f.write_str("SessionValidator"),
104            Self::ElectionCandidate => f.write_str("ElectionCandidate"),
105            Self::ElectionWinner => f.write_str("ElectionWinner"),
106            Self::Indeterminate(_) => f.write_str("Indeterminate"),
107        }
108    }
109}
110
111// ===============================================================================
112// ```````````````````````````````` ROLE ACTIVITY ````````````````````````````````
113// ===============================================================================
114
115/// Implementation of the [`RoleActivity`] trait for authors.
116///
117/// This implementation determines whether an author is currently *idle* / *active*
118/// or *blocked* by an active protocol duty.
119///
120/// ## Design Notes
121/// - Activity is computed dynamically on each invocation.
122/// - No state is cached or persisted.
123/// - Time gating is derived from session timing and affidavit windows.
124///
125/// ## Caller Responsibility
126/// - Callers must handle the returned `AuthorActivity` and propagate
127///   its associated [`DispatchError`] to the user for exit solutions.
128impl<T: Config> RoleActivity<AuthorOf<T>, AuthorTimeStampOf<T>> for Pallet<T> {
129    /// Represents the duty, the author is currently performing.
130    type Activity = AuthorActivity<T>;
131
132    /// Determines whether an author is currently idle or blocked by an active duty.
133    ///
134    /// ## Semantics
135    /// - Returns `Ok(())` if the author is idle
136    /// - Returns `Err(AuthorActivity)` describing the blocking duty
137    fn is_idle(who: &AuthorOf<T>) -> Result<(), AuthorActivity<T>> {
138        // If the author cannot be mapped to a session validator ID,
139        // they are not actively validating.
140        let Some(validator) =
141            <Pallet<T> as Convert<AuthorOf<T>, Option<SessionId<T>>>>::convert(who.clone())
142        else {
143            return Ok(());
144        };
145
146        // Block if the author is an active validator in the current session.
147        if pallet_session::Pallet::<T>::validators().contains(&validator) {
148            return Err(AuthorActivity::<T>::SessionValidator);
149        }
150
151        let current_session = CurrentSession::<T>::get();
152        let next_session = current_session.saturating_add(One::one());
153
154        // Compute affidavit submission window boundaries.
155        let Ok(aff_window) = Pallet::<T>::compute_affidavit_window() else {
156            return Err(AuthorActivity::Indeterminate(PhantomData));
157        };
158        let start_affidavit = aff_window.start;
159        let end_affidavit = aff_window.end;
160
161        let current_block = frame_system::Pallet::<T>::block_number();
162
163        // Before affidavit submission begins, non-validating authors are idle.
164        if current_block < start_affidavit {
165            return Ok(());
166        }
167
168        // During affidavit submission, block authors who have
169        // submitted an affidavit and are participating in the election.
170        if current_block < end_affidavit {
171            if AuthorAffidavits::<T>::contains_key((next_session, who)) {
172                return Err(AuthorActivity::ElectionCandidate);
173            }
174        }
175
176        // After the election window, block authors who were elected
177        // and are awaiting the next validation session.
178        if current_block > end_affidavit {
179            if let Some(elected) =
180                <Internals<T> as ElectAuthors<AuthorOf<T>, ElectionVia<T>>>::reveal()
181            {
182                for elect in elected.into_iter() {
183                    if *who == elect {
184                        return Err(AuthorActivity::ElectionWinner);
185                    }
186                }
187            }
188        }
189
190        Ok(())
191    }
192}
193
194// ===============================================================================
195// ````````````````````````````````` ROLES TESTS `````````````````````````````````
196// ===============================================================================
197
198#[cfg(test)]
199mod tests {
200
201    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
202    // ```````````````````````````````````` IMPORTS ``````````````````````````````````
203    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
204
205    // --- Local crate imports ---
206    use crate::mock::*;
207
208    // --- FRAME Suite ---
209    use frame_suite::roles::*;
210
211    // --- FRAME Support ---
212    use frame_support::{assert_err, assert_ok, traits::tokens::Fortitude};
213
214    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
215    // ````````````````````````````````` ROLE ACTIVITY ```````````````````````````````
216    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
217
218    #[test]
219    fn is_idle_ok_author_cannot_be_mapped_to_session_validator_id() {
220        chain_manager_test_ext().execute_with(|| {
221            set_default_user_balance_and_hold(ALICE).unwrap();
222            RoleAdapter::enroll(&ALICE, 1000, Fortitude::Force).unwrap();
223
224            assert_ok!(Pallet::is_idle(&BOB));
225        })
226    }
227
228    #[test]
229    fn is_idle_err_author_is_an_active_validator() {
230        chain_manager_test_ext().execute_with(|| {
231            set_session_config();
232            System::set_block_number(SESSION_START);
233            set_default_user_balance_and_hold(ALICE).unwrap();
234            set_default_user_balance_and_hold(BOB).unwrap();
235            set_default_user_balance_and_hold(CHARLIE).unwrap();
236            set_default_user_balance_and_hold(MIKE).unwrap();
237            set_default_user_balance_and_hold(ALAN).unwrap();
238
239            enroll_authors_with_default_collateral(vec![ALICE, BOB, CHARLIE]).unwrap();
240
241            direct_fund_author(MIKE, ALICE, STANDARD_FUND).unwrap();
242            direct_fund_author(ALAN, BOB, LARGE_FUND).unwrap();
243
244            let aff_pairs = insert_affidavit_keys_for_authors(vec![ALICE, BOB, CHARLIE], 1);
245            let alice_aff = aff_pairs[0].2.clone();
246            let bob_aff = aff_pairs[1].2.clone();
247            let charlie_aff = aff_pairs[2].2.clone();
248
249            System::set_block_number(AFDT_SUBMISSION_START);
250            submit_affidavit_for_authors(vec![alice_aff, bob_aff, charlie_aff]).unwrap();
251
252            System::set_block_number(ELECTION_START);
253            let actual_elected = run_election_and_elect_authors(ALICE).unwrap();
254
255            let expected_elected = vec![BOB, ALICE, CHARLIE];
256            assert_eq!(actual_elected, expected_elected);
257            insert_into_validator_set(actual_elected).unwrap();
258
259            System::set_block_number(AFDT_SUBMISSION_END);
260            assert_err!(Pallet::is_idle(&BOB), AuthorActivity::SessionValidator);
261        })
262    }
263
264    #[test]
265    fn is_idle_ok_non_validating_author_idle_before_affidavit_window() {
266        chain_manager_test_ext().execute_with(|| {
267            set_session_config();
268            CurrentSession::put(0);
269            System::set_block_number(SESSION_START);
270            set_default_user_balance_and_hold(ALICE).unwrap();
271            set_default_user_balance_and_hold(BOB).unwrap();
272            set_default_user_balance_and_hold(CHARLIE).unwrap();
273            set_default_user_balance_and_hold(NIX).unwrap();
274
275            set_default_user_balance_and_hold(MIKE).unwrap();
276            set_default_user_balance_and_hold(ALAN).unwrap();
277
278            enroll_authors_with_default_collateral(vec![ALICE, BOB, CHARLIE, NIX]).unwrap();
279
280            direct_fund_author(MIKE, ALICE, STANDARD_FUND).unwrap();
281            direct_fund_author(ALAN, BOB, LARGE_FUND).unwrap();
282
283            let aff_pairs = insert_affidavit_keys_for_authors(vec![ALICE, BOB, CHARLIE], 1);
284            let alice_aff = aff_pairs[0].2.clone();
285            let bob_aff = aff_pairs[1].2.clone();
286            let charlie_aff = aff_pairs[2].2.clone();
287
288            System::set_block_number(AFDT_SUBMISSION_START);
289            submit_affidavit_for_authors(vec![alice_aff, bob_aff, charlie_aff]).unwrap();
290
291            System::set_block_number(ELECTION_START);
292            let actual_elected = run_election_and_elect_authors(ALICE).unwrap();
293
294            let expected_elected = vec![BOB, ALICE, CHARLIE];
295            assert_eq!(actual_elected, expected_elected);
296            insert_into_validator_set(actual_elected).unwrap();
297
298            System::set_block_number(SESSION_END);
299
300            CurrentSession::put(1);
301            System::set_block_number(SESSION_END + SESSION_START);
302
303            assert_ok!(Pallet::is_idle(&NIX),);
304        })
305    }
306
307    #[test]
308    fn is_idle_err_author_submited_affidavit_and_participating_in_election() {
309        chain_manager_test_ext().execute_with(|| {
310            set_session_config();
311            CurrentSession::put(0);
312            System::set_block_number(SESSION_START);
313            set_default_user_balance_and_hold(ALICE).unwrap();
314            set_default_user_balance_and_hold(BOB).unwrap();
315            set_default_user_balance_and_hold(CHARLIE).unwrap();
316            set_default_user_balance_and_hold(NIX).unwrap();
317
318            set_default_user_balance_and_hold(MIKE).unwrap();
319            set_default_user_balance_and_hold(ALAN).unwrap();
320
321            enroll_authors_with_default_collateral(vec![ALICE, BOB, CHARLIE, NIX]).unwrap();
322
323            direct_fund_author(MIKE, ALICE, STANDARD_FUND).unwrap();
324            direct_fund_author(ALAN, BOB, LARGE_FUND).unwrap();
325
326            let aff_pairs = insert_affidavit_keys_for_authors(vec![ALICE, BOB, CHARLIE], 1);
327            let alice_aff = aff_pairs[0].2.clone();
328            let bob_aff = aff_pairs[1].2.clone();
329            let charlie_aff = aff_pairs[2].2.clone();
330
331            System::set_block_number(AFDT_SUBMISSION_START);
332            submit_affidavit_for_authors(vec![alice_aff, bob_aff, charlie_aff]).unwrap();
333
334            assert_err!(Pallet::is_idle(&ALICE), AuthorActivity::ElectionCandidate);
335        })
336    }
337
338    #[test]
339    fn is_idle_err_author_elected_and_awaiting_the_next_validation_session() {
340        chain_manager_test_ext().execute_with(|| {
341            set_session_config();
342            System::set_block_number(SESSION_START);
343            set_default_user_balance_and_hold(ALICE).unwrap();
344            set_default_user_balance_and_hold(BOB).unwrap();
345            set_default_user_balance_and_hold(CHARLIE).unwrap();
346            set_default_user_balance_and_hold(MIKE).unwrap();
347            set_default_user_balance_and_hold(ALAN).unwrap();
348
349            enroll_authors_with_default_collateral(vec![ALICE, BOB, CHARLIE]).unwrap();
350
351            direct_fund_author(MIKE, ALICE, STANDARD_FUND).unwrap();
352            direct_fund_author(ALAN, BOB, LARGE_FUND).unwrap();
353
354            let aff_pairs = insert_affidavit_keys_for_authors(vec![ALICE, BOB, CHARLIE], 1);
355            let alice_aff = aff_pairs[0].2.clone();
356            let bob_aff = aff_pairs[1].2.clone();
357            let charlie_aff = aff_pairs[2].2.clone();
358
359            System::set_block_number(AFDT_SUBMISSION_START);
360            submit_affidavit_for_authors(vec![alice_aff, bob_aff, charlie_aff]).unwrap();
361
362            System::set_block_number(ELECTION_START);
363            let actual_elected = run_election_and_elect_authors(ALICE).unwrap();
364
365            let expected_elected = vec![BOB, ALICE, CHARLIE];
366            assert_eq!(actual_elected, expected_elected);
367
368            System::set_block_number(AFDT_SUBMISSION_END + 1);
369            assert_err!(Pallet::is_idle(&ALICE), AuthorActivity::ElectionWinner);
370        })
371    }
372}