From 1cefacbae0f8d7201727f84a4409e05950d47c24 Mon Sep 17 00:00:00 2001
From: cgeek <cem.moreau@gmail.com>
Date: Sun, 17 Dec 2023 14:58:31 +0100
Subject: [PATCH] feat(smith-members): smith account can be excluded, not
 deleted

---
 pallets/smith-members/src/lib.rs   | 104 +++++++++++++++-----
 pallets/smith-members/src/tests.rs | 153 ++++++++++++++++++++++++++---
 pallets/smith-members/src/types.rs |   4 +-
 3 files changed, 219 insertions(+), 42 deletions(-)

diff --git a/pallets/smith-members/src/lib.rs b/pallets/smith-members/src/lib.rs
index 131b93d70..3a7cd8bec 100644
--- a/pallets/smith-members/src/lib.rs
+++ b/pallets/smith-members/src/lib.rs
@@ -56,6 +56,8 @@ pub enum SmithStatus {
     Pending,
     /// The identity has reached the requirements to become a smith and can now goGoOnline() or invite/certify other smiths
     Smith,
+    /// The identity has been removed from the smiths set but is kept to keep track of its certifications
+    Excluded,
 }
 
 #[frame_support::pallet]
@@ -148,8 +150,7 @@ pub mod pallet {
     impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
         fn build(&self) {
             CurrentSession::<T>::put(0);
-            let mut cert_meta_by_issuer =
-                BTreeMap::<T::IdtyIndex, SmithCertMeta<T::BlockNumber>>::new();
+            let mut cert_meta_by_issuer = BTreeMap::<T::IdtyIndex, Vec<T::IdtyIndex>>::new();
             for (receiver, issuers) in &self.certs_by_receiver {
                 // Forbid self-cert
                 assert!(
@@ -157,18 +158,13 @@ pub mod pallet {
                     "Identity cannot certify it-self."
                 );
 
-                // We should insert cert_meta for receivers that have not issued any cert.
-                cert_meta_by_issuer
-                    .entry(*receiver)
-                    .or_insert(SmithCertMeta {
-                        issued_count: 0,
-                        next_issuable_on: sp_runtime::traits::Zero::zero(),
-                        received_count: 0,
-                    })
-                    .received_count = issuers.len() as u32;
-
                 let mut issuers_: Vec<_> = Vec::with_capacity(issuers.len());
                 for (issuer, maybe_removable_on) in issuers {
+                    // Count issued certs
+                    cert_meta_by_issuer
+                        .entry(*issuer)
+                        .or_insert(vec![])
+                        .push(*receiver);
                     issuers_.push(*issuer);
                 }
 
@@ -187,6 +183,7 @@ pub mod pallet {
                         expires_on: Some(
                             CurrentSession::<T>::get() + T::InactivityMaxDuration::get(),
                         ),
+                        issued_certs: vec![],
                         received_certs: issuers_,
                     },
                 );
@@ -195,6 +192,16 @@ pub mod pallet {
                     receiver,
                 );
             }
+
+            for (issuer, issued_certs) in cert_meta_by_issuer {
+                // Write CertsByIssuer
+                Smiths::<T>::mutate(issuer, |maybe_smith_meta| {
+                    let maybe_smith_meta = maybe_smith_meta
+                        .as_mut()
+                        .expect("issuers must have received certs as well");
+                    maybe_smith_meta.issued_certs = issued_certs;
+                });
+            }
         }
     }
 
@@ -227,7 +234,7 @@ pub mod pallet {
         /// Invitation must not have been accepted yet
         AlreadyAcceptedInvitation,
         /// Recevier must not be known among smiths
-        ReceveirAlreadyHasSmithStatus,
+        OnlyExcludedCanComeBack,
         /// Recevier must have accepted to eventually become a smith
         ReceveirMustAcceptInvitation,
         /// Issuer must be a smith
@@ -292,24 +299,37 @@ impl<T: Config> Pallet<T> {
             issuer_status == SmithStatus::Smith,
             Error::<T>::CannotInvite
         );
-        ensure!(
-            Smiths::<T>::get(receiver).is_none(),
-            Error::<T>::ReceveirAlreadyHasSmithStatus
-        );
+        if let Some(receiver_meta) = Smiths::<T>::get(receiver) {
+            ensure!(
+                receiver_meta.status == SmithStatus::Excluded,
+                Error::<T>::OnlyExcludedCanComeBack
+            );
+        }
 
         Ok(().into())
     }
 
     fn do_invite_smith(receiver: T::IdtyIndex) {
         let new_expires_on = CurrentSession::<T>::get() + T::InactivityMaxDuration::get();
-        Smiths::<T>::insert(
-            receiver,
-            SmithMeta {
-                status: SmithStatus::Invited,
-                expires_on: Some(new_expires_on),
-                received_certs: vec![],
-            },
-        );
+        // TODO: another way to write this?
+        if Smiths::<T>::get(receiver).is_some() {
+            Smiths::<T>::mutate(receiver, |maybe_smith_meta| {
+                let maybe_smith_meta = maybe_smith_meta.as_mut().expect("checked earlier");
+                maybe_smith_meta.status = SmithStatus::Invited;
+                maybe_smith_meta.expires_on = Some(new_expires_on);
+                maybe_smith_meta.received_certs = vec![];
+            });
+        } else {
+            Smiths::<T>::insert(
+                receiver,
+                SmithMeta {
+                    status: SmithStatus::Invited,
+                    expires_on: Some(new_expires_on),
+                    issued_certs: vec![],
+                    received_certs: vec![],
+                },
+            );
+        }
         ExpiresOn::<T>::append(new_expires_on, receiver);
     }
 
