From 613d0d49ecc763f408e51254eb9b64288e21cdef Mon Sep 17 00:00:00 2001
From: bgallois <benjamin@gallois.cc>
Date: Fri, 5 May 2023 18:01:55 +0200
Subject: [PATCH] wip handle offence pallet_im_online

---
 Cargo.lock                             |  26 ++-
 pallets/authority-members/Cargo.toml   |   2 +
 pallets/authority-members/src/impls.rs |  68 ++++++
 pallets/authority-members/src/lib.rs   |  19 +-
 pallets/authority-members/src/mock.rs  |  13 ++
 pallets/authority-members/src/tests.rs | 254 +++++++++++++++++++++-
 pallets/offences/Cargo.toml            |  46 ++++
 pallets/offences/src/lib.rs            | 246 +++++++++++++++++++++
 pallets/offences/src/mock.rs           | 159 ++++++++++++++
 pallets/offences/src/tests.rs          | 286 +++++++++++++++++++++++++
 pallets/offences/src/traits.rs         |  12 ++
 runtime/common/src/pallets_config.rs   |   2 +-
 runtime/gdev/Cargo.toml                |   2 +-
 13 files changed, 1127 insertions(+), 8 deletions(-)
 create mode 100644 pallets/authority-members/src/impls.rs
 create mode 100644 pallets/offences/Cargo.toml
 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 5edc011c7..50ca8a3ff 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2502,7 +2502,7 @@ dependencies = [
  "pallet-im-online",
  "pallet-membership",
  "pallet-multisig",
- "pallet-offences",
+ "pallet-offences 4.0.0-dev (git+https://github.com/duniter/substrate?branch=duniter-substrate-v0.9.32)",
  "pallet-oneshot-account",
  "pallet-preimage",
  "pallet-provide-randomness",
@@ -2571,7 +2571,7 @@ dependencies = [
  "pallet-im-online",
  "pallet-membership",
  "pallet-multisig",
- "pallet-offences",
+ "pallet-offences 4.0.0-dev",
  "pallet-oneshot-account",
  "pallet-preimage",
  "pallet-provide-randomness",
@@ -2865,7 +2865,7 @@ dependencies = [
  "pallet-im-online",
  "pallet-membership",
  "pallet-multisig",
- "pallet-offences",
+ "pallet-offences 4.0.0-dev (git+https://github.com/duniter/substrate?branch=duniter-substrate-v0.9.32)",
  "pallet-oneshot-account",
  "pallet-preimage",
  "pallet-provide-randomness",
@@ -5031,6 +5031,7 @@ dependencies = [
  "frame-system",
  "log",
  "maplit",
+ "pallet-offences 4.0.0-dev",
  "pallet-session",
  "parity-scale-codec",
  "scale-info",
@@ -5296,7 +5297,6 @@ dependencies = [
 [[package]]
 name = "pallet-offences"
 version = "4.0.0-dev"
-source = "git+https://github.com/duniter/substrate?branch=duniter-substrate-v0.9.32#7f8b8db65b441ce1d1b2ffb26ebde314b54e117c"
 dependencies = [
  "frame-support",
  "frame-system",
@@ -5305,6 +5305,24 @@ dependencies = [
  "parity-scale-codec",
  "scale-info",
  "serde",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-staking",
+ "sp-std",
+]
+
+[[package]]
+name = "pallet-offences"
+version = "4.0.0-dev"
+source = "git+https://github.com/duniter/substrate?branch=duniter-substrate-v0.9.32#7f8b8db65b441ce1d1b2ffb26ebde314b54e117c"
+dependencies = [
+ "frame-support",
+ "frame-system",
+ "log",
+ "pallet-balances",
+ "parity-scale-codec",
+ "scale-info",
  "sp-runtime",
  "sp-staking",
  "sp-std",
diff --git a/pallets/authority-members/Cargo.toml b/pallets/authority-members/Cargo.toml
index 485f2b203..94040f67c 100644
--- a/pallets/authority-members/Cargo.toml
+++ b/pallets/authority-members/Cargo.toml
@@ -19,6 +19,7 @@ std = [
     'frame-benchmarking/std',
     'log/std',
     'pallet-session/std',
+    'pallet-offences/std',
     'serde',
     'sp-core/std',
     'sp-membership/std',
@@ -30,6 +31,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/impls.rs b/pallets/authority-members/src/impls.rs
new file mode 100644
index 000000000..6d0cccb54
--- /dev/null
+++ b/pallets/authority-members/src/impls.rs
@@ -0,0 +1,68 @@
+// Copyright 2021 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)]
+#![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;
+
+// This is intended to be used with `FilterHistoricalOffences`.
+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 {
+        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.clone());
+                            }
+                            Self::insert_out(member_id);
+                        }
+                    })
+                }
+            }
+            SlashStrategy::Disconnect => {
+                for offender in offenders {
+                    if let Some(member_id) = T::MemberIdOf::convert(offender.offender.0.clone()) {
+                        Self::insert_out(member_id);
+                    }
+                }
+            }
+        }
+        T::DbWeight::get().reads(3) // TODO
+    }
+}
diff --git a/pallets/authority-members/src/lib.rs b/pallets/authority-members/src/lib.rs
index 21b23b64d..66c18493e 100644
--- a/pallets/authority-members/src/lib.rs
+++ b/pallets/authority-members/src/lib.rs
@@ -29,6 +29,9 @@ mod tests;
 /*#[cfg(feature = "runtime-benchmarks")]
 mod benchmarking;*/
 
