From dabb2c0905cf0212470968261720d66da9eb3312 Mon Sep 17 00:00:00 2001
From: Benjamin Gallois <business@gallois.cc>
Date: Mon, 5 Jun 2023 15:39:12 +0200
Subject: [PATCH] Offences management (nodes/rust/duniter-v2s!161)

* fix cargo files

* fix slashing strategy

* refactore remove_member_from_blacklist

* add missing test

* add license

* refac tests

adds comments
remove some repetitions
reorder tests (offence test are below)
rename tests

* feat(pallet_grandpa) add offences handler

* feat(pallet_authority_members) add offences execution

* feat(pallet_offences) add pallet offences
---
 Cargo.lock                                    |   6 +-
 pallets/README.md                             |   1 +
 pallets/authority-members/Cargo.toml          |   2 +
 pallets/authority-members/src/benchmarking.rs |   9 +
 pallets/authority-members/src/impls.rs        |  81 +++++
 pallets/authority-members/src/lib.rs          |  41 ++-
 pallets/authority-members/src/mock.rs         |  13 +
 pallets/authority-members/src/tests.rs        | 224 ++++++++++++-
 pallets/authority-members/src/weights.rs      |   8 +
 pallets/offences/Cargo.toml                   |  46 +++
 pallets/offences/README.md                    |   9 +
 pallets/offences/src/lib.rs                   | 259 +++++++++++++++
 pallets/offences/src/mock.rs                  | 156 +++++++++
 pallets/offences/src/tests.rs                 | 303 ++++++++++++++++++
 pallets/offences/src/traits.rs                |  28 ++
 runtime/common/src/pallets_config.rs          |  10 +-
 .../src/weights/pallet_authority_members.rs   |  53 +--
 runtime/g1/Cargo.toml                         |   2 +-
 runtime/g1/src/lib.rs                         |   2 +-
 runtime/gdev/Cargo.toml                       |   2 +-
 runtime/gdev/src/lib.rs                       |   2 +-
 runtime/gtest/Cargo.toml                      |   2 +-
 runtime/gtest/src/lib.rs                      |   2 +-
 23 files changed, 1227 insertions(+), 34 deletions(-)
 create mode 100644 pallets/authority-members/src/impls.rs
 create mode 100644 pallets/offences/Cargo.toml
 create mode 100644 pallets/offences/README.md
 create mode 100644 pallets/offences/src/lib.rs
 create mode 100644 pallets/offences/src/mock.rs
 create mode 100644 pallets/offences/src/tests.rs
 create mode 100644 pallets/offences/src/traits.rs