@@ -356,8 +376,13 @@ impl<T: Config> Pallet<T> {
     }
 
     fn do_certify_smith(receiver: T::IdtyIndex, issuer: T::IdtyIndex) {
+        Smiths::<T>::mutate(issuer, |maybe_smith_meta| {
+            let maybe_smith_meta = maybe_smith_meta.as_mut().expect("issuer checked earlier");
+            maybe_smith_meta.issued_certs.push(receiver);
+            maybe_smith_meta.issued_certs.sort();
+        });
         Smiths::<T>::mutate(receiver, |maybe_smith_meta| {
-            let maybe_smith_meta = maybe_smith_meta.as_mut().expect("status checked earlier");
+            let maybe_smith_meta = maybe_smith_meta.as_mut().expect("receiver checked earlier");
             maybe_smith_meta.received_certs.push(issuer);
             maybe_smith_meta.received_certs.sort();
             // TODO: "as u32" allowed?
@@ -381,11 +406,36 @@ impl<T: Config> Pallet<T> {
                 if let Some(smith_meta) = Smiths::<T>::get(smith) {
                     if let Some(expires_on) = smith_meta.expires_on {
                         if expires_on == at {
-                            Smiths::<T>::remove(smith);
+                            let mut lost_certs = vec![];
+                            Smiths::<T>::mutate(smith, |maybe_smith_meta| {
+                                let maybe_smith_meta =
+                                    maybe_smith_meta.as_mut().expect("checked earlier");
+                                maybe_smith_meta.expires_on = None;
+                                maybe_smith_meta.status = SmithStatus::Excluded;
+                                for cert in &maybe_smith_meta.received_certs {
+                                    lost_certs.push(cert.clone());
+                                }
+                                maybe_smith_meta.received_certs = vec![];
+                                // N.B.: the issued certs are kept in case the smith joins back
+                            });
+                            // We remove the lost certs from their issuer's stock
+                            for lost_cert in lost_certs {
+                                Smiths::<T>::mutate(lost_cert, |maybe_smith_meta| {
+                                    let maybe_smith_meta =
+                                        maybe_smith_meta.as_mut().expect("checked earlier");
+                                    if let Ok(index) =
+                                        maybe_smith_meta.issued_certs.binary_search(&smith)
+                                    {
+                                        maybe_smith_meta.issued_certs.remove(index);
+                                    }
+                                });
+                            }
+                            // Deletion done
                             T::OnSmithDelete::on_smith_delete(
                                 smith,
                                 SmithRemovalReason::OfflineTooLong,
                             );
+                            // TODO: add event? (+ add others too)
                         }
                     }
                 }
diff --git a/pallets/smith-members/src/tests.rs b/pallets/smith-members/src/tests.rs
index 3f2215510..42b417077 100644
--- a/pallets/smith-members/src/tests.rs
+++ b/pallets/smith-members/src/tests.rs
@@ -34,10 +34,11 @@ fn process_to_become_a_smith_and_lose_it() {
                 4 => None,
             ],
             2 => btreemap![
+                3 => None,
                 4 => None,
-                4 => None,
-                5 => None,
-            ]
+            ],
+            3 => btreemap![],
+            4 => btreemap![],
         ],
     })
     .execute_with(|| {
@@ -55,6 +56,7 @@ fn process_to_become_a_smith_and_lose_it() {
             SmithMeta {
                 status: SmithStatus::Pending,
                 expires_on: Some(5),
+                issued_certs: vec![],
                 received_certs: vec![],
             }
         );
@@ -68,6 +70,7 @@ fn process_to_become_a_smith_and_lose_it() {
             SmithMeta {
                 status: SmithStatus::Pending,
                 expires_on: Some(5),
+                issued_certs: vec![],
                 received_certs: vec![1],
             }
         );
@@ -81,6 +84,7 @@ fn process_to_become_a_smith_and_lose_it() {
             SmithMeta {
                 status: SmithStatus::Smith,
                 expires_on: Some(5),
+                issued_certs: vec![],
                 received_certs: vec![1, 2],
             }
         );
@@ -92,9 +96,33 @@ fn process_to_become_a_smith_and_lose_it() {
         assert_eq!(Smiths::<Runtime>::get(5).is_some(), true);
         // On session 5 no more smiths because of lack of activity
         Pallet::<Runtime>::on_new_session(5);
-        assert_eq!(Smiths::<Runtime>::get(1), None); // TODO: noautoremove (remaining certs)
-        assert_eq!(Smiths::<Runtime>::get(2), None); // TODO: noautoremove (remaining certs)
-        assert_eq!(Smiths::<Runtime>::get(5), None); // TODO: noautoremove (remaining certs)
+        assert_eq!(
+            Smiths::<Runtime>::get(1),
+            Some(SmithMeta {
+                status: SmithStatus::Excluded,
+                expires_on: None,
+                issued_certs: vec![],
+                received_certs: vec![]
+            })
+        );
+        assert_eq!(
+            Smiths::<Runtime>::get(2),
+            Some(SmithMeta {
+                status: SmithStatus::Excluded,
+                expires_on: None,
+                issued_certs: vec![],
+                received_certs: vec![]
+            })
+        );
+        assert_eq!(
+            Smiths::<Runtime>::get(5),
+            Some(SmithMeta {
+                status: SmithStatus::Excluded,
+                expires_on: None,
+                issued_certs: vec![],
+                received_certs: vec![]
+            })
+        );
     });
 }
 
