From 3555ef425d5b221251b4f1370ed9277f05f8787f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <andre.beat@gmail.com> Date: Thu, 10 Jan 2019 15:03:15 +0000 Subject: [PATCH] Improve offline slashing calculation (#1371) * srml: apply slashing for all offline reports at once * srml: add regression test for slashing overflow --- substrate/srml/staking/src/lib.rs | 87 +++++++++++++++++------------ substrate/srml/staking/src/tests.rs | 39 +++++++++++++ 2 files changed, 89 insertions(+), 37 deletions(-) diff --git a/substrate/srml/staking/src/lib.rs b/substrate/srml/staking/src/lib.rs index 527c916008a..70f71de1527 100644 --- a/substrate/srml/staking/src/lib.rs +++ b/substrate/srml/staking/src/lib.rs @@ -508,55 +508,68 @@ impl<T: Trait> Module<T> { /// Call when a validator is determined to be offline. `count` is the /// number of offences the validator has committed. pub fn on_offline_validator(v: T::AccountId, count: usize) { - use primitives::traits::CheckedShl; + use primitives::traits::{CheckedAdd, CheckedShl}; // Early exit if validator is invulnerable. if Self::invulnerables().contains(&v) { return } - for _ in 0..count { - let slash_count = Self::slash_count(&v); - <SlashCount<T>>::insert(v.clone(), slash_count + 1); - let grace = Self::offline_slash_grace(); + let slash_count = Self::slash_count(&v); + let new_slash_count = slash_count + count as u32; + <SlashCount<T>>::insert(v.clone(), new_slash_count); + let grace = Self::offline_slash_grace(); - let event = if slash_count >= grace { + let event = if new_slash_count > grace { + let slash = { + let base_slash = Self::current_offline_slash(); let instances = slash_count - grace; - let base_slash = Self::current_offline_slash(); - let slash = match base_slash.checked_shl(instances) { - Some(slash) => slash, - None => { - // freeze at last maximum valid slash if this starts - // to overflow. - <SlashCount<T>>::insert(v.clone(), slash_count); - base_slash.checked_shl(instances - 1) - .expect("slash count no longer incremented after overflow; \ - prior check only fails with instances >= 1; \ - thus instances - 1 always works and is a valid amount of bits; qed") + let mut total_slash = T::Balance::default(); + for i in instances..(instances + count as u32) { + if let Some(total) = base_slash.checked_shl(i) + .and_then(|slash| total_slash.checked_add(&slash)) { + total_slash = total; + } else { + // reset slash count only up to the current + // instance. the total slash overflows the unit for + // balance in the system therefore we can slash all + // the slashable balance for the account + <SlashCount<T>>::insert(v.clone(), slash_count + i); + total_slash = Self::slashable_balance(&v); + break; } - }; - - let next_slash = slash << 1; - - let _ = Self::slash_validator(&v, slash); - if instances >= Self::validator_preferences(&v).unstake_threshold - || Self::slashable_balance(&v) < next_slash - { - if let Some(pos) = Self::intentions().into_iter().position(|x| &x == &v) { - Self::apply_unstake(&v, pos) - .expect("pos derived correctly from Self::intentions(); \ - apply_unstake can only fail if pos wrong; \ - Self::intentions() doesn't change; qed"); - } - let _ = Self::apply_force_new_era(false); } - RawEvent::OfflineSlash(v.clone(), slash) - } else { - RawEvent::OfflineWarning(v.clone(), slash_count) + + total_slash }; - Self::deposit_event(event); - } + + let _ = Self::slash_validator(&v, slash); + + let next_slash = match slash.checked_shl(1) { + Some(slash) => slash, + None => Self::slashable_balance(&v), + }; + + let instances = new_slash_count - grace; + if instances > Self::validator_preferences(&v).unstake_threshold + || Self::slashable_balance(&v) < next_slash + || next_slash <= slash + { + if let Some(pos) = Self::intentions().into_iter().position(|x| &x == &v) { + Self::apply_unstake(&v, pos) + .expect("pos derived correctly from Self::intentions(); \ + apply_unstake can only fail if pos wrong; \ + Self::intentions() doesn't change; qed"); + } + let _ = Self::apply_force_new_era(false); + } + RawEvent::OfflineSlash(v.clone(), slash) + } else { + RawEvent::OfflineWarning(v.clone(), slash_count) + }; + + Self::deposit_event(event); } } diff --git a/substrate/srml/staking/src/tests.rs b/substrate/srml/staking/src/tests.rs index 801c15d3e12..fc3209bae34 100644 --- a/substrate/srml/staking/src/tests.rs +++ b/substrate/srml/staking/src/tests.rs @@ -557,3 +557,42 @@ fn slash_value_calculation_does_not_overflow() { Staking::on_offline_validator(10, 100); }); } + +#[test] +fn next_slash_value_calculation_does_not_overflow() { + with_externalities(&mut new_test_ext(0, 3, 3, 0, true, 10), || { + assert_eq!(Staking::era_length(), 9); + assert_eq!(Staking::sessions_per_era(), 3); + assert_eq!(Staking::last_era_length_change(), 0); + assert_eq!(Staking::current_era(), 0); + assert_eq!(Session::current_index(), 0); + assert_eq!(Balances::total_balance(&10), 1); + assert_eq!(Staking::intentions(), vec![10, 20]); + assert_eq!(Staking::offline_slash_grace(), 0); + + // set validator preferences so the validator doesn't back down after + // slashing. + <ValidatorPreferences<Test>>::insert(10, ValidatorPrefs { + unstake_threshold: u32::max_value(), + validator_payment: 0, + }); + + // we have enough balance to cover the last slash before overflow + Balances::set_free_balance(&10, u64::max_value()); + assert_eq!(Balances::total_balance(&10), u64::max_value()); + + // the balance type is u64, so after slashing 64 times, + // the slash value should have overflowed. add a couple extra for + // good measure with the slash grace. + trait TypeEq {} + impl<A> TypeEq for (A, A) {} + fn assert_type_eq<A: TypeEq>() {} + assert_type_eq::<(u64, <Test as balances::Trait>::Balance)>(); + + // the total slash value should overflow the balance type + // therefore the total validator balance should be slashed + Staking::on_offline_validator(10, 100); + + assert_eq!(Balances::total_balance(&10), 0); + }); +} -- GitLab