#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc = include_str!("../README.md")] #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; mod embedded_elliptic_curve_keys; use embedded_elliptic_curve_keys::*; mod allocations; use allocations::*; mod sessions; use sessions::{*, GenesisValidators as GenesisValidatorsContainer}; /* use core::marker::PhantomData; use scale::{Encode, Decode}; 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, PartialEq, Eq, Clone)] pub struct MembershipProof(pub Public, pub PhantomData); impl GetSessionNumber for MembershipProof { fn session(&self) -> u32 { let current = Pallet::::session(NetworkId::Serai).unwrap().0; if Babe::::is_member(&BabeAuthorityId::from(self.0)) { current } else { // if it isn't in the current session, it should have been in the previous one. current - 1 } } } impl GetValidatorCount for MembershipProof { // We only implement and this interface to satisfy trait requirements // Although this might return the wrong count if the offender was in the previous set, we don't // rely on it and Substrate only relies on it to offer economic calculations we also don't rely // on fn validator_count(&self) -> u32 { u32::try_from(Babe::::authorities().len()).unwrap() } } */ #[expect(clippy::ignored_unit_patterns, clippy::cast_possible_truncation)] #[frame_support::pallet] mod pallet { use sp_core::sr25519::Public; use frame_system::pallet_prelude::*; use frame_support::pallet_prelude::*; use serai_primitives::{ crypto::KeyPair, network_id::*, coin::*, balance::*, validator_sets::*, address::SeraiAddress, }; use coins_pallet::Pallet as Coins; use super::*; #[pallet::config] pub trait Config: frame_system::Config + coins_pallet::Config { type RuntimeEvent: IsType<::RuntimeEvent> + From>; // type ShouldEndSession: ShouldEndSession>; } /* TODO #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, serde::Serialize, serde::Deserialize)] pub struct AllEmbeddedEllipticCurveKeysAtGenesis { pub embedwards25519: BoundedVec>, pub secq256k1: BoundedVec>, } #[pallet::genesis_config] #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] pub struct GenesisConfig { /// Networks to spawn Serai with, and the stake requirement per key share. /// /// Every participant at genesis will automatically be assumed to have this much stake. /// This stake cannot be withdrawn however as there's no actual stake behind it. pub networks: Vec<(NetworkId, Amount)>, /// List of participants to place in the initial validator sets. pub participants: Vec<(T::AccountId, AllEmbeddedEllipticCurveKeysAtGenesis)>, } impl Default for GenesisConfig { fn default() -> Self { GenesisConfig { networks: Default::default(), participants: Default::default() } } } */ #[pallet::pallet] pub struct Pallet(PhantomData); /* /// The allocation required per key share. // Uses Identity for the lookup to avoid a hash of a severely limited fixed key-space. #[pallet::storage] #[pallet::getter(fn allocation_per_key_share)] pub type AllocationPerKeyShare = StorageMap<_, Identity, NetworkId, Amount, OptionQuery>; /// The validators selected to be in-set (and their key shares), regardless of if removed. /// /// This method allows iterating over all validators and their stake. #[pallet::storage] #[pallet::getter(fn participants_for_latest_decided_set)] pub(crate) type Participants = StorageMap< _, Identity, NetworkId, BoundedVec<(Public, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET_U32 }>>, OptionQuery, >; /// The validators selected to be in-set, regardless of if removed. /// /// This method allows quickly checking for presence in-set and looking up a validator's key /// shares. // Uses Identity for NetworkId to avoid a hash of a severely limited fixed key-space. #[pallet::storage] pub(crate) type InSet = StorageDoubleMap<_, Identity, NetworkId, Blake2_128Concat, Public, u64, OptionQuery>; } */ struct Abstractions(PhantomData); // Satisfy the `EmbeddedEllipticCurveKeys` abstraction #[pallet::storage] type EmbeddedEllipticCurveKeys = StorageDoubleMap< _, Identity, ExternalNetworkId, Blake2_128Concat, Public, serai_primitives::crypto::EmbeddedEllipticCurveKeys, OptionQuery, >; impl EmbeddedEllipticCurveKeysStorage for Abstractions { type EmbeddedEllipticCurveKeys = EmbeddedEllipticCurveKeys; } // Satisfy the `Allocations` abstraction #[pallet::storage] type Allocations = StorageMap<_, Blake2_128Concat, AllocationsKey, Amount, OptionQuery>; // This has to use `Identity` per the documentation of `AllocationsStorage` #[pallet::storage] type SortedAllocations = StorageMap<_, Identity, SortedAllocationsKey, (), OptionQuery>; impl AllocationsStorage for Abstractions { type Allocations = Allocations; type SortedAllocations = SortedAllocations; } // Satisfy the `Sessions` abstraction // We use `Identity` as the hasher for `NetworkId` due to how constrained it is #[pallet::storage] type GenesisValidators = StorageValue<_, GenesisValidatorsContainer, OptionQuery>; #[pallet::storage] type AllocationPerKeyShare = StorageMap<_, Identity, NetworkId, Amount, OptionQuery>; #[pallet::storage] type CurrentSession = StorageMap<_, Identity, NetworkId, Session, OptionQuery>; #[pallet::storage] type LatestDecidedSession = StorageMap<_, Identity, NetworkId, Session, OptionQuery>; // This has to use `Identity` per the documentation of `SessionsStorage` #[pallet::storage] type SelectedValidators = StorageMap<_, Identity, SelectedValidatorsKey, u64, OptionQuery>; #[pallet::storage] type TotalAllocatedStake = StorageMap<_, Identity, NetworkId, Amount, OptionQuery>; #[pallet::storage] type DelayedDeallocations = StorageDoubleMap<_, Blake2_128Concat, Public, Identity, Session, Amount, OptionQuery>; impl SessionsStorage for Abstractions { type GenesisValidators = GenesisValidators; type AllocationPerKeyShare = AllocationPerKeyShare; type CurrentSession = CurrentSession; type LatestDecidedSession = LatestDecidedSession; type SelectedValidators = SelectedValidators; type TotalAllocatedStake = TotalAllocatedStake; type DelayedDeallocations = DelayedDeallocations; } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event {} /* /// The generated key pair for a given validator set instance. #[pallet::storage] #[pallet::getter(fn keys)] pub type Keys = StorageMap<_, Twox64Concat, ExternalValidatorSet, KeyPair, OptionQuery>; /// The key for validator sets which can (and still need to) publish their slash reports. #[pallet::storage] pub type PendingSlashReport = StorageMap<_, Identity, ExternalNetworkId, Public, OptionQuery>; /// Disabled validators. #[pallet::storage] pub type SeraiDisabledIndices = StorageMap<_, Identity, u32, Public, OptionQuery>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { 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, }, DeallocationClaimed { validator: T::AccountId, network: NetworkId, session: Session, }, } impl Pallet { fn new_set(network: NetworkId) { // TODO let include_genesis_validators = true; // TODO: prevent new set if it doesn't have enough stake for economic security. Abstractions::::attempt_new_session(network, include_genesis_validators) /* TODO let set = ValidatorSet { network, session }; Pallet::::deposit_event(Event::NewSet { set }); */ } } */ #[pallet::error] pub enum Error { /// The provided embedded elliptic curve keys were invalid. InvalidEmbeddedEllipticCurveKeys, /// Allocation was erroneous. AllocationError(AllocationError), /// Deallocation was erroneous. DeallocationError(DeallocationError), } /* TODO #[pallet::hooks] impl Hooks> for Pallet { fn on_initialize(n: BlockNumberFor) -> Weight { if T::ShouldEndSession::should_end_session(n) { Self::rotate_session(); // TODO: set the proper weights T::BlockWeights::get().max_block } else { Weight::zero() } } } #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { for (id, stake) in self.networks.clone() { AllocationPerKeyShare::::set(id, Some(stake)); for participant in &self.participants { if Abstractions::::set_allocation(id, participant.0, stake) { panic!("participants contained duplicates"); } EmbeddedEllipticCurveKeys::::set( participant.0, EmbeddedEllipticCurve::Embedwards25519, Some(participant.1.embedwards25519.clone()), ); EmbeddedEllipticCurveKeys::::set( participant.0, EmbeddedEllipticCurve::Secq256k1, Some(participant.1.secq256k1.clone()), ); } Pallet::::new_set(id); } } } */ impl Pallet { fn account() -> T::AccountId { SeraiAddress::system(b"ValidatorSets").into() } /* // is_bft returns if the network is able to survive any single node becoming byzantine. fn is_bft(network: NetworkId) -> bool { let allocation_per_key_share = AllocationPerKeyShare::::get(network).unwrap().0; let mut validators_len = 0; let mut top = None; let mut key_shares = 0; for (_, amount) in Abstractions::::iter_allocations(network, allocation_per_key_share) { 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_U32) { break; } } let Some(top) = top else { return false }; // key_shares may be over MAX_KEY_SHARES_PER_SET, which will cause a round robin reduction of // each validator's key shares until their sum is MAX_KEY_SHARES_PER_SET // 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_U32.into()) } fn increase_allocation( network: NetworkId, account: T::AccountId, amount: Amount, block_reward: bool, ) -> DispatchResult { /* TODO // 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_U32 / 3).into() { Err(Error::::AllocationWouldPreventFaultTolerance)?; } */ Abstractions::::increase_allocation(network, account, amount, block_reward) } fn session_to_unlock_on_for_current_set(network: NetworkId) -> Option { let mut to_unlock_on = Self::session(network)?; // Move to the next session, as deallocating currently in-use stake is obviously invalid to_unlock_on.0 += 1; if network == NetworkId::Serai { // Since the next Serai set will already have been decided, we can only deallocate one // session later to_unlock_on.0 += 1; } // Increase the session by one, creating a cooldown period to_unlock_on.0 += 1; Some(to_unlock_on) } /// Decreases a validator's allocation to a set. /// /// Errors if the capacity provided by this allocation is in use. /// /// Errors if a partial decrease of allocation which puts the remaining allocation below the /// minimum requirement. /// /// The capacity prior provided by the allocation is immediately removed, in order to ensure it /// doesn't become used (preventing deallocation). /// /// Returns if the amount is immediately eligible for deallocation. fn decrease_allocation( network: NetworkId, account: T::AccountId, amount: Amount, ) -> Result { /* TODO // Check it's safe to decrease this set's stake by this amount 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::::NotEnoughAllocated)?; let required_stake = Self::required_stake_for_network(n); if new_total_staked < required_stake { Err(Error::::DeallocationWouldRemoveEconomicSecurity)?; } } let decreased_key_shares = (old_allocation / allocation_per_key_share) > (new_allocation / allocation_per_key_share); // If this decreases the validator's key shares, error if the new set is unable to handle // byzantine faults let mut was_bft = None; if decreased_key_shares { was_bft = Some(Self::is_bft(network)); } if let Some(was_bft) = was_bft { if was_bft && (!Self::is_bft(network)) { Err(Error::::DeallocationWouldRemoveFaultTolerance)?; } } */ Sessions::::decrease_allocation(network, account, amount) } // Checks if this session has completed the handover from the prior session. fn handover_completed(network: NetworkId, session: Session) -> bool { let Some(current_session) = Self::session(network) else { return false }; // If the session we've been queried about is old, it must have completed its handover if current_session.0 > session.0 { return true; } // If the session we've been queried about has yet to start, it can't have completed its // handover if current_session.0 < session.0 { return false; } let NetworkId::External(n) = network else { // Handover is automatically complete for Serai as it doesn't have a handover protocol return true; }; // The current session must have set keys for its handover to be completed if !Keys::::contains_key(ExternalValidatorSet { network: n, session }) { return false; } // This must be the first session (which has set keys) OR the prior session must have been // retired (signified by its keys no longer being present) (session.0 == 0) || (!Keys::::contains_key(ExternalValidatorSet { network: n, session: Session(session.0 - 1), })) } fn new_session() { for network in serai_primitives::NETWORKS { // If this network hasn't started sessions yet, don't start one now let Some(current_session) = Self::session(network) else { continue }; // Only spawn a new set if: // - This is Serai, as we need to rotate Serai upon a new session (per Babe) // - The current set was actually established with a completed handover protocol if (network == NetworkId::Serai) || Self::handover_completed(network, current_session) { Pallet::::new_set(network); // let the Dex know session is rotated. Dex::::on_new_session(network); } } } // TODO: This is called retire_set, yet just starts retiring the set // Update the nomenclature within this function pub fn retire_set(set: ValidatorSet) { // Serai doesn't set keys and network slashes are handled by BABE/GRANDPA if let NetworkId::External(n) = set.network { // If the prior prior set didn't report, emit they're retired now if PendingSlashReport::::get(n).is_some() { Self::deposit_event(Event::SetRetired { set: ValidatorSet { network: set.network, session: Session(set.session.0 - 1) }, }); } // This overwrites the prior value as the prior to-report set's stake presumably just // unlocked, making their report unenforceable let keys = Keys::::take(ExternalValidatorSet { network: n, session: set.session }).unwrap(); PendingSlashReport::::set(n, Some(keys.0)); } else { // emit the event for serai network Self::deposit_event(Event::SetRetired { set }); } // We're retiring this set because the set after it accepted the handover Self::deposit_event(Event::AcceptedHandover { set: ValidatorSet { network: set.network, session: Session(set.session.0 + 1) }, }); } /// Take the amount deallocatable. /// /// `session` refers to the Session the stake becomes deallocatable on. fn take_deallocatable_amount( network: NetworkId, session: Session, key: Public, ) -> Option { // Check this Session has properly started, completing the handover from the prior session. if !Self::handover_completed(network, session) { return None; } PendingDeallocations::::take((network, key), session) } pub(crate) fn rotate_session() { // next serai validators that is in the queue. let now_validators = Participants::::get(NetworkId::Serai) .expect("no Serai participants upon rotate_session"); let prior_serai_session = Self::session(NetworkId::Serai).unwrap(); // TODO: T::SessionHandler::on_before_session_ending() was here. // end the current serai session. Self::retire_set(ValidatorSet { network: NetworkId::Serai, session: prior_serai_session }); // make a new session and get the next validator set. Self::new_session(); // Update Babe and Grandpa let session = prior_serai_session.0 + 1; let next_validators = Participants::::get(NetworkId::Serai).unwrap(); Babe::::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::::new_session( true, session, now_validators.into_iter().map(|(id, w)| (GrandpaAuthorityId::from(id), w)).collect(), ); // Clear SeraiDisabledIndices, only preserving keys still present in the new session // First drain so we don't mutate as we iterate let mut disabled = vec![]; for (_, validator) in SeraiDisabledIndices::::drain() { disabled.push(validator); } for disabled in disabled { Self::disable_serai_validator(disabled); } } /// Returns the required stake in terms SRI for a given `Balance`. pub fn required_stake(balance: &ExternalBalance) -> SubstrateAmount { use dex_pallet::HigherPrecisionBalance; // This is inclusive to an increase in accuracy let sri_per_coin = Dex::::security_oracle_value(balance.coin).unwrap_or(Amount(0)); // See dex-pallet for the reasoning on these 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); // required stake formula (COIN_VALUE * 1.5) + margin(20%) let required_stake = total_coin_value.saturating_mul(3).saturating_div(2); required_stake.saturating_add(total_coin_value.saturating_div(5)) } /// Returns the current total required stake for a given `network`. pub fn required_stake_for_network(network: ExternalNetworkId) -> SubstrateAmount { let mut total_required = 0; for coin in network.coins() { let supply = Coins::::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 { // TODO: Should this call be part of the `increase_allocation` since we have to have it // before each call to it? Coins::::transfer_fn( account, Self::account(), Balance { coin: Coin::Serai, amount }, )?; Self::increase_allocation(network, account, amount, true) } fn can_slash_serai_validator(validator: Public) -> bool { // Checks if they're active or actively deallocating (letting us still slash them) // We could check if they're upcoming/still allocating, yet that'd mean the equivocation is // invalid (as they aren't actively signing anything) or severely dated // It's not an edge case worth being comprehensive to due to the complexity of being so Babe::::is_member(&BabeAuthorityId::from(validator)) || PendingDeallocations::::iter_prefix((NetworkId::Serai, validator)).next().is_some() } fn slash_serai_validator(validator: Public) { let network = NetworkId::Serai; let mut allocation = Abstractions::::get_allocation((network, validator)) .unwrap_or(Amount(0)); // reduce the current allocation to 0. Abstractions::::set_allocation(network, validator, Amount(0)); // Take the pending deallocation from the current session allocation.0 += PendingDeallocations::::take( (network, validator), Self::session_to_unlock_on_for_current_set(network).unwrap(), ) .unwrap_or(Amount(0)) .0; // Reduce the TotalAllocatedStake for the network, if in set // TotalAllocatedStake is the sum of allocations and pending deallocations from the current // session, since pending deallocations can still be slashed and therefore still contribute // to economic security, hence the allocation calculations above being above and the ones // below being below if InSet::::contains_key(NetworkId::Serai, validator) { let current_staked = Self::total_allocated_stake(network).unwrap(); TotalAllocatedStake::::set(network, Some(current_staked - allocation)); } // Clear any other pending deallocations. for (_, pending) in PendingDeallocations::::drain_prefix((network, validator)) { allocation.0 += pending.0; } // burn the allocation from the stake account Coins::::burn( RawOrigin::Signed(Self::account()).into(), Balance { coin: Coin::Serai, amount: allocation }, ) .unwrap(); } /// Disable a Serai validator, preventing them from further authoring blocks. /// /// Returns true if the validator-to-disable was actually a validator. /// Returns false if they weren't. fn disable_serai_validator(validator: Public) -> bool { if let Some(index) = Babe::::authorities().into_iter().position(|(id, _)| id.into_inner() == validator) { SeraiDisabledIndices::::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 } } /// Returns the external network key for a given external network pub fn external_network_key(network: ExternalNetworkId) -> Option> { let current_session = Self::session(NetworkId::from(network))?; let keys = Keys::::get(ExternalValidatorSet { network, session: current_session })?; Some(keys.1.into_inner()) } */ } #[pallet::call] impl Pallet { /* #[pallet::call_index(0)] #[pallet::weight((0, DispatchClass::Operational))] // TODO pub fn set_keys( origin: OriginFor, network: ExternalNetworkId, key_pair: KeyPair, signature_participants: bitvec::vec::BitVec, signature: Signature, ) -> DispatchResult { ensure_none(origin)?; // signature isn't checked as this is an unsigned transaction, and validate_unsigned // (called by pre_dispatch) checks it let _ = signature_participants; let _ = signature; let session = Self::session(NetworkId::from(network)).unwrap(); let set = ExternalValidatorSet { network, session }; Keys::::set(set, Some(key_pair.clone())); // If this is the first ever set for this network, set TotalAllocatedStake now // We generally set TotalAllocatedStake when the prior set retires, and the new set is fully // active and liable. Since this is the first set, there is no prior set to wait to retire if session == Session(0) { Self::set_total_allocated_stake(NetworkId::from(network)); } Self::deposit_event(Event::KeyGen { set, key_pair }); Ok(()) } #[pallet::call_index(1)] #[pallet::weight((0, DispatchClass::Operational))] // TODO pub fn report_slashes( origin: OriginFor, network: ExternalNetworkId, slashes: SlashReport, signature: Signature, ) -> DispatchResult { ensure_none(origin)?; // signature isn't checked as this is an unsigned transaction, and validate_unsigned // (called by pre_dispatch) checks it let _ = signature; // TODO: Handle slashes let _ = slashes; // Emit set retireed Pallet::::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, DispatchClass::Normal))] // TODO pub fn set_embedded_elliptic_curve_keys( origin: OriginFor, keys: serai_primitives::crypto::SignedEmbeddedEllipticCurveKeys, ) -> DispatchResult { let signer = ensure_signed(origin)?; as crate::EmbeddedEllipticCurveKeys>::set_embedded_elliptic_curve_keys( signer, keys, ) .map_err(|()| Error::::InvalidEmbeddedEllipticCurveKeys)?; Ok(()) } #[pallet::call_index(3)] #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn allocate(origin: OriginFor, network: NetworkId, amount: Amount) -> DispatchResult { let validator = ensure_signed(origin)?; Coins::::transfer_fn(validator, Self::account(), Balance { coin: Coin::Serai, amount })?; Abstractions::::increase_allocation(network, validator, amount, false) .map_err(Error::::AllocationError)?; Ok(()) } #[pallet::call_index(4)] #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn deallocate(origin: OriginFor, network: NetworkId, amount: Amount) -> DispatchResult { let account = ensure_signed(origin)?; let deallocation_timeline = Abstractions::::decrease_allocation(network, account, amount) .map_err(Error::::DeallocationError)?; if matches!(deallocation_timeline, DeallocationTimeline::Immediate) { Coins::::transfer_fn(Self::account(), account, Balance { coin: Coin::Serai, amount })?; } Ok(()) } #[pallet::call_index(5)] #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn claim_deallocation( origin: OriginFor, network: NetworkId, session: Session, ) -> DispatchResult { let account = ensure_signed(origin)?; let amount = Abstractions::::claim_delayed_deallocation(account, network, session) .map_err(Error::::DeallocationError)?; Coins::::transfer_fn(Self::account(), account, Balance { coin: Coin::Serai, amount })?; Ok(()) } } /* #[pallet::validate_unsigned] impl ValidateUnsigned for Pallet { type Call = Call; fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity { // Match to be exhaustive match call { Call::set_keys { network, ref key_pair, ref signature_participants, ref signature } => { let network = *network; // Confirm this set has a session let Some(current_session) = Self::session(NetworkId::from(network)) else { Err(InvalidTransaction::Custom(1))? }; let set = ExternalValidatorSet { network, session: current_session }; // Confirm it has yet to set keys if Keys::::get(set).is_some() { Err(InvalidTransaction::Stale)?; } // This is a needed precondition as this uses storage variables for the latest decided // session on this assumption assert_eq!(Pallet::::latest_decided_session(network.into()), Some(current_session)); let participants = Participants::::get(NetworkId::from(network)) .expect("session existed without participants"); // Check the bitvec is of the proper length if participants.len() != signature_participants.len() { Err(InvalidTransaction::Custom(2))?; } let mut all_key_shares = 0; let mut signers = vec![]; let mut signing_key_shares = 0; for (participant, in_use) in participants.into_iter().zip(signature_participants) { let participant = participant.0; let shares = InSet::::get(NetworkId::from(network), participant) .expect("participant from Participants wasn't InSet"); all_key_shares += shares; if !in_use { 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))?; } } // Verify the signature with the MuSig key of the signers // We theoretically don't need set_keys_message to bind to removed_participants, as the // key we're signing with effectively already does so, yet there's no reason not to if !musig_key(set.into(), &signers).verify(&set_keys_message(&set, 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::::take(network) else { // Assumed already published Err(InvalidTransaction::Stale)? }; // There must have been a previous session is PendingSlashReport is populated let set = ExternalValidatorSet { network, session: Session(Self::session(NetworkId::from(network)).unwrap().0 - 1), }; if !key.verify(&slashes.report_slashes_message(), signature) { Err(InvalidTransaction::BadProof)?; } ValidTransaction::with_tag_prefix("ValidatorSets") .and_provides((1, set)) .longevity(MAX_KEY_SHARES_PER_SET_U32.into()) .propagate(true) .build() } Call::set_embedded_elliptic_curve_key { .. } | Call::allocate { .. } | Call::deallocate { .. } | Call::claim_deallocation { .. } => Err(InvalidTransaction::Call)?, Call::__Ignore(_, _) => unreachable!(), } } // Explicitly provide a pre-dispatch which calls validate_unsigned fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> { Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ()) } } impl AllowMint for Pallet { fn is_allowed(balance: &ExternalBalance) -> bool { // get the required stake let current_required = Self::required_stake_for_network(balance.coin.network()); let new_required = current_required + Self::required_stake(balance); // get the total stake for the network & compare. let staked = Self::total_allocated_stake(NetworkId::from(balance.coin.network())).unwrap_or(Amount(0)); staked.0 >= new_required } } impl + From> KeyOwnerProofSystem<(KeyTypeId, V)> for Pallet { type Proof = MembershipProof; type IdentificationTuple = Public; fn prove(key: (KeyTypeId, V)) -> Option { Some(MembershipProof(key.1.into(), PhantomData)) } fn check_proof(key: (KeyTypeId, V), proof: Self::Proof) -> Option { let validator = key.1.into(); // check the offender and the proof offender are the same. if validator != proof.0 { return None; } // check validator is valid if !Self::can_slash_serai_validator(validator) { return None; } Some(validator) } } impl ReportOffence> for Pallet { /// Report an `offence` and reward given `reporters`. fn report_offence( _: Vec, offence: BabeEquivocationOffence, ) -> Result<(), OffenceError> { // slash the offender let offender = offence.offender; Self::slash_serai_validator(offender); // disable it Self::disable_serai_validator(offender); Ok(()) } fn is_known_offence( offenders: &[Public], _: & as Offence>::TimeSlot, ) -> bool { for offender in offenders { // It's not a known offence if we can still slash them if Self::can_slash_serai_validator(*offender) { return false; } } true } } impl ReportOffence> for Pallet { /// Report an `offence` and reward given `reporters`. fn report_offence( _: Vec, offence: GrandpaEquivocationOffence, ) -> Result<(), OffenceError> { // slash the offender let offender = offence.offender; Self::slash_serai_validator(offender); // disable it Self::disable_serai_validator(offender); Ok(()) } fn is_known_offence( offenders: &[Public], _slot: & as Offence>::TimeSlot, ) -> bool { for offender in offenders { if Self::can_slash_serai_validator(*offender) { return false; } } true } } impl FindAuthor for Pallet { fn find_author<'a, I>(digests: I) -> Option where I: 'a + IntoIterator, { let i = Babe::::find_author(digests)?; Some(Babe::::authorities()[i as usize].0.clone().into()) } } impl DisabledValidators for Pallet { fn is_disabled(index: u32) -> bool { SeraiDisabledIndices::::get(index).is_some() } } */ } sp_api::decl_runtime_apis! { #[api_version(1)] pub trait ValidatorSetsApi { /// Returns the validator set for a given network. fn validators( network_id: serai_primitives::network_id::NetworkId, ) -> Vec; /// Returns the external network key for a given external network. fn external_network_key( network: serai_primitives::network_id::ExternalNetworkId, ) -> Option; } } pub use pallet::*;