@@ -127,6 +155,7 @@ fn should_have_checks_on_certify() {
             SmithMeta {
                 status: SmithStatus::Smith,
                 expires_on: Some(5),
+                issued_certs: vec![4],
                 received_certs: vec![2, 3, 4],
             }
         );
@@ -135,6 +164,7 @@ fn should_have_checks_on_certify() {
             SmithMeta {
                 status: SmithStatus::Smith,
                 expires_on: Some(5),
+                issued_certs: vec![1, 4],
                 received_certs: vec![3, 4],
             }
         );
@@ -143,6 +173,7 @@ fn should_have_checks_on_certify() {
             SmithMeta {
                 status: SmithStatus::Pending,
                 expires_on: Some(5),
+                issued_certs: vec![1, 2],
                 received_certs: vec![4],
             }
         );
@@ -151,6 +182,7 @@ fn should_have_checks_on_certify() {
             SmithMeta {
                 status: SmithStatus::Smith,
                 expires_on: Some(5),
+                issued_certs: vec![1, 2, 3],
                 received_certs: vec![1, 2],
             }
         );
@@ -179,6 +211,7 @@ fn should_have_checks_on_certify() {
             SmithMeta {
                 status: SmithStatus::Pending,
                 expires_on: Some(5),
+                issued_certs: vec![1, 2],
                 received_certs: vec![4],
             }
         );
@@ -193,6 +226,7 @@ fn should_have_checks_on_certify() {
             SmithMeta {
                 status: SmithStatus::Smith,
                 expires_on: Some(5),
+                issued_certs: vec![1, 2],
                 received_certs: vec![1, 4],
             }
         );
