From a7fef2ba7a55dd0f41c5f27ed767a58b26a70dd8 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Tue, 14 Jan 2025 07:51:39 -0500 Subject: [PATCH] Redesign Slash/SlashReport types with a function to calculate the penalty --- coordinator/substrate/src/ephemeral.rs | 3 +- coordinator/tributary/src/lib.rs | 7 +- coordinator/tributary/src/transaction.rs | 4 +- substrate/abi/src/validator_sets.rs | 2 +- substrate/client/src/serai/validator_sets.rs | 2 +- substrate/emissions/pallet/src/lib.rs | 4 +- substrate/primitives/src/constants.rs | 1 + substrate/runtime/src/lib.rs | 2 +- substrate/validator-sets/pallet/src/lib.rs | 18 +- .../validator-sets/primitives/src/lib.rs | 58 ++-- .../primitives/src/slash_points.rs | 258 ++++++++++++++++++ 11 files changed, 307 insertions(+), 52 deletions(-) create mode 100644 substrate/validator-sets/primitives/src/slash_points.rs diff --git a/coordinator/substrate/src/ephemeral.rs b/coordinator/substrate/src/ephemeral.rs index eacfed9d..3ea8de98 100644 --- a/coordinator/substrate/src/ephemeral.rs +++ b/coordinator/substrate/src/ephemeral.rs @@ -159,8 +159,9 @@ impl ContinuallyRan for EphemeralEventStream { Err("validator's weight exceeded u16::MAX".to_string())? }; + // Do the summation in u32 so we don't risk a u16 overflow let total_weight = validators.iter().map(|(_, weight)| u32::from(*weight)).sum::(); - if total_weight > MAX_KEY_SHARES_PER_SET { + if total_weight > u32::from(MAX_KEY_SHARES_PER_SET) { Err(format!( "{set:?} has {total_weight} key shares when the max is {MAX_KEY_SHARES_PER_SET}" ))?; diff --git a/coordinator/tributary/src/lib.rs b/coordinator/tributary/src/lib.rs index 80724c76..f0aa8029 100644 --- a/coordinator/tributary/src/lib.rs +++ b/coordinator/tributary/src/lib.rs @@ -352,8 +352,11 @@ impl<'a, TD: Db, TDT: DbTxn, P: P2p> ScanBlock<'a, TD, TDT, P> { // Create the resulting slash report let mut slash_report = vec![]; for (validator, points) in self.validators.iter().copied().zip(amortized_slash_report) { - if points != 0 { - slash_report.push(Slash { key: validator.into(), points }); + // TODO: Natively store this as a `Slash` + if points == u32::MAX { + slash_report.push(Slash::Fatal); + } else { + slash_report.push(Slash::Points(points)); } } assert!(slash_report.len() <= f); diff --git a/coordinator/tributary/src/transaction.rs b/coordinator/tributary/src/transaction.rs index befad461..2cc4600c 100644 --- a/coordinator/tributary/src/transaction.rs +++ b/coordinator/tributary/src/transaction.rs @@ -301,14 +301,14 @@ impl TransactionTrait for Transaction { Transaction::Batch { .. } => {} Transaction::Sign { data, .. } => { - if data.len() > usize::try_from(MAX_KEY_SHARES_PER_SET).unwrap() { + if data.len() > usize::from(MAX_KEY_SHARES_PER_SET) { Err(TransactionError::InvalidContent)? } // TODO: MAX_SIGN_LEN } Transaction::SlashReport { slash_points, .. } => { - if slash_points.len() > usize::try_from(MAX_KEY_SHARES_PER_SET).unwrap() { + if slash_points.len() > usize::from(MAX_KEY_SHARES_PER_SET) { Err(TransactionError::InvalidContent)? } } diff --git a/substrate/abi/src/validator_sets.rs b/substrate/abi/src/validator_sets.rs index 7a7bdc00..ec8a5714 100644 --- a/substrate/abi/src/validator_sets.rs +++ b/substrate/abi/src/validator_sets.rs @@ -21,7 +21,7 @@ pub enum Call { }, report_slashes { network: NetworkId, - slashes: BoundedVec<(SeraiAddress, u32), ConstU32<{ MAX_KEY_SHARES_PER_SET / 3 }>>, + slashes: BoundedVec<(SeraiAddress, u32), ConstU32<{ MAX_KEY_SHARES_PER_SET_U32 / 3 }>>, signature: Signature, }, allocate { diff --git a/substrate/client/src/serai/validator_sets.rs b/substrate/client/src/serai/validator_sets.rs index 8eb50b70..c92e4f89 100644 --- a/substrate/client/src/serai/validator_sets.rs +++ b/substrate/client/src/serai/validator_sets.rs @@ -242,7 +242,7 @@ impl<'a> SeraiValidatorSets<'a> { // (50 * (32 + 4)) > (150 * 4) slashes: sp_runtime::BoundedVec< (SeraiAddress, u32), - sp_core::ConstU32<{ primitives::MAX_KEY_SHARES_PER_SET / 3 }>, + sp_core::ConstU32<{ primitives::MAX_KEY_SHARES_PER_SET_U32 / 3 }>, >, signature: Signature, ) -> Transaction { diff --git a/substrate/emissions/pallet/src/lib.rs b/substrate/emissions/pallet/src/lib.rs index 400f8921..54c39c46 100644 --- a/substrate/emissions/pallet/src/lib.rs +++ b/substrate/emissions/pallet/src/lib.rs @@ -23,7 +23,7 @@ pub mod pallet { use economic_security_pallet::{Config as EconomicSecurityConfig, Pallet as EconomicSecurity}; use serai_primitives::*; - use validator_sets_primitives::{MAX_KEY_SHARES_PER_SET, Session}; + use validator_sets_primitives::{MAX_KEY_SHARES_PER_SET_U32, Session}; pub use emissions_primitives as primitives; use primitives::*; @@ -74,7 +74,7 @@ pub mod pallet { _, Identity, NetworkId, - BoundedVec<(PublicKey, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET }>>, + BoundedVec<(PublicKey, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET_U32 }>>, OptionQuery, >; diff --git a/substrate/primitives/src/constants.rs b/substrate/primitives/src/constants.rs index b3db7317..a3d4b6f9 100644 --- a/substrate/primitives/src/constants.rs +++ b/substrate/primitives/src/constants.rs @@ -3,6 +3,7 @@ use crate::BlockNumber; // 1 MB pub const BLOCK_SIZE: u32 = 1024 * 1024; // 6 seconds +// TODO: Use Duration pub const TARGET_BLOCK_TIME: u64 = 6; /// Measured in blocks. diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index e55270cb..3bb56a74 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -282,7 +282,7 @@ impl pallet_authorship::Config for Runtime { } // Maximum number of authorities per session. -pub type MaxAuthorities = ConstU32<{ validator_sets::primitives::MAX_KEY_SHARES_PER_SET }>; +pub type MaxAuthorities = ConstU32<{ validator_sets::primitives::MAX_KEY_SHARES_PER_SET_U32 }>; /// Longevity of an offence report. pub type ReportLongevity = ::EpochDuration; diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index 655c6722..2ba1b45f 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -141,7 +141,7 @@ pub mod pallet { _, Identity, NetworkId, - BoundedVec<(Public, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET }>>, + BoundedVec<(Public, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET_U32 }>>, OptionQuery, >; /// The validators selected to be in-set, regardless of if removed. @@ -402,7 +402,7 @@ pub mod pallet { // Clear the current InSet assert_eq!( - InSet::::clear_prefix(network, MAX_KEY_SHARES_PER_SET, None).maybe_cursor, + InSet::::clear_prefix(network, MAX_KEY_SHARES_PER_SET_U32, None).maybe_cursor, None ); @@ -412,11 +412,11 @@ pub mod pallet { { let mut iter = SortedAllocationsIter::::new(network); let mut key_shares = 0; - while key_shares < u64::from(MAX_KEY_SHARES_PER_SET) { + while key_shares < u64::from(MAX_KEY_SHARES_PER_SET_U32) { let Some((key, amount)) = iter.next() else { break }; let these_key_shares = - (amount.0 / allocation_per_key_share).min(u64::from(MAX_KEY_SHARES_PER_SET)); + (amount.0 / allocation_per_key_share).min(u64::from(MAX_KEY_SHARES_PER_SET_U32)); participants.push((key, these_key_shares)); key_shares += these_key_shares; @@ -535,7 +535,7 @@ pub mod pallet { top = Some(key_shares); } - if key_shares > u64::from(MAX_KEY_SHARES_PER_SET) { + if key_shares > u64::from(MAX_KEY_SHARES_PER_SET_U32) { break; } } @@ -547,7 +547,7 @@ pub mod pallet { // post_amortization_key_shares_for_top_validator yields what the top validator's key shares // would be after such a reduction, letting us evaluate this correctly let top = post_amortization_key_shares_for_top_validator(validators_len, top, key_shares); - (top * 3) < key_shares.min(MAX_KEY_SHARES_PER_SET.into()) + (top * 3) < key_shares.min(MAX_KEY_SHARES_PER_SET_U32.into()) } fn increase_allocation( @@ -586,7 +586,7 @@ pub mod pallet { // The above is_bft calls are only used to check a BFT net doesn't become non-BFT // Check here if this call would prevent a non-BFT net from *ever* becoming BFT - if (new_allocation / allocation_per_key_share) >= (MAX_KEY_SHARES_PER_SET / 3).into() { + if (new_allocation / allocation_per_key_share) >= (MAX_KEY_SHARES_PER_SET_U32 / 3).into() { Err(Error::::AllocationWouldPreventFaultTolerance)?; } @@ -1010,7 +1010,7 @@ pub mod pallet { pub fn report_slashes( origin: OriginFor, network: NetworkId, - slashes: BoundedVec<(Public, u32), ConstU32<{ MAX_KEY_SHARES_PER_SET / 3 }>>, + slashes: BoundedVec<(Public, u32), ConstU32<{ MAX_KEY_SHARES_PER_SET_U32 / 3 }>>, signature: Signature, ) -> DispatchResult { ensure_none(origin)?; @@ -1209,7 +1209,7 @@ pub mod pallet { ValidTransaction::with_tag_prefix("ValidatorSets") .and_provides((1, set)) - .longevity(MAX_KEY_SHARES_PER_SET.into()) + .longevity(MAX_KEY_SHARES_PER_SET_U32.into()) .propagate(true) .build() } diff --git a/substrate/validator-sets/primitives/src/lib.rs b/substrate/validator-sets/primitives/src/lib.rs index fe78fbca..8822aa00 100644 --- a/substrate/validator-sets/primitives/src/lib.rs +++ b/substrate/validator-sets/primitives/src/lib.rs @@ -1,5 +1,7 @@ #![cfg_attr(not(feature = "std"), no_std)] +use core::time::Duration; + #[cfg(feature = "std")] use zeroize::Zeroize; @@ -13,20 +15,30 @@ use borsh::{BorshSerialize, BorshDeserialize}; #[cfg(feature = "serde")] use serde::{Serialize, Deserialize}; -use sp_core::{ConstU32, sr25519::Public, bounded::BoundedVec}; +use sp_core::{ConstU32, bounded::BoundedVec, sr25519::Public}; #[cfg(not(feature = "std"))] use sp_std::vec::Vec; use serai_primitives::NetworkId; -/// The maximum amount of key shares per set. -pub const MAX_KEY_SHARES_PER_SET: u32 = 150; +mod slash_points; +pub use slash_points::*; + +/// The expected duration for a session. +// 1 week +pub const SESSION_LENGTH: Duration = Duration::from_secs(7 * 24 * 60 * 60); + +/// The maximum length for a key. // Support keys up to 96 bytes (BLS12-381 G2). pub const MAX_KEY_LEN: u32 = 96; +/// The maximum amount of key shares per set. +pub const MAX_KEY_SHARES_PER_SET: u16 = 150; +pub const MAX_KEY_SHARES_PER_SET_U32: u32 = MAX_KEY_SHARES_PER_SET as u32; + /// The type used to identify a specific session of validators. #[derive( - Clone, Copy, PartialEq, Eq, Hash, Default, Debug, Encode, Decode, TypeInfo, MaxEncodedLen, + Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Encode, Decode, TypeInfo, MaxEncodedLen, )] #[cfg_attr(feature = "std", derive(Zeroize))] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] @@ -34,7 +46,9 @@ pub const MAX_KEY_LEN: u32 = 96; pub struct Session(pub u32); /// The type used to identify a specific validator set during a specific session. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[derive( + Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Encode, Decode, TypeInfo, MaxEncodedLen, +)] #[cfg_attr(feature = "std", derive(Zeroize))] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -43,13 +57,13 @@ pub struct ValidatorSet { pub network: NetworkId, } -type MaxKeyLen = ConstU32; /// The type representing a Key from an external network. -pub type ExternalKey = BoundedVec; +pub type ExternalKey = BoundedVec>; /// The key pair for a validator set. /// -/// This is their Ristretto key, used for signing Batches, and their key on the external network. +/// This is their Ristretto key, used for publishing data onto Serai, and their key on the external +/// network. #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -81,12 +95,12 @@ impl Zeroize for KeyPair { /// The MuSig context for a validator set. pub fn musig_context(set: ValidatorSet) -> Vec { - [b"ValidatorSets-musig_key".as_ref(), &set.encode()].concat() + (b"ValidatorSets-musig_key".as_ref(), set).encode() } /// The MuSig public key for a validator set. /// -/// This function panics on invalid input. +/// This function panics on invalid input, per the definition of `dkg::musig::musig_key`. pub fn musig_key(set: ValidatorSet, set_keys: &[Public]) -> Public { let mut keys = Vec::new(); for key in set_keys { @@ -98,33 +112,11 @@ pub fn musig_key(set: ValidatorSet, set_keys: &[Public]) -> Public { Public(dkg::musig::musig_key::(&musig_context(set), &keys).unwrap().to_bytes()) } -/// The message for the set_keys signature. +/// The message for the `set_keys` signature. pub fn set_keys_message(set: &ValidatorSet, key_pair: &KeyPair) -> Vec { (b"ValidatorSets-set_keys", set, key_pair).encode() } -#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Slash { - #[cfg_attr( - feature = "borsh", - borsh( - serialize_with = "serai_primitives::borsh_serialize_public", - deserialize_with = "serai_primitives::borsh_deserialize_public" - ) - )] - pub key: Public, - pub points: u32, -} -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct SlashReport(pub BoundedVec>); - -pub fn report_slashes_message(set: &ValidatorSet, slashes: &SlashReport) -> Vec { - (b"ValidatorSets-report_slashes", set, slashes).encode() -} - /// For a set of validators whose key shares may exceed the maximum, reduce until they equal the /// maximum. /// diff --git a/substrate/validator-sets/primitives/src/slash_points.rs b/substrate/validator-sets/primitives/src/slash_points.rs new file mode 100644 index 00000000..d6fd0d68 --- /dev/null +++ b/substrate/validator-sets/primitives/src/slash_points.rs @@ -0,0 +1,258 @@ +use core::{num::NonZero, time::Duration}; + +#[cfg(feature = "std")] +use zeroize::Zeroize; + +use scale::{Encode, Decode, MaxEncodedLen}; +use scale_info::TypeInfo; + +#[cfg(feature = "borsh")] +use borsh::{BorshSerialize, BorshDeserialize}; +#[cfg(feature = "serde")] +use serde::{Serialize, Deserialize}; + +use sp_core::{ConstU32, bounded::BoundedVec}; +#[cfg(not(feature = "std"))] +use sp_std::vec::Vec; + +use serai_primitives::{TARGET_BLOCK_TIME, Amount}; + +use crate::{SESSION_LENGTH, MAX_KEY_SHARES_PER_SET_U32}; + +/// Each slash point is equivalent to the downtime implied by missing a block proposal. +// Takes a NonZero so that the result is never 0. +fn downtime_per_slash_point(validators: NonZero) -> Duration { + Duration::from_secs(TARGET_BLOCK_TIME) * u32::from(u16::from(validators)) +} + +/// A slash for a validator. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Zeroize))] +#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Slash { + /// The slash points accumulated by this validator. + /// + /// Each point is considered as `downtime_per_slash_point(validators)` downtime, where + /// `validators` is the amount of validators present in the set. + Points(u32), + /// A fatal slash due to fundamentally faulty behavior. + /// + /// This should only be used for misbehavior with explicit evidence of impropriety. This should + /// not be used for liveness failures. The validator will be penalized all allocated stake. + Fatal, +} + +impl Slash { + /// Calculate the penalty which should be applied to the validator. + /// + /// Does not panic, even due to overflows, if `allocated_stake + session_rewards <= u64::MAX`. + pub fn penalty( + self, + validators: NonZero, + allocated_stake: Amount, + session_rewards: Amount, + ) -> Amount { + match self { + Self::Points(slash_points) => { + let mut slash_points = u64::from(slash_points); + // Do the logic with the stake in u128 to prevent overflow from multiplying u64s + let allocated_stake = u128::from(allocated_stake.0); + let session_rewards = u128::from(session_rewards.0); + + // A Serai validator is allowed to be offline for an average of one day every two weeks + // with no additional penalty. They'll solely not earn rewards for the time they were + // offline. + const GRACE_WINDOW: Duration = Duration::from_secs(2 * 7 * 24 * 60 * 60); + const GRACE: Duration = Duration::from_secs(24 * 60 * 60); + + // GRACE / GRACE_WINDOW is the fraction of the time a validator is allowed to be offline + // This means we want SESSION_LENGTH * (GRACE / GRACE_WINDOW), but with the parentheses + // moved so we don't incur the floordiv and hit 0 + const PENALTY_FREE_DOWNTIME: Duration = Duration::from_secs( + (SESSION_LENGTH.as_secs() * GRACE.as_secs()) / GRACE_WINDOW.as_secs(), + ); + + let downtime_per_slash_point = downtime_per_slash_point(validators); + let penalty_free_slash_points = + PENALTY_FREE_DOWNTIME.as_secs() / downtime_per_slash_point.as_secs(); + + /* + In practice, the following means: + + - Hours 0-12 are penalized as if they're hours 0-12. + - Hours 12-24 are penalized as if they're hours 12-36. + - Hours 24-36 are penalized as if they're hours 36-96. + - Hours 36-48 are penalized as if they're hours 96-168. + - Hours 48-168 are penalized for 0-2% of stake. + - 168-336 hours of slashes, for a session only lasting 168 hours, is penalized for 2-10% + of stake. + + This means a validator offline has to be offline for more than two days to start having + their stake slashed. + */ + + const MULTIPLIERS: [u64; 4] = [1, 2, 5, 6]; + let reward_slash = { + // In intervals of the penalty-free slash points, weight the slash points accrued + // The multiplier for the first interval is 1 as it's penalty-free + let mut weighted_slash_points_for_reward_slash = 0; + let mut total_possible_slash_points_for_rewards_slash = 0; + for mult in MULTIPLIERS { + let slash_points_in_interval = slash_points.min(penalty_free_slash_points); + weighted_slash_points_for_reward_slash += slash_points_in_interval * mult; + total_possible_slash_points_for_rewards_slash += penalty_free_slash_points * mult; + slash_points -= slash_points_in_interval; + } + // If there are no penalty-free slash points, and the validator was slashed, slash the + // entire reward + (u128::from(weighted_slash_points_for_reward_slash) * session_rewards) + .checked_div(u128::from(total_possible_slash_points_for_rewards_slash)) + .unwrap_or({ + if weighted_slash_points_for_reward_slash == 0 { + 0 + } else { + session_rewards + } + }) + }; + + let slash_points_for_entire_session = + SESSION_LENGTH.as_secs() / downtime_per_slash_point.as_secs(); + + let offline_slash = { + // The amount of stake to slash for being offline + const MAX_STAKE_SLASH_PERCENTAGE_OFFLINE: u64 = 2; + + let stake_to_slash_for_being_offline = + (allocated_stake * u128::from(MAX_STAKE_SLASH_PERCENTAGE_OFFLINE)) / 100; + + // We already removed the slash points for `intervals * penalty_free_slash_points` + let slash_points_for_reward_slash = + penalty_free_slash_points * u64::try_from(MULTIPLIERS.len()).unwrap(); + let slash_points_for_offline_stake_slash = + slash_points_for_entire_session.saturating_sub(slash_points_for_reward_slash); + + let slash_points_in_interval = slash_points.min(slash_points_for_offline_stake_slash); + slash_points -= slash_points_in_interval; + // If there are no slash points for the entire session, don't slash stake + // That's an extreme edge case which shouldn't start penalizing validators + (u128::from(slash_points_in_interval) * stake_to_slash_for_being_offline) + .checked_div(u128::from(slash_points_for_offline_stake_slash)) + .unwrap_or(0) + }; + + let disruptive_slash = { + /* + A validator may have more slash points than `slash_points_for_stake_slash` if they + didn't just accrue slashes for missing block proposals, yet also accrued slashes for + being disruptive. In that case, we still want to bound their slash points so they can't + somehow be slashed for 100% of their stake (which should only happen on a fatal slash). + */ + const MAX_STAKE_SLASH_PERCENTAGE_DISRUPTIVE: u64 = 8; + + let stake_to_slash_for_being_disruptive = + (allocated_stake * u128::from(MAX_STAKE_SLASH_PERCENTAGE_DISRUPTIVE)) / 100; + // Follows the offline slash for `unwrap_or` policy + (u128::from(slash_points.min(slash_points_for_entire_session)) * + stake_to_slash_for_being_disruptive) + .checked_div(u128::from(slash_points_for_entire_session)) + .unwrap_or(0) + }; + + // The penalty is all slashes, but never more than the validator's balance + // (handles any rounding errors which may or may not exist) + let penalty_u128 = + (reward_slash + offline_slash + disruptive_slash).min(allocated_stake + session_rewards); + // saturating_into + Amount(u64::try_from(penalty_u128).unwrap_or(u64::MAX)) + } + // On fatal slash, their entire stake is removed + Self::Fatal => Amount(allocated_stake.0 + session_rewards.0), + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SlashReport(pub BoundedVec>); + +// This is assumed binding to the ValidatorSet via the key signed with +pub fn report_slashes_message(slashes: &SlashReport) -> Vec { + (b"ValidatorSets-report_slashes", slashes).encode() +} + +#[test] +fn test_penalty() { + for validators in [1, 50, 100, crate::MAX_KEY_SHARES_PER_SET] { + let validators = NonZero::new(validators).unwrap(); + // 12 hours of slash points should only decrease the rewards proportionately + let twelve_hours_of_slash_points = + u32::try_from((12 * 60 * 60) / downtime_per_slash_point(validators).as_secs()).unwrap(); + assert_eq!( + Slash::Points(twelve_hours_of_slash_points).penalty( + validators, + Amount(u64::MAX), + Amount(168) + ), + Amount(12) + ); + // 24 hours of slash points should be counted as 36 hours + assert_eq!( + Slash::Points(2 * twelve_hours_of_slash_points).penalty( + validators, + Amount(u64::MAX), + Amount(168) + ), + Amount(36) + ); + // 36 hours of slash points should be counted as 96 hours + assert_eq!( + Slash::Points(3 * twelve_hours_of_slash_points).penalty( + validators, + Amount(u64::MAX), + Amount(168) + ), + Amount(96) + ); + // 48 hours of slash points should be counted as 168 hours + assert_eq!( + Slash::Points(4 * twelve_hours_of_slash_points).penalty( + validators, + Amount(u64::MAX), + Amount(168) + ), + Amount(168) + ); + + // A full week of slash points should slash 2% + let week_of_slash_points = 14 * twelve_hours_of_slash_points; + assert_eq!( + Slash::Points(week_of_slash_points).penalty(validators, Amount(1000), Amount(168)), + Amount(20 + 168) + ); + + // Two weeks of slash points should slash 10% + assert_eq!( + Slash::Points(2 * week_of_slash_points).penalty(validators, Amount(1000), Amount(168)), + Amount(100 + 168) + ); + + // Anything greater should still only slash 10% + assert_eq!( + Slash::Points(u32::MAX).penalty(validators, Amount(1000), Amount(168)), + Amount(100 + 168) + ); + } +} + +#[test] +fn no_overflow() { + Slash::Points(u32::MAX).penalty( + NonZero::new(u16::MAX).unwrap(), + Amount(u64::MAX), + Amount(u64::MAX), + ); + + Slash::Points(u32::MAX).penalty(NonZero::new(1).unwrap(), Amount(u64::MAX), Amount(u64::MAX)); +}