From d6706d9d82251c431b0213da3bf80770cd3c9d51 Mon Sep 17 00:00:00 2001
From: Hugo Trentesaux <hugo@trentesaux.fr>
Date: Wed, 8 Jan 2025 14:57:48 +0100
Subject: [PATCH 1/5] deduplicate authorities events

---
 pallets/authority-members/src/lib.rs | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

diff --git a/pallets/authority-members/src/lib.rs b/pallets/authority-members/src/lib.rs
index 587e82aa3..cb02e5b92 100644
--- a/pallets/authority-members/src/lib.rs
+++ b/pallets/authority-members/src/lib.rs
@@ -575,18 +575,25 @@ impl<T: Config> pallet_session::SessionManager<T::ValidatorId> for Pallet<T> {
             return None;
         }
 
+        // -- handle incoming members
+        // callback when smith is incoming
         for member_id in members_ids_to_add.iter() {
             T::OnIncomingMember::on_incoming_member(*member_id);
-            Self::deposit_event(Event::IncomingAuthorities {
-                members: members_ids_to_add.clone(),
-            });
         }
+        // a single event with all authorities
+        Self::deposit_event(Event::IncomingAuthorities {
+            members: members_ids_to_add.clone(),
+        });
+
+        // -- handle outgoing members
+        // callback when smith is outgoing
         for member_id in members_ids_to_del.iter() {
             T::OnOutgoingMember::on_outgoing_member(*member_id);
-            Self::deposit_event(Event::OutgoingAuthorities {
-                members: members_ids_to_del.clone(),
-            });
         }