+mod impls;
+pub use impls::*;
+
 pub use pallet::*;
 pub use types::*;
 
@@ -172,6 +175,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 //
@@ -210,6 +218,8 @@ pub mod pallet {
         /// Not found owner key
         MemberIdNotFound,
         /// Member not found
+        MemberIdBlackListed,
+        /// Member is blacklisted
         MemberNotFound,
         /// Neither online nor scheduled
         NotOnlineNorIncoming,
@@ -261,6 +271,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());
             }
@@ -449,7 +462,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);
@@ -486,6 +499,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 d28782a4a..46d8c2577 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>;
@@ -204,3 +207,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..9055f733a 100644
--- a/pallets/authority-members/src/tests.rs
+++ b/pallets/authority-members/src/tests.rs
@@ -17,8 +17,9 @@
 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 sp_runtime::testing::UintAuthorityId;
+use sp_staking::offence::OffenceDetails;
 
 const EMPTY: Vec<u64> = Vec::new();
 
@@ -178,6 +179,257 @@ fn test_go_offline() {
     });
 }
 
+#[test]
+fn test_go_offline_at_offence_with_black_list() {
+    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,
+            })
+        );
+
+        // Member 9 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::members_expire_on(4), vec![9],);
+        assert_eq!(Session::current_index(), 1);
+        assert_eq!(Session::validators(), vec![3, 6, 9]);
+        assert_eq!(Session::queued_keys().len(), 2);
+        assert_eq!(Session::queued_keys()[0].0, 3);
+        assert_eq!(Session::queued_keys()[1].0, 6);
+
+        // 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]);
+
+        // 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);
+    });
+}
+
+#[test]
+fn test_black_list_ok() {
+    new_test_ext(3).execute_with(|| {
+        run_to_block(1);
+
+        on_offence(
+            &[OffenceDetails {
+                offender: (9, ()),
+                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![9]);
+        assert_eq!(AuthorityMembers::blacklist(), EMPTY);
+        assert_eq!(
+            AuthorityMembers::member(9),
+            Some(MemberData {
+                expire_on_session: 0,
+                must_rotate_keys_before: 5,
+                owner_key: 9,
+            })
+        );
+
+        // Member 9 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::members_expire_on(4), vec![9],);
+        assert_eq!(Session::current_index(), 1);
+        assert_eq!(Session::validators(), vec![3, 6, 9]);
+        assert_eq!(Session::queued_keys().len(), 2);
+        assert_eq!(Session::queued_keys()[0].0, 3);
+        assert_eq!(Session::queued_keys()[1].0, 6);
+
+        // 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]);
+
+        // 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);
+
+        // Member 9 should be allowed to go online
+        run_to_block(25);
+        assert_ok!(AuthorityMembers::set_session_keys(
+            RuntimeOrigin::signed(9),
+            UintAuthorityId(9).into(),
+        ));
+        assert_ok!(AuthorityMembers::go_online(RuntimeOrigin::signed(9)),);
+    });
+}
+
+#[test]
+fn test_black_list_err() {
+    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,
+            })
+        );
+
+        // Member 9 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::members_expire_on(4), vec![9],);
+        assert_eq!(Session::current_index(), 1);
+        assert_eq!(Session::validators(), vec![3, 6, 9]);
+        assert_eq!(Session::queued_keys().len(), 2);
+        assert_eq!(Session::queued_keys()[0].0, 3);
+        assert_eq!(Session::queued_keys()[1].0, 6);
+
+        // 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]);
+
+        // 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);
+
+        // 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
+        );
+    });
+}
+
+#[test]
+fn test_go_offline_at_offence_no_black_list() {
+    new_test_ext(3).execute_with(|| {
+        run_to_block(1);
+
+        on_offence(
+            &[OffenceDetails {
+                offender: (9, ()),
+                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![9]);
+        assert_eq!(AuthorityMembers::blacklist(), EMPTY);
+        assert_eq!(
+            AuthorityMembers::member(9),
+            Some(MemberData {
+                expire_on_session: 0,
+                must_rotate_keys_before: 5,
+                owner_key: 9,
+            })
+        );
+
+        // Member 9 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::members_expire_on(4), vec![9],);
+        assert_eq!(Session::current_index(), 1);
+        assert_eq!(Session::validators(), vec![3, 6, 9]);
+        assert_eq!(Session::queued_keys().len(), 2);
+        assert_eq!(Session::queued_keys()[0].0, 3);
+        assert_eq!(Session::queued_keys()[1].0, 6);
+
+        // 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]);
+
+        // 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);
+    });
+}
+
 #[test]
 fn test_go_online() {
     new_test_ext(3).execute_with(|| {
diff --git a/pallets/offences/Cargo.toml b/pallets/offences/Cargo.toml
new file mode 100644
index 000000000..60262da7f
--- /dev/null
+++ b/pallets/offences/Cargo.toml
@@ -0,0 +1,46 @@
+[package]
+name = "pallet-offences"
+version = "4.0.0-dev"
+authors = ["Parity Technologies <admin@parity.io>"]
+edition = "2021"
+license = "Apache-2.0"
+homepage = "https://substrate.io"
+repository = "https://github.com/paritytech/substrate/"
+description = "FRAME offences pallet"
+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/src/lib.rs b/pallets/offences/src/lib.rs
new file mode 100644
index 000000000..207acbd6c
--- /dev/null
+++ b/pallets/offences/src/lib.rs
@@ -0,0 +1,246 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+
+#[cfg(test)]
+mod mock;
+
+#[cfg(test)]
+mod tests;
+
+/*#[cfg(feature = "runtime-benchmarks")]
+mod benchmarking;*/
+
+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..d461e840c
--- /dev/null
+++ b/pallets/offences/src/mock.rs
@@ -0,0 +1,159 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Test utilities
+
+#![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..c7da5a768
--- /dev/null
+++ b/pallets/offences/src/tests.rs
@@ -0,0 +1,286 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Tests for the offences module.
+
+#![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
+    });
+}
+
+#[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
+    });
+}
+
+#[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..c7b4bc3d9
--- /dev/null
+++ b/pallets/offences/src/traits.rs
@@ -0,0 +1,12 @@
+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 892cb3723..d98d20537 100644
--- a/runtime/common/src/pallets_config.rs
+++ b/runtime/common/src/pallets_config.rs
@@ -227,7 +227,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;
diff --git a/runtime/gdev/Cargo.toml b/runtime/gdev/Cargo.toml
index 643fc4b03..da3f43577 100644
--- a/runtime/gdev/Cargo.toml
+++ b/runtime/gdev/Cargo.toml
@@ -142,6 +142,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 }
@@ -170,7 +171,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 }
-- 
GitLab