mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 20:29:23 +00:00
Redesign Slash/SlashReport types with a function to calculate the penalty
This commit is contained in:
@@ -159,8 +159,9 @@ impl<D: Db> ContinuallyRan for EphemeralEventStream<D> {
|
|||||||
Err("validator's weight exceeded u16::MAX".to_string())?
|
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::<u32>();
|
let total_weight = validators.iter().map(|(_, weight)| u32::from(*weight)).sum::<u32>();
|
||||||
if total_weight > MAX_KEY_SHARES_PER_SET {
|
if total_weight > u32::from(MAX_KEY_SHARES_PER_SET) {
|
||||||
Err(format!(
|
Err(format!(
|
||||||
"{set:?} has {total_weight} key shares when the max is {MAX_KEY_SHARES_PER_SET}"
|
"{set:?} has {total_weight} key shares when the max is {MAX_KEY_SHARES_PER_SET}"
|
||||||
))?;
|
))?;
|
||||||
|
|||||||
@@ -352,8 +352,11 @@ impl<'a, TD: Db, TDT: DbTxn, P: P2p> ScanBlock<'a, TD, TDT, P> {
|
|||||||
// Create the resulting slash report
|
// Create the resulting slash report
|
||||||
let mut slash_report = vec![];
|
let mut slash_report = vec![];
|
||||||
for (validator, points) in self.validators.iter().copied().zip(amortized_slash_report) {
|
for (validator, points) in self.validators.iter().copied().zip(amortized_slash_report) {
|
||||||
if points != 0 {
|
// TODO: Natively store this as a `Slash`
|
||||||
slash_report.push(Slash { key: validator.into(), points });
|
if points == u32::MAX {
|
||||||
|
slash_report.push(Slash::Fatal);
|
||||||
|
} else {
|
||||||
|
slash_report.push(Slash::Points(points));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert!(slash_report.len() <= f);
|
assert!(slash_report.len() <= f);
|
||||||
|
|||||||
@@ -301,14 +301,14 @@ impl TransactionTrait for Transaction {
|
|||||||
Transaction::Batch { .. } => {}
|
Transaction::Batch { .. } => {}
|
||||||
|
|
||||||
Transaction::Sign { data, .. } => {
|
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)?
|
Err(TransactionError::InvalidContent)?
|
||||||
}
|
}
|
||||||
// TODO: MAX_SIGN_LEN
|
// TODO: MAX_SIGN_LEN
|
||||||
}
|
}
|
||||||
|
|
||||||
Transaction::SlashReport { slash_points, .. } => {
|
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)?
|
Err(TransactionError::InvalidContent)?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub enum Call {
|
|||||||
},
|
},
|
||||||
report_slashes {
|
report_slashes {
|
||||||
network: NetworkId,
|
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,
|
signature: Signature,
|
||||||
},
|
},
|
||||||
allocate {
|
allocate {
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ impl<'a> SeraiValidatorSets<'a> {
|
|||||||
// (50 * (32 + 4)) > (150 * 4)
|
// (50 * (32 + 4)) > (150 * 4)
|
||||||
slashes: sp_runtime::BoundedVec<
|
slashes: sp_runtime::BoundedVec<
|
||||||
(SeraiAddress, u32),
|
(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,
|
signature: Signature,
|
||||||
) -> Transaction {
|
) -> Transaction {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ pub mod pallet {
|
|||||||
use economic_security_pallet::{Config as EconomicSecurityConfig, Pallet as EconomicSecurity};
|
use economic_security_pallet::{Config as EconomicSecurityConfig, Pallet as EconomicSecurity};
|
||||||
|
|
||||||
use serai_primitives::*;
|
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;
|
pub use emissions_primitives as primitives;
|
||||||
use primitives::*;
|
use primitives::*;
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ pub mod pallet {
|
|||||||
_,
|
_,
|
||||||
Identity,
|
Identity,
|
||||||
NetworkId,
|
NetworkId,
|
||||||
BoundedVec<(PublicKey, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET }>>,
|
BoundedVec<(PublicKey, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET_U32 }>>,
|
||||||
OptionQuery,
|
OptionQuery,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::BlockNumber;
|
|||||||
// 1 MB
|
// 1 MB
|
||||||
pub const BLOCK_SIZE: u32 = 1024 * 1024;
|
pub const BLOCK_SIZE: u32 = 1024 * 1024;
|
||||||
// 6 seconds
|
// 6 seconds
|
||||||
|
// TODO: Use Duration
|
||||||
pub const TARGET_BLOCK_TIME: u64 = 6;
|
pub const TARGET_BLOCK_TIME: u64 = 6;
|
||||||
|
|
||||||
/// Measured in blocks.
|
/// Measured in blocks.
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ impl pallet_authorship::Config for Runtime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Maximum number of authorities per session.
|
// 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.
|
/// Longevity of an offence report.
|
||||||
pub type ReportLongevity = <Runtime as pallet_babe::Config>::EpochDuration;
|
pub type ReportLongevity = <Runtime as pallet_babe::Config>::EpochDuration;
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ pub mod pallet {
|
|||||||
_,
|
_,
|
||||||
Identity,
|
Identity,
|
||||||
NetworkId,
|
NetworkId,
|
||||||
BoundedVec<(Public, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET }>>,
|
BoundedVec<(Public, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET_U32 }>>,
|
||||||
OptionQuery,
|
OptionQuery,
|
||||||
>;
|
>;
|
||||||
/// The validators selected to be in-set, regardless of if removed.
|
/// The validators selected to be in-set, regardless of if removed.
|
||||||
@@ -402,7 +402,7 @@ pub mod pallet {
|
|||||||
|
|
||||||
// Clear the current InSet
|
// Clear the current InSet
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
InSet::<T>::clear_prefix(network, MAX_KEY_SHARES_PER_SET, None).maybe_cursor,
|
InSet::<T>::clear_prefix(network, MAX_KEY_SHARES_PER_SET_U32, None).maybe_cursor,
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -412,11 +412,11 @@ pub mod pallet {
|
|||||||
{
|
{
|
||||||
let mut iter = SortedAllocationsIter::<T>::new(network);
|
let mut iter = SortedAllocationsIter::<T>::new(network);
|
||||||
let mut key_shares = 0;
|
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 Some((key, amount)) = iter.next() else { break };
|
||||||
|
|
||||||
let these_key_shares =
|
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));
|
participants.push((key, these_key_shares));
|
||||||
|
|
||||||
key_shares += these_key_shares;
|
key_shares += these_key_shares;
|
||||||
@@ -535,7 +535,7 @@ pub mod pallet {
|
|||||||
top = Some(key_shares);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,7 +547,7 @@ pub mod pallet {
|
|||||||
// post_amortization_key_shares_for_top_validator yields what the top validator's key shares
|
// 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
|
// 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);
|
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(
|
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
|
// 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
|
// 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::<T>::AllocationWouldPreventFaultTolerance)?;
|
Err(Error::<T>::AllocationWouldPreventFaultTolerance)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1010,7 +1010,7 @@ pub mod pallet {
|
|||||||
pub fn report_slashes(
|
pub fn report_slashes(
|
||||||
origin: OriginFor<T>,
|
origin: OriginFor<T>,
|
||||||
network: NetworkId,
|
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,
|
signature: Signature,
|
||||||
) -> DispatchResult {
|
) -> DispatchResult {
|
||||||
ensure_none(origin)?;
|
ensure_none(origin)?;
|
||||||
@@ -1209,7 +1209,7 @@ pub mod pallet {
|
|||||||
|
|
||||||
ValidTransaction::with_tag_prefix("ValidatorSets")
|
ValidTransaction::with_tag_prefix("ValidatorSets")
|
||||||
.and_provides((1, set))
|
.and_provides((1, set))
|
||||||
.longevity(MAX_KEY_SHARES_PER_SET.into())
|
.longevity(MAX_KEY_SHARES_PER_SET_U32.into())
|
||||||
.propagate(true)
|
.propagate(true)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
|
use core::time::Duration;
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
@@ -13,20 +15,30 @@ use borsh::{BorshSerialize, BorshDeserialize};
|
|||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
use sp_core::{ConstU32, sr25519::Public, bounded::BoundedVec};
|
use sp_core::{ConstU32, bounded::BoundedVec, sr25519::Public};
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use sp_std::vec::Vec;
|
use sp_std::vec::Vec;
|
||||||
|
|
||||||
use serai_primitives::NetworkId;
|
use serai_primitives::NetworkId;
|
||||||
|
|
||||||
/// The maximum amount of key shares per set.
|
mod slash_points;
|
||||||
pub const MAX_KEY_SHARES_PER_SET: u32 = 150;
|
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).
|
// Support keys up to 96 bytes (BLS12-381 G2).
|
||||||
pub const MAX_KEY_LEN: u32 = 96;
|
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.
|
/// The type used to identify a specific session of validators.
|
||||||
#[derive(
|
#[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 = "std", derive(Zeroize))]
|
||||||
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
|
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
|
||||||
@@ -34,7 +46,9 @@ pub const MAX_KEY_LEN: u32 = 96;
|
|||||||
pub struct Session(pub u32);
|
pub struct Session(pub u32);
|
||||||
|
|
||||||
/// The type used to identify a specific validator set during a specific session.
|
/// 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 = "std", derive(Zeroize))]
|
||||||
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
|
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
@@ -43,13 +57,13 @@ pub struct ValidatorSet {
|
|||||||
pub network: NetworkId,
|
pub network: NetworkId,
|
||||||
}
|
}
|
||||||
|
|
||||||
type MaxKeyLen = ConstU32<MAX_KEY_LEN>;
|
|
||||||
/// The type representing a Key from an external network.
|
/// The type representing a Key from an external network.
|
||||||
pub type ExternalKey = BoundedVec<u8, MaxKeyLen>;
|
pub type ExternalKey = BoundedVec<u8, ConstU32<MAX_KEY_LEN>>;
|
||||||
|
|
||||||
/// The key pair for a validator set.
|
/// 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)]
|
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)]
|
||||||
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
|
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
@@ -81,12 +95,12 @@ impl Zeroize for KeyPair {
|
|||||||
|
|
||||||
/// The MuSig context for a validator set.
|
/// The MuSig context for a validator set.
|
||||||
pub fn musig_context(set: ValidatorSet) -> Vec<u8> {
|
pub fn musig_context(set: ValidatorSet) -> Vec<u8> {
|
||||||
[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.
|
/// 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 {
|
pub fn musig_key(set: ValidatorSet, set_keys: &[Public]) -> Public {
|
||||||
let mut keys = Vec::new();
|
let mut keys = Vec::new();
|
||||||
for key in set_keys {
|
for key in set_keys {
|
||||||
@@ -98,33 +112,11 @@ pub fn musig_key(set: ValidatorSet, set_keys: &[Public]) -> Public {
|
|||||||
Public(dkg::musig::musig_key::<Ristretto>(&musig_context(set), &keys).unwrap().to_bytes())
|
Public(dkg::musig::musig_key::<Ristretto>(&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<u8> {
|
pub fn set_keys_message(set: &ValidatorSet, key_pair: &KeyPair) -> Vec<u8> {
|
||||||
(b"ValidatorSets-set_keys", set, key_pair).encode()
|
(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<Slash, ConstU32<{ MAX_KEY_SHARES_PER_SET / 3 }>>);
|
|
||||||
|
|
||||||
pub fn report_slashes_message(set: &ValidatorSet, slashes: &SlashReport) -> Vec<u8> {
|
|
||||||
(b"ValidatorSets-report_slashes", set, slashes).encode()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For a set of validators whose key shares may exceed the maximum, reduce until they equal the
|
/// For a set of validators whose key shares may exceed the maximum, reduce until they equal the
|
||||||
/// maximum.
|
/// maximum.
|
||||||
///
|
///
|
||||||
|
|||||||
258
substrate/validator-sets/primitives/src/slash_points.rs
Normal file
258
substrate/validator-sets/primitives/src/slash_points.rs
Normal file
@@ -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<u16> so that the result is never 0.
|
||||||
|
fn downtime_per_slash_point(validators: NonZero<u16>) -> 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<u16>,
|
||||||
|
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<Slash, ConstU32<{ MAX_KEY_SHARES_PER_SET_U32 }>>);
|
||||||
|
|
||||||
|
// This is assumed binding to the ValidatorSet via the key signed with
|
||||||
|
pub fn report_slashes_message(slashes: &SlashReport) -> Vec<u8> {
|
||||||
|
(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));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user