From b55ed721b377b40bb85904174a99343e34218dd9 Mon Sep 17 00:00:00 2001 From: MikhailK Date: Sun, 8 Jan 2023 17:40:58 +0300 Subject: [PATCH] Unlock rewards to slash offline validators --- Cargo.lock | 3 + pallets/rewards/src/lib.rs | 57 ++++++++++ pallets/validator-set/Cargo.toml | 3 + pallets/validator-set/src/lib.rs | 186 ++++++++++++++++++++++--------- runtime/src/lib.rs | 4 +- traits/rewards/src/lib.rs | 9 +- 6 files changed, 197 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1fcd76b..12ba5a11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4236,17 +4236,20 @@ dependencies = [ "frame-support", "frame-system", "log", + "pallet-balances", "pallet-session", "pallet-treasury", "parity-scale-codec", "rewards-api", "scale-info", "serde", + "sp-consensus-poscan", "sp-core", "sp-io", "sp-runtime", "sp-staking", "sp-std", + "sp-version", "validator-set-api", ] diff --git a/pallets/rewards/src/lib.rs b/pallets/rewards/src/lib.rs index 43dc46ad..d0df153a 100644 --- a/pallets/rewards/src/lib.rs +++ b/pallets/rewards/src/lib.rs @@ -481,6 +481,59 @@ impl Module { ::RewardLocks::insert(author, locks); } + pub fn unlock_upto(author: &T::AccountId, amount: BalanceOf) { + let mut locks = Self::reward_locks(&author); + let locked_amount = Self::total_locked(&author); + let mut total_unlock: BalanceOf = Zero::zero(); + let mut to_unlock = Vec::new(); + let unlock_amount = locked_amount - amount; + + let d = u128::from_le_bytes(locked_amount.encode().try_into().unwrap()); + log::debug!(target: LOG_TARGET, "locked_amount: {}", d); + let d = u128::from_le_bytes(amount.encode().try_into().unwrap()); + log::debug!(target: LOG_TARGET, "amount: {}", d); + + for (block_number, locked_balance) in locks.iter() { + if total_unlock.saturating_add(*locked_balance) >= unlock_amount { + let adj = total_unlock.saturating_add(*locked_balance) - unlock_amount; + total_unlock = total_unlock.saturating_add(adj); + if adj == Zero::zero() { + to_unlock.push(*block_number); + } + break + } + to_unlock.push(*block_number); + total_unlock = total_unlock.saturating_add(*locked_balance); + } + let d = u128::from_le_bytes(total_unlock.encode().try_into().unwrap()); + log::debug!(target: LOG_TARGET, "total_unlocked: {}", d); + + let total_locked = locked_amount - total_unlock; + + for block_number in to_unlock { + locks.remove(&block_number); + } + + let d = u128::from_le_bytes(total_locked.encode().try_into().unwrap()); + log::debug!(target: LOG_TARGET, "total_locked: {}", d); + + if total_locked <= Zero::zero() { + T::Currency::remove_lock( + REWARDS_ID, + &author, + ); + } + else { + T::Currency::set_lock( + REWARDS_ID, + &author, + total_locked, + WithdrawReasons::except(WithdrawReasons::TRANSACTION_PAYMENT), + ); + } + ::RewardLocks::insert(author, locks); + } + fn do_mints(mints: &BTreeMap>) { for (destination, mint) in mints { drop(T::Currency::deposit_creating(&destination, *mint)); @@ -497,4 +550,8 @@ impl RewardLocksApi> for Pallet { |s, (_block_number, locked_balance)| s.saturating_add(*locked_balance) ) } + + fn unlock_upto(author: &T::AccountId, amount: BalanceOf) { + Self::unlock_upto(author, amount); + } } diff --git a/pallets/validator-set/Cargo.toml b/pallets/validator-set/Cargo.toml index 21816813..e976de61 100644 --- a/pallets/validator-set/Cargo.toml +++ b/pallets/validator-set/Cargo.toml @@ -18,16 +18,19 @@ sp-io = { default-features = false, git = "https://github.com/paritytech/substra sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", rev = "b0777b4c7f7" } sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", rev = "b0777b4c7f7" } sp-staking = { default-features = false, git = "https://github.com/paritytech/substrate", rev = "b0777b4c7f7" } +sp-version = { default-features = false, git = "https://github.com/paritytech/substrate", rev = "b0777b4c7f7" } frame-benchmarking = { default-features = false, git = "tps://github.com/paritytech/substrate", rev = "b0777b4c7f7", optional = true } frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", rev = "b0777b4c7f7" } frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", rev = "b0777b4c7f7" } pallet-session = { default-features = false, features = ['historical'], git = "https://github.com/paritytech/substrate", rev = "b0777b4c7f7" } pallet-treasury = { default-features = false, git = "https://github.com/paritytech/substrate", rev = "b0777b4c7f7" } +pallet-balances = { default-features = false, git = "https://github.com/paritytech/substrate", rev = "b0777b4c7f7" } scale-info = { default-features = false, features = ['derive'], version = '2.1.1' } rewards-api = { default-features = false, path = "../../traits/rewards" } validator-set-api = { default-features = false, path = "../../traits/validator-set" } +sp-consensus-poscan = { path = "../../primitives/consensus/poscan", default-features = false } [features] default = ['std'] diff --git a/pallets/validator-set/src/lib.rs b/pallets/validator-set/src/lib.rs index 5a6714a0..e8e73448 100644 --- a/pallets/validator-set/src/lib.rs +++ b/pallets/validator-set/src/lib.rs @@ -30,12 +30,16 @@ use log; pub use pallet::*; use sp_runtime::traits::{Convert, Zero}; use sp_staking::offence::{Offence, OffenceError, ReportOffence}; +use sp_version::RuntimeVersion; use sp_std::{collections::btree_set::BTreeSet, prelude::*}; use core::convert::TryInto; +use sp_consensus_poscan::HOURS; use rewards_api::RewardLocksApi; use validator_set_api::ValidatorSetApi; +const CUR_SPEC_VERSION: u32 = 101; +const UPGRADE_SLASH_DELAY: u32 = 5 * 24 * HOURS; const LOCK_ID: LockIdentifier = *b"validatr"; pub const LOG_TARGET: &'static str = "runtime::validator-set"; @@ -46,7 +50,6 @@ type NegativeImbalanceOf = <::Currency as Currency< ::AccountId, >>::NegativeImbalance; -const VALIDATOR_LOCKS_DEADLINE: u32 = 5_000; #[derive(Encode, Decode, Debug, Clone, PartialEq, TypeInfo)] pub enum RemoveReason { @@ -69,7 +72,7 @@ pub mod pallet { /// Configure the pallet by specifying the parameters and types on which it /// depends. #[pallet::config] - pub trait Config: frame_system::Config + pallet_session::Config + pallet_treasury::Config { + pub trait Config: frame_system::Config + pallet_session::Config + pallet_treasury::Config + pallet_balances::Config { /// The Event type. type Event: From> + IsType<::Event>; @@ -144,6 +147,10 @@ pub mod pallet { #[pallet::getter(fn removed)] pub type AccountRemoveReason = StorageMap<_, Twox64Concat, T::AccountId, Option<(T::BlockNumber, RemoveReason)>, ValueQuery>; + #[pallet::storage] + #[pallet::getter(fn upgrades)] + pub type LastUpgrade = StorageValue<_, T::BlockNumber, ValueQuery>; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -173,14 +180,14 @@ pub mod pallet { BadOrigin, /// Has not mined. ValidatorHasNotMined, - /// Has not mined. - TooLowDeposit, + /// Locked amount too low. + AmountLockedBelowLimit, /// decrease lock amount not allowed . DecreaseLockAmountNotAllowed, /// Decrease lcck prolongation not allowed. DecreaseLockPeriodNotAllowed, /// Lcck prolongation period too little. - LockPeriodBelowLimit, // {pub limit: u32}, + PeriodLockBelowLimit, // {pub limit: u32}, /// No lock. NotLocked, /// Unsufficient Balance, @@ -212,15 +219,20 @@ pub mod pallet { let deposit = T::RewardLocksApi::locks(&author); let d = u128::from_le_bytes(deposit.encode().try_into().unwrap()); - log::debug!(target: LOG_TARGET, "Account: {:?}", author.encode()); - log::debug!(target: LOG_TARGET, "Deposit: {:?}", d); - // let cur_block_number = >::block_number(); + log::debug!(target: LOG_TARGET, "Account: {:?}", &author); + log::debug!(target: LOG_TARGET, "Deposit: {}", d); Authors::::insert(n, Some(author)); } else { log::debug!(target: LOG_TARGET, "No authon"); } } + + fn on_runtime_upgrade() -> frame_support::weights::Weight { + let current_block = frame_system::Pallet::::block_number(); + >::put(current_block); + 0 + } } #[pallet::genesis_config] @@ -255,7 +267,7 @@ pub mod pallet { pub fn add_validator(origin: OriginFor, validator_id: T::AccountId) -> DispatchResult { T::AddRemoveOrigin::ensure_origin(origin)?; - Self::do_add_validator(validator_id.clone(), true)?; + Self::do_add_validator(validator_id.clone(), false)?; Self::approve_validator(validator_id)?; Ok(()) @@ -294,7 +306,7 @@ pub mod pallet { /// /// For this call, the dispatch origin must be the validator itself. #[pallet::weight(0)] - pub fn add_validator_again( + pub fn rejoin_validator( origin: OriginFor, validator_id: T::AccountId, ) -> DispatchResult { @@ -352,12 +364,12 @@ pub mod pallet { } if until - current_number < min_period.into() { - return Err(Error::::LockPeriodBelowLimit.into()); + return Err(Error::::PeriodLockBelowLimit.into()); } if let Some(per) = period { if per < min_period { - return Err(Error::::LockPeriodBelowLimit.into()); + return Err(Error::::PeriodLockBelowLimit.into()); } } @@ -440,18 +452,31 @@ impl Pallet { fn do_add_validator(validator_id: T::AccountId, check_block_num: bool) -> DispatchResult { let cur_block_number = >::block_number(); - let item_lock = ValidatorLock::::get(&validator_id).ok_or(Error::::TooLowDeposit)?; - let deposit = item_lock.1; + let item_lock = ValidatorLock::::get(&validator_id).ok_or(Error::::AmountLockedBelowLimit)?; + let deposit = + if item_lock.0 < cur_block_number { + BalanceOf::::zero() + } + else { + item_lock.1 + }; { - let deposit = u128::from_le_bytes(deposit.encode().try_into().unwrap()); - log::debug!(target: LOG_TARGET, "Deposit: {:?}", deposit.encode()); + let d = u128::from_le_bytes(deposit.encode().try_into().unwrap()); + log::debug!(target: LOG_TARGET, "Deposit: {}", d); } if check_block_num { let levels = T::FilterLevels::get(); let mut depth: u32 = T::MaxMinerDepth::get(); - for i in 0..levels.len() { - if deposit < levels[i].0.saturated_into() { - depth = levels[i].1 + + if deposit < levels[0].0.saturated_into() { + log::debug!(target: LOG_TARGET, "Too low deposit to be validator"); + return Err(Error::::AmountLockedBelowLimit.into()); + } + + for i in (0..levels.len()).rev() { + if deposit >= levels[i].0.saturated_into() { + depth = levels[i].1; + break } } @@ -476,6 +501,11 @@ impl Pallet { return Err(Error::::ValidatorHasNotMined.into()); } } + else { + if !Self::check_lock(&validator_id) { + return Err(Error::::AmountLockedBelowLimit.into()); + } + } let validator_set: BTreeSet<_> = >::get().into_iter().collect(); ensure!(!validator_set.contains(&validator_id), Error::::Duplicate); @@ -527,33 +557,51 @@ impl Pallet { let current_block = >::block_number(); AccountRemoveReason::::insert(&validator_id, Some((current_block, reason))); - >::mutate(|v| v.push(validator_id)); - log::debug!(target: LOG_TARGET, "Offline validator marked for auto removal."); + >::mutate(|v| v.push(validator_id.clone())); + log::debug!(target: LOG_TARGET, "Offline validator marked for auto removal: {:#?}", validator_id); } - // Adds offline validators to a local cache for removal at new session. - fn slash(validator_id: &T::AccountId, mut amount: BalanceOf) { + fn slash(validator_id: &T::AccountId, amount: BalanceOf) { let pot_id = pallet_treasury::Pallet::::account_id(); - let free = ::Currency::free_balance(&validator_id); let min_bal = ::Currency::minimum_balance(); + + let zero = BalanceOf::::zero(); let maybe_lock = ValidatorLock::::get(&validator_id); + let mut usable: u128 = pallet_balances::Pallet::::usable_balance(validator_id).saturated_into(); + + let unlock_amount = amount - usable.saturated_into(); + if unlock_amount > zero { + if let Some(lock_item) = maybe_lock { + if unlock_amount <= lock_item.1 { + Self::set_lock( + validator_id.clone(), + lock_item.0, + lock_item.1 - unlock_amount, + lock_item.2, + ); + usable = pallet_balances::Pallet::::usable_balance(validator_id).saturated_into(); + } + } + let unlock_amount = amount - usable.saturated_into(); + if unlock_amount > zero { + log::debug!(target: LOG_TARGET, "Try to unlock rewards locks for {:#?}, usable: {}", validator_id, usable); + let total_reward_locked = T::RewardLocksApi::locks(&validator_id); + T::RewardLocksApi::unlock_upto(&validator_id, total_reward_locked - unlock_amount); + usable = pallet_balances::Pallet::::usable_balance(validator_id).saturated_into(); + log::debug!(target: LOG_TARGET, "After unlock rewards locks for {:#?} usable: {}", validator_id, usable); + } + } - // if free - min_bal < amount { - // amount = free - min_bal; - // } - amount = core::cmp::min(amount, free - min_bal); + let amount = core::cmp::min(amount, usable.saturated_into()) - min_bal; + let res = ::Currency::transfer( + &validator_id, &pot_id, amount, ExistenceRequirement::KeepAlive, + ); - if let Err(_) = ::Currency::transfer(&validator_id, &pot_id, amount, ExistenceRequirement::KeepAlive) { - log::error!(target: LOG_TARGET, "Error slash validator {:?} by {:?}.", validator_id, &amount); + if let Err(e) = res { + log::error!(target: LOG_TARGET, "Error slash validator {:#?} by {:?}: {:?}.", validator_id, &amount, &e); return } - if let Some(lock_item) = maybe_lock { - if free - amount < lock_item.1 { - Self::set_lock(validator_id.clone(), lock_item.0, free - amount, lock_item.2); - } - } - log::debug!(target: LOG_TARGET, "Slash validator {:?} by {:?}.", validator_id, &amount); Self::deposit_event(Event::ValidatorSlash(validator_id.clone(), amount)); } @@ -590,35 +638,33 @@ impl Pallet { fn mark_if_no_locks() { let current_block = >::block_number(); - // WIP: do not check locks in first 100 blocks if current_block < 100u32.into() { return } - let levels = T::FilterLevels::get(); - let default_enter_depo: BalanceOf = levels[0].0.saturated_into(); - for v in Self::validators().into_iter() { - let maybe_enter_depo = EnterDeposit::::get(&v); - let maybe_lock = ValidatorLock::::get(&v); - let reward_locks = T::RewardLocksApi::locks(&v); - - let (enter_depo, locked_amount): (BalanceOf, BalanceOf) = match (maybe_enter_depo, maybe_lock) { - (Some(enter_depo), Some(lock_item)) => (enter_depo.1, lock_item.1), - (None, Some(lock_item)) => (default_enter_depo, lock_item.1), - (None, None) if current_block < VALIDATOR_LOCKS_DEADLINE.into() => (default_enter_depo, reward_locks), - _ => { - Self::mark_for_removal(v, RemoveReason::DepositBelowLimit); - continue; - }, - }; - - if locked_amount < enter_depo { + if !Self::check_lock(&v) { Self::mark_for_removal(v, RemoveReason::DepositBelowLimit) } } } + fn check_lock(validator_id: &T::AccountId) -> bool { + let levels = T::FilterLevels::get(); + let zero = BalanceOf::::zero(); + + let maybe_enter_depo = EnterDeposit::::get(&validator_id); + let maybe_lock = ValidatorLock::::get(&validator_id); + let mut true_locked: BalanceOf = zero; + + if let Some(lock) = maybe_lock { + let current_block = frame_system::Pallet::::block_number(); + true_locked = if lock.0 < current_block { zero } else { lock.1.into() }; + } + + true_locked >= maybe_enter_depo.map_or_else(|| levels[0].0.saturated_into(), |d| d.1) + } + fn set_lock( validator_id: T::AccountId, when: T::BlockNumber, @@ -647,6 +693,21 @@ impl Pallet { } } } + + fn is_slash_delay() -> bool { + let s: RuntimeVersion = ::Version::get(); + let sv = s.spec_version; + + if sv == CUR_SPEC_VERSION { + let current_block = frame_system::Pallet::::block_number(); + let upgrade_block = >::get(); + + if current_block - upgrade_block <= UPGRADE_SLASH_DELAY.into() { + return true + } + } + false + } } // Provides the new set of validators to the session module when session is @@ -655,6 +716,13 @@ impl pallet_session::SessionManager for Pallet { // Plan a new session and provide new validator set. fn new_session(_new_index: u32) -> Option> { Self::renew_locks(); + + if Self::is_slash_delay() { + log::debug!(target: LOG_TARGET, "New session called; within slash delay."); + >::put(Vec::::new()); + return Some(Self::validators()) + } + Self::mark_if_no_locks(); // Remove any offline and slashed validators. Self::remove_offline_validators(); @@ -718,6 +786,10 @@ impl> ReportOffence for Pallet { fn report_offence(_reporters: Vec, offence: O) -> Result<(), OffenceError> { + if Self::is_slash_delay() { + return Ok(()) + } + let offenders = offence.offenders(); let penalty: u128 = T::PenaltyOffline::get(); let val: BalanceOf = penalty.saturated_into(); @@ -725,6 +797,10 @@ impl> for (v, _) in offenders.into_iter() { log::debug!(target: LOG_TARGET, "offender reported: {:?}", &v); + if !Self::validators().contains(&v) { + continue + } + Self::slash(&v, val); Self::mark_for_removal(v, RemoveReason::ImOnlineSlash); } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 66b5d120..d15e0495 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -290,7 +290,7 @@ impl pallet_timestamp::Config for Runtime { } parameter_types! { - pub const ExistentialDeposit: u128 = 0; + pub const ExistentialDeposit: u128 = 1; pub const MaxLocks: u32 = 50; pub const MaxReserves: u32 = 50; } @@ -800,7 +800,7 @@ parameter_types! { pub const MinAuthorities: u32 = 3; pub const PoscanEngineId: [u8;4] = POSCAN_ENGINE_ID; pub const FilterLevels: [(u128, u32);4] = LEVELS; - pub const MaxMinerDepth: u32 = 1000; + pub const MaxMinerDepth: u32 = 10000; pub const PenaltyOffline: u128 = 20_000 * DOLLARS; pub const MinLockAmount: u128 = 100_000 * DOLLARS; pub const MinLockPeriod: u32 = 30 * 24 * HOURS; diff --git a/traits/rewards/src/lib.rs b/traits/rewards/src/lib.rs index 8d76948a..e6657861 100644 --- a/traits/rewards/src/lib.rs +++ b/traits/rewards/src/lib.rs @@ -1,13 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std)] -// use sp_std::collections::btree_set::BTreeSet; -// -// pub trait AccountSet { -// type AccountId; -// -// fn accounts() -> BTreeSet; -// } - pub trait RewardLocksApi { fn locks(account_id: &AccountId) -> Balance; + fn unlock_upto(author: &AccountId, amount: Balance); }