@@ -211,7 +245,9 @@ fn smith_activity_postpones_expiration() {
             2 => btreemap![
                 3 => None,
                 4 => None,
-            ]
+            ],
+            3 => btreemap![],
+            4 => btreemap![]
         ],
     })
     .execute_with(|| {
@@ -223,14 +259,24 @@ fn smith_activity_postpones_expiration() {
         // Smith #2 is online but not #1
         Pallet::<Runtime>::smith_goes_online(2);
 
-        // On session 5 no more smiths because of lack of activity
+        // On session 5: exclusion for lack of activity
         Pallet::<Runtime>::on_new_session(5);
-        assert_eq!(Smiths::<Runtime>::get(1), None);
+        assert_eq!(
+            Smiths::<Runtime>::get(1),
+            Some(SmithMeta {
+                status: SmithStatus::Excluded,
+                expires_on: None,
+                issued_certs: vec![],
+                received_certs: vec![]
+            })
+        );
+        // issued_certs is empty because #1 was excluded
         assert_eq!(
             Smiths::<Runtime>::get(2),
             Some(SmithMeta {
                 status: SmithStatus::Smith,
                 expires_on: None,
+                issued_certs: vec![],
                 received_certs: vec![3, 4],
             })
         );
@@ -238,29 +284,108 @@ fn smith_activity_postpones_expiration() {
         // Smith #2 goes offline
         Pallet::<Runtime>::on_new_session(6);
         Pallet::<Runtime>::smith_goes_offline(2);
-        assert_eq!(Smiths::<Runtime>::get(1), None);
         assert_eq!(
             Smiths::<Runtime>::get(2),
             Some(SmithMeta {
                 status: SmithStatus::Smith,
                 expires_on: Some(11),
+                issued_certs: vec![],
                 received_certs: vec![3, 4],
             })
         );
         // Still not expired on session 10
         Pallet::<Runtime>::on_new_session(10);
-        assert_eq!(Smiths::<Runtime>::get(1), None);
         assert_eq!(
             Smiths::<Runtime>::get(2),
             Some(SmithMeta {
                 status: SmithStatus::Smith,
                 expires_on: Some(11),
+                issued_certs: vec![],
                 received_certs: vec![3, 4],
             })
         );
-        // Finally expired on session 11
+        // But expired on session 11
         Pallet::<Runtime>::on_new_session(11);
-        assert_eq!(Smiths::<Runtime>::get(1), None);
-        assert_eq!(Smiths::<Runtime>::get(2), None);
+        assert_eq!(
+            Smiths::<Runtime>::get(1),
+            Some(SmithMeta {
+                status: SmithStatus::Excluded,
+                expires_on: None,
+                issued_certs: vec![],
+                received_certs: vec![]
+            })
+        );
+        assert_eq!(
+            Smiths::<Runtime>::get(2),
+            Some(SmithMeta {
+                status: SmithStatus::Excluded,
+                expires_on: None,
+                issued_certs: vec![],
+                received_certs: vec![]
+            })
+        );
+    });
+}
+
+#[test]
+fn smith_coming_back_recovers_its_issued_certs() {
+    new_test_ext(GenesisConfig {
+        certs_by_receiver: btreemap![
+            1 => btreemap![
+                2 => None,
+                3 => None,
+                4 => None,
+            ],
+            2 => btreemap![
+                3 => None,
+                4 => None,
+            ],
+            3 => btreemap![
+                4 => None,
+                1 => None,
+            ],
+            4 => btreemap![]
+        ],
+    })
+    .execute_with(|| {
+        // Not activity for Smith #2
+        Pallet::<Runtime>::smith_goes_online(1);
+        Pallet::<Runtime>::smith_goes_online(3);
+        Pallet::<Runtime>::smith_goes_online(4);
+        // Smith #2 gets excluded
+        Pallet::<Runtime>::on_new_session(5);
+        // The issued certs are preserved
+        assert_eq!(
+            Smiths::<Runtime>::get(2),
+            Some(SmithMeta {
+                status: SmithStatus::Excluded,
+                expires_on: None,
+                issued_certs: vec![1],
+                received_certs: vec![]
+            })
+        );
+        // Smith #2 comes back
+        assert_ok!(Pallet::<Runtime>::invite_smith(RuntimeOrigin::signed(1), 2));
+        assert_ok!(Pallet::<Runtime>::accept_invitation(RuntimeOrigin::signed(
+            2
+        )));
+        assert_ok!(Pallet::<Runtime>::certify_smith(
+            RuntimeOrigin::signed(1),
+            2
+        ));
+        assert_ok!(Pallet::<Runtime>::certify_smith(
+            RuntimeOrigin::signed(3),
+            2
+        ));
+        // Smith #2 is back with its issued certs recovered, but not its received certs
+        assert_eq!(
+            Smiths::<Runtime>::get(2),
+            Some(SmithMeta {
+                status: SmithStatus::Smith,
+                expires_on: Some(10),
+                issued_certs: vec![1],
+                received_certs: vec![1, 3]
+            })
+        );
     });
 }
diff --git a/pallets/smith-members/src/types.rs b/pallets/smith-members/src/types.rs
index 2242b81a6..a6591f0e3 100644
--- a/pallets/smith-members/src/types.rs
+++ b/pallets/smith-members/src/types.rs
@@ -30,7 +30,9 @@ pub struct SmithMeta<IdtyIndex> {
     pub status: SmithStatus,
     /// the session at which the smith will expire (for lack of validation activity)
     pub expires_on: Option<SessionIndex>,
-    /// the recertifications received from other smiths
+    /// the certifications issued to other smiths
+    pub issued_certs: Vec<IdtyIndex>,
+    /// the certifications received from other smiths
     pub received_certs: Vec<IdtyIndex>,
 }
 
-- 
GitLab