title: "Balances: add failsafe for consumer ref underflow"
doc:
- audience: Runtime Dev
description: |
Pallet balances now handles the case that historic accounts violate a invariant that they should have a consumer ref on `reserved > 0` balance.
This disallows such accounts from reaping and should prevent TI from getting messed up even more.
crates:
- name: pallet-balances
bump: patch
# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json
title: "Snowbridge: deposit extra fee to beneficiary on Asset Hub"
doc:
- audience: Runtime Dev
description: |
Snowbridge transfers arriving on Asset Hub will deposit both asset and fees to beneficiary so the fees will not get trapped.
Another benefit is when fees left more than ED, could be used to create the beneficiary account in case it does not exist on asset hub.
crates:
- name: snowbridge-router-primitives
...@@ -654,7 +654,6 @@ parameter_types! { ...@@ -654,7 +654,6 @@ parameter_types! {
pub const SlashDeferDuration: sp_staking::EraIndex = 24 * 7; // 1/4 the bonding duration. pub const SlashDeferDuration: sp_staking::EraIndex = 24 * 7; // 1/4 the bonding duration.
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
pub const MaxNominators: u32 = 64; pub const MaxNominators: u32 = 64;
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
pub const MaxControllersInDeprecationBatch: u32 = 5900; pub const MaxControllersInDeprecationBatch: u32 = 5900;
pub OffchainRepeat: BlockNumber = 5; pub OffchainRepeat: BlockNumber = 5;
pub HistoryDepth: u32 = 84; pub HistoryDepth: u32 = 84;
...@@ -690,7 +689,6 @@ impl pallet_staking::Config for Runtime { ...@@ -690,7 +689,6 @@ impl pallet_staking::Config for Runtime {
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>; type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
type NextNewSession = Session; type NextNewSession = Session;
type MaxExposurePageSize = ConstU32<256>; type MaxExposurePageSize = ConstU32<256>;
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
type ElectionProvider = ElectionProviderMultiPhase; type ElectionProvider = ElectionProviderMultiPhase;
type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>; type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
type VoterList = VoterList; type VoterList = VoterList;
...@@ -703,6 +701,7 @@ impl pallet_staking::Config for Runtime { ...@@ -703,6 +701,7 @@ impl pallet_staking::Config for Runtime {
type EventListeners = NominationPools; type EventListeners = NominationPools;
type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>; type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>;
type BenchmarkingConfig = StakingBenchmarkingConfig; type BenchmarkingConfig = StakingBenchmarkingConfig;
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
} }
impl pallet_fast_unstake::Config for Runtime { impl pallet_fast_unstake::Config for Runtime {
......
...@@ -86,7 +86,7 @@ where ...@@ -86,7 +86,7 @@ where
BasicQueue::new(ManualSealVerifier, block_import, None, spawner, registry) BasicQueue::new(ManualSealVerifier, block_import, None, spawner, registry)
} }
/// Params required to start the instant sealing authorship task. /// Params required to start the manual sealing authorship task.
pub struct ManualSealParams<B: BlockT, BI, E, C: ProvideRuntimeApi<B>, TP, SC, CS, CIDP, P> { pub struct ManualSealParams<B: BlockT, BI, E, C: ProvideRuntimeApi<B>, TP, SC, CS, CIDP, P> {
/// Block import instance. /// Block import instance.
pub block_import: BI, pub block_import: BI,
...@@ -114,7 +114,7 @@ pub struct ManualSealParams<B: BlockT, BI, E, C: ProvideRuntimeApi<B>, TP, SC, C ...@@ -114,7 +114,7 @@ pub struct ManualSealParams<B: BlockT, BI, E, C: ProvideRuntimeApi<B>, TP, SC, C
pub create_inherent_data_providers: CIDP, pub create_inherent_data_providers: CIDP,
} }
/// Params required to start the manual sealing authorship task. /// Params required to start the instant sealing authorship task.
pub struct InstantSealParams<B: BlockT, BI, E, C: ProvideRuntimeApi<B>, TP, SC, CIDP, P> { pub struct InstantSealParams<B: BlockT, BI, E, C: ProvideRuntimeApi<B>, TP, SC, CIDP, P> {
/// Block import instance for well. importing blocks. /// Block import instance for well. importing blocks.
pub block_import: BI, pub block_import: BI,
......
...@@ -144,7 +144,6 @@ parameter_types! { ...@@ -144,7 +144,6 @@ parameter_types! {
pub const BondingDuration: EraIndex = 3; pub const BondingDuration: EraIndex = 3;
pub const SlashDeferDuration: EraIndex = 0; pub const SlashDeferDuration: EraIndex = 0;
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(16);
pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build(); pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build();
} }
...@@ -174,7 +173,6 @@ impl pallet_staking::Config for Test { ...@@ -174,7 +173,6 @@ impl pallet_staking::Config for Test {
type UnixTime = pallet_timestamp::Pallet<Test>; type UnixTime = pallet_timestamp::Pallet<Test>;
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>; type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
type MaxExposurePageSize = ConstU32<64>; type MaxExposurePageSize = ConstU32<64>;
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
type NextNewSession = Session; type NextNewSession = Session;
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>; type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
...@@ -187,6 +185,7 @@ impl pallet_staking::Config for Test { ...@@ -187,6 +185,7 @@ impl pallet_staking::Config for Test {
type EventListeners = (); type EventListeners = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = (); type WeightInfo = ();
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
} }
impl pallet_offences::Config for Test { impl pallet_offences::Config for Test {
......
...@@ -28,6 +28,7 @@ docify = "0.2.8" ...@@ -28,6 +28,7 @@ docify = "0.2.8"
[dev-dependencies] [dev-dependencies]
pallet-transaction-payment = { path = "../transaction-payment" } pallet-transaction-payment = { path = "../transaction-payment" }
frame-support = { path = "../support", features = ["experimental"] }
sp-core = { path = "../../primitives/core" } sp-core = { path = "../../primitives/core" }
sp-io = { path = "../../primitives/io" } sp-io = { path = "../../primitives/io" }
paste = "1.0.12" paste = "1.0.12"
......
...@@ -954,6 +954,13 @@ pub mod pallet { ...@@ -954,6 +954,13 @@ pub mod pallet {
if !did_consume && does_consume { if !did_consume && does_consume {
frame_system::Pallet::<T>::inc_consumers(who)?; frame_system::Pallet::<T>::inc_consumers(who)?;
} }
if does_consume && frame_system::Pallet::<T>::consumers(who) == 0 {
// NOTE: This is a failsafe and should not happen for normal accounts. A normal
// account should have gotten a consumer ref in `!did_consume && does_consume`
// at some point.
log::error!(target: LOG_TARGET, "Defensively bumping a consumer ref.");
frame_system::Pallet::<T>::inc_consumers(who)?;
}
if did_provide && !does_provide { if did_provide && !does_provide {
// This could reap the account so must go last. // This could reap the account so must go last.
frame_system::Pallet::<T>::dec_providers(who).map_err(|r| { frame_system::Pallet::<T>::dec_providers(who).map_err(|r| {
......
// 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.
#![cfg(test)]
use crate::{
system::AccountInfo,
tests::{ensure_ti_valid, Balances, ExtBuilder, System, Test, TestId, UseSystem},
AccountData, ExtraFlags, TotalIssuance,
};
use frame_support::{
assert_noop, assert_ok, hypothetically,
traits::{
fungible::{Mutate, MutateHold},
tokens::Precision,
},
};
use sp_runtime::DispatchError;
/// There are some accounts that have one consumer ref too few. These accounts are at risk of losing
/// their held (reserved) balance. They do not just lose it - it is also not accounted for in the
/// Total Issuance. Here we test the case that the account does not reap in such a case, but gets
/// one consumer ref for its reserved balance.
#[test]
fn regression_historic_acc_does_not_evaporate_reserve() {
ExtBuilder::default().build_and_execute_with(|| {
UseSystem::set(true);
let (alice, bob) = (0, 1);
// Alice is in a bad state with consumer == 0 && reserved > 0:
Balances::set_balance(&alice, 100);
TotalIssuance::<Test>::put(100);
ensure_ti_valid();
assert_ok!(Balances::hold(&TestId::Foo, &alice, 10));
// This is the issue of the account:
System::dec_consumers(&alice);
assert_eq!(
System::account(&alice),
AccountInfo {
data: AccountData {
free: 90,
reserved: 10,
frozen: 0,
flags: ExtraFlags(1u128 << 127),
},
nonce: 0,
consumers: 0, // should be 1 on a good acc
providers: 1,
sufficients: 0,
}
);
ensure_ti_valid();
// Reaping the account is prevented by the new logic:
assert_noop!(
Balances::transfer_allow_death(Some(alice).into(), bob, 90),
DispatchError::ConsumerRemaining
);
assert_noop!(
Balances::transfer_all(Some(alice).into(), bob, false),
DispatchError::ConsumerRemaining
);
// normal transfers still work:
hypothetically!({
assert_ok!(Balances::transfer_keep_alive(Some(alice).into(), bob, 40));
// Alice got back her consumer ref:
assert_eq!(System::consumers(&alice), 1);
ensure_ti_valid();
});
hypothetically!({
assert_ok!(Balances::transfer_all(Some(alice).into(), bob, true));
// Alice got back her consumer ref:
assert_eq!(System::consumers(&alice), 1);
ensure_ti_valid();
});
// un-reserving all does not add a consumer ref:
hypothetically!({
assert_ok!(Balances::release(&TestId::Foo, &alice, 10, Precision::Exact));
assert_eq!(System::consumers(&alice), 0);
assert_ok!(Balances::transfer_keep_alive(Some(alice).into(), bob, 40));
assert_eq!(System::consumers(&alice), 0);
ensure_ti_valid();
});
// un-reserving some does add a consumer ref:
hypothetically!({
assert_ok!(Balances::release(&TestId::Foo, &alice, 5, Precision::Exact));
assert_eq!(System::consumers(&alice), 1);
assert_ok!(Balances::transfer_keep_alive(Some(alice).into(), bob, 40));
assert_eq!(System::consumers(&alice), 1);
ensure_ti_valid();
});
});
}
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
#![cfg(test)] #![cfg(test)]
use crate::{self as pallet_balances, AccountData, Config, CreditOf, Error, Pallet}; use crate::{self as pallet_balances, AccountData, Config, CreditOf, Error, Pallet, TotalIssuance};
use codec::{Decode, Encode, MaxEncodedLen}; use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::{ use frame_support::{
assert_err, assert_noop, assert_ok, assert_storage_noop, derive_impl, assert_err, assert_noop, assert_ok, assert_storage_noop, derive_impl,
...@@ -47,6 +47,7 @@ mod currency_tests; ...@@ -47,6 +47,7 @@ mod currency_tests;
mod dispatchable_tests; mod dispatchable_tests;
mod fungible_conformance_tests; mod fungible_conformance_tests;
mod fungible_tests; mod fungible_tests;
mod general_tests;
mod reentrancy_tests; mod reentrancy_tests;
type Block = frame_system::mocking::MockBlock<Test>; type Block = frame_system::mocking::MockBlock<Test>;
...@@ -278,6 +279,23 @@ pub fn info_from_weight(w: Weight) -> DispatchInfo { ...@@ -278,6 +279,23 @@ pub fn info_from_weight(w: Weight) -> DispatchInfo {
DispatchInfo { weight: w, ..Default::default() } DispatchInfo { weight: w, ..Default::default() }
} }
/// Check that the total-issuance matches the sum of all accounts' total balances.
pub fn ensure_ti_valid() {
let mut sum = 0;
for acc in frame_system::Account::<Test>::iter_keys() {
if UseSystem::get() {
let data = frame_system::Pallet::<Test>::account(acc);
sum += data.data.total();
} else {
let data = crate::Account::<Test>::get(acc);
sum += data.total();
}
}
assert_eq!(TotalIssuance::<Test>::get(), sum, "Total Issuance wrong");
}
#[test] #[test]
fn weights_sane() { fn weights_sane() {
let info = crate::Call::<Test>::transfer_allow_death { dest: 10, value: 4 }.get_dispatch_info(); let info = crate::Call::<Test>::transfer_allow_death { dest: 10, value: 4 }.get_dispatch_info();
......
...@@ -111,7 +111,7 @@ pub struct AccountData<Balance> { ...@@ -111,7 +111,7 @@ pub struct AccountData<Balance> {
const IS_NEW_LOGIC: u128 = 0x80000000_00000000_00000000_00000000u128; const IS_NEW_LOGIC: u128 = 0x80000000_00000000_00000000_00000000u128;
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
pub struct ExtraFlags(u128); pub struct ExtraFlags(pub(crate) u128);
impl Default for ExtraFlags { impl Default for ExtraFlags {
fn default() -> Self { fn default() -> Self {
Self(IS_NEW_LOGIC) Self(IS_NEW_LOGIC)
......
...@@ -158,7 +158,6 @@ parameter_types! { ...@@ -158,7 +158,6 @@ parameter_types! {
pub const SessionsPerEra: SessionIndex = 3; pub const SessionsPerEra: SessionIndex = 3;
pub const BondingDuration: EraIndex = 3; pub const BondingDuration: EraIndex = 3;
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build(); pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build();
} }
...@@ -188,7 +187,6 @@ impl pallet_staking::Config for Test { ...@@ -188,7 +187,6 @@ impl pallet_staking::Config for Test {
type UnixTime = pallet_timestamp::Pallet<Test>; type UnixTime = pallet_timestamp::Pallet<Test>;
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>; type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
type MaxExposurePageSize = ConstU32<64>; type MaxExposurePageSize = ConstU32<64>;
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
type NextNewSession = Session; type NextNewSession = Session;
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>; type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
...@@ -201,6 +199,7 @@ impl pallet_staking::Config for Test { ...@@ -201,6 +199,7 @@ impl pallet_staking::Config for Test {
type EventListeners = (); type EventListeners = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = (); type WeightInfo = ();
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
} }
impl pallet_offences::Config for Test { impl pallet_offences::Config for Test {
......
...@@ -23,7 +23,6 @@ pub(crate) const LOG_TARGET: &str = "tests::e2e-epm"; ...@@ -23,7 +23,6 @@ pub(crate) const LOG_TARGET: &str = "tests::e2e-epm";
use frame_support::{assert_err, assert_noop, assert_ok}; use frame_support::{assert_err, assert_noop, assert_ok};
use mock::*; use mock::*;
use sp_core::Get; use sp_core::Get;
use sp_npos_elections::{to_supports, StakedAssignment};
use sp_runtime::Perbill; use sp_runtime::Perbill;
use crate::mock::RuntimeOrigin; use crate::mock::RuntimeOrigin;
...@@ -127,75 +126,48 @@ fn offchainify_works() { ...@@ -127,75 +126,48 @@ fn offchainify_works() {
} }
#[test] #[test]
/// Replicates the Kusama incident of 8th Dec 2022 and its resolution through the governance /// Inspired by the Kusama incident of 8th Dec 2022 and its resolution through the governance
/// fallback. /// fallback.
/// ///
/// After enough slashes exceeded the `Staking::OffendingValidatorsThreshold`, the staking pallet /// Mass slash of validators shouldn't disable more than 1/3 of them (the byzantine threshold). Also
/// set `Forcing::ForceNew`. When a new session starts, staking will start to force a new era and /// no new era should be forced which could lead to EPM entering emergency mode.
/// calls <EPM as election_provider>::elect(). If at this point EPM and the staking miners did not fn mass_slash_doesnt_enter_emergency_phase() {
/// have enough time to queue a new solution (snapshot + solution submission), the election request
/// fails. If there is no election fallback mechanism in place, EPM enters in emergency mode.
/// Recovery: Once EPM is in emergency mode, subsequent calls to `elect()` will fail until a new
/// solution is added to EPM's `QueuedSolution` queue. This can be achieved through
/// `Call::set_emergency_election_result` or `Call::governance_fallback` dispatchables. Once a new
/// solution is added to the queue, EPM phase transitions to `Phase::Off` and the election flow
/// restarts. Note that in this test case, the emergency throttling is disabled.
fn enters_emergency_phase_after_forcing_before_elect() {
let epm_builder = EpmExtBuilder::default().disable_emergency_throttling(); let epm_builder = EpmExtBuilder::default().disable_emergency_throttling();
let (ext, pool_state, _) = ExtBuilder::default().epm(epm_builder).build_offchainify(); let staking_builder = StakingExtBuilder::default().validator_count(7);
let (mut ext, _, _) = ExtBuilder::default()
execute_with(ext, || { .epm(epm_builder)
log!( .staking(staking_builder)
trace, .build_offchainify();
"current validators (staking): {:?}",
<Runtime as pallet_staking::SessionInterface<AccountId>>::validators()
);
let session_validators_before = Session::validators();
roll_to_epm_off();
assert!(ElectionProviderMultiPhase::current_phase().is_off());
ext.execute_with(|| {
assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::NotForcing); assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::NotForcing);
// slashes so that staking goes into `Forcing::ForceNew`.
slash_through_offending_threshold();
assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::ForceNew); let active_set_size_before_slash = Session::validators().len();
advance_session_delayed_solution(pool_state.clone()); // Slash more than 1/3 of the active validators
assert!(ElectionProviderMultiPhase::current_phase().is_emergency()); let mut slashed = slash_half_the_active_set();
log_current_time();
let era_before_delayed_next = Staking::current_era(); let active_set_size_after_slash = Session::validators().len();
// try to advance 2 eras.
assert!(start_next_active_era_delayed_solution(pool_state.clone()).is_ok());
assert_eq!(Staking::current_era(), era_before_delayed_next);
assert!(start_next_active_era(pool_state).is_err());
assert_eq!(Staking::current_era(), era_before_delayed_next);
// EPM is still in emergency phase. // active set should stay the same before and after the slash
assert!(ElectionProviderMultiPhase::current_phase().is_emergency()); assert_eq!(active_set_size_before_slash, active_set_size_after_slash);
// session validator set remains the same. // Slashed validators are disabled up to a limit
assert_eq!(Session::validators(), session_validators_before); slashed.truncate(
pallet_staking::UpToLimitDisablingStrategy::<SLASHING_DISABLING_FACTOR>::disable_limit(
// performs recovery through the set emergency result. active_set_size_after_slash,
let supports = to_supports(&vec![ ),
StakedAssignment { who: 21, distribution: vec![(21, 10)] }, );
StakedAssignment { who: 31, distribution: vec![(21, 10), (31, 10)] },
StakedAssignment { who: 41, distribution: vec![(41, 10)] },
]);
assert!(ElectionProviderMultiPhase::set_emergency_election_result(
RuntimeOrigin::root(),
supports
)
.is_ok());
// EPM can now roll to signed phase to proceed with elections. The validator set is the // Find the indices of the disabled validators
// expected (ie. set through `set_emergency_election_result`). let active_set = Session::validators();
roll_to_epm_signed(); let expected_disabled = slashed
//assert!(ElectionProviderMultiPhase::current_phase().is_signed()); .into_iter()
assert_eq!(Session::validators(), vec![21, 31, 41]); .map(|d| active_set.iter().position(|a| *a == d).unwrap() as u32)
assert_eq!(Staking::current_era(), era_before_delayed_next.map(|e| e + 1)); .collect::<Vec<_>>();
assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::NotForcing);
assert_eq!(Session::disabled_validators(), expected_disabled);
}); });
} }
...@@ -253,77 +225,7 @@ fn continuous_slashes_below_offending_threshold() { ...@@ -253,77 +225,7 @@ fn continuous_slashes_below_offending_threshold() {
} }
#[test] #[test]
/// Slashed validator sets intentions in the same era of slashing. /// Active ledger balance may fall below ED if account chills before unbounding.
///
/// When validators are slashed, they are chilled and removed from the current `VoterList`. Thus,
/// the slashed validator should not be considered in the next validator set. However, if the
/// slashed validator sets its intention to validate again in the same era when it was slashed and
/// chilled, the validator may not be removed from the active validator set across eras, provided
/// it would selected in the subsequent era if there was no slash. Nominators of the slashed
/// validator will also be slashed and chilled, as expected, but the nomination intentions will
/// remain after the validator re-set the intention to be validating again.
///
/// This behaviour is due to removing implicit chill upon slash
/// <https://github.com/paritytech/substrate/pull/12420>.
///
/// Related to <https://github.com/paritytech/substrate/issues/13714>.
fn set_validation_intention_after_chilled() {
use frame_election_provider_support::SortedListProvider;
use pallet_staking::{Event, Forcing, Nominators};
let (ext, pool_state, _) = ExtBuilder::default()
.epm(EpmExtBuilder::default())
.staking(StakingExtBuilder::default())
.build_offchainify();
execute_with(ext, || {
assert_eq!(active_era(), 0);
// validator is part of the validator set.
assert!(Session::validators().contains(&41));
assert!(<Runtime as pallet_staking::Config>::VoterList::contains(&41));
// nominate validator 81.
assert_ok!(Staking::nominate(RuntimeOrigin::signed(21), vec![41]));
assert_eq!(Nominators::<Runtime>::get(21).unwrap().targets, vec![41]);
// validator is slashed. it is removed from the `VoterList` through chilling but in the
// current era, the validator is still part of the active validator set.
add_slash(&41);
assert!(Session::validators().contains(&41));
assert!(!<Runtime as pallet_staking::Config>::VoterList::contains(&41));
assert_eq!(
staking_events(),
[
Event::Chilled { stash: 41 },
Event::ForceEra { mode: Forcing::ForceNew },
Event::SlashReported {
validator: 41,
slash_era: 0,
fraction: Perbill::from_percent(10)
}
],
);
// after the nominator is slashed and chilled, the nominations remain.
assert_eq!(Nominators::<Runtime>::get(21).unwrap().targets, vec![41]);
// validator sets intention to stake again in the same era it was chilled.
assert_ok!(Staking::validate(RuntimeOrigin::signed(41), Default::default()));
// progress era and check that the slashed validator is still part of the validator
// set.
assert!(start_next_active_era(pool_state).is_ok());
assert_eq!(active_era(), 1);
assert!(Session::validators().contains(&41));
assert!(<Runtime as pallet_staking::Config>::VoterList::contains(&41));
// nominations are still active as before the slash.
assert_eq!(Nominators::<Runtime>::get(21).unwrap().targets, vec![41]);
})
}
#[test]
/// Active ledger balance may fall below ED if account chills before unbonding.
/// ///
/// Unbonding call fails if the remaining ledger's stash balance falls below the existential /// Unbonding call fails if the remaining ledger's stash balance falls below the existential
/// deposit. However, if the stash is chilled before unbonding, the ledger's active balance may /// deposit. However, if the stash is chilled before unbonding, the ledger's active balance may
......
...@@ -35,7 +35,7 @@ use sp_runtime::{ ...@@ -35,7 +35,7 @@ use sp_runtime::{
transaction_validity, BuildStorage, PerU16, Perbill, Percent, transaction_validity, BuildStorage, PerU16, Perbill, Percent,
}; };
use sp_staking::{ use sp_staking::{
offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}, offence::{OffenceDetails, OnOffenceHandler},
EraIndex, SessionIndex, EraIndex, SessionIndex,
}; };
use sp_std::prelude::*; use sp_std::prelude::*;
...@@ -236,7 +236,6 @@ parameter_types! { ...@@ -236,7 +236,6 @@ parameter_types! {
pub const SessionsPerEra: sp_staking::SessionIndex = 2; pub const SessionsPerEra: sp_staking::SessionIndex = 2;
pub static BondingDuration: sp_staking::EraIndex = 28; pub static BondingDuration: sp_staking::EraIndex = 28;
pub const SlashDeferDuration: sp_staking::EraIndex = 7; // 1/4 the bonding duration. pub const SlashDeferDuration: sp_staking::EraIndex = 7; // 1/4 the bonding duration.
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(40);
pub HistoryDepth: u32 = 84; pub HistoryDepth: u32 = 84;
} }
...@@ -290,6 +289,8 @@ parameter_types! { ...@@ -290,6 +289,8 @@ parameter_types! {
/// Upper limit on the number of NPOS nominations. /// Upper limit on the number of NPOS nominations.
const MAX_QUOTA_NOMINATIONS: u32 = 16; const MAX_QUOTA_NOMINATIONS: u32 = 16;
/// Disabling factor set explicitly to byzantine threshold
pub(crate) const SLASHING_DISABLING_FACTOR: usize = 3;
impl pallet_staking::Config for Runtime { impl pallet_staking::Config for Runtime {
type Currency = Balances; type Currency = Balances;
...@@ -308,7 +309,6 @@ impl pallet_staking::Config for Runtime { ...@@ -308,7 +309,6 @@ impl pallet_staking::Config for Runtime {
type EraPayout = (); type EraPayout = ();
type NextNewSession = Session; type NextNewSession = Session;
type MaxExposurePageSize = ConstU32<256>; type MaxExposurePageSize = ConstU32<256>;
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
type ElectionProvider = ElectionProviderMultiPhase; type ElectionProvider = ElectionProviderMultiPhase;
type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>; type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
type VoterList = BagsList; type VoterList = BagsList;
...@@ -320,6 +320,7 @@ impl pallet_staking::Config for Runtime { ...@@ -320,6 +320,7 @@ impl pallet_staking::Config for Runtime {
type EventListeners = Pools; type EventListeners = Pools;
type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>; type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>;
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy<SLASHING_DISABLING_FACTOR>;
} }
impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Runtime impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Runtime
...@@ -871,7 +872,6 @@ pub(crate) fn on_offence_now( ...@@ -871,7 +872,6 @@ pub(crate) fn on_offence_now(
offenders, offenders,
slash_fraction, slash_fraction,
Staking::eras_start_session_index(now).unwrap(), Staking::eras_start_session_index(now).unwrap(),
DisableStrategy::WhenSlashed,
); );
} }
...@@ -886,19 +886,16 @@ pub(crate) fn add_slash(who: &AccountId) { ...@@ -886,19 +886,16 @@ pub(crate) fn add_slash(who: &AccountId) {
); );
} }
// Slashes enough validators to cross the `Staking::OffendingValidatorsThreshold`. // Slashes 1/2 of the active set. Returns the `AccountId`s of the slashed validators.
pub(crate) fn slash_through_offending_threshold() { pub(crate) fn slash_half_the_active_set() -> Vec<AccountId> {
let validators = Session::validators(); let mut slashed = Session::validators();
let mut remaining_slashes = slashed.truncate(slashed.len() / 2);
<Runtime as pallet_staking::Config>::OffendingValidatorsThreshold::get() *
validators.len() as u32;
for v in validators.into_iter() { for v in slashed.iter() {
if remaining_slashes != 0 { add_slash(v);
add_slash(&v);
remaining_slashes -= 1;
}
} }
slashed
} }
// Slashes a percentage of the active nominators that haven't been slashed yet, with // Slashes a percentage of the active nominators that haven't been slashed yet, with
......
...@@ -134,7 +134,6 @@ impl pallet_staking::Config for Runtime { ...@@ -134,7 +134,6 @@ impl pallet_staking::Config for Runtime {
type NextNewSession = (); type NextNewSession = ();
type HistoryDepth = ConstU32<84>; type HistoryDepth = ConstU32<84>;
type MaxExposurePageSize = ConstU32<64>; type MaxExposurePageSize = ConstU32<64>;
type OffendingValidatorsThreshold = ();
type ElectionProvider = MockElection; type ElectionProvider = MockElection;
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
...@@ -145,6 +144,7 @@ impl pallet_staking::Config for Runtime { ...@@ -145,6 +144,7 @@ impl pallet_staking::Config for Runtime {
type EventListeners = (); type EventListeners = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = (); type WeightInfo = ();
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
} }
pub struct BalanceToU256; pub struct BalanceToU256;
......
...@@ -146,7 +146,6 @@ parameter_types! { ...@@ -146,7 +146,6 @@ parameter_types! {
pub const SessionsPerEra: SessionIndex = 3; pub const SessionsPerEra: SessionIndex = 3;
pub const BondingDuration: EraIndex = 3; pub const BondingDuration: EraIndex = 3;
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build(); pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build();
} }
...@@ -176,7 +175,6 @@ impl pallet_staking::Config for Test { ...@@ -176,7 +175,6 @@ impl pallet_staking::Config for Test {
type UnixTime = pallet_timestamp::Pallet<Test>; type UnixTime = pallet_timestamp::Pallet<Test>;
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>; type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
type MaxExposurePageSize = ConstU32<64>; type MaxExposurePageSize = ConstU32<64>;
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
type NextNewSession = Session; type NextNewSession = Session;
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>; type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
...@@ -189,6 +187,7 @@ impl pallet_staking::Config for Test { ...@@ -189,6 +187,7 @@ impl pallet_staking::Config for Test {
type EventListeners = (); type EventListeners = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = (); type WeightInfo = ();
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
} }
impl pallet_offences::Config for Test { impl pallet_offences::Config for Test {
......
...@@ -104,7 +104,7 @@ use sp_runtime::{ ...@@ -104,7 +104,7 @@ use sp_runtime::{
PerThing, Perbill, Permill, RuntimeDebug, SaturatedConversion, PerThing, Perbill, Permill, RuntimeDebug, SaturatedConversion,
}; };
use sp_staking::{ use sp_staking::{
offence::{DisableStrategy, Kind, Offence, ReportOffence}, offence::{Kind, Offence, ReportOffence},
SessionIndex, SessionIndex,
}; };
use sp_std::prelude::*; use sp_std::prelude::*;
...@@ -847,10 +847,6 @@ impl<Offender: Clone> Offence<Offender> for UnresponsivenessOffence<Offender> { ...@@ -847,10 +847,6 @@ impl<Offender: Clone> Offence<Offender> for UnresponsivenessOffence<Offender> {
self.session_index self.session_index
} }
fn disable_strategy(&self) -> DisableStrategy {
DisableStrategy::Never
}
fn slash_fraction(&self, offenders: u32) -> Perbill { fn slash_fraction(&self, offenders: u32) -> Perbill {
// the formula is min((3 * (k - (n / 10 + 1))) / n, 1) * 0.07 // the formula is min((3 * (k - (n / 10 + 1))) / n, 1) * 0.07
// basically, 10% can be offline with no slash, but after that, it linearly climbs up to 7% // basically, 10% can be offline with no slash, but after that, it linearly climbs up to 7%
......
...@@ -50,9 +50,6 @@ fn test_unresponsiveness_slash_fraction() { ...@@ -50,9 +50,6 @@ fn test_unresponsiveness_slash_fraction() {
dummy_offence.slash_fraction(17), dummy_offence.slash_fraction(17),
Perbill::from_parts(46200000), // 4.62% Perbill::from_parts(46200000), // 4.62%
); );
// Offline offences should never lead to being disabled.
assert_eq!(dummy_offence.disable_strategy(), DisableStrategy::Never);
} }
#[test] #[test]
......
...@@ -111,7 +111,6 @@ impl pallet_staking::Config for Runtime { ...@@ -111,7 +111,6 @@ impl pallet_staking::Config for Runtime {
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>; type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
type NextNewSession = (); type NextNewSession = ();
type MaxExposurePageSize = ConstU32<64>; type MaxExposurePageSize = ConstU32<64>;
type OffendingValidatorsThreshold = ();
type ElectionProvider = type ElectionProvider =
frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>; frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>;
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
...@@ -124,6 +123,7 @@ impl pallet_staking::Config for Runtime { ...@@ -124,6 +123,7 @@ impl pallet_staking::Config for Runtime {
type EventListeners = Pools; type EventListeners = Pools;
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = (); type WeightInfo = ();
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
} }
parameter_types! { parameter_types! {
......
...@@ -125,7 +125,6 @@ impl pallet_staking::Config for Runtime { ...@@ -125,7 +125,6 @@ impl pallet_staking::Config for Runtime {
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>; type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
type NextNewSession = (); type NextNewSession = ();
type MaxExposurePageSize = ConstU32<64>; type MaxExposurePageSize = ConstU32<64>;
type OffendingValidatorsThreshold = ();
type ElectionProvider = type ElectionProvider =
frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>; frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>;
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
...@@ -138,6 +137,7 @@ impl pallet_staking::Config for Runtime { ...@@ -138,6 +137,7 @@ impl pallet_staking::Config for Runtime {
type EventListeners = Pools; type EventListeners = Pools;
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = (); type WeightInfo = ();
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
} }
parameter_types! { parameter_types! {
......
...@@ -174,7 +174,6 @@ impl pallet_staking::Config for Test { ...@@ -174,7 +174,6 @@ impl pallet_staking::Config for Test {
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>; type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
type NextNewSession = Session; type NextNewSession = Session;
type MaxExposurePageSize = ConstU32<64>; type MaxExposurePageSize = ConstU32<64>;
type OffendingValidatorsThreshold = ();
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>; type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
type GenesisElectionProvider = Self::ElectionProvider; type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
...@@ -186,6 +185,7 @@ impl pallet_staking::Config for Test { ...@@ -186,6 +185,7 @@ impl pallet_staking::Config for Test {
type EventListeners = (); type EventListeners = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = (); type WeightInfo = ();
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
} }
impl pallet_im_online::Config for Test { impl pallet_im_online::Config for Test {
......