diff --git a/Cargo.lock b/Cargo.lock
index f6684b603..d00e4ff66 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5040,6 +5040,7 @@ dependencies = [
  "frame-system",
  "log",
  "maplit",
+ "pallet-offences",
  "pallet-session",
  "parity-scale-codec",
  "scale-info",
@@ -5304,8 +5305,7 @@ dependencies = [
 
 [[package]]
 name = "pallet-offences"
-version = "4.0.0-dev"
-source = "git+https://github.com/duniter/substrate?branch=duniter-substrate-v0.9.32#7f8b8db65b441ce1d1b2ffb26ebde314b54e117c"
+version = "3.0.0"
 dependencies = [
  "frame-support",
  "frame-system",
@@ -5314,6 +5314,8 @@ dependencies = [
  "parity-scale-codec",
  "scale-info",
  "serde",
+ "sp-core",
+ "sp-io",
  "sp-runtime",
  "sp-staking",
  "sp-std",
diff --git a/pallets/README.md b/pallets/README.md
index 718e3cdf4..cf67622f8 100644
--- a/pallets/README.md
+++ b/pallets/README.md
@@ -18,6 +18,7 @@ These pallets are at the core of Duniter/Äž1 currency
 ## Functional pallets
 
 - **`duniter-test-parameters`** Test parameters only used in ÄžDev to allow tweaking parameters more easily.
+- **`offences`** Sorts offences that will be executed by the `authority-members` pallet.
 - **`oneshot-account`** Oneshot accounts are light accounts only used once for anonimity or convenience use case.
 - **`provide-randomness`** Lets blockchain users ask for a verifiable random number.
 - **`session-benchmarking`** Benchmarks the session pallet.
diff --git a/pallets/authority-members/Cargo.toml b/pallets/authority-members/Cargo.toml
index 01c379921..81cfc5435 100644
--- a/pallets/authority-members/Cargo.toml
+++ b/pallets/authority-members/Cargo.toml
@@ -21,6 +21,7 @@ std = [
     'frame-benchmarking/std',
     'log/std',
     'pallet-session/std',
+    'pallet-offences/std',
     'serde',
     'sp-core/std',
     'sp-membership/std',
@@ -32,6 +33,7 @@ try-runtime = ['frame-support/try-runtime']
 
 [dependencies]
 # local
+pallet-offences = { path = "../offences", default-features = false }
 sp-membership = { path = "../../primitives/membership", default-features = false }
 
 # crates.io
diff --git a/pallets/authority-members/src/benchmarking.rs b/pallets/authority-members/src/benchmarking.rs
index dc7ae8358..d19e73ccf 100644
--- a/pallets/authority-members/src/benchmarking.rs
+++ b/pallets/authority-members/src/benchmarking.rs
@@ -68,6 +68,15 @@ benchmarks! {
     verify {
         assert_has_event::<T>(Event::<T>::MemberRemoved(id).into());
     }
+     remove_member_from_blacklist {
+        let id: T::MemberId = OnlineAuthorities::<T>::get()[0];
+        BlackList::<T>::mutate(|blacklist| {
+            blacklist.push(id);
+        });
+    }: _<T::RuntimeOrigin>(RawOrigin::Root.into(), id)
+    verify {
+        assert_has_event::<T>(Event::<T>::MemberRemovedFromBlackList(id).into());
+    }
 
      impl_benchmark_test_suite!(
             Pallet,
diff --git a/pallets/authority-members/src/impls.rs b/pallets/authority-members/src/impls.rs
new file mode 100644
index 000000000..80f9eef04
--- /dev/null
+++ b/pallets/authority-members/src/impls.rs
@@ -0,0 +1,81 @@
+// Copyright 2021-2023 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+//! Implementation of the Slashing execution logic.
+//!
+//! Offences are sorted in the `offences` pallet.
+//! The offences are executed here based. The offenders are disconnected and
+//! can be added to a blacklist to avoid futur connection.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+#![allow(clippy::type_complexity)]
+
+use super::pallet::*;
+use frame_support::pallet_prelude::Weight;
+use frame_support::traits::Get;
+use pallet_offences::traits::OnOffenceHandler;
+use pallet_offences::SlashStrategy;
+use sp_runtime::traits::Convert;
+use sp_staking::offence::OffenceDetails;
+use sp_staking::SessionIndex;
+
+impl<T: Config>
+    OnOffenceHandler<T::AccountId, pallet_session::historical::IdentificationTuple<T>, Weight>
+    for Pallet<T>
+where
+    T: pallet_session::Config<ValidatorId = <T as frame_system::Config>::AccountId>,
+{
+    fn on_offence(
+        offenders: &[OffenceDetails<
+            T::AccountId,
+            pallet_session::historical::IdentificationTuple<T>,
+        >],
+        strategy: SlashStrategy,
+        _slash_session: SessionIndex,
+    ) -> Weight {
+        let mut consumed_weight = Weight::from_parts(0, 0);
+        let mut add_db_reads_writes = |reads, writes| {
+            consumed_weight += T::DbWeight::get().reads_writes(reads, writes);
+        };
+
+        match strategy {
+            SlashStrategy::BlackList => {
+                for offender in offenders {
+                    BlackList::<T>::mutate(|blacklist| {
+                        if let Some(member_id) = T::MemberIdOf::convert(offender.offender.0.clone())
+                        {
+                            if !blacklist.contains(&member_id) {
+                                blacklist.push(member_id);
+                                add_db_reads_writes(0, 1);
+                            }
+                            Self::insert_out(member_id);
+                            add_db_reads_writes(2, 1);
+                        }
+                    })
+                }
+            }
+            SlashStrategy::Disconnect => {
+                for offender in offenders {
+                    if let Some(member_id) = T::MemberIdOf::convert(offender.offender.0.clone()) {
+                        Self::insert_out(member_id);
+                        add_db_reads_writes(1, 1);
+                    }
+                }
+            }
+        }
+        consumed_weight
+    }
+}
diff --git a/pallets/authority-members/src/lib.rs b/pallets/authority-members/src/lib.rs
index 22865875f..d4f299c36 100644
--- a/pallets/authority-members/src/lib.rs
+++ b/pallets/authority-members/src/lib.rs
@@ -30,6 +30,9 @@ mod tests;
 #[cfg(feature = "runtime-benchmarks")]
 mod benchmarking;
 
+pub mod impls;
+pub use impls::*;
+
 pub use pallet::*;
 pub use types::*;
 pub use weights::WeightInfo;
@@ -175,6 +178,11 @@ pub mod pallet {
     pub type MustRotateKeysBefore<T: Config> =
         StorageMap<_, Twox64Concat, SessionIndex, Vec<T::MemberId>, ValueQuery>;
 
+    // Blacklist.
+    #[pallet::storage]
+    #[pallet::getter(fn blacklist)]
+    pub type BlackList<T: Config> = StorageValue<_, Vec<T::MemberId>, ValueQuery>;
+
     // HOOKS //
 
     // EVENTS //
@@ -198,6 +206,9 @@ pub mod pallet {
         /// this member will be removed from the authority set in 2 sessions.
         /// [member_id]
         MemberRemoved(T::MemberId),
+        /// A member has been removed from the blacklist.
+        /// [member_id]
+        MemberRemovedFromBlackList(T::MemberId),
     }
 
     // ERRORS //
@@ -212,6 +223,10 @@ pub mod pallet {
         AlreadyOutgoing,
         /// Not found owner key
         MemberIdNotFound,
+        /// Member is blacklisted
+        MemberIdBlackListed,
+        /// Member is not blacklisted
+        MemberNotBlackListed,
         /// Member not found
         MemberNotFound,
         /// Neither online nor scheduled
@@ -264,6 +279,9 @@ pub mod pallet {
             let who = ensure_signed(origin)?;
             let member_id = Self::verify_ownership_and_membership(&who)?;
 
+            if Self::is_blacklisted(member_id) {
+                return Err(Error::<T>::MemberIdBlackListed.into());
+            }
             if !Members::<T>::contains_key(member_id) {
                 return Err(Error::<T>::MemberNotFound.into());
             }
@@ -340,6 +358,23 @@ pub mod pallet {
 
             Ok(().into())
         }
+        #[pallet::weight(<T as pallet::Config>::WeightInfo::remove_member_from_blacklist())]
+        /// remove an identity from the blacklist
+        pub fn remove_member_from_blacklist(
+            origin: OriginFor<T>,
+            member_id: T::MemberId,
+        ) -> DispatchResultWithPostInfo {
+            T::RemoveMemberOrigin::ensure_origin(origin)?;
+            BlackList::<T>::mutate(|members_ids| {
+                if let Ok(index) = members_ids.binary_search(&member_id) {
+                    members_ids.remove(index);
+                    Self::deposit_event(Event::MemberRemovedFromBlackList(member_id));
+                    Ok(().into())
+                } else {
+                    Err(Error::<T>::MemberNotBlackListed.into())
+                }
+            })
+        }
     }
 
     // PUBLIC FUNCTIONS //
@@ -452,7 +487,7 @@ pub mod pallet {
             not_already_inserted
         }
         /// perform outgoing authority insertion
-        fn insert_out(member_id: T::MemberId) -> bool {
+        pub fn insert_out(member_id: T::MemberId) -> bool {
             let not_already_inserted = OutgoingAuthorities::<T>::mutate(|members_ids| {
                 if let Err(index) = members_ids.binary_search(&member_id) {
                     members_ids.insert(index, member_id);
@@ -489,6 +524,10 @@ pub mod pallet {
                 .binary_search(&member_id)
                 .is_ok()
         }
+        /// check if member is blacklisted
+        fn is_blacklisted(member_id: T::MemberId) -> bool {
+            BlackList::<T>::get().contains(&member_id)
+        }
         /// perform removal from incoming authorities
         fn remove_in(member_id: T::MemberId) {
             AuthoritiesCounter::<T>::mutate(|counter| counter.saturating_sub(1));
diff --git a/pallets/authority-members/src/mock.rs b/pallets/authority-members/src/mock.rs
index 15a820816..5cc6fd83f 100644
--- a/pallets/authority-members/src/mock.rs
+++ b/pallets/authority-members/src/mock.rs
@@ -23,6 +23,8 @@ use frame_support::{
     BasicExternalities,
 };
 use frame_system as system;
+use pallet_offences::traits::OnOffenceHandler;
+use pallet_offences::SlashStrategy;
 use pallet_session::ShouldEndSession;
 use sp_core::{crypto::key_types::DUMMY, H256};
 use sp_runtime::{
@@ -31,6 +33,7 @@ use sp_runtime::{
     traits::{BlakeTwo256, ConvertInto, IdentityLookup, IsMember, OpaqueKeys},
     KeyTypeId,
 };
+use sp_staking::offence::OffenceDetails;
 
 type AccountId = u64;
 type Block = frame_system::mocking::MockBlock<Test>;
@@ -205,3 +208,13 @@ pub fn run_to_block(n: u64) {
         Session::on_initialize(System::block_number());
     }
 }
+
+pub(crate) fn on_offence(
+    offenders: &[OffenceDetails<
+        AccountId,
+        pallet_session::historical::IdentificationTuple<Test>,
+    >],
+    slash_strategy: SlashStrategy,
+) {
+    AuthorityMembers::on_offence(offenders, slash_strategy, 0);
+}
diff --git a/pallets/authority-members/src/tests.rs b/pallets/authority-members/src/tests.rs
index d26c3f410..6c99f0528 100644
--- a/pallets/authority-members/src/tests.rs
+++ b/pallets/authority-members/src/tests.rs
@@ -17,8 +17,11 @@
 use super::*;
 use crate::mock::*;
 use crate::MemberData;
-use frame_support::{assert_noop, assert_ok};
+use frame_support::{assert_err, assert_noop, assert_ok};
+use frame_system::RawOrigin;
 use sp_runtime::testing::UintAuthorityId;
+use sp_runtime::traits::BadOrigin;
+use sp_staking::offence::OffenceDetails;
 
 const EMPTY: Vec<u64> = Vec::new();
 
@@ -126,6 +129,7 @@ fn test_max_keys_life_rule() {
     });
 }
 
+/// tests consequences of go_offline call
 #[test]
 fn test_go_offline() {
     new_test_ext(3).execute_with(|| {
@@ -135,9 +139,11 @@ fn test_go_offline() {
         assert_ok!(AuthorityMembers::go_offline(RuntimeOrigin::signed(9)),);
 
         // Verify state
+        assert_eq!(Session::current_index(), 0); // we are currently at session 0
         assert_eq!(AuthorityMembers::incoming(), EMPTY);
         assert_eq!(AuthorityMembers::online(), vec![3, 6, 9]);
         assert_eq!(AuthorityMembers::outgoing(), vec![9]);
+        assert_eq!(AuthorityMembers::blacklist(), EMPTY);
         assert_eq!(
             AuthorityMembers::member(9),
             Some(MemberData {
@@ -147,7 +153,9 @@ fn test_go_offline() {
             })
         );
 
-        // Member 9 should be "deprogrammed" at the next session
+        // Member 9 should be "deprogrammed" at the next session (session 1)
+        // it should be out at session 2 and
+        // the expiry should be 2 sessions after that (session 4)
         run_to_block(5);
         assert_eq!(
             AuthorityMembers::member(9),
@@ -178,6 +186,7 @@ fn test_go_offline() {
     });
 }
 
+/// tests consequences of go_online call
 #[test]
 fn test_go_online() {
     new_test_ext(3).execute_with(|| {
@@ -322,3 +331,214 @@ fn test_go_offline_then_go_online_in_same_session() {
         );
     });
 }
+
+// === offence handling tests below ===
+
+/// test offence handling with disconnect strategy
+// the offenders should be disconnected (same as go_offline)
+// they should be able to go_online after
+#[test]
+fn test_offence_disconnect() {
+    new_test_ext(3).execute_with(|| {
+        run_to_block(1);
+
+        on_offence(
+            &[OffenceDetails {
+                offender: (9, ()),
+                reporters: vec![],
+            }],
+            pallet_offences::SlashStrategy::Disconnect,
+        );
+        on_offence(
+            &[OffenceDetails {
+                offender: (3, ()),
+                reporters: vec![],
+            }],
+            pallet_offences::SlashStrategy::Disconnect,
+        );
+
+        // Verify state
+        assert_eq!(AuthorityMembers::incoming(), EMPTY);
+        assert_eq!(AuthorityMembers::online(), vec![3, 6, 9]);
+        assert_eq!(AuthorityMembers::outgoing(), vec![3, 9]);
+        assert_eq!(AuthorityMembers::blacklist(), EMPTY);
+
+        // Member 9 and 3 should be "deprogrammed" at the next session
+        run_to_block(5);
+        assert_eq!(
+            AuthorityMembers::member(9),
+            Some(MemberData {
+                expire_on_session: 4,
+                must_rotate_keys_before: 5,
+                owner_key: 9,
+            })
+        );
+        assert_eq!(
+            AuthorityMembers::member(3),
+            Some(MemberData {
+                expire_on_session: 4,
+                must_rotate_keys_before: 5,
+                owner_key: 3,
+            })
+        );
+        assert_eq!(AuthorityMembers::members_expire_on(4), vec![3, 9],);
+        assert_eq!(Session::current_index(), 1);
+        assert_eq!(Session::validators(), vec![3, 6, 9]);
+        assert_eq!(Session::queued_keys().len(), 1);
+        assert_eq!(Session::queued_keys()[0].0, 6);
+
+        // Member 9 and 3 should be **effectively** out at session 2
+        run_to_block(10);
+        assert_eq!(Session::current_index(), 2);
+        assert_eq!(Session::validators(), vec![6]);
+
+        // Member 9 and 3 should be removed at session 4
+        run_to_block(20);
+        assert_eq!(Session::current_index(), 4);
+        assert_eq!(Session::validators(), vec![6]);
+        assert_eq!(AuthorityMembers::members_expire_on(4), EMPTY);
+        assert_eq!(AuthorityMembers::member(3), None);
+        assert_eq!(AuthorityMembers::member(9), None);
+
+        // Member 9 and 3 should be allowed to set session keys and go online
+        run_to_block(25);
+        assert_ok!(AuthorityMembers::set_session_keys(
+            RuntimeOrigin::signed(9),
+            UintAuthorityId(9).into(),
+        ));
+        assert_ok!(AuthorityMembers::set_session_keys(
+            RuntimeOrigin::signed(3),
+            UintAuthorityId(3).into(),
+        ));
+        assert_ok!(AuthorityMembers::go_online(RuntimeOrigin::signed(9)),);
+        assert_ok!(AuthorityMembers::go_online(RuntimeOrigin::signed(3)),);
+
+        // Report an offence again
+        run_to_block(35);
+        on_offence(
+            &[OffenceDetails {
+                offender: (3, ()),
+                reporters: vec![],
+            }],
+            pallet_offences::SlashStrategy::Disconnect,
+        );
+
+        // Verify state, 6 is out of life now, only 3 should be outgoing now
+        assert_eq!(AuthorityMembers::incoming(), EMPTY);
+        assert_eq!(AuthorityMembers::online(), vec![3, 9]);
+        assert_eq!(AuthorityMembers::outgoing(), vec![3]);
+        assert_eq!(AuthorityMembers::blacklist(), EMPTY);
+    });
+}
+
+/// test offence handling with blacklist strategy
+// member 9 is offender, should be blacklisted
+#[test]
+fn test_offence_black_list() {
+    new_test_ext(3).execute_with(|| {
+        // at block 0 begins session 0
+        run_to_block(1);
+
+        on_offence(
+            &[OffenceDetails {
+                offender: (9, ()),
+                reporters: vec![],
+            }],
+            pallet_offences::SlashStrategy::BlackList,
+        );
+
+        // Verify state
+        // same as `test_go_offline`
+        assert_eq!(AuthorityMembers::online(), vec![3, 6, 9]);
+        assert_eq!(AuthorityMembers::outgoing(), vec![9]);
+        assert_eq!(AuthorityMembers::blacklist(), vec![9]);
+
+        // Member 9 should be "deprogrammed" at the next session
+        run_to_block(5);
+        assert_eq!(AuthorityMembers::members_expire_on(4), vec![9],);
+        assert_eq!(Session::current_index(), 1);
+        assert_eq!(Session::validators(), vec![3, 6, 9]);
+        assert_eq!(AuthorityMembers::blacklist(), vec![9]); // still in blacklist
+
+        // Member 9 should be **effectively** out at session 2
+        run_to_block(10);
+        assert_eq!(Session::current_index(), 2);
+        assert_eq!(Session::validators(), vec![3, 6]);
+        assert_eq!(AuthorityMembers::blacklist(), vec![9]); // still in blacklist
+
+        // Member 9 should be removed at session 4
+        run_to_block(20);
+        assert_eq!(Session::current_index(), 4);
+        assert_eq!(Session::validators(), vec![3, 6]);
+        assert_eq!(AuthorityMembers::members_expire_on(4), EMPTY);
+        assert_eq!(AuthorityMembers::member(9), None);
+        assert_eq!(AuthorityMembers::blacklist(), vec![9]); // still in blacklist
+    });
+}
+
+/// tests that blacklisting prevents 9 from going online
+#[test]
+fn test_offence_black_list_prevent_from_going_online() {
+    new_test_ext(3).execute_with(|| {
+        run_to_block(1);
+
+        on_offence(
+            &[OffenceDetails {
+                offender: (9, ()),
+                reporters: vec![],
+            }],
+            pallet_offences::SlashStrategy::BlackList,
+        );
+
+        // Verify state
+        assert_eq!(AuthorityMembers::incoming(), EMPTY);
+        assert_eq!(AuthorityMembers::online(), vec![3, 6, 9]);
+        assert_eq!(AuthorityMembers::outgoing(), vec![9]);
+        assert_eq!(AuthorityMembers::blacklist(), vec![9]);
+        assert_eq!(
+            AuthorityMembers::member(9),
+            Some(MemberData {
+                expire_on_session: 0,
+                must_rotate_keys_before: 5,
+                owner_key: 9,
+            })
+        );
+
+        // for detail, see `test_go_offline`
+        // Member 9 is "deprogrammed" at the next session
+        // Member 9 is **effectively** out at session 2
+        // Member 9 is removed at session 4
+
+        // Member 9 should not be allowed to go online
+        run_to_block(25);
+        assert_ok!(AuthorityMembers::set_session_keys(
+            RuntimeOrigin::signed(9),
+            UintAuthorityId(9).into(),
+        ));
+        assert_err!(
+            AuthorityMembers::go_online(RuntimeOrigin::signed(9)),
+            Error::<Test>::MemberIdBlackListed
+        );
+
+        // Should not be able to auto remove from blacklist
+        assert_err!(
+            AuthorityMembers::remove_member_from_blacklist(RuntimeOrigin::signed(9), 9),
+            BadOrigin
+        );
+        assert_eq!(AuthorityMembers::blacklist(), vec![9]);
+
+        // Authorized should be able to remove from blacklist
+        assert_ok!(AuthorityMembers::remove_member_from_blacklist(
+            RawOrigin::Root.into(),
+            9
+        ));
+        assert_eq!(AuthorityMembers::blacklist(), EMPTY);
+        System::assert_last_event(Event::MemberRemovedFromBlackList(9).into());
+
+        // Authorized should not be able to remove a non-existing member from blacklist
+        assert_err!(
+            AuthorityMembers::remove_member_from_blacklist(RawOrigin::Root.into(), 9),
+            Error::<Test>::MemberNotBlackListed
+        );
+    });
+}
diff --git a/pallets/authority-members/src/weights.rs b/pallets/authority-members/src/weights.rs
index 3b9d8a03c..7682a59d4 100644
--- a/pallets/authority-members/src/weights.rs
+++ b/pallets/authority-members/src/weights.rs
@@ -24,6 +24,7 @@ pub trait WeightInfo {
     fn go_online() -> Weight;
     fn set_session_keys() -> Weight;
     fn remove_member() -> Weight;
+    fn remove_member_from_blacklist() -> Weight;
 }
 
 // Insecure weights implementation, use it for tests only!
@@ -85,4 +86,11 @@ impl WeightInfo for () {
             .saturating_add(RocksDbWeight::get().reads(9 as u64))
             .saturating_add(RocksDbWeight::get().writes(13 as u64))
     }
+    // Storage: AuthorityMembers BlackList (r:1 w:1)
+    fn remove_member_from_blacklist() -> Weight {
+        // Minimum execution time: 60_023 nanoseconds.
+        Weight::from_ref_time(60_615_000 as u64)
+            .saturating_add(RocksDbWeight::get().reads(1 as u64))
+            .saturating_add(RocksDbWeight::get().writes(1 as u64))
+    }
 }
diff --git a/pallets/offences/Cargo.toml b/pallets/offences/Cargo.toml
new file mode 100644
index 000000000..560b22f5b
--- /dev/null
+++ b/pallets/offences/Cargo.toml
@@ -0,0 +1,46 @@
+[package]
+name = "pallet-offences"
+authors = ["Parity Technologies <admin@parity.io>", "Axiom-Team Developers <https://axiom-team.fr>"]
+description = 'FRAME pallet to handle offences.'
+edition = "2021"
+homepage = 'https://duniter.org'
+license = 'AGPL-3.0'
+repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
+version = '3.0.0'
+readme = "README.md"
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[dependencies]
+codec = { package = "parity-scale-codec", version = "3.1.5", features = ["derive"], default-features = false }
+log = { version = "0.4.17", default-features = false }
+scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }
+serde = { version = "1.0.101", default-features = false, optional = true }
+frame-support = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
+frame-system = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
+pallet-balances = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
+sp-runtime = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
+sp-staking = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
+sp-std = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
+
+[dev-dependencies]
+sp-core = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
+sp-io = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32' }
+
+[features]
+default = ["std"]
+std = [
+	"codec/std",
+	"frame-support/std",
+	"frame-system/std",
+	"log/std",
+	"pallet-balances/std",
+	"scale-info/std",
+	"serde",
+	"sp-runtime/std",
+	"sp-staking/std",
+	"sp-std/std",
+]
+runtime-benchmarks = []
+try-runtime = ["frame-support/try-runtime"]
diff --git a/pallets/offences/README.md b/pallets/offences/README.md
new file mode 100644
index 000000000..62fa734c3
--- /dev/null
+++ b/pallets/offences/README.md
@@ -0,0 +1,9 @@
+# Duniter offences pallet
+
+This is a fork of the Substrate `offences` pallet that is modified to agree with the offence rules based on the `authority-member` pallet and not in the Substrate `staking` pallet.
+
+Duniter provides a basic way to process offences:
+* On offences from `im-online` pallet, the offender disconnection is required.
+* On other offences, the offender disconnection is required and the offender is required to be blacklisted and only an authorized origin can remove the offender from the blacklist.
+
+The offences triage is realized in the `offences` pallet and the slashing execution is done in the `authority-member` pallet.
\ No newline at end of file
diff --git a/pallets/offences/src/lib.rs b/pallets/offences/src/lib.rs
new file mode 100644
index 000000000..02316c4f6
--- /dev/null
+++ b/pallets/offences/src/lib.rs
@@ -0,0 +1,259 @@
+// Copyright 2021-2023 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+#[cfg(test)]
+mod mock;
+
+#[cfg(test)]
+mod tests;
+
+use core::marker::PhantomData;
+
+use codec::Encode;
+use frame_support::weights::Weight;
+use sp_runtime::traits::Hash;
+use sp_staking::offence::{Kind, Offence, OffenceDetails, OffenceError, ReportOffence};
+use sp_std::prelude::*;
+
+pub use pallet::*;
+
+pub mod traits;
+use self::traits::*;
+
+/// A binary blob which represents a SCALE codec-encoded `O::TimeSlot`.
+type OpaqueTimeSlot = Vec<u8>;
+
+/// A type alias for a report identifier.
+type ReportIdOf<T> = <T as frame_system::Config>::Hash;
+
+pub enum SlashStrategy {
+    Disconnect,
+    BlackList,
+}
+
+#[frame_support::pallet]
+pub mod pallet {
+    use super::*;
+    use frame_support::pallet_prelude::*;
+
+    const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
+
+    #[pallet::pallet]
+    #[pallet::storage_version(STORAGE_VERSION)]
+    #[pallet::without_storage_info]
+    pub struct Pallet<T>(_);
+
+    /// The pallet's config trait.
+    #[pallet::config]
+    pub trait Config: frame_system::Config {
+        /// The overarching event type.
+        type RuntimeEvent: From<Event> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
+        /// Full identification of the validator.
+        type IdentificationTuple: Parameter;
+        /// A handler called for every offence report.
+        type OnOffenceHandler: OnOffenceHandler<Self::AccountId, Self::IdentificationTuple, Weight>;
+    }
+
+    /// The primary structure that holds all offence records keyed by report identifiers.
+    #[pallet::storage]
+    #[pallet::getter(fn reports)]
+    pub type Reports<T: Config> = StorageMap<
+        _,
+        Twox64Concat,
+        ReportIdOf<T>,
+        OffenceDetails<T::AccountId, T::IdentificationTuple>,
+    >;
+
+    /// A vector of reports of the same kind that happened at the same time slot.
+    #[pallet::storage]
+    pub type ConcurrentReportsIndex<T: Config> = StorageDoubleMap<
+        _,
+        Twox64Concat,
+        Kind,
+        Twox64Concat,
+        OpaqueTimeSlot,
+        Vec<ReportIdOf<T>>,
+        ValueQuery,
+    >;
+
+    /// Events type.
+    #[pallet::event]
+    #[pallet::generate_deposit(pub(super) fn deposit_event)]
+    pub enum Event {
+        /// There is an offence reported of the given `kind` happened at the `session_index` and
+        /// (kind-specific) time slot. This event is not deposited for duplicate slashes.
+        /// \[kind, timeslot\].
+        Offence {
+            kind: Kind,
+            timeslot: OpaqueTimeSlot,
+        },
+    }
+}
+
+impl<T, O> ReportOffence<T::AccountId, T::IdentificationTuple, O> for Pallet<T>
+where
+    T: Config,
+    O: Offence<T::IdentificationTuple>,
+{
+    fn report_offence(reporters: Vec<T::AccountId>, offence: O) -> Result<(), OffenceError> {
+        let offenders = offence.offenders();
+        let time_slot = offence.time_slot();
+
+        // Go through all offenders in the offence report and find all offenders that were spotted
+        // in unique reports.
+        let TriageOutcome {
+            concurrent_offenders,
+        } = match Self::triage_offence_report::<O>(reporters, &time_slot, offenders) {
+            Some(triage) => triage,
+            None => return Err(OffenceError::DuplicateReport),
+        };
+
+        // Define the slash strategy.
+        let slash_strategy = if O::ID == *b"im-online:offlin" {
+            SlashStrategy::Disconnect
+        } else {
+            SlashStrategy::BlackList
+        };
+
+        T::OnOffenceHandler::on_offence(
+            &concurrent_offenders,
+            slash_strategy,
+            offence.session_index(),
+        );
+
+        Self::deposit_event(Event::Offence {
+            kind: O::ID,
+            timeslot: time_slot.encode(),
+        });
+
+        Ok(())
+    }
+
+    fn is_known_offence(offenders: &[T::IdentificationTuple], time_slot: &O::TimeSlot) -> bool {
+        let any_unknown = offenders.iter().any(|offender| {
+            let report_id = Self::report_id::<O>(time_slot, offender);
+            !<Reports<T>>::contains_key(report_id)
+        });
+
+        !any_unknown
+    }
+}
+
+impl<T: Config> Pallet<T> {
+    /// Compute the ID for the given report properties.
+    ///
+    /// The report id depends on the offence kind, time slot and the id of offender.
+    fn report_id<O: Offence<T::IdentificationTuple>>(
+        time_slot: &O::TimeSlot,
+        offender: &T::IdentificationTuple,
+    ) -> ReportIdOf<T> {
+        (O::ID, time_slot.encode(), offender).using_encoded(T::Hashing::hash)
+    }
+
+    /// Triages the offence report and returns the set of offenders that was involved in unique
+    /// reports along with the list of the concurrent offences.
+    fn triage_offence_report<O: Offence<T::IdentificationTuple>>(
+        reporters: Vec<T::AccountId>,
+        time_slot: &O::TimeSlot,
+        offenders: Vec<T::IdentificationTuple>,
+    ) -> Option<TriageOutcome<T>> {
+        let mut storage = ReportIndexStorage::<T, O>::load(time_slot);
+
+        let mut any_new = false;
+        for offender in offenders {
+            let report_id = Self::report_id::<O>(time_slot, &offender);
+
+            if !<Reports<T>>::contains_key(report_id) {
+                any_new = true;
+                <Reports<T>>::insert(
+                    report_id,
+                    OffenceDetails {
+                        offender,
+                        reporters: reporters.clone(),
+                    },
+                );
+
+                storage.insert(report_id);
+            }
+        }
+
+        if any_new {
+            // Load report details for the all reports happened at the same time.
+            let concurrent_offenders = storage
+                .concurrent_reports
+                .iter()
+                .filter_map(<Reports<T>>::get)
+                .collect::<Vec<_>>();
+
+            storage.save();
+
+            Some(TriageOutcome {
+                concurrent_offenders,
+            })
+        } else {
+            None
+        }
+    }
+}
+
+struct TriageOutcome<T: Config> {
+    /// Other reports for the same report kinds.
+    concurrent_offenders: Vec<OffenceDetails<T::AccountId, T::IdentificationTuple>>,
+}
+
+/// An auxiliary struct for working with storage of indexes localized for a specific offence
+/// kind (specified by the `O` type parameter).
+///
+/// This struct is responsible for aggregating storage writes and the underlying storage should not
+/// accessed directly meanwhile.
+#[must_use = "The changes are not saved without called `save`"]
+struct ReportIndexStorage<T: Config, O: Offence<T::IdentificationTuple>> {
+    opaque_time_slot: OpaqueTimeSlot,
+    concurrent_reports: Vec<ReportIdOf<T>>,
+    _phantom: PhantomData<O>,
+}
+
+impl<T: Config, O: Offence<T::IdentificationTuple>> ReportIndexStorage<T, O> {
+    /// Preload indexes from the storage for the specific `time_slot` and the kind of the offence.
+    fn load(time_slot: &O::TimeSlot) -> Self {
+        let opaque_time_slot = time_slot.encode();
+
+        let concurrent_reports = <ConcurrentReportsIndex<T>>::get(O::ID, &opaque_time_slot);
+
+        Self {
+            opaque_time_slot,
+            concurrent_reports,
+            _phantom: Default::default(),
+        }
+    }
+
+    /// Insert a new report to the index.
+    fn insert(&mut self, report_id: ReportIdOf<T>) {
+        // Update the list of concurrent reports.
+        self.concurrent_reports.push(report_id);
+    }
+
+    /// Dump the indexes to the storage.
+    fn save(self) {
+        <ConcurrentReportsIndex<T>>::insert(
+            O::ID,
+            &self.opaque_time_slot,
+            &self.concurrent_reports,
+        );
+    }
+}
diff --git a/pallets/offences/src/mock.rs b/pallets/offences/src/mock.rs
new file mode 100644
index 000000000..1229f7559
--- /dev/null
+++ b/pallets/offences/src/mock.rs
@@ -0,0 +1,156 @@
+// Copyright 2021-2023 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+#![cfg(test)]
+
+use crate::Config;
+use crate::{self as pallet_offences, SlashStrategy};
+use codec::Encode;
+use frame_support::{
+    parameter_types,
+    traits::{ConstU32, ConstU64},
+    weights::{constants::RocksDbWeight, Weight},
+};
+use sp_core::H256;
+use sp_runtime::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use sp_staking::{
+    offence::{Kind, OffenceDetails},
+    SessionIndex,
+};
+
+pub struct OnOffenceHandler;
+
+parameter_types! {
+    pub static OnOffencePerbill: Vec<Perbill> = Default::default();
+    pub static OffenceWeight: Weight = Default::default();
+}
+
+impl<Reporter, Offender> pallet_offences::OnOffenceHandler<Reporter, Offender, Weight>
+    for OnOffenceHandler
+{
+    fn on_offence(
+        _offenders: &[OffenceDetails<Reporter, Offender>],
+        _strategy: SlashStrategy,
+        _offence_session: SessionIndex,
+    ) -> Weight {
+        OffenceWeight::get()
+    }
+}
+
+type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Runtime>;
+type Block = frame_system::mocking::MockBlock<Runtime>;
+
+frame_support::construct_runtime!(
+    pub struct Runtime where
+        Block = Block,
+        NodeBlock = Block,
+        UncheckedExtrinsic = UncheckedExtrinsic,
+    {
+        System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
+        Offences: pallet_offences::{Pallet, Storage, Event},
+    }
+);
+
+impl frame_system::Config for Runtime {
+    type BaseCallFilter = frame_support::traits::Everything;
+    type BlockWeights = ();
+    type BlockLength = ();
+    type DbWeight = RocksDbWeight;
+    type RuntimeOrigin = RuntimeOrigin;
+    type Index = u64;
+    type BlockNumber = u64;
+    type RuntimeCall = RuntimeCall;
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type RuntimeEvent = RuntimeEvent;
+    type BlockHashCount = ConstU64<250>;
+    type Version = ();
+    type PalletInfo = PalletInfo;
+    type AccountData = ();
+    type OnNewAccount = ();
+    type OnKilledAccount = ();
+    type SystemWeightInfo = ();
+    type SS58Prefix = ();
+    type OnSetCode = ();
+    type MaxConsumers = ConstU32<16>;
+}
+
+impl Config for Runtime {
+    type RuntimeEvent = RuntimeEvent;
+    type IdentificationTuple = u64;
+    type OnOffenceHandler = OnOffenceHandler;
+}
+
+pub fn new_test_ext() -> sp_io::TestExternalities {
+    let t = frame_system::GenesisConfig::default()
+        .build_storage::<Runtime>()
+        .unwrap();
+    let mut ext = sp_io::TestExternalities::new(t);
+    ext.execute_with(|| System::set_block_number(1));
+    ext
+}
+
+pub const KIND: [u8; 16] = *b"test_report_1234";
+
+/// Returns all offence details for the specific `kind` happened at the specific time slot.
+pub fn offence_reports(kind: Kind, time_slot: u128) -> Vec<OffenceDetails<u64, u64>> {
+    <crate::ConcurrentReportsIndex<Runtime>>::get(kind, &time_slot.encode())
+        .into_iter()
+        .map(|report_id| {
+            <crate::Reports<Runtime>>::get(report_id)
+                .expect("dangling report id is found in ConcurrentReportsIndex")
+        })
+        .collect()
+}
+
+#[derive(Clone)]
+pub struct Offence {
+    pub validator_set_count: u32,
+    pub offenders: Vec<u64>,
+    pub time_slot: u128,
+}
+
+impl pallet_offences::Offence<u64> for Offence {
+    const ID: pallet_offences::Kind = KIND;
+    type TimeSlot = u128;
+
+    fn offenders(&self) -> Vec<u64> {
+        self.offenders.clone()
+    }
+
+    fn validator_set_count(&self) -> u32 {
+        self.validator_set_count
+    }
+
+    fn time_slot(&self) -> u128 {
+        self.time_slot
+    }
+
+    fn session_index(&self) -> SessionIndex {
+        1
+    }
+
+    fn slash_fraction(&self, offenders_count: u32) -> Perbill {
+        Perbill::from_percent(5 + offenders_count * 100 / self.validator_set_count)
+    }
+}
diff --git a/pallets/offences/src/tests.rs b/pallets/offences/src/tests.rs
new file mode 100644
index 000000000..1b5fefff8
--- /dev/null
+++ b/pallets/offences/src/tests.rs
@@ -0,0 +1,303 @@
+// Copyright 2021-2023 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+#![cfg(test)]
+
+use super::*;
+use crate::mock::{new_test_ext, offence_reports, Offence, Offences, RuntimeEvent, System, KIND};
+use frame_system::{EventRecord, Phase};
+
+#[test]
+fn should_report_an_authority_and_trigger_on_offence_and_add_to_blacklist() {
+    new_test_ext().execute_with(|| {
+        // given
+        let time_slot = 42;
+        assert_eq!(offence_reports(KIND, time_slot), vec![]);
+
+        let offence = Offence {
+            validator_set_count: 5,
+            time_slot,
+            offenders: vec![5, 9],
+        };
+
+        // when
+        Offences::report_offence(vec![], offence).unwrap();
+
+        // then
+        assert_eq!(
+            offence_reports(KIND, time_slot),
+            vec![
+                OffenceDetails {
+                    offender: 5,
+                    reporters: vec![]
+                },
+                OffenceDetails {
+                    offender: 9,
+                    reporters: vec![]
+                }
+            ]
+        );
+    });
+}
+
+#[test]
+fn should_not_report_the_same_authority_twice_in_the_same_slot() {
+    new_test_ext().execute_with(|| {
+        // given
+        let time_slot = 42;
+        assert_eq!(offence_reports(KIND, time_slot), vec![]);
+
+        let offence = Offence {
+            validator_set_count: 5,
+            time_slot,
+            offenders: vec![5],
+        };
+        Offences::report_offence(vec![], offence.clone()).unwrap();
+
+        // when
+        // report for the second time
+        assert_eq!(
+            Offences::report_offence(vec![], offence),
+            Err(OffenceError::DuplicateReport)
+        );
+
+        // then
+        assert_eq!(
+            offence_reports(KIND, time_slot),
+            vec![OffenceDetails {
+                offender: 5,
+                reporters: vec![]
+            },]
+        );
+    });
+}
+
+#[test]
+fn should_report_in_different_time_slot() {
+    new_test_ext().execute_with(|| {
+        // given
+        let time_slot = 42;
+        assert_eq!(offence_reports(KIND, time_slot), vec![]);
+
+        let mut offence = Offence {
+            validator_set_count: 5,
+            time_slot,
+            offenders: vec![5],
+        };
+        Offences::report_offence(vec![], offence.clone()).unwrap();
+        System::assert_last_event(
+            Event::Offence {
+                kind: KIND,
+                timeslot: time_slot.encode(),
+            }
+            .into(),
+        );
+
+        // when
+        // report for the second time
+        offence.time_slot += 1;
+        Offences::report_offence(vec![], offence.clone()).unwrap();
+
+        // then
+        System::assert_last_event(
+            Event::Offence {
+                kind: KIND,
+                timeslot: offence.time_slot.encode(),
+            }
+            .into(),
+        );
+    });
+}
+
+#[test]
+fn should_deposit_event() {
+    new_test_ext().execute_with(|| {
+        // given
+        let time_slot = 42;
+        assert_eq!(offence_reports(KIND, time_slot), vec![]);
+
+        let offence = Offence {
+            validator_set_count: 5,
+            time_slot,
+            offenders: vec![5],
+        };
+
+        // when
+        Offences::report_offence(vec![], offence).unwrap();
+
+        // then
+        assert_eq!(
+            System::events(),
+            vec![EventRecord {
+                phase: Phase::Initialization,
+                event: RuntimeEvent::Offences(crate::Event::Offence {
+                    kind: KIND,
+                    timeslot: time_slot.encode()
+                }),
+                topics: vec![],
+            }]
+        );
+    });
+}
+
+#[test]
+fn doesnt_deposit_event_for_dups() {
+    new_test_ext().execute_with(|| {
+        // given
+        let time_slot = 42;
+        assert_eq!(offence_reports(KIND, time_slot), vec![]);
+
+        let offence = Offence {
+            validator_set_count: 5,
+            time_slot,
+            offenders: vec![5],
+        };
+        Offences::report_offence(vec![], offence.clone()).unwrap();
+
+        // when
+        // report for the second time
+        assert_eq!(
+            Offences::report_offence(vec![], offence),
+            Err(OffenceError::DuplicateReport)
+        );
+
+        // then
+        // there is only one event.
+        assert_eq!(
+            System::events(),
+            vec![EventRecord {
+                phase: Phase::Initialization,
+                event: RuntimeEvent::Offences(crate::Event::Offence {
+                    kind: KIND,
+                    timeslot: time_slot.encode()
+                }),
+                topics: vec![],
+            }]
+        );
+    });
+}
+
+#[test]
+fn reports_if_an_offence_is_dup() {
+    new_test_ext().execute_with(|| {
+        let time_slot = 42;
+        assert_eq!(offence_reports(KIND, time_slot), vec![]);
+
+        let offence = |time_slot, offenders| Offence {
+            validator_set_count: 5,
+            time_slot,
+            offenders,
+        };
+
+        let mut test_offence = offence(time_slot, vec![0]);
+
+        // the report for authority 0 at time slot 42 should not be a known
+        // offence
+        assert!(
+            !<Offences as ReportOffence<_, _, Offence>>::is_known_offence(
+                &test_offence.offenders,
+                &test_offence.time_slot
+            )
+        );
+
+        // we report an offence for authority 0 at time slot 42
+        Offences::report_offence(vec![], test_offence.clone()).unwrap();
+
+        // the same report should be a known offence now
+        assert!(
+            <Offences as ReportOffence<_, _, Offence>>::is_known_offence(
+                &test_offence.offenders,
+                &test_offence.time_slot
+            )
+        );
+
+        // and reporting it again should yield a duplicate report error
+        assert_eq!(
+            Offences::report_offence(vec![], test_offence.clone()),
+            Err(OffenceError::DuplicateReport)
+        );
+
+        // after adding a new offender to the offence report
+        test_offence.offenders.push(1);
+
+        // it should not be a known offence anymore
+        assert!(
+            !<Offences as ReportOffence<_, _, Offence>>::is_known_offence(
+                &test_offence.offenders,
+                &test_offence.time_slot
+            )
+        );
+
+        // and reporting it again should work without any error
+        assert_eq!(
+            Offences::report_offence(vec![], test_offence.clone()),
+            Ok(())
+        );
+
+        // creating a new offence for the same authorities on the next slot
+        // should be considered a new offence and thefore not known
+        let test_offence_next_slot = offence(time_slot + 1, vec![0, 1]);
+        assert!(
+            !<Offences as ReportOffence<_, _, Offence>>::is_known_offence(
+                &test_offence_next_slot.offenders,
+                &test_offence_next_slot.time_slot
+            )
+        );
+    });
+}
+
+#[test]
+fn should_properly_count_offences() {
+    // We report two different authorities for the same issue. Ultimately, the 1st authority
+    // should have `count` equal 2 and the count of the 2nd one should be equal to 1.
+    new_test_ext().execute_with(|| {
+        // given
+        let time_slot = 42;
+        assert_eq!(offence_reports(KIND, time_slot), vec![]);
+
+        let offence1 = Offence {
+            validator_set_count: 5,
+            time_slot,
+            offenders: vec![5],
+        };
+        let offence2 = Offence {
+            validator_set_count: 5,
+            time_slot,
+            offenders: vec![4],
+        };
+        Offences::report_offence(vec![], offence1).unwrap();
+
+        // when
+        // report for the second time
+        Offences::report_offence(vec![], offence2).unwrap();
+
+        // then
+        // the 1st authority should have count 2 and the 2nd one should be reported only once.
+        assert_eq!(
+            offence_reports(KIND, time_slot),
+            vec![
+                OffenceDetails {
+                    offender: 5,
+                    reporters: vec![]
+                },
+                OffenceDetails {
+                    offender: 4,
+                    reporters: vec![]
+                },
+            ]
+        );
+    });
+}
diff --git a/pallets/offences/src/traits.rs b/pallets/offences/src/traits.rs
new file mode 100644
index 000000000..69dfe649d
--- /dev/null
+++ b/pallets/offences/src/traits.rs
@@ -0,0 +1,28 @@
+// Copyright 2021-2023 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+use sp_staking::{offence::OffenceDetails, SessionIndex};
+
+use crate::SlashStrategy;
+
+pub trait OnOffenceHandler<Reporter, Offender, Res> {
+    // Required method
+    fn on_offence(
+        offenders: &[OffenceDetails<Reporter, Offender>],
+        slash_strategy: SlashStrategy,
+        session: SessionIndex,
+    ) -> Res;
+}
diff --git a/runtime/common/src/pallets_config.rs b/runtime/common/src/pallets_config.rs
index bb1315bd6..f289dfac1 100644
--- a/runtime/common/src/pallets_config.rs
+++ b/runtime/common/src/pallets_config.rs
@@ -235,7 +235,7 @@ macro_rules! pallets_config {
         impl pallet_offences::Config for Runtime {
             type RuntimeEvent = RuntimeEvent;
             type IdentificationTuple = pallet_session::historical::IdentificationTuple<Self>;
-            type OnOffenceHandler = ();
+            type OnOffenceHandler = AuthorityMembers;
         }
         impl pallet_session::Config for Runtime {
             type RuntimeEvent = RuntimeEvent;
@@ -255,7 +255,7 @@ macro_rules! pallets_config {
         impl pallet_grandpa::Config for Runtime {
             type RuntimeEvent = RuntimeEvent;
 
-            type KeyOwnerProofSystem = ();
+            type KeyOwnerProofSystem = Historical;
 
             type KeyOwnerProof =
                 <Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(KeyTypeId, GrandpaId)>>::Proof;
@@ -265,7 +265,11 @@ macro_rules! pallets_config {
                 GrandpaId,
             )>>::IdentificationTuple;
 
-            type HandleEquivocation = ();
+        type HandleEquivocation = pallet_grandpa::EquivocationHandler<
+                Self::KeyOwnerIdentification,
+                Offences,
+                ReportLongevity,
+            >;
 
             type WeightInfo = common_runtime::weights::pallet_grandpa::WeightInfo<Runtime>;
 
diff --git a/runtime/common/src/weights/pallet_authority_members.rs b/runtime/common/src/weights/pallet_authority_members.rs
index 06b47a3e5..679df9183 100644
--- a/runtime/common/src/weights/pallet_authority_members.rs
+++ b/runtime/common/src/weights/pallet_authority_members.rs
@@ -17,24 +17,29 @@
 //! Autogenerated weights for `pallet_authority_members`
 //!
 //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
-//! DATE: 2023-04-25, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! DATE: 2023-05-13, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]`
 //! HOSTNAME: `benjamin-xps139380`, CPU: `Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz`
-//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
+//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("gdev-benchmark"), DB CACHE: 1024
 
 // Executed Command:
-// target/release/duniter
+// ./target/release/duniter
 // benchmark
 // pallet
-// --chain=dev
-// --steps=50
-// --repeat=20
-// --pallet=pallet_authority_members
-// --extrinsic=*
+// --chain
+// gdev-benchmark
 // --execution=wasm
 // --wasm-execution=compiled
-// --heap-pages=4096
-// --header=./file_header.txt
-// --output=./runtime/common/src/weights/
+// --pallet
+// pallet-authority-members
+// --extrinsic
+// *
+// --steps
+// 50
+// --repeat
+// 20
+// --output=runtime/common/src/weights/
+// --header
+// file_header.txt
 
 #![cfg_attr(rustfmt, rustfmt_skip)]
 #![allow(unused_parens)]
@@ -54,13 +59,14 @@ impl<T: frame_system::Config> pallet_authority_members::WeightInfo for WeightInf
 	// Storage: AuthorityMembers OnlineAuthorities (r:1 w:0)
 	// Storage: AuthorityMembers AuthoritiesCounter (r:1 w:1)
 	fn go_offline() -> Weight {
-		// Minimum execution time: 116_372 nanoseconds.
-		Weight::from_ref_time(120_732_000 as u64)
+		// Minimum execution time: 106_597 nanoseconds.
+		Weight::from_ref_time(109_880_000 as u64)
 			.saturating_add(T::DbWeight::get().reads(7 as u64))
 			.saturating_add(T::DbWeight::get().writes(2 as u64))
 	}
 	// Storage: Identity IdentityIndexOf (r:1 w:0)
 	// Storage: SmithMembership Membership (r:1 w:0)
+	// Storage: AuthorityMembers BlackList (r:1 w:0)
 	// Storage: AuthorityMembers Members (r:1 w:0)
 	// Storage: Session NextKeys (r:1 w:0)
 	// Storage: AuthorityMembers IncomingAuthorities (r:1 w:1)
@@ -68,9 +74,9 @@ impl<T: frame_system::Config> pallet_authority_members::WeightInfo for WeightInf
 	// Storage: AuthorityMembers OnlineAuthorities (r:1 w:0)
 	// Storage: AuthorityMembers AuthoritiesCounter (r:1 w:1)
 	fn go_online() -> Weight {
-		// Minimum execution time: 144_006 nanoseconds.
-		Weight::from_ref_time(157_859_000 as u64)
-			.saturating_add(T::DbWeight::get().reads(8 as u64))
+		// Minimum execution time: 132_009 nanoseconds.
+		Weight::from_ref_time(204_473_000 as u64)
+			.saturating_add(T::DbWeight::get().reads(9 as u64))
 			.saturating_add(T::DbWeight::get().writes(2 as u64))
 	}
 	// Storage: Identity IdentityIndexOf (r:1 w:0)
@@ -82,8 +88,8 @@ impl<T: frame_system::Config> pallet_authority_members::WeightInfo for WeightInf
 	// Storage: AuthorityMembers Members (r:1 w:1)
 	// Storage: AuthorityMembers MustRotateKeysBefore (r:1 w:1)
 	fn set_session_keys() -> Weight {
-		// Minimum execution time: 175_589 nanoseconds.
-		Weight::from_ref_time(178_397_000 as u64)
+		// Minimum execution time: 161_156 nanoseconds.
+		Weight::from_ref_time(182_210_000 as u64)
 			.saturating_add(T::DbWeight::get().reads(11 as u64))
 			.saturating_add(T::DbWeight::get().writes(3 as u64))
 	}
@@ -98,9 +104,16 @@ impl<T: frame_system::Config> pallet_authority_members::WeightInfo for WeightInf
 	// Storage: SmithMembership CounterForMembership (r:1 w:1)
 	// Storage: Session KeyOwner (r:0 w:4)
 	fn remove_member() -> Weight {
-		// Minimum execution time: 242_728 nanoseconds.
-		Weight::from_ref_time(296_778_000 as u64)
+		// Minimum execution time: 225_027 nanoseconds.
+		Weight::from_ref_time(243_550_000 as u64)
 			.saturating_add(T::DbWeight::get().reads(9 as u64))
 			.saturating_add(T::DbWeight::get().writes(13 as u64))
 	}
+	// Storage: AuthorityMembers BlackList (r:1 w:1)
+	fn remove_member_from_blacklist() -> Weight {
+		// Minimum execution time: 60_023 nanoseconds.
+		Weight::from_ref_time(60_615_000 as u64)
+			.saturating_add(T::DbWeight::get().reads(1 as u64))
+			.saturating_add(T::DbWeight::get().writes(1 as u64))
+	}
 }
diff --git a/runtime/g1/Cargo.toml b/runtime/g1/Cargo.toml
index 03f057737..93b8f3464 100644
--- a/runtime/g1/Cargo.toml
+++ b/runtime/g1/Cargo.toml
@@ -123,6 +123,7 @@ pallet-provide-randomness = { path = '../../pallets/provide-randomness', default
 pallet-universal-dividend = { path = '../../pallets/universal-dividend', default-features = false }
 pallet-upgrade-origin = { path = '../../pallets/upgrade-origin', default-features = false }
 sp-membership = { path = '../../primitives/membership', default-features = false }
+pallet-offences = { path = '../../pallets/offences', default-features = false }
 
 # crates.io
 codec = { package = "parity-scale-codec", version = "3.1.5", features = ["derive"], default-features = false }
@@ -147,7 +148,6 @@ pallet-balances = { git = 'https://github.com/duniter/substrate', branch = 'duni
 pallet-collective = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-grandpa = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-im-online = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
-pallet-offences = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-multisig = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-preimage = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-proxy = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
diff --git a/runtime/g1/src/lib.rs b/runtime/g1/src/lib.rs
index c6b533d68..fdb472726 100644
--- a/runtime/g1/src/lib.rs
+++ b/runtime/g1/src/lib.rs
@@ -259,7 +259,7 @@ construct_runtime!(
         Offences: pallet_offences::{Pallet, Storage, Event} = 12,
         Historical: session_historical::{Pallet} = 13,
         Session: pallet_session::{Pallet, Call, Storage, Event, Config<T>} = 14,
-        Grandpa: pallet_grandpa::{Pallet, Call, Storage, Config, Event} = 15,
+        Grandpa: pallet_grandpa::{Pallet, Call, Storage, Config, Event, ValidateUnsigned} = 15,
         ImOnline: pallet_im_online::{Pallet, Call, Storage, Event<T>, ValidateUnsigned, Config<T>} = 16,
         AuthorityDiscovery: pallet_authority_discovery::{Pallet, Config} = 17,
 
diff --git a/runtime/gdev/Cargo.toml b/runtime/gdev/Cargo.toml
index a30d763c4..8c8fd7788 100644
--- a/runtime/gdev/Cargo.toml
+++ b/runtime/gdev/Cargo.toml
@@ -143,6 +143,7 @@ pallet-duniter-account = { path = '../../pallets/duniter-account', default-featu
 pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false }
 pallet-identity = { path = '../../pallets/identity', default-features = false }
 pallet-membership = { path = '../../pallets/membership', default-features = false }
+pallet-offences = { path = '../../pallets/offences', default-features = false }
 pallet-oneshot-account = { path = '../../pallets/oneshot-account', default-features = false }
 pallet-provide-randomness = { path = '../../pallets/provide-randomness', default-features = false }
 pallet-universal-dividend = { path = '../../pallets/universal-dividend', default-features = false }
@@ -172,7 +173,6 @@ pallet-balances = { git = 'https://github.com/duniter/substrate', branch = 'duni
 pallet-collective = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-grandpa = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-im-online = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
-pallet-offences = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-multisig = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-preimage = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-proxy = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
diff --git a/runtime/gdev/src/lib.rs b/runtime/gdev/src/lib.rs
index 90350ab3d..770191a20 100644
--- a/runtime/gdev/src/lib.rs
+++ b/runtime/gdev/src/lib.rs
@@ -318,7 +318,7 @@ construct_runtime!(
         Offences: pallet_offences::{Pallet, Storage, Event} = 12,
         Historical: session_historical::{Pallet} = 13,
         Session: pallet_session::{Pallet, Call, Storage, Event, Config<T>} = 14,
-        Grandpa: pallet_grandpa::{Pallet, Call, Storage, Config, Event} = 15,
+        Grandpa: pallet_grandpa::{Pallet, Call, Storage, Config, Event, ValidateUnsigned} = 15,
         ImOnline: pallet_im_online::{Pallet, Call, Storage, Event<T>, ValidateUnsigned, Config<T>} = 16,
         AuthorityDiscovery: pallet_authority_discovery::{Pallet, Config} = 17,
 
diff --git a/runtime/gtest/Cargo.toml b/runtime/gtest/Cargo.toml
index a3802c838..43ff07fb3 100644
--- a/runtime/gtest/Cargo.toml
+++ b/runtime/gtest/Cargo.toml
@@ -146,6 +146,7 @@ pallet-universal-dividend = { path = '../../pallets/universal-dividend', default
 pallet-session-benchmarking = { path = '../../pallets/session-benchmarking', default-features = false }
 pallet-upgrade-origin = { path = '../../pallets/upgrade-origin', default-features = false }
 sp-membership = { path = '../../primitives/membership', default-features = false }
+pallet-offences = { path = '../../pallets/offences', default-features = false }
 
 # crates.io
 codec = { package = "parity-scale-codec", version = "3.1.5", features = ["derive"], default-features = false }
@@ -169,7 +170,6 @@ pallet-balances = { git = 'https://github.com/duniter/substrate', branch = 'duni
 pallet-collective = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-grandpa = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-im-online = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
-pallet-offences = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-multisig = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-preimage = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
 pallet-proxy = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', default-features = false }
diff --git a/runtime/gtest/src/lib.rs b/runtime/gtest/src/lib.rs
index 3b94d4add..29ec85aae 100644
--- a/runtime/gtest/src/lib.rs
+++ b/runtime/gtest/src/lib.rs
@@ -275,7 +275,7 @@ construct_runtime!(
         Offences: pallet_offences::{Pallet, Storage, Event} = 12,
         Historical: session_historical::{Pallet} = 13,
         Session: pallet_session::{Pallet, Call, Storage, Event, Config<T>} = 14,
-        Grandpa: pallet_grandpa::{Pallet, Call, Storage, Config, Event} = 15,
+        Grandpa: pallet_grandpa::{Pallet, Call, Storage, Config, Event, ValidateUnsigned} = 15,
         ImOnline: pallet_im_online::{Pallet, Call, Storage, Event<T>, ValidateUnsigned, Config<T>} = 16,
         AuthorityDiscovery: pallet_authority_discovery::{Pallet, Config} = 17,
 
-- 
GitLab