#![cfg_attr(not(feature = "std"), no_std)]
use core::marker::PhantomData;
use scale::{Encode, Decode};
use scale_info::TypeInfo;
use sp_std::{vec, vec::Vec};
use sp_core::sr25519::{Public, Signature};
use sp_application_crypto::RuntimePublic;
use sp_session::{ShouldEndSession, GetSessionNumber, GetValidatorCount};
use sp_runtime::{KeyTypeId, ConsensusEngineId, traits::IsMember};
use sp_staking::offence::{ReportOffence, Offence, OffenceError};
use frame_system::{pallet_prelude::*, RawOrigin};
use frame_support::{
pallet_prelude::*,
sp_runtime::SaturatedConversion,
traits::{DisabledValidators, KeyOwnerProofSystem, FindAuthor},
BoundedVec, WeakBoundedVec, StoragePrefixedMap,
};
use serai_primitives::*;
pub use validator_sets_primitives as primitives;
use primitives::*;
use coins_pallet::{Pallet as Coins, AllowMint};
use dex_pallet::Pallet as Dex;
use pallet_babe::{
Pallet as Babe, AuthorityId as BabeAuthorityId, EquivocationOffence as BabeEquivocationOffence,
};
use pallet_grandpa::{
Pallet as Grandpa, AuthorityId as GrandpaAuthorityId,
EquivocationOffence as GrandpaEquivocationOffence,
};
#[derive(Debug, Encode, Decode, TypeInfo, PartialEq, Eq, Clone)]
pub struct MembershipProof<T: pallet::Config>(pub Public, pub PhantomData<T>);
impl<T: pallet::Config> GetSessionNumber for MembershipProof<T> {
fn session(&self) -> u32 {
let current = Pallet::<T>::session(NetworkId::Serai).unwrap().0;
if Babe::<T>::is_member(&BabeAuthorityId::from(self.0)) {
current
} else {
current - 1
}
}
}
impl<T: pallet::Config> GetValidatorCount for MembershipProof<T> {
fn validator_count(&self) -> u32 {
u32::try_from(Babe::<T>::authorities().len()).unwrap()
}
}
#[allow(
deprecated,
unreachable_patterns,
clippy::let_unit_value,
clippy::cast_possible_truncation,
clippy::ignored_unit_patterns
)] #[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::config]
pub trait Config:
frame_system::Config<AccountId = Public>
+ coins_pallet::Config
+ dex_pallet::Config
+ pallet_babe::Config
+ pallet_grandpa::Config
+ TypeInfo
{
type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>;
type ShouldEndSession: ShouldEndSession<BlockNumberFor<Self>>;
}
#[pallet::genesis_config]
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
pub struct GenesisConfig<T: Config> {
pub networks: Vec<(NetworkId, Amount)>,
pub participants: Vec<T::AccountId>,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
GenesisConfig { networks: Default::default(), participants: Default::default() }
}
}
#[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>);
#[pallet::storage]
#[pallet::getter(fn session)]
pub type CurrentSession<T: Config> = StorageMap<_, Identity, NetworkId, Session, OptionQuery>;
impl<T: Config> Pallet<T> {
pub fn latest_decided_session(network: NetworkId) -> Option<Session> {
let session = Self::session(network);
if network == NetworkId::Serai {
return session.map(|s| Session(s.0 + 1));
}
session
}
}
#[pallet::storage]
#[pallet::getter(fn allocation_per_key_share)]
pub type AllocationPerKeyShare<T: Config> =
StorageMap<_, Identity, NetworkId, Amount, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn participants_for_latest_decided_set)]
pub(crate) type Participants<T: Config> = StorageMap<
_,
Identity,
NetworkId,
BoundedVec<(Public, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET }>>,
OptionQuery,
>;
#[pallet::storage]
pub(crate) type InSet<T: Config> =
StorageDoubleMap<_, Identity, NetworkId, Blake2_128Concat, Public, u64, OptionQuery>;
impl<T: Config> Pallet<T> {
#[inline]
fn in_active_serai_set(account: Public) -> bool {
Babe::<T>::is_member(&BabeAuthorityId::from(account))
}
pub fn in_active_set(network: NetworkId, account: Public) -> bool {
if network == NetworkId::Serai {
Self::in_active_serai_set(account)
} else {
InSet::<T>::contains_key(network, account)
}
}
pub fn in_set(network: NetworkId, account: Public) -> bool {
if InSet::<T>::contains_key(network, account) {
return true;
}
if network == NetworkId::Serai {
return Self::in_active_serai_set(account);
}
false
}
pub fn in_latest_decided_set(network: NetworkId, account: Public) -> bool {
InSet::<T>::contains_key(network, account)
}
}
#[pallet::storage]
#[pallet::getter(fn total_allocated_stake)]
pub type TotalAllocatedStake<T: Config> = StorageMap<_, Identity, NetworkId, Amount, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn allocation)]
pub type Allocations<T: Config> =
StorageMap<_, Blake2_128Concat, (NetworkId, Public), Amount, OptionQuery>;
#[pallet::storage]
type SortedAllocations<T: Config> =
StorageMap<_, Identity, (NetworkId, [u8; 8], [u8; 16], Public), (), OptionQuery>;
impl<T: Config> Pallet<T> {
#[inline]
fn sorted_allocation_key(
network: NetworkId,
key: Public,
amount: Amount,
) -> (NetworkId, [u8; 8], [u8; 16], Public) {
let amount = reverse_lexicographic_order(amount.0.to_be_bytes());
let hash = sp_io::hashing::blake2_128(&(network, amount, key).encode());
(network, amount, hash, key)
}
fn recover_amount_from_sorted_allocation_key(key: &[u8]) -> Amount {
let distance_from_end = 8 + 16 + 32;
let start_pos = key.len() - distance_from_end;
let mut raw: [u8; 8] = key[start_pos .. (start_pos + 8)].try_into().unwrap();
for byte in &mut raw {
*byte = !*byte;
}
Amount(u64::from_be_bytes(raw))
}
fn recover_key_from_sorted_allocation_key(key: &[u8]) -> Public {
Public(key[(key.len() - 32) ..].try_into().unwrap())
}
fn set_allocation(network: NetworkId, key: Public, amount: Amount) -> bool {
let prior = Allocations::<T>::take((network, key));
if let Some(amount) = prior {
SortedAllocations::<T>::remove(Self::sorted_allocation_key(network, key, amount));
}
if amount.0 != 0 {
Allocations::<T>::set((network, key), Some(amount));
SortedAllocations::<T>::set(Self::sorted_allocation_key(network, key, amount), Some(()));
}
prior.is_some()
}
}
struct SortedAllocationsIter<T: Config> {
_t: PhantomData<T>,
prefix: Vec<u8>,
last: Vec<u8>,
allocation_per_key_share: Amount,
}
impl<T: Config> SortedAllocationsIter<T> {
fn new(network: NetworkId) -> Self {
let mut prefix = SortedAllocations::<T>::final_prefix().to_vec();
prefix.extend(&network.encode());
Self {
_t: PhantomData,
prefix: prefix.clone(),
last: prefix,
allocation_per_key_share: Pallet::<T>::allocation_per_key_share(network).expect(
"SortedAllocationsIter iterating over a network without a set allocation per key share",
),
}
}
}
impl<T: Config> Iterator for SortedAllocationsIter<T> {
type Item = (Public, Amount);
fn next(&mut self) -> Option<Self::Item> {
let next = sp_io::storage::next_key(&self.last)?;
if !next.starts_with(&self.prefix) {
None?;
}
let key = Pallet::<T>::recover_key_from_sorted_allocation_key(&next);
let amount = Pallet::<T>::recover_amount_from_sorted_allocation_key(&next);
if amount.0 < self.allocation_per_key_share.0 {
None?;
}
self.last = next;
Some((key, amount))
}
}
#[pallet::storage]
type PendingDeallocations<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
(NetworkId, Public),
Identity,
Session,
Amount,
OptionQuery,
>;
#[pallet::storage]
#[pallet::getter(fn keys)]
pub type Keys<T: Config> =
StorageMap<_, Twox64Concat, ExternalValidatorSet, KeyPair, OptionQuery>;
#[pallet::storage]
pub type PendingSlashReport<T: Config> =
StorageMap<_, Identity, ExternalNetworkId, Public, OptionQuery>;
#[pallet::storage]
pub type SeraiDisabledIndices<T: Config> = StorageMap<_, Identity, u32, Public, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn session_begin_block)]
pub type SessionBeginBlock<T: Config> =
StorageDoubleMap<_, Identity, NetworkId, Identity, Session, u64, ValueQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
NewSet {
set: ValidatorSet,
},
ParticipantRemoved {
set: ValidatorSet,
removed: T::AccountId,
},
KeyGen {
set: ExternalValidatorSet,
key_pair: KeyPair,
},
AcceptedHandover {
set: ValidatorSet,
},
SetRetired {
set: ValidatorSet,
},
AllocationIncreased {
validator: T::AccountId,
network: NetworkId,
amount: Amount,
},
AllocationDecreased {
validator: T::AccountId,
network: NetworkId,
amount: Amount,
delayed_until: Option<Session>,
},
DeallocationClaimed {
validator: T::AccountId,
network: NetworkId,
session: Session,
},
}
impl<T: Config> Pallet<T> {
fn new_set(network: NetworkId) {
let session = {
let new_session =
CurrentSession::<T>::get(network).map_or(Session(0), |session| Session(session.0 + 1));
CurrentSession::<T>::set(network, Some(new_session));
new_session
};
assert_eq!(
InSet::<T>::clear_prefix(network, MAX_KEY_SHARES_PER_SET, None).maybe_cursor,
None
);
let allocation_per_key_share = Self::allocation_per_key_share(network).unwrap().0;
let mut participants = vec![];
{
let mut iter = SortedAllocationsIter::<T>::new(network);
let mut key_shares = 0;
while key_shares < u64::from(MAX_KEY_SHARES_PER_SET) {
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));
participants.push((key, these_key_shares));
key_shares += these_key_shares;
}
amortize_excess_key_shares(&mut participants);
}
for (key, shares) in &participants {
InSet::<T>::set(network, key, Some(*shares));
}
let set = ValidatorSet { network, session };
Pallet::<T>::deposit_event(Event::NewSet { set });
Participants::<T>::set(network, Some(participants.try_into().unwrap()));
SessionBeginBlock::<T>::set(
network,
session,
<frame_system::Pallet<T>>::block_number().saturated_into::<u64>(),
);
}
}
#[pallet::error]
pub enum Error<T> {
NonExistentValidatorSet,
InsufficientAllocation,
NotEnoughAllocated,
AllocationWouldRemoveFaultTolerance,
AllocationWouldPreventFaultTolerance,
DeallocationWouldRemoveParticipant,
DeallocationWouldRemoveFaultTolerance,
NonExistentDeallocation,
AlreadyGeneratedKeys,
BadSignature,
NonExistentValidator,
DeallocationWouldRemoveEconomicSecurity,
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(n: BlockNumberFor<T>) -> Weight {
if T::ShouldEndSession::should_end_session(n) {
Self::rotate_session();
T::BlockWeights::get().max_block
} else {
Weight::zero()
}
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
for (id, stake) in self.networks.clone() {
AllocationPerKeyShare::<T>::set(id, Some(stake));
for participant in self.participants.clone() {
if Pallet::<T>::set_allocation(id, participant, stake) {
panic!("participants contained duplicates");
}
}
Pallet::<T>::new_set(id);
}
}
}
impl<T: Config> Pallet<T> {
fn account() -> T::AccountId {
system_address(b"ValidatorSets").into()
}
fn is_bft(network: NetworkId) -> bool {
let allocation_per_key_share = AllocationPerKeyShare::<T>::get(network).unwrap().0;
let mut validators_len = 0;
let mut top = None;
let mut key_shares = 0;
for (_, amount) in SortedAllocationsIter::<T>::new(network) {
validators_len += 1;
key_shares += amount.0 / allocation_per_key_share;
if top.is_none() {
top = Some(key_shares);
}
if key_shares > u64::from(MAX_KEY_SHARES_PER_SET) {
break;
}
}
let Some(top) = top else { return false };
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())
}
fn increase_allocation(
network: NetworkId,
account: T::AccountId,
amount: Amount,
block_reward: bool,
) -> DispatchResult {
let old_allocation = Self::allocation((network, account)).unwrap_or(Amount(0)).0;
let new_allocation = old_allocation + amount.0;
let allocation_per_key_share = Self::allocation_per_key_share(network).unwrap().0;
if (new_allocation < allocation_per_key_share) && (!block_reward) {
Err(Error::<T>::InsufficientAllocation)?;
}
let increased_key_shares =
(old_allocation / allocation_per_key_share) < (new_allocation / allocation_per_key_share);
let mut was_bft = None;
if increased_key_shares {
was_bft = Some(Self::is_bft(network));
}
Self::set_allocation(network, account, Amount(new_allocation));
Self::deposit_event(Event::AllocationIncreased { validator: account, network, amount });
if let Some(was_bft) = was_bft {
if was_bft && (!Self::is_bft(network)) {
Err(Error::<T>::AllocationWouldRemoveFaultTolerance)?;
}
}
if (new_allocation / allocation_per_key_share) >= (MAX_KEY_SHARES_PER_SET / 3).into() {
Err(Error::<T>::AllocationWouldPreventFaultTolerance)?;
}
if let Some(session) = Self::session(network) {
if InSet::<T>::contains_key(network, account) && Self::handover_completed(network, session)
{
TotalAllocatedStake::<T>::set(
network,
Some(Amount(TotalAllocatedStake::<T>::get(network).unwrap_or(Amount(0)).0 + amount.0)),
);
}
}
Ok(())
}
fn session_to_unlock_on_for_current_set(network: NetworkId) -> Option<Session> {
let mut to_unlock_on = Self::session(network)?;
to_unlock_on.0 += 1;
if network == NetworkId::Serai {
to_unlock_on.0 += 1;
}
to_unlock_on.0 += 1;
Some(to_unlock_on)
}
fn decrease_allocation(
network: NetworkId,
account: T::AccountId,
amount: Amount,
) -> Result<bool, DispatchError> {
if let NetworkId::External(n) = network {
let new_total_staked = Self::total_allocated_stake(NetworkId::from(n))
.unwrap()
.0
.checked_sub(amount.0)
.ok_or(Error::<T>::NotEnoughAllocated)?;
let required_stake = Self::required_stake_for_network(n);
if new_total_staked < required_stake {
Err(Error::<T>::DeallocationWouldRemoveEconomicSecurity)?;
}
}
let old_allocation =
Self::allocation((network, account)).ok_or(Error::<T>::NonExistentValidator)?.0;
let new_allocation =
old_allocation.checked_sub(amount.0).ok_or(Error::<T>::NotEnoughAllocated)?;
let allocation_per_key_share = Self::allocation_per_key_share(network).unwrap().0;
if (new_allocation != 0) && (new_allocation < allocation_per_key_share) {
Err(Error::<T>::DeallocationWouldRemoveParticipant)?;
}
let decreased_key_shares =
(old_allocation / allocation_per_key_share) > (new_allocation / allocation_per_key_share);
let mut was_bft = None;
if decreased_key_shares {
was_bft = Some(Self::is_bft(network));
}
Self::set_allocation(network, account, Amount(new_allocation));
if let Some(was_bft) = was_bft {
if was_bft && (!Self::is_bft(network)) {
Err(Error::<T>::DeallocationWouldRemoveFaultTolerance)?;
}
}
if !Self::in_set(network, account) {
Self::deposit_event(Event::AllocationDecreased {
validator: account,
network,
amount,
delayed_until: None,
});
return Ok(true);
}
let to_unlock_on = Self::session_to_unlock_on_for_current_set(network).unwrap();
let existing =
PendingDeallocations::<T>::get((network, account), to_unlock_on).unwrap_or(Amount(0));
PendingDeallocations::<T>::set(
(network, account),
to_unlock_on,
Some(Amount(existing.0 + amount.0)),
);
Self::deposit_event(Event::AllocationDecreased {
validator: account,
network,
amount,
delayed_until: Some(to_unlock_on),
});
Ok(false)
}
fn handover_completed(network: NetworkId, session: Session) -> bool {
let Some(current_session) = Self::session(network) else { return false };
if current_session.0 > session.0 {
return true;
}
if current_session.0 < session.0 {
return false;
}
let NetworkId::External(n) = network else {
return true;
};
if !Keys::<T>::contains_key(ExternalValidatorSet { network: n, session }) {
return false;
}
(session.0 == 0) ||
(!Keys::<T>::contains_key(ExternalValidatorSet {
network: n,
session: Session(session.0 - 1),
}))
}
fn new_session() {
for network in serai_primitives::NETWORKS {
let Some(current_session) = Self::session(network) else { continue };
if (network == NetworkId::Serai) || Self::handover_completed(network, current_session) {
Pallet::<T>::new_set(network);
Dex::<T>::on_new_session(network);
}
}
}
fn set_total_allocated_stake(network: NetworkId) {
let participants = Participants::<T>::get(network)
.expect("setting TotalAllocatedStake for a network without participants");
let total_stake = participants.iter().fold(0, |acc, (addr, _)| {
acc + Allocations::<T>::get((network, addr)).unwrap_or(Amount(0)).0
});
TotalAllocatedStake::<T>::set(network, Some(Amount(total_stake)));
}
pub fn retire_set(set: ValidatorSet) {
if let NetworkId::External(n) = set.network {
if PendingSlashReport::<T>::get(n).is_some() {
Self::deposit_event(Event::SetRetired {
set: ValidatorSet { network: set.network, session: Session(set.session.0 - 1) },
});
}
let keys =
Keys::<T>::take(ExternalValidatorSet { network: n, session: set.session }).unwrap();
PendingSlashReport::<T>::set(n, Some(keys.0));
} else {
Self::deposit_event(Event::SetRetired { set });
}
Self::deposit_event(Event::AcceptedHandover {
set: ValidatorSet { network: set.network, session: Session(set.session.0 + 1) },
});
Self::set_total_allocated_stake(set.network);
}
fn take_deallocatable_amount(
network: NetworkId,
session: Session,
key: Public,
) -> Option<Amount> {
if !Self::handover_completed(network, session) {
return None;
}
PendingDeallocations::<T>::take((network, key), session)
}
fn rotate_session() {
let now_validators = Participants::<T>::get(NetworkId::Serai)
.expect("no Serai participants upon rotate_session");
let prior_serai_session = Self::session(NetworkId::Serai).unwrap();
Self::retire_set(ValidatorSet { network: NetworkId::Serai, session: prior_serai_session });
Self::new_session();
let session = prior_serai_session.0 + 1;
let next_validators = Participants::<T>::get(NetworkId::Serai).unwrap();
Babe::<T>::enact_epoch_change(
WeakBoundedVec::force_from(
now_validators.iter().copied().map(|(id, w)| (BabeAuthorityId::from(id), w)).collect(),
None,
),
WeakBoundedVec::force_from(
next_validators.iter().copied().map(|(id, w)| (BabeAuthorityId::from(id), w)).collect(),
None,
),
Some(session),
);
Grandpa::<T>::new_session(
true,
session,
now_validators.into_iter().map(|(id, w)| (GrandpaAuthorityId::from(id), w)).collect(),
);
let mut disabled = vec![];
for (_, validator) in SeraiDisabledIndices::<T>::drain() {
disabled.push(validator);
}
for disabled in disabled {
Self::disable_serai_validator(disabled);
}
}
pub fn required_stake(balance: &ExternalBalance) -> SubstrateAmount {
use dex_pallet::HigherPrecisionBalance;
let sri_per_coin = Dex::<T>::security_oracle_value(balance.coin).unwrap_or(Amount(0));
let coin_decimals = balance.coin.decimals().max(5);
let accuracy_increase = HigherPrecisionBalance::from(SubstrateAmount::pow(10, coin_decimals));
let total_coin_value = u64::try_from(
HigherPrecisionBalance::from(balance.amount.0) *
HigherPrecisionBalance::from(sri_per_coin.0) /
accuracy_increase,
)
.unwrap_or(u64::MAX);
let required_stake = total_coin_value.saturating_mul(3).saturating_div(2);
required_stake.saturating_add(total_coin_value.saturating_div(5))
}
pub fn required_stake_for_network(network: ExternalNetworkId) -> SubstrateAmount {
let mut total_required = 0;
for coin in network.coins() {
let supply = Coins::<T>::supply(Coin::from(coin));
total_required += Self::required_stake(&ExternalBalance { coin, amount: Amount(supply) });
}
total_required
}
pub fn distribute_block_rewards(
network: NetworkId,
account: T::AccountId,
amount: Amount,
) -> DispatchResult {
Coins::<T>::transfer_internal(
account,
Self::account(),
Balance { coin: Coin::Serai, amount },
)?;
Self::increase_allocation(network, account, amount, true)
}
fn can_slash_serai_validator(validator: Public) -> bool {
Babe::<T>::is_member(&BabeAuthorityId::from(validator)) ||
PendingDeallocations::<T>::iter_prefix((NetworkId::Serai, validator)).next().is_some()
}
fn slash_serai_validator(validator: Public) {
let network = NetworkId::Serai;
let mut allocation = Self::allocation((network, validator)).unwrap_or(Amount(0));
Self::set_allocation(network, validator, Amount(0));
allocation.0 += PendingDeallocations::<T>::take(
(network, validator),
Self::session_to_unlock_on_for_current_set(network).unwrap(),
)
.unwrap_or(Amount(0))
.0;
if InSet::<T>::contains_key(NetworkId::Serai, validator) {
let current_staked = Self::total_allocated_stake(network).unwrap();
TotalAllocatedStake::<T>::set(network, Some(current_staked - allocation));
}
for (_, pending) in PendingDeallocations::<T>::drain_prefix((network, validator)) {
allocation.0 += pending.0;
}
Coins::<T>::burn(
RawOrigin::Signed(Self::account()).into(),
Balance { coin: Coin::Serai, amount: allocation },
)
.unwrap();
}
fn disable_serai_validator(validator: Public) -> bool {
if let Some(index) =
Babe::<T>::authorities().into_iter().position(|(id, _)| id.into_inner() == validator)
{
SeraiDisabledIndices::<T>::set(u32::try_from(index).unwrap(), Some(validator));
let session = Self::session(NetworkId::Serai).unwrap();
Self::deposit_event(Event::ParticipantRemoved {
set: ValidatorSet { network: NetworkId::Serai, session },
removed: validator,
});
true
} else {
false
}
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(0)] pub fn set_keys(
origin: OriginFor<T>,
network: ExternalNetworkId,
removed_participants: BoundedVec<Public, ConstU32<{ MAX_KEY_SHARES_PER_SET / 3 }>>,
key_pair: KeyPair,
signature: Signature,
) -> DispatchResult {
ensure_none(origin)?;
let _ = signature;
let session = Self::session(NetworkId::from(network)).unwrap();
let set = ExternalValidatorSet { network, session };
Keys::<T>::set(set, Some(key_pair.clone()));
if session == Session(0) {
Self::set_total_allocated_stake(NetworkId::from(network));
}
for removed in removed_participants {
Self::deposit_event(Event::ParticipantRemoved { set: set.into(), removed });
}
Self::deposit_event(Event::KeyGen { set, key_pair });
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(0)] pub fn report_slashes(
origin: OriginFor<T>,
network: ExternalNetworkId,
slashes: BoundedVec<(Public, u32), ConstU32<{ MAX_KEY_SHARES_PER_SET / 3 }>>,
signature: Signature,
) -> DispatchResult {
ensure_none(origin)?;
let _ = signature;
let _ = slashes;
Pallet::<T>::deposit_event(Event::SetRetired {
set: ValidatorSet {
network: network.into(),
session: Session(Self::session(NetworkId::from(network)).unwrap().0 - 1),
},
});
Ok(())
}
#[pallet::call_index(2)]
#[pallet::weight(0)] pub fn allocate(origin: OriginFor<T>, network: NetworkId, amount: Amount) -> DispatchResult {
let validator = ensure_signed(origin)?;
Coins::<T>::transfer_internal(
validator,
Self::account(),
Balance { coin: Coin::Serai, amount },
)?;
Self::increase_allocation(network, validator, amount, false)
}
#[pallet::call_index(3)]
#[pallet::weight(0)] pub fn deallocate(origin: OriginFor<T>, network: NetworkId, amount: Amount) -> DispatchResult {
let account = ensure_signed(origin)?;
let can_immediately_deallocate = Self::decrease_allocation(network, account, amount)?;
if can_immediately_deallocate {
Coins::<T>::transfer_internal(
Self::account(),
account,
Balance { coin: Coin::Serai, amount },
)?;
}
Ok(())
}
#[pallet::call_index(4)]
#[pallet::weight((0, DispatchClass::Operational))] pub fn claim_deallocation(
origin: OriginFor<T>,
network: NetworkId,
session: Session,
) -> DispatchResult {
let account = ensure_signed(origin)?;
let Some(amount) = Self::take_deallocatable_amount(network, session, account) else {
Err(Error::<T>::NonExistentDeallocation)?
};
Coins::<T>::transfer_internal(
Self::account(),
account,
Balance { coin: Coin::Serai, amount },
)?;
Self::deposit_event(Event::DeallocationClaimed { validator: account, network, session });
Ok(())
}
}
#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity {
match call {
Call::set_keys { network, ref removed_participants, ref key_pair, ref signature } => {
let network = *network;
let Some(current_session) = Self::session(NetworkId::from(network)) else {
Err(InvalidTransaction::Custom(1))?
};
let set = ExternalValidatorSet { network, session: current_session };
if Keys::<T>::get(set).is_some() {
Err(InvalidTransaction::Stale)?;
}
assert_eq!(Pallet::<T>::latest_decided_session(network.into()), Some(current_session));
let mut removed = hashbrown::HashSet::new();
for participant in removed_participants {
if removed.contains(&participant.0) {
Err(InvalidTransaction::Custom(2))?;
}
removed.insert(participant.0);
}
let participants = Participants::<T>::get(NetworkId::from(network))
.expect("session existed without participants");
let mut all_key_shares = 0;
let mut signers = vec![];
let mut signing_key_shares = 0;
for participant in participants {
let participant = participant.0;
let shares = InSet::<T>::get(NetworkId::from(network), participant)
.expect("participant from Participants wasn't InSet");
all_key_shares += shares;
if removed.contains(&participant.0) {
continue;
}
signers.push(participant);
signing_key_shares += shares;
}
{
let f = all_key_shares - signing_key_shares;
if signing_key_shares < ((2 * f) + 1) {
Err(InvalidTransaction::Custom(3))?;
}
}
if !musig_key(set.into(), &signers)
.verify(&set_keys_message(&set, removed_participants, key_pair), signature)
{
Err(InvalidTransaction::BadProof)?;
}
ValidTransaction::with_tag_prefix("ValidatorSets")
.and_provides((0, set))
.longevity(u64::MAX)
.propagate(true)
.build()
}
Call::report_slashes { network, ref slashes, ref signature } => {
let network = *network;
let Some(key) = PendingSlashReport::<T>::take(network) else {
Err(InvalidTransaction::Stale)?
};
let set = ExternalValidatorSet {
network,
session: Session(Self::session(NetworkId::from(network)).unwrap().0 - 1),
};
if !key.verify(&report_slashes_message(&set, slashes), signature) {
Err(InvalidTransaction::BadProof)?;
}
ValidTransaction::with_tag_prefix("ValidatorSets")
.and_provides((1, set))
.longevity(MAX_KEY_SHARES_PER_SET.into())
.propagate(true)
.build()
}
Call::allocate { .. } | Call::deallocate { .. } | Call::claim_deallocation { .. } => {
Err(InvalidTransaction::Call)?
}
Call::__Ignore(_, _) => unreachable!(),
}
}
fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ()).map_err(Into::into)
}
}
impl<T: Config> AllowMint for Pallet<T> {
fn is_allowed(balance: &ExternalBalance) -> bool {
let current_required = Self::required_stake_for_network(balance.coin.network());
let new_required = current_required + Self::required_stake(balance);
let staked =
Self::total_allocated_stake(NetworkId::from(balance.coin.network())).unwrap_or(Amount(0));
staked.0 >= new_required
}
}
#[rustfmt::skip]
impl<T: Config, V: Into<Public> + From<Public>> KeyOwnerProofSystem<(KeyTypeId, V)> for Pallet<T> {
type Proof = MembershipProof<T>;
type IdentificationTuple = Public;
fn prove(key: (KeyTypeId, V)) -> Option<Self::Proof> {
Some(MembershipProof(key.1.into(), PhantomData))
}
fn check_proof(key: (KeyTypeId, V), proof: Self::Proof) -> Option<Self::IdentificationTuple> {
let validator = key.1.into();
if validator != proof.0 {
return None;
}
if !Self::can_slash_serai_validator(validator) {
return None;
}
Some(validator)
}
}
impl<T: Config> ReportOffence<Public, Public, BabeEquivocationOffence<Public>> for Pallet<T> {
fn report_offence(
_: Vec<Public>,
offence: BabeEquivocationOffence<Public>,
) -> Result<(), OffenceError> {
let offender = offence.offender;
Self::slash_serai_validator(offender);
Self::disable_serai_validator(offender);
Ok(())
}
fn is_known_offence(
offenders: &[Public],
_: &<BabeEquivocationOffence<Public> as Offence<Public>>::TimeSlot,
) -> bool {
for offender in offenders {
if Self::can_slash_serai_validator(*offender) {
return false;
}
}
true
}
}
impl<T: Config> ReportOffence<Public, Public, GrandpaEquivocationOffence<Public>> for Pallet<T> {
fn report_offence(
_: Vec<Public>,
offence: GrandpaEquivocationOffence<Public>,
) -> Result<(), OffenceError> {
let offender = offence.offender;
Self::slash_serai_validator(offender);
Self::disable_serai_validator(offender);
Ok(())
}
fn is_known_offence(
offenders: &[Public],
_slot: &<GrandpaEquivocationOffence<Public> as Offence<Public>>::TimeSlot,
) -> bool {
for offender in offenders {
if Self::can_slash_serai_validator(*offender) {
return false;
}
}
true
}
}
impl<T: Config> FindAuthor<Public> for Pallet<T> {
fn find_author<'a, I>(digests: I) -> Option<Public>
where
I: 'a + IntoIterator<Item = (ConsensusEngineId, &'a [u8])>,
{
let i = Babe::<T>::find_author(digests)?;
Some(Babe::<T>::authorities()[i as usize].0.clone().into())
}
}
impl<T: Config> DisabledValidators for Pallet<T> {
fn is_disabled(index: u32) -> bool {
SeraiDisabledIndices::<T>::get(index).is_some()
}
}
}
pub use pallet::*;