+        // a single event with all authorities
+        Self::deposit_event(Event::OutgoingAuthorities {
+            members: members_ids_to_del.clone(),
+        });
 
         // updates the list of OnlineAuthorities and returns the list of their key
         Some(
-- 
GitLab


From f36c580f7583d387f4874abb82650e075bfe90af Mon Sep 17 00:00:00 2001
From: Hugo Trentesaux <hugo@trentesaux.fr>
Date: Wed, 8 Jan 2025 16:00:33 +0100
Subject: [PATCH 2/5] add std for sp keyring

---
 runtime/gdev/Cargo.toml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/runtime/gdev/Cargo.toml b/runtime/gdev/Cargo.toml
index 3299b812d..08fc4435a 100644
--- a/runtime/gdev/Cargo.toml
+++ b/runtime/gdev/Cargo.toml
@@ -112,6 +112,7 @@ std = [
 	"sp-genesis-builder/std",
 	"sp-inherents/std",
 	"sp-io/std",
+	"sp-keyring/std",
 	"sp-membership/std",
 	"sp-offchain/std",
 	"sp-runtime/std",
-- 
GitLab


From a46e1971af667e77ac6d346d55ab86c8375a2bc9 Mon Sep 17 00:00:00 2001
From: Hugo Trentesaux <hugo@trentesaux.fr>
Date: Wed, 8 Jan 2025 16:55:34 +0100
Subject: [PATCH 3/5] no need for event when no changes to authority

---
 pallets/authority-members/src/lib.rs | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/pallets/authority-members/src/lib.rs b/pallets/authority-members/src/lib.rs
index cb02e5b92..f4118ee3a 100644
--- a/pallets/authority-members/src/lib.rs
+++ b/pallets/authority-members/src/lib.rs
@@ -580,20 +580,24 @@ impl<T: Config> pallet_session::SessionManager<T::ValidatorId> for Pallet<T> {
         for member_id in members_ids_to_add.iter() {
             T::OnIncomingMember::on_incoming_member(*member_id);
         }
-        // a single event with all authorities
-        Self::deposit_event(Event::IncomingAuthorities {
-            members: members_ids_to_add.clone(),
-        });
+        // a single event with all authorities if some
+        if !members_ids_to_add.is_empty() {
+            Self::deposit_event(Event::IncomingAuthorities {
+                members: members_ids_to_add.clone(),
+            });
+        }
 
         // -- handle outgoing members
         // callback when smith is outgoing
         for member_id in members_ids_to_del.iter() {
             T::OnOutgoingMember::on_outgoing_member(*member_id);
         }
-        // a single event with all authorities
-        Self::deposit_event(Event::OutgoingAuthorities {
-            members: members_ids_to_del.clone(),
-        });
+        // a single event with all authorities if some
+        if !members_ids_to_del.is_empty() {
+            Self::deposit_event(Event::OutgoingAuthorities {
+                members: members_ids_to_del.clone(),
+            });
+        }
 
         // updates the list of OnlineAuthorities and returns the list of their key
         Some(
-- 
GitLab


From 2aa18ec55ef5128194c9340371675089ee3244ad Mon Sep 17 00:00:00 2001
From: Hugo Trentesaux <hugo@trentesaux.fr>
Date: Wed, 8 Jan 2025 17:00:22 +0100
Subject: [PATCH 4/5] reveal that expires_on is non null

issue #243
---
 runtime/gdev/tests/integration_tests.rs | 90 +++++++++++++++++++++++++
 1 file changed, 90 insertions(+)

diff --git a/runtime/gdev/tests/integration_tests.rs b/runtime/gdev/tests/integration_tests.rs
index 5ca299b43..af4094518 100644
--- a/runtime/gdev/tests/integration_tests.rs
+++ b/runtime/gdev/tests/integration_tests.rs
@@ -25,6 +25,7 @@ use frame_support::{
 use gdev_runtime::*;
 use pallet_identity::{RevocationPayload, REVOCATION_PAYLOAD_PREFIX};
 use pallet_membership::MembershipRemovalReason;
+use pallet_session::historical::SessionManager;
 use pallet_smith_members::{SmithMeta, SmithStatus};
 use scale_info::prelude::num::NonZeroU16;
 use sp_core::{Encode, Pair};
@@ -1020,6 +1021,95 @@ fn test_smith_process() {
         })
 }
 
+// reveal bug from #243
+#[test]
+fn test_expired_smith_with_null_expires_on() {
+    // initial_authorities_len = 2 → Alice and Bob are online
+    // initial_smiths_len = 3 → Charlie is offline Smith
+    // initial_identities_len = 4 → Dave is member but not smith
+    ExtBuilder::new(2, 3, 4).build().execute_with(|| {
+        run_to_block(1);
+
+        // Bob is smith
+        assert_eq!(
+            SmithMembers::smiths(2),
+            Some(pallet_smith_members::SmithMeta {
+                status: SmithStatus::Smith,
+                expires_on: None, // because online
+                issued_certs: vec![1, 3],
+                received_certs: vec![1, 3],
+            })
+        );
+
+        // force Bob to leave by expiring his main WoT membership
+        Membership::do_remove_membership(2, MembershipRemovalReason::System);
+
+        // check events
+        // membership removal
+        System::assert_has_event(RuntimeEvent::Membership(
+            pallet_membership::Event::MembershipRemoved {
+                member: 2,
+                reason: MembershipRemovalReason::System,
+            },
+        ));
+        // smith membership removal
+        System::assert_has_event(RuntimeEvent::SmithMembers(
+            pallet_smith_members::Event::SmithMembershipRemoved { idty_index: 2 },
+        ));
+        System::assert_has_event(RuntimeEvent::AuthorityMembers(
+            pallet_authority_members::Event::MemberRemoved { member: 2 },
+        ));
+        // also events for certifications
+
+        // check state
+        // Bob is not Smith anymore
+        assert_eq!(
+            SmithMembers::smiths(2),
+            Some(pallet_smith_members::SmithMeta {
+                status: SmithStatus::Excluded, // automatically excluded
+                expires_on: None,              // because excluded, no expiry is scheduled
+                issued_certs: vec![1, 3],
+                received_certs: vec![], // received certs are deleted
+            })
+        );
+        // Alice smith cert to Bob has been deleted
+        assert_eq!(
+            SmithMembers::smiths(1),
+            Some(pallet_smith_members::SmithMeta {
+                status: SmithStatus::Smith,
+                expires_on: None,      // because online
+                issued_certs: vec![3], // cert to Bob has been deleted
+                received_certs: vec![2, 3],
+            })
+        );
+
+        // run to next block
+        run_to_block(2);
+
+        // simulate new session
+        AuthorityMembers::new_session(2);
+        // check event
+        System::assert_has_event(RuntimeEvent::AuthorityMembers(
+            pallet_authority_members::Event::OutgoingAuthorities { members: vec![2] },
+        ));
+
+        // show that expires_on is non null
+        // this is issue #243
+        // Bob is not Smith anymore
+        assert_eq!(
+            SmithMembers::smiths(2),
+            Some(pallet_smith_members::SmithMeta {
+                status: SmithStatus::Excluded, // still excluded
+                expires_on: Some(48),          // should be still None !!
+                issued_certs: vec![1, 3],
+                received_certs: vec![],
+            })
+        );
+
+        // println!("{:#?}", System::events()); // with -- --nocapture
+    })
+}
+
 /// test new account creation
 #[test]
 fn test_create_new_account() {
-- 
GitLab


From c8247b0625291d1b3b6c7c32bb0641b50242fa9c Mon Sep 17 00:00:00 2001
From: Hugo Trentesaux <hugo@trentesaux.fr>
Date: Wed, 8 Jan 2025 17:19:35 +0100
Subject: [PATCH 5/5] fix #243

---
 pallets/smith-members/src/lib.rs        | 8 +++++---
 runtime/gdev/tests/integration_tests.rs | 8 +++-----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/pallets/smith-members/src/lib.rs b/pallets/smith-members/src/lib.rs
index 35918f55a..1ed34c47b 100644
--- a/pallets/smith-members/src/lib.rs
+++ b/pallets/smith-members/src/lib.rs
@@ -582,7 +582,7 @@ impl<T: Config> Pallet<T> {
                     if let Some(smith_meta) = maybe_smith_meta {
                         // As long as the smith is online, it cannot expire
                         smith_meta.expires_on = None;
-                        // FIXME: unschedule old expiry
+                        // FIXME: unschedule old expiry (#182)
                     }
                 });
             }
@@ -592,10 +592,12 @@ impl<T: Config> Pallet<T> {
     /// Handle the event when a Smith goes offline.
     pub fn on_smith_goes_offline(idty_index: T::IdtyIndex) {
         if let Some(smith_meta) = Smiths::<T>::get(idty_index) {
-            if smith_meta.expires_on.is_none() {
+            // Smith can go offline after main membership expiry
+            // in this case, there is no scheduled expiry since it is already excluded
+            if smith_meta.status != SmithStatus::Excluded {
                 Smiths::<T>::mutate(idty_index, |maybe_smith_meta| {
                     if let Some(smith_meta) = maybe_smith_meta {
-                        // As long as the smith is online, it cannot expire
+                        // schedule expiry
                         let new_expires_on =
                             CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get();
                         smith_meta.expires_on = Some(new_expires_on);
diff --git a/runtime/gdev/tests/integration_tests.rs b/runtime/gdev/tests/integration_tests.rs
index af4094518..a8b3cb47b 100644
--- a/runtime/gdev/tests/integration_tests.rs
+++ b/runtime/gdev/tests/integration_tests.rs
@@ -1023,7 +1023,7 @@ fn test_smith_process() {
 
 // reveal bug from #243
 #[test]
-fn test_expired_smith_with_null_expires_on() {
+fn test_expired_smith_has_null_expires_on() {
     // initial_authorities_len = 2 → Alice and Bob are online
     // initial_smiths_len = 3 → Charlie is offline Smith
     // initial_identities_len = 4 → Dave is member but not smith
@@ -1093,14 +1093,12 @@ fn test_expired_smith_with_null_expires_on() {
             pallet_authority_members::Event::OutgoingAuthorities { members: vec![2] },
         ));
 
-        // show that expires_on is non null
-        // this is issue #243
-        // Bob is not Smith anymore
+        // control state is still ok
         assert_eq!(
             SmithMembers::smiths(2),
             Some(pallet_smith_members::SmithMeta {
                 status: SmithStatus::Excluded, // still excluded
-                expires_on: Some(48),          // should be still None !!
+                expires_on: None,              // should be still None
                 issued_certs: vec![1, 3],
                 received_certs: vec![],
             })
-- 
GitLab