Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Bound pallet-offences #11585

Closed
wants to merge 17 commits into from
Next Next commit
Bound pallet-offences
Signed-off-by: Oliver Tale-Yazdi <[email protected]>
ggwpez committed Jun 2, 2022
commit 9d27600261db36b4a526bdd15e3bde0c1306ba80
2 changes: 1 addition & 1 deletion frame/offences/benchmarking/src/lib.rs
Original file line number Diff line number Diff line change
@@ -270,7 +270,7 @@ fn check_events<T: Config, I: Iterator<Item = <T as SystemConfig>::Event>>(expec

benchmarks! {
report_offence_im_online {
let r in 1 .. MAX_REPORTERS;
let r in 1 .. T::MaxReportersPerOffence;
// we skip 1 offender, because in such case there is no slashing
let o in 2 .. MAX_OFFENDERS;
let n in 0 .. MAX_NOMINATORS.min(<T as pallet_staking::Config>::MaxNominations::get());
12 changes: 12 additions & 0 deletions frame/offences/benchmarking/src/mock.rs
Original file line number Diff line number Diff line change
@@ -201,6 +201,18 @@ impl pallet_offences::Config for Test {
type Event = Event;
type IdentificationTuple = pallet_session::historical::IdentificationTuple<Self>;
type OnOffenceHandler = Staking;

type MaxReports = ConstU32<100>;
type MaxReportersPerOffence = ConstU32<MAX_REPORTERS>;

type MaxConcurrentReports = Self::MaxReports;
type MaxConcurrentReportsPerKindAndTime = Self::MaxReports;

type MaxSameKindReports = Self::MaxReports;
type MaxSameKindReportsPerKind = Self::MaxReports;

type MaxSameKindReportsEncodedLen = ConstU32<1_000>; // Guessed...
type MaxOpaqueTimeSlotLen = ConstU32<1_000>;
}

impl<T> frame_system::offchain::SendTransactionTypes<T> for Test
208 changes: 158 additions & 50 deletions frame/offences/src/lib.rs
Original file line number Diff line number Diff line change
@@ -26,8 +26,8 @@ pub mod migration;
mod mock;
mod tests;

use codec::{Decode, Encode};
use frame_support::weights::Weight;
use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::{weights::Weight, BoundedVec};
use sp_runtime::{traits::Hash, Perbill};
use sp_staking::{
offence::{Kind, Offence, OffenceDetails, OffenceError, OnOffenceHandler, ReportOffence},
@@ -38,10 +38,22 @@ use sp_std::prelude::*;
pub use pallet::*;

/// A binary blob which represents a SCALE codec-encoded `O::TimeSlot`.
type OpaqueTimeSlot = Vec<u8>;
///
/// Needs to be bounded since it is included in events.
type OpaqueTimeSlotOf<T> = BoundedVec<u8, <T as Config>::MaxOpaqueTimeSlotLen>;

/// A type alias for a report identifier.
type ReportIdOf<T> = <T as frame_system::Config>::Hash;
// NOTE: This type should rather reside in sp-primitives/staking but
// cannot since `BoundedVec` is defined in `frame_support`.
type ReportersOf<T> =
BoundedVec<<T as frame_system::Config>::AccountId, <T as Config>::MaxReportersPerOffence>;

type SameKindReportsOf<T, TimeSlot> =
BoundedVec<(TimeSlot, ReportIdOf<T>), <T as Config>::MaxSameKindReportsPerKind>;

type ConcurrentReportsOf<T> =
BoundedVec<ReportIdOf<T>, <T as Config>::MaxConcurrentReportsPerKindAndTime>;

#[frame_support::pallet]
pub mod pallet {
@@ -50,40 +62,87 @@ pub mod pallet {

#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
#[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 Event: From<Event> + IsType<<Self as frame_system::Config>::Event>;
type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;

/// Full identification of the validator.
type IdentificationTuple: Parameter;
type IdentificationTuple: Parameter + MaxEncodedLen;
/// A handler called for every offence report.
type OnOffenceHandler: OnOffenceHandler<Self::AccountId, Self::IdentificationTuple, Weight>;
type OnOffenceHandler: OnOffenceHandler<
ReportersOf<Self>,
Self::IdentificationTuple,
Weight,
>;

// Maximum number of reports in total.
#[pallet::constant]
type MaxReports: Get<Option<u32>>;

/// The maximum number of reporters per offence.
#[pallet::constant]
type MaxReportersPerOffence: Get<u32>;

/// Maximum number of different kinds and different time slots reports that can be tracked.
///
/// Can be trivially set to `MaxReports`.
#[pallet::constant]
type MaxConcurrentReports: Get<Option<u32>>;

/// Maximum number of reports of the same kind that happened at a specific time slot.
///
/// Can be trivially set to `MaxReports`.
#[pallet::constant]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do all of these really need to be in metadata? harmless, but I wonder if you had thought about each.

type MaxConcurrentReportsPerKindAndTime: Get<u32>;

/// The maximum number of reports for the same kind.
///
/// Can be trivially set to `MaxReports`.
#[pallet::constant]
type MaxSameKindReports: Get<Option<u32>>;

/// The maximum number of reports for the same kind.
///
/// Can be trivially set to `MaxReports`.
#[pallet::constant]
type MaxSameKindReportsPerKind: Get<u32>;

/// Maximum encoded length of same-kind reports `SameKindReportsOf<T>`.
///
/// Should be AT LEAST `MaxSameKindReports` * `(TimeSlot,
/// ReportIdOf<T>)::max_encoded_len()`.
#[pallet::constant]
type MaxSameKindReportsEncodedLen: Get<u32>;

/// The maximum encoded length of an offence `TimeSlot`.
#[pallet::constant]
type MaxOpaqueTimeSlotLen: Get<u32>;
}

/// 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>,
Hasher = Twox64Concat,
Key = ReportIdOf<T>,
Value = OffenceDetails<ReportersOf<T>, T::IdentificationTuple>,
MaxValues = T::MaxReports,
>;

/// 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,
Hasher1 = Twox64Concat,
Key1 = Kind,
Hasher2 = Twox64Concat,
Key2 = OpaqueTimeSlotOf<T>,
Value = ConcurrentReportsOf<T>,
QueryKind = ValueQuery,
MaxValues = T::MaxConcurrentReports,
>;

/// Enumerates all reports of a kind along with the time they happened.
@@ -93,39 +152,51 @@ pub mod pallet {
/// Note that the actual type of this mapping is `Vec<u8>`, this is because values of
/// different types are not supported at the moment so we are doing the manual serialization.
#[pallet::storage]
pub type ReportsByKindIndex<T> = StorageMap<
_,
Twox64Concat,
Kind,
Vec<u8>, // (O::TimeSlot, ReportIdOf<T>)
ValueQuery,
pub type ReportsByKindIndex<T: Config> = StorageMap<
Hasher = Twox64Concat,
Key = Kind,
Value = BoundedVec<u8, T::MaxSameKindReportsEncodedLen>, // (O::TimeSlot, ReportIdOf<T>)
QueryKind = ValueQuery,
MaxValues = T::MaxSameKindReports,
>;

/// Events type.
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event {
pub enum Event<T: Config> {
/// 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 },
Offence { kind: Kind, timeslot: OpaqueTimeSlotOf<T> },
}

#[pallet::error]
#[derive(PartialEq, Eq)]
pub enum Error<T> {
TooManyConcurrentReports,
TooManySameKindReports,

/// The length of an encoded time slot is too long.
TimeSlotEncodingTooLong,
/// The length of the encoded same kind reports is too long.
SameKindReportsEncodingTooLong,
}
}

impl<T: Config, O: Offence<T::IdentificationTuple>>
ReportOffence<T::AccountId, T::IdentificationTuple, O> for Pallet<T>
ReportOffence<ReportersOf<T>, T::IdentificationTuple, O> for Pallet<T>
where
T::IdentificationTuple: Clone,
{
fn report_offence(reporters: Vec<T::AccountId>, offence: O) -> Result<(), OffenceError> {
fn report_offence(reporters: ReportersOf<T>, offence: O) -> Result<(), OffenceError> {
let offenders = offence.offenders();
let time_slot = offence.time_slot();
let validator_set_count = offence.validator_set_count();

// 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) {
match Self::triage_offence_report::<O>(reporters, &time_slot, offenders)? {
Some(triage) => triage,
// The report contained only duplicates, so there is no need to slash again.
None => return Err(OffenceError::DuplicateReport),
@@ -146,7 +217,13 @@ where
);

// Deposit the event.
Self::deposit_event(Event::Offence { kind: O::ID, timeslot: time_slot.encode() });
Self::deposit_event(Event::Offence {
kind: O::ID,
timeslot: time_slot
.encode()
.try_into()
.map_err(|_| Error::<T>::TimeSlotEncodingTooLong)?,
});

Ok(())
}
@@ -161,6 +238,20 @@ where
}
}

impl<T: Config> From<Error<T>> for OffenceError {
fn from(err: Error<T>) -> OffenceError {
let index = match err {
Error::TooManyConcurrentReports => 0,
Error::TooManySameKindReports => 1,
Error::TimeSlotEncodingTooLong => 2,
Error::SameKindReportsEncodingTooLong => 3,
// This case is unreachable but don't panic, just in case.
Error::__Ignore(_, _) => 255,
};
OffenceError::Other(index)
}
}

impl<T: Config> Pallet<T> {
/// Compute the ID for the given report properties.
///
@@ -175,11 +266,11 @@ impl<T: Config> Pallet<T> {
/// 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>,
reporters: ReportersOf<T>,
time_slot: &O::TimeSlot,
offenders: Vec<T::IdentificationTuple>,
) -> Option<TriageOutcome<T>> {
let mut storage = ReportIndexStorage::<T, O>::load(time_slot);
) -> Result<Option<TriageOutcome<T>>, Error<T>> {
let mut storage = ReportIndexStorage::<T, O>::load(time_slot)?;

let mut any_new = false;
for offender in offenders {
@@ -192,7 +283,7 @@ impl<T: Config> Pallet<T> {
OffenceDetails { offender, reporters: reporters.clone() },
);

storage.insert(time_slot, report_id);
storage.insert(time_slot, report_id)?;
}
}

@@ -204,65 +295,82 @@ impl<T: Config> Pallet<T> {
.filter_map(<Reports<T>>::get)
.collect::<Vec<_>>();

storage.save();
storage.save()?;

Some(TriageOutcome { concurrent_offenders })
Ok(Some(TriageOutcome { concurrent_offenders }))
} else {
None
Ok(None)
}
}
}

struct TriageOutcome<T: Config> {
/// Other reports for the same report kinds.
concurrent_offenders: Vec<OffenceDetails<T::AccountId, T::IdentificationTuple>>,
concurrent_offenders: Vec<OffenceDetails<ReportersOf<T>, 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.
///
/// The vectors are bounded for easier loading/saving into storage.
#[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>>,
same_kind_reports: Vec<(O::TimeSlot, ReportIdOf<T>)>,
opaque_time_slot: OpaqueTimeSlotOf<T>,
concurrent_reports: ConcurrentReportsOf<T>,
same_kind_reports: SameKindReportsOf<T, O::TimeSlot>,
}

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();
fn load(time_slot: &O::TimeSlot) -> Result<Self, Error<T>> {
let opaque_time_slot: OpaqueTimeSlotOf<T> =
time_slot.encode().try_into().map_err(|_| Error::<T>::TimeSlotEncodingTooLong)?;

let same_kind_reports = ReportsByKindIndex::<T>::get(&O::ID);
let same_kind_reports =
Vec::<(O::TimeSlot, ReportIdOf<T>)>::decode(&mut &same_kind_reports[..])
SameKindReportsOf::<T, O::TimeSlot>::decode(&mut &same_kind_reports[..])
.unwrap_or_default();

let concurrent_reports = <ConcurrentReportsIndex<T>>::get(&O::ID, &opaque_time_slot);

Self { opaque_time_slot, concurrent_reports, same_kind_reports }
Ok(Self { opaque_time_slot, concurrent_reports, same_kind_reports })
}

/// Insert a new report to the index.
fn insert(&mut self, time_slot: &O::TimeSlot, report_id: ReportIdOf<T>) {
fn insert(
&mut self,
time_slot: &O::TimeSlot,
report_id: ReportIdOf<T>,
) -> Result<(), Error<T>> {
// Insert the report id into the list while maintaining the ordering by the time
// slot.
let pos = self.same_kind_reports.partition_point(|&(ref when, _)| when <= time_slot);
self.same_kind_reports.insert(pos, (time_slot.clone(), report_id));
self.same_kind_reports
.try_insert(pos, (time_slot.clone(), report_id))
.map_err(|_| crate::Error::<T>::TooManySameKindReports)?;

// Update the list of concurrent reports.
self.concurrent_reports.push(report_id);
self.concurrent_reports
.try_push(report_id)
.map_err(|_| crate::Error::<T>::TooManyConcurrentReports)
}

/// Dump the indexes to the storage.
fn save(self) {
ReportsByKindIndex::<T>::insert(&O::ID, self.same_kind_reports.encode());
fn save(self) -> Result<(), Error<T>> {
let encoded: BoundedVec<u8, _> = self
.same_kind_reports
.encode()
.try_into()
.map_err(|_| crate::Error::<T>::SameKindReportsEncodingTooLong)?;
ReportsByKindIndex::<T>::insert(&O::ID, encoded);
<ConcurrentReportsIndex<T>>::insert(
&O::ID,
&self.opaque_time_slot,
&self.concurrent_reports,
);
Ok(())
}
}
Loading