Skip to content

Commit

Permalink
Validator offence and slashing (#313)
Browse files Browse the repository at this point in the history
  • Loading branch information
salman01zp authored Nov 30, 2023
1 parent 9103eff commit 58b1dfe
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 70 deletions.
8 changes: 4 additions & 4 deletions pallets/jobs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use sp_runtime::{
};
use sp_std::{prelude::*, vec::Vec};
use tangle_primitives::{
jobs::{DKGResult, JobId, JobInfo, JobKey, JobResult, PhaseOneResult, ValidatorOffence},
jobs::{DKGResult, JobId, JobInfo, JobKey, JobResult, PhaseOneResult, ValidatorOffenceType},
traits::{
jobs::{JobToFee, MPCHandler},
roles::RolesHandler,
Expand Down Expand Up @@ -506,7 +506,7 @@ pub mod module {
job_key: JobKey,
job_id: JobId,
validator: T::AccountId,
offence: ValidatorOffence,
offence: ValidatorOffenceType,
signatures: Vec<Vec<u8>>,
) -> DispatchResult {
let _caller = ensure_signed(origin)?;
Expand Down Expand Up @@ -535,8 +535,8 @@ pub mod module {
// Validate the result
T::MPCHandler::verify_validator_report(validator.clone(), offence.clone(), signatures)?;

// Slash the validator
T::RolesHandler::slash_validator(validator.clone(), offence)?;
// TODO: Report validator offence.
// T::RolesHandler::report_offence(validator.clone(), offence)?;

// Trigger validator removal
Self::try_validator_removal_from_job(job_key, job_id, validator)?;
Expand Down
4 changes: 2 additions & 2 deletions pallets/jobs/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ impl RolesHandler<AccountId> for MockRolesHandler {
validators.contains(&address)
}

fn slash_validator(_address: AccountId, _offence: ValidatorOffence) -> DispatchResult {
fn report_offence(_offence_report: ReportValidatorOffence<AccountId>) -> DispatchResult {
Ok(())
}

Expand All @@ -150,7 +150,7 @@ impl MPCHandler<AccountId, BlockNumber, Balance> for MockMPCHandler {

fn verify_validator_report(
_validator: AccountId,
_offence: ValidatorOffence,
_offence: ValidatorOffenceType,
_signatures: Vec<Vec<u8>>,
) -> DispatchResult {
Ok(())
Expand Down
131 changes: 94 additions & 37 deletions pallets/roles/src/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,24 @@
// You should have received a copy of the GNU General Public License
// along with Tangle. If not, see <http://www.gnu.org/licenses/>.

use crate::offences::ValidatorOffence;

use super::*;
use frame_support::{
log,
pallet_prelude::DispatchResult,
traits::{Currency, OneSessionHandler},
};
use sp_runtime::{traits::CheckedDiv, Percent, Saturating};
use sp_runtime::{
traits::{CheckedDiv, Convert},
Perbill, Percent,
};

use tangle_primitives::{jobs::JobKey, traits::roles::RolesHandler};
use sp_staking::offence::Offence;
use tangle_primitives::{
jobs::{JobKey, ReportValidatorOffence},
traits::roles::RolesHandler,
};

/// Implements RolesHandler for the pallet.
impl<T: Config> RolesHandler<T::AccountId> for Pallet<T> {
Expand All @@ -40,23 +49,19 @@ impl<T: Config> RolesHandler<T::AccountId> for Pallet<T> {
assigned_roles.contains(&job_role)
}

/// Slash validator stake for the reported offence. The function should be a best effort
/// slashing, slash upto max possible by the offence type.
/// Report offence for the given validator.
/// This function will report validators for committing offence.
///
/// # Parameters
/// - `address`: The account ID of the validator.
/// - `offence`: The offence reported against the validator
/// - `offence_report`: The offence report.
///
/// # Returns
/// DispatchResult emitting `Slashed` event if validator is slashed
fn slash_validator(
address: T::AccountId,
_offence: tangle_primitives::jobs::ValidatorOffence,
///
/// Returns Ok() if validator offence report is submitted successfully.
fn report_offence(
offence_report: ReportValidatorOffence<T::AccountId>,
) -> sp_runtime::DispatchResult {
// TODO: implement calculation of slash amount.
let slash_amount = 1000u64;
Self::do_slash(address, slash_amount.into())?;
Ok(())
Self::report_offence(offence_report)
}

fn get_validator_metadata(address: T::AccountId, job_key: JobKey) -> Option<RoleTypeMetadata> {
Expand All @@ -83,7 +88,7 @@ impl<T: Config> Pallet<T> {
/// # Parameters
/// - `account`: The account ID of the validator.
/// - `role`: Selected role type.
pub fn add_role(account: T::AccountId, role: RoleType) -> DispatchResult {
pub(crate) fn add_role(account: T::AccountId, role: RoleType) -> DispatchResult {
AccountRolesMapping::<T>::try_mutate(&account, |roles| {
if !roles.contains(&role) {
roles.try_push(role.clone()).map_err(|_| Error::<T>::MaxRoles)?;
Expand All @@ -100,7 +105,7 @@ impl<T: Config> Pallet<T> {
/// # Parameters
/// - `account`: The account ID of the validator.
/// - `role`: Selected role type.
pub fn remove_role(account: T::AccountId, role: RoleType) -> DispatchResult {
pub(crate) fn remove_role(account: T::AccountId, role: RoleType) -> DispatchResult {
AccountRolesMapping::<T>::try_mutate(&account, |roles| {
if roles.contains(&role) {
roles.retain(|r| r != &role);
Expand All @@ -120,7 +125,7 @@ impl<T: Config> Pallet<T> {
///
/// # Returns
/// Returns `true` if the validator is permitted to work with this job type, otherwise `false`.
pub fn has_role(account: T::AccountId, role: RoleType) -> bool {
pub(crate) fn has_role(account: T::AccountId, role: RoleType) -> bool {
let assigned_roles = AccountRolesMapping::<T>::get(account);
match assigned_roles.iter().find(|r| **r == role) {
Some(_) => true,
Expand All @@ -135,7 +140,7 @@ impl<T: Config> Pallet<T> {
///
/// # Returns
/// Returns boolean value.
pub fn can_exit(account: T::AccountId) -> bool {
pub(crate) fn can_exit(account: T::AccountId) -> bool {
let assigned_roles = AccountRolesMapping::<T>::get(account);
if assigned_roles.is_empty() {
// Role is cleared, account can chill, unbound and withdraw funds.
Expand All @@ -151,37 +156,89 @@ impl<T: Config> Pallet<T> {
///
/// # Returns
/// Returns the max re-stake amount.
pub fn calculate_max_re_stake_amount(total_stake: BalanceOf<T>) -> BalanceOf<T> {
pub(crate) fn calculate_max_re_stake_amount(total_stake: BalanceOf<T>) -> BalanceOf<T> {
// User can re-stake max 50% of the total stake
Percent::from_percent(50) * total_stake
}

/// Get the total amount of the balance that is locked for the given stash.
/// Calculate slash value for re-staked amount
///
/// # Parameters
/// - `stash`: The stash account ID.
/// - slash_fraction: Slash fraction of total-stake
/// - `total_stake`: Total stake of the validator
///
/// # Returns
/// The total amount of the balance that can be slashed.
pub fn slashable_balance_of(stash: &T::AccountId) -> BalanceOf<T> {
// Weight note: consider making the stake accessible through stash.
Self::ledger(&stash).map(|l| l.total).unwrap_or_default()
/// Returns the slash value
pub(crate) fn calculate_re_stake_slash_value(
slash_fraction: Perbill,
total_stake: BalanceOf<T>,
) -> BalanceOf<T> {
// Slash value for given slash fraction
slash_fraction * total_stake
}

/// Apply slash on re-staked amount.
/// This function will apply slash on re-staked amount and update ledger.
///
/// # Parameters
/// - `account`: The account ID of the validator.
/// - `slash_value`: Slash value.
pub(crate) fn apply_slash_on_re_stake(account: T::AccountId, slash_value: BalanceOf<T>) {
// Get ledger for the validator account
let mut ledger = Ledger::<T>::get(&account)
.unwrap_or(RoleStakingLedger::<T>::default_from(account.clone()));

// apply slash
ledger.total = ledger.total.saturating_sub(slash_value);
println!("ledger.total: {:?}", ledger.total);
// Update ledger
Self::update_ledger(&account, &ledger);
}

/// Slash the given amount from the stash account.
/// Report offence for the given validator.
/// This function will report validators for committing offence.
///
/// # Parameters
/// - `address`: The stash account ID.
/// - `slash_amount`: The amount to be slashed.
pub(crate) fn do_slash(
address: T::AccountId,
slash_amount: T::CurrencyBalance,
/// - `offence_report`: The offence report.
///
/// # Returns
///
/// Returns Ok() if validator offence report is submitted successfully.
pub(crate) fn report_offence(
offence_report: ReportValidatorOffence<T::AccountId>,
) -> sp_runtime::DispatchResult {
let mut ledger = Self::ledger(&address).ok_or(Error::<T>::AccountNotPaired)?;
let (_imbalance, _missing) = T::Currency::slash(&address, slash_amount.into());
ledger.total = ledger.total.saturating_sub(slash_amount.into());
Self::update_ledger(&address, &ledger);
Self::deposit_event(Event::Slashed { account: address, amount: slash_amount });
let offenders = offence_report
.clone()
.offenders
.into_iter()
.enumerate()
.filter_map(|(_, id)| {
// Get validator id from account id.
let id = <<T as Config>::ValidatorSet as ValidatorSet<
<T as frame_system::Config>::AccountId,
>>::ValidatorIdOf::convert(id)?;
<T::ValidatorSet as ValidatorSetWithIdentification<T::AccountId>>::IdentificationOf::convert(
id.clone(),
).map(|full_id| (id, full_id))
})
.collect::<Vec<IdentificationTuple<T>>>();

let offence = ValidatorOffence {
session_index: offence_report.session_index,
validator_set_count: offence_report.validator_set_count,
offenders,
offence_type: offence_report.offence_type,
};
let _ = T::ReportOffences::report_offence(sp_std::vec![], offence.clone());
// Update and apply slash on ledger for all offenders
let slash_fraction = offence.slash_fraction(offence.validator_set_count);
for offender in offence_report.offenders {
let staking_ledger =
pallet_staking::Ledger::<T>::get(&offender).ok_or(Error::<T>::NotValidator)?;
let re_stake_slash_value =
Self::calculate_re_stake_slash_value(slash_fraction, staking_ledger.total);
Self::apply_slash_on_re_stake(offender, re_stake_slash_value);
}

Ok(())
}

Expand Down
30 changes: 29 additions & 1 deletion pallets/roles/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
use codec::MaxEncodedLen;
use frame_support::{
ensure,
traits::{Currency, Get},
traits::{Currency, Get, ValidatorSet, ValidatorSetWithIdentification},
CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
};
use tangle_primitives::roles::ValidatorRewardDistribution;
Expand All @@ -30,6 +30,7 @@ pub use pallet::*;
use parity_scale_codec::{Decode, Encode};
use scale_info::TypeInfo;
use sp_runtime::{codec, traits::Zero, Saturating};
use sp_staking::offence::ReportOffence;
use sp_std::{convert::TryInto, prelude::*, vec};
use tangle_primitives::{
roles::{RoleType, RoleTypeMetadata},
Expand All @@ -39,6 +40,7 @@ mod impls;
use sp_std::collections::btree_map::BTreeMap;
#[cfg(test)]
pub(crate) mod mock;
mod offences;
#[cfg(test)]
mod tests;
mod weights;
Expand Down Expand Up @@ -98,6 +100,8 @@ pub type BalanceOf<T> =

#[frame_support::pallet]
pub mod pallet {
use crate::offences::ValidatorOffence;

use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
Expand All @@ -106,6 +110,20 @@ pub mod pallet {
traits::jobs::MPCHandler,
};

/// A type for representing the validator id in a session.
pub type ValidatorId<T> = <<T as Config>::ValidatorSet as ValidatorSet<
<T as frame_system::Config>::AccountId,
>>::ValidatorId;

/// A tuple of (ValidatorId, Identification) where `Identification` is the full identification
/// of `ValidatorId`.
pub type IdentificationTuple<T> = (
ValidatorId<T>,
<<T as Config>::ValidatorSet as ValidatorSetWithIdentification<
<T as frame_system::Config>::AccountId,
>>::Identification,
);

#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
Expand Down Expand Up @@ -135,6 +153,16 @@ pub mod pallet {
/// The type used to identify an authority
type AuthorityId: RuntimeAppPublic + Decode;

/// A type for retrieving the validators supposed to be online in a session.
type ValidatorSet: ValidatorSetWithIdentification<Self::AccountId>;

/// A type to submit offence reports against the validators.
type ReportOffences: ReportOffence<
Self::AccountId,
IdentificationTuple<Self>,
ValidatorOffence<IdentificationTuple<Self>>,
>;

type WeightInfo: WeightInfo;
}

Expand Down
Loading

0 comments on commit 58b1dfe

Please sign in to comment.