mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-13 14:39:25 +00:00
704 lines
24 KiB
Rust
704 lines
24 KiB
Rust
#![cfg_attr(docsrs, feature(doc_cfg))]
|
|
#![doc = include_str!("../README.md")]
|
|
#![cfg_attr(not(feature = "std"), no_std)]
|
|
|
|
extern crate alloc;
|
|
use alloc::{vec, vec::Vec};
|
|
|
|
mod embedded_elliptic_curve_keys;
|
|
use embedded_elliptic_curve_keys::*;
|
|
|
|
mod allocations;
|
|
use allocations::*;
|
|
|
|
mod sessions;
|
|
use sessions::{*, GenesisValidators as GenesisValidatorsContainer};
|
|
|
|
mod keys;
|
|
use keys::{KeysStorage, Keys as _};
|
|
|
|
#[expect(clippy::cast_possible_truncation)]
|
|
#[frame_support::pallet]
|
|
mod pallet {
|
|
use sp_core::sr25519::Public;
|
|
use sp_application_crypto::RuntimePublic;
|
|
|
|
use frame_system::pallet_prelude::*;
|
|
use frame_support::{pallet_prelude::*, traits::OneSessionHandler};
|
|
|
|
use pallet_session::ShouldEndSession;
|
|
use pallet_babe::Pallet as Babe;
|
|
use pallet_grandpa::Pallet as Grandpa;
|
|
|
|
use serai_abi::{
|
|
primitives::{
|
|
crypto::{
|
|
EmbeddedEllipticCurveKeys as EmbeddedEllipticCurveKeysStruct,
|
|
SignedEmbeddedEllipticCurveKeys, ExternalKey, KeyPair, Signature,
|
|
},
|
|
network_id::*,
|
|
coin::*,
|
|
balance::*,
|
|
validator_sets::{
|
|
Session, ExternalValidatorSet, ValidatorSet, KeyShares as KeySharesStruct, SlashReport,
|
|
},
|
|
address::SeraiAddress,
|
|
},
|
|
economic_security::EconomicSecurity,
|
|
validator_sets::{DeallocationTimeline, Event},
|
|
};
|
|
|
|
use serai_core_pallet::Pallet as Core;
|
|
use serai_coins_pallet::AllowMint;
|
|
type Coins<T> = serai_coins_pallet::Pallet<T, serai_coins_pallet::CoinsInstance>;
|
|
|
|
use super::*;
|
|
|
|
#[pallet::config]
|
|
pub trait Config:
|
|
frame_system::Config
|
|
+ pallet_session::Config
|
|
+ pallet_babe::Config
|
|
+ pallet_grandpa::Config
|
|
+ serai_core_pallet::Config
|
|
+ serai_coins_pallet::Config<serai_coins_pallet::CoinsInstance>
|
|
{
|
|
type ShouldEndSession: ShouldEndSession<BlockNumberFor<Self>>;
|
|
type EconomicSecurity: EconomicSecurity;
|
|
}
|
|
|
|
#[pallet::genesis_config]
|
|
#[derive(Clone, Debug)]
|
|
pub struct GenesisConfig<T: Config> {
|
|
/// List of participants to place in the initial validator sets.
|
|
pub participants: Vec<(T::AccountId, Vec<SignedEmbeddedEllipticCurveKeys>)>,
|
|
}
|
|
impl<T: Config> Default for GenesisConfig<T> {
|
|
fn default() -> Self {
|
|
Self { participants: Default::default() }
|
|
}
|
|
}
|
|
|
|
#[pallet::pallet]
|
|
pub struct Pallet<T>(PhantomData<T>);
|
|
|
|
struct Abstractions<T: Config>(PhantomData<T>);
|
|
|
|
// Satisfy the `EmbeddedEllipticCurveKeys` abstraction
|
|
|
|
#[pallet::storage]
|
|
type EmbeddedEllipticCurveKeys<T: Config> = StorageDoubleMap<
|
|
_,
|
|
Identity,
|
|
ExternalNetworkId,
|
|
Blake2_128Concat,
|
|
Public,
|
|
serai_abi::primitives::crypto::EmbeddedEllipticCurveKeys,
|
|
OptionQuery,
|
|
>;
|
|
|
|
impl<T: Config> EmbeddedEllipticCurveKeysStorage for Abstractions<T> {
|
|
type EmbeddedEllipticCurveKeys = EmbeddedEllipticCurveKeys<T>;
|
|
}
|
|
|
|
// Satisfy the `Allocations` abstraction
|
|
|
|
#[pallet::storage]
|
|
type Allocations<T: Config> =
|
|
StorageMap<_, Blake2_128Concat, AllocationsKey, Amount, OptionQuery>;
|
|
// This has to use `Identity` per the documentation of `AllocationsStorage`
|
|
#[pallet::storage]
|
|
type SortedAllocations<T: Config> =
|
|
StorageMap<_, Identity, SortedAllocationsKey, (), OptionQuery>;
|
|
|
|
impl<T: Config> AllocationsStorage for Abstractions<T> {
|
|
type Allocations = Allocations<T>;
|
|
type SortedAllocations = SortedAllocations<T>;
|
|
}
|
|
|
|
// Satisfy the `Sessions` abstraction
|
|
|
|
// We use `Identity` as the hasher for `NetworkId` due to how constrained it is
|
|
#[pallet::storage]
|
|
type GenesisValidators<T: Config> = StorageValue<_, GenesisValidatorsContainer, OptionQuery>;
|
|
#[pallet::storage]
|
|
type AllocationPerKeyShare<T: Config> = StorageMap<_, Identity, NetworkId, Amount, OptionQuery>;
|
|
#[pallet::storage]
|
|
type CurrentSession<T: Config> = StorageMap<_, Identity, NetworkId, Session, OptionQuery>;
|
|
#[pallet::storage]
|
|
type LatestDecidedSession<T: Config> = StorageMap<_, Identity, NetworkId, Session, OptionQuery>;
|
|
#[pallet::storage]
|
|
type KeyShares<T: Config> = StorageMap<_, Identity, ValidatorSet, KeySharesStruct, OptionQuery>;
|
|
// This has to use `Identity` per the documentation of `SessionsStorage`
|
|
#[pallet::storage]
|
|
type SelectedValidators<T: Config> =
|
|
StorageMap<_, Identity, SelectedValidatorsKey, KeySharesStruct, OptionQuery>;
|
|
#[pallet::storage]
|
|
type TotalAllocatedStake<T: Config> = StorageMap<_, Identity, NetworkId, Amount, OptionQuery>;
|
|
#[pallet::storage]
|
|
type DelayedDeallocations<T: Config> =
|
|
StorageDoubleMap<_, Blake2_128Concat, Public, Identity, Session, Amount, OptionQuery>;
|
|
#[pallet::storage]
|
|
type PendingSlashReport<T: Config> = StorageMap<_, Identity, ExternalNetworkId, (), OptionQuery>;
|
|
|
|
impl<T: Config> SessionsStorage for Abstractions<T> {
|
|
type Config = T;
|
|
|
|
type GenesisValidators = GenesisValidators<T>;
|
|
type AllocationPerKeyShare = AllocationPerKeyShare<T>;
|
|
type CurrentSession = CurrentSession<T>;
|
|
type LatestDecidedSession = LatestDecidedSession<T>;
|
|
type KeyShares = KeyShares<T>;
|
|
type SelectedValidators = SelectedValidators<T>;
|
|
type TotalAllocatedStake = TotalAllocatedStake<T>;
|
|
type DelayedDeallocations = DelayedDeallocations<T>;
|
|
type PendingSlashReport = PendingSlashReport<T>;
|
|
}
|
|
|
|
// Satisfy the `Keys` abstractions
|
|
#[pallet::storage]
|
|
type OraclizationKeys<T: Config> =
|
|
StorageMap<_, Identity, ExternalValidatorSet, Public, OptionQuery>;
|
|
#[pallet::storage]
|
|
type ExternalKeys<T: Config> =
|
|
StorageMap<_, Identity, ExternalValidatorSet, ExternalKey, OptionQuery>;
|
|
|
|
impl<T: Config> KeysStorage for Abstractions<T> {
|
|
type OraclizationKeys = OraclizationKeys<T>;
|
|
type ExternalKeys = ExternalKeys<T>;
|
|
}
|
|
|
|
#[pallet::error]
|
|
pub enum Error<T> {
|
|
/// The provided embedded elliptic curve keys were invalid.
|
|
InvalidEmbeddedEllipticCurveKeys,
|
|
/// Allocation was erroneous.
|
|
AllocationError(AllocationError),
|
|
/// Deallocation was erroneous.
|
|
DeallocationError(DeallocationError),
|
|
}
|
|
|
|
#[pallet::genesis_build]
|
|
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
|
|
fn build(&self) {
|
|
GenesisValidators::<T>::set(Some(
|
|
self
|
|
.participants
|
|
.iter()
|
|
.map(|(participant, _keys)| *participant)
|
|
.collect::<Vec<_>>()
|
|
.try_into()
|
|
.expect("amount of genesis validators exceeded the maximum allowed per set"),
|
|
));
|
|
for (participant, keys) in &self.participants {
|
|
for (network, keys) in ExternalNetworkId::all().zip(keys.iter().cloned()) {
|
|
assert_eq!(network, keys.network());
|
|
Pallet::<T>::set_embedded_elliptic_curve_keys_internal(*participant, keys)
|
|
.expect("genesis embedded elliptic curve keys weren't valid");
|
|
}
|
|
}
|
|
for network in NetworkId::all() {
|
|
assert!(
|
|
Abstractions::<T>::attempt_new_session(network, true),
|
|
"failed to attempt a new session on genesis"
|
|
);
|
|
}
|
|
|
|
// Immediately accept the handover for the genesis validators, for the Serai network
|
|
Abstractions::<T>::accept_handover(NetworkId::Serai);
|
|
// And decide the next session for the Serai network, as BABE requires selecting the next one
|
|
// already
|
|
assert!(
|
|
Abstractions::<T>::attempt_new_session(NetworkId::Serai, true),
|
|
"failed to attempt the next session for the Serai network on genesis"
|
|
);
|
|
|
|
// Spawn BABE's, GRANDPA's genesis session
|
|
let genesis_serai_validators = Abstractions::<T>::serai_validators(Session(0));
|
|
Babe::<T>::on_genesis_session(
|
|
genesis_serai_validators.iter().map(|(validator, key)| (validator, (*key).into())),
|
|
);
|
|
Grandpa::<T>::on_genesis_session(
|
|
genesis_serai_validators.iter().map(|(validator, key)| (validator, (*key).into())),
|
|
);
|
|
}
|
|
}
|
|
|
|
impl<T: Config> Pallet<T> {
|
|
fn account() -> T::AccountId {
|
|
SeraiAddress::system(b"ValidatorSets").into()
|
|
}
|
|
|
|
/// The current session for a network.
|
|
pub fn current_session(network: NetworkId) -> Option<Session> {
|
|
Abstractions::<T>::current_session(network)
|
|
}
|
|
|
|
/// The latest decided session for a network.
|
|
pub fn latest_decided_session(network: NetworkId) -> Option<Session> {
|
|
Abstractions::<T>::latest_decided_session(network)
|
|
}
|
|
|
|
/// The amount of key shares a validator has.
|
|
///
|
|
/// Returns `None` for historic sessions which we no longer have the data for.
|
|
pub fn key_shares(set: ValidatorSet) -> Option<KeySharesStruct> {
|
|
Abstractions::<T>::key_shares(set)
|
|
}
|
|
|
|
/// If a validator is present within the specified validator set.
|
|
///
|
|
/// This MAY return `false` for _any_ historic session, even if the validator _was_ present,
|
|
pub fn in_validator_set(set: ValidatorSet, validator: Public) -> bool {
|
|
Abstractions::<T>::in_validator_set(set, validator)
|
|
}
|
|
|
|
/// The key shares possessed by a validator, within a validator set.
|
|
///
|
|
/// This MAY return `None` for _any_ historic session, even if the validator _was_ present,
|
|
pub fn key_shares_possessed_by_validator(
|
|
set: ValidatorSet,
|
|
validator: Public,
|
|
) -> Option<KeySharesStruct> {
|
|
Abstractions::<T>::key_shares_possessed_by_validator(set, validator)
|
|
}
|
|
|
|
/// The stake for the current validator set.
|
|
pub fn stake_for_current_validator_set(network: NetworkId) -> Option<Amount> {
|
|
Abstractions::<T>::stake_for_current_validator_set(network)
|
|
}
|
|
|
|
fn include_genesis_validators(network: NetworkId) -> bool {
|
|
match network {
|
|
// For Serai, we include the genesis validators as long as any other set does
|
|
NetworkId::Serai => {
|
|
ExternalNetworkId::all().all(T::EconomicSecurity::achieved_economic_security)
|
|
}
|
|
// For the other networks, we include the genesis validators if they have yet to achieve
|
|
// economic security
|
|
NetworkId::External(network) => T::EconomicSecurity::achieved_economic_security(network),
|
|
}
|
|
}
|
|
|
|
/// The required amount of stake for a balance.
|
|
fn stake_requirement(balance: ExternalBalance) -> AmountRepr {
|
|
let value = T::EconomicSecurity::sri_value(balance).0;
|
|
// As 67% can misbehave, 67% of stake must be sufficient to secure this
|
|
let requirement = value.saturating_mul(3) / 2;
|
|
// We add an additional margin of 20%
|
|
let margin = requirement / 5;
|
|
requirement.saturating_add(margin)
|
|
}
|
|
|
|
/// The required amount of stake for a network.
|
|
fn network_stake_requirement(network: ExternalNetworkId) -> AmountRepr {
|
|
let mut requirement = AmountRepr::zero();
|
|
for coin in network.coins() {
|
|
let supply = Coins::<T>::supply(Coin::from(coin));
|
|
requirement = requirement
|
|
.saturating_add(Self::stake_requirement(ExternalBalance { coin, amount: supply }));
|
|
}
|
|
requirement
|
|
}
|
|
|
|
pub fn selected_validators(
|
|
set: ValidatorSet,
|
|
) -> impl Iterator<Item = (Public, KeySharesStruct)> {
|
|
Abstractions::<T>::selected_validators(set)
|
|
}
|
|
|
|
pub fn oraclization_key(set: ExternalValidatorSet) -> Option<Public> {
|
|
Abstractions::<T>::oraclization_key(set)
|
|
}
|
|
|
|
pub fn external_key(set: ExternalValidatorSet) -> Option<ExternalKey> {
|
|
Abstractions::<T>::external_key(set)
|
|
}
|
|
|
|
pub fn pending_slash_report(network: ExternalNetworkId) -> bool {
|
|
Abstractions::<T>::waiting_for_slash_report(network).is_some()
|
|
}
|
|
|
|
pub fn embedded_elliptic_curve_keys(
|
|
validator: Public,
|
|
network: ExternalNetworkId,
|
|
) -> Option<EmbeddedEllipticCurveKeysStruct> {
|
|
<Abstractions<T> as crate::EmbeddedEllipticCurveKeys>::embedded_elliptic_curve_keys(
|
|
validator, network,
|
|
)
|
|
}
|
|
|
|
fn set_embedded_elliptic_curve_keys_internal(
|
|
validator: Public,
|
|
keys: SignedEmbeddedEllipticCurveKeys,
|
|
) -> DispatchResult {
|
|
let network = keys.network();
|
|
let keys =
|
|
<Abstractions<T> as crate::EmbeddedEllipticCurveKeys>::set_embedded_elliptic_curve_keys(
|
|
validator, keys,
|
|
)
|
|
.map_err(|()| Error::<T>::InvalidEmbeddedEllipticCurveKeys)?;
|
|
Core::<T>::emit_event(Event::SetEmbeddedEllipticCurveKeys {
|
|
validator: validator.into(),
|
|
keys,
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
/// Have the latest decided session become the current session.
|
|
///
|
|
/// This is restricted to `ExternalNetworkId` as this process happens internally for
|
|
/// `NetworkId::Serai`.
|
|
pub fn accept_handover(network: ExternalNetworkId) {
|
|
Abstractions::<T>::accept_handover(network.into());
|
|
}
|
|
|
|
/* TODO
|
|
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::<T>::transfer_fn(
|
|
account,
|
|
Self::account(),
|
|
Balance { coin: Coin::Serai, amount },
|
|
)?;
|
|
Self::increase_allocation(network, account, amount, true)
|
|
}
|
|
*/
|
|
}
|
|
|
|
#[pallet::hooks]
|
|
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
|
fn on_initialize(n: BlockNumberFor<T>) -> Weight {
|
|
if <T as Config>::ShouldEndSession::should_end_session(n) {
|
|
Babe::<T>::on_before_session_ending();
|
|
Grandpa::<T>::on_before_session_ending();
|
|
|
|
{
|
|
// Accept the hand-over to the next session for the Serai network
|
|
Abstractions::<T>::accept_handover(NetworkId::Serai);
|
|
// Decide the next session for the Serai network
|
|
assert!(
|
|
Abstractions::<T>::attempt_new_session(
|
|
NetworkId::Serai,
|
|
Self::include_genesis_validators(NetworkId::Serai)
|
|
),
|
|
"failed to attempt the next session for the Serai network"
|
|
);
|
|
}
|
|
|
|
// Update BABE, GRANDPA
|
|
{
|
|
let current_serai_session = Abstractions::<T>::current_session(NetworkId::Serai)
|
|
.expect("never selected a session for Serai");
|
|
let latest_decided_serai_session =
|
|
Abstractions::<T>::latest_decided_session(NetworkId::Serai)
|
|
.expect("current session yet no latest decided session for Serai");
|
|
assert_eq!(
|
|
Session(current_serai_session.0 + 1),
|
|
latest_decided_serai_session,
|
|
"latest decided Serai session wasn't the session after the current session"
|
|
);
|
|
|
|
let prior_serai_validators = Abstractions::<T>::serai_validators(Session(
|
|
current_serai_session.0.checked_sub(1).expect("ShouldEndSession triggered on genesis"),
|
|
));
|
|
assert!(
|
|
!prior_serai_validators.is_empty(),
|
|
"prior Serai validators weren't able to be fetched from storage",
|
|
);
|
|
|
|
let serai_validators = Abstractions::<T>::serai_validators(current_serai_session);
|
|
|
|
let validators_changed = prior_serai_validators != serai_validators;
|
|
|
|
let queued_serai_validators =
|
|
Abstractions::<T>::serai_validators(latest_decided_serai_session);
|
|
|
|
fn map_babe((validator, key): &(Public, Public)) -> (&Public, pallet_babe::AuthorityId) {
|
|
(validator, (*key).into())
|
|
}
|
|
Babe::<T>::on_new_session(
|
|
validators_changed,
|
|
serai_validators.iter().map(map_babe),
|
|
queued_serai_validators.iter().map(map_babe),
|
|
);
|
|
|
|
fn map_grandpa(
|
|
(validator, key): &(Public, Public),
|
|
) -> (&Public, pallet_grandpa::AuthorityId) {
|
|
(validator, (*key).into())
|
|
}
|
|
Grandpa::<T>::on_new_session(
|
|
validators_changed,
|
|
serai_validators.iter().map(map_grandpa),
|
|
queued_serai_validators.iter().map(map_grandpa),
|
|
);
|
|
}
|
|
|
|
// Attempt new sessions for all external networks
|
|
for network in ExternalNetworkId::all() {
|
|
Abstractions::<T>::attempt_new_session(
|
|
network.into(),
|
|
Self::include_genesis_validators(network.into()),
|
|
);
|
|
}
|
|
|
|
// TODO Dex::<T>::on_new_session(network);
|
|
|
|
Weight::zero() // TODO
|
|
} else {
|
|
Weight::zero()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[pallet::call]
|
|
impl<T: Config> Pallet<T> {
|
|
#[pallet::call_index(0)]
|
|
#[pallet::weight((0, DispatchClass::Operational))] // TODO
|
|
pub fn set_keys(
|
|
origin: OriginFor<T>,
|
|
network: ExternalNetworkId,
|
|
key_pair: KeyPair,
|
|
signature_participants: bitvec::vec::BitVec<u8, bitvec::order::Lsb0>,
|
|
signature: Signature,
|
|
) -> DispatchResult {
|
|
ensure_none(origin)?;
|
|
|
|
// `signature_participants`, `signature` are checked within `ValidateUnsigned`
|
|
let _ = signature_participants;
|
|
let _ = signature;
|
|
|
|
let session = Self::current_session(NetworkId::from(network))
|
|
.expect("validated `set_keys` for a non-existent session");
|
|
let set = ExternalValidatorSet { network, session };
|
|
Abstractions::<T>::set_keys(set, key_pair.clone());
|
|
|
|
Core::<T>::emit_event(Event::SetKeys { set, key_pair });
|
|
|
|
// If this is the first session of an external network, mark them current, not solely decided
|
|
if session == Session(0) {
|
|
Abstractions::<T>::accept_handover(network.into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[pallet::call_index(1)]
|
|
#[pallet::weight((0, DispatchClass::Operational))] // TODO
|
|
pub fn report_slashes(
|
|
origin: OriginFor<T>,
|
|
network: ExternalNetworkId,
|
|
slashes: SlashReport,
|
|
signature: Signature,
|
|
) -> DispatchResult {
|
|
ensure_none(origin)?;
|
|
|
|
// `signature` is checked within `ValidateUnsigned`
|
|
let _ = signature;
|
|
|
|
Abstractions::<T>::handle_slash_report(network, slashes);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[pallet::call_index(2)]
|
|
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
|
pub fn set_embedded_elliptic_curve_keys(
|
|
origin: OriginFor<T>,
|
|
keys: SignedEmbeddedEllipticCurveKeys,
|
|
) -> DispatchResult {
|
|
let validator = ensure_signed(origin)?;
|
|
Self::set_embedded_elliptic_curve_keys_internal(validator, keys)
|
|
}
|
|
|
|
#[pallet::call_index(3)]
|
|
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
|
pub fn allocate(origin: OriginFor<T>, network: NetworkId, amount: Amount) -> DispatchResult {
|
|
let validator = ensure_signed(origin)?;
|
|
Coins::<T>::transfer_fn(validator, Self::account(), Balance { coin: Coin::Serai, amount })?;
|
|
Abstractions::<T>::increase_allocation(network, validator, amount, false)
|
|
.map_err(Error::<T>::AllocationError)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[pallet::call_index(4)]
|
|
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
|
pub fn deallocate(origin: OriginFor<T>, network: NetworkId, amount: Amount) -> DispatchResult {
|
|
let validator = ensure_signed(origin)?;
|
|
|
|
let timeline = Abstractions::<T>::decrease_allocation(network, validator, amount)
|
|
.map_err(Error::<T>::DeallocationError)?;
|
|
|
|
Core::<T>::emit_event(Event::Deallocation {
|
|
validator: validator.into(),
|
|
network,
|
|
amount,
|
|
timeline,
|
|
});
|
|
|
|
if matches!(timeline, DeallocationTimeline::Immediate) {
|
|
Coins::<T>::transfer_fn(Self::account(), validator, Balance { coin: Coin::Serai, amount })?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[pallet::call_index(5)]
|
|
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
|
pub fn claim_deallocation(
|
|
origin: OriginFor<T>,
|
|
network: NetworkId,
|
|
session: Session,
|
|
) -> DispatchResult {
|
|
let validator = ensure_signed(origin)?;
|
|
let amount = Abstractions::<T>::claim_delayed_deallocation(validator, network, session)
|
|
.map_err(Error::<T>::DeallocationError)?;
|
|
|
|
Core::<T>::emit_event(Event::DelayedDeallocationClaimed {
|
|
validator: validator.into(),
|
|
deallocation: ValidatorSet { network, session },
|
|
});
|
|
|
|
Coins::<T>::transfer_fn(Self::account(), validator, Balance { coin: Coin::Serai, amount })?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[pallet::validate_unsigned]
|
|
impl<T: Config> ValidateUnsigned for Pallet<T> {
|
|
type Call = Call<T>;
|
|
|
|
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 network has a session decided
|
|
let Some(latest_decided_session) = Self::latest_decided_session(network.into()) else {
|
|
Err(InvalidTransaction::BadSigner)?
|
|
};
|
|
|
|
let set = ExternalValidatorSet { network, session: latest_decided_session };
|
|
|
|
// Confirm this set has yet to set keys
|
|
if Abstractions::<T>::needs_to_set_keys(set) {
|
|
Err(InvalidTransaction::Stale)?;
|
|
}
|
|
|
|
let participants = Abstractions::<T>::selected_validators(set.into()).collect::<Vec<_>>();
|
|
assert!(
|
|
!participants.is_empty(),
|
|
"set which was decided had no selected participants stored"
|
|
);
|
|
|
|
// Check the bitvec is of the proper length
|
|
if participants.len() != signature_participants.len() {
|
|
Err(InvalidTransaction::BadProof)?;
|
|
}
|
|
|
|
// Find the participating, total key shares
|
|
let mut all_key_shares = 0;
|
|
let mut signers = vec![];
|
|
let mut signing_key_shares = 0;
|
|
for ((participant, shares), in_use) in
|
|
participants.into_iter().zip(signature_participants)
|
|
{
|
|
all_key_shares += shares.0;
|
|
|
|
if !in_use {
|
|
continue;
|
|
}
|
|
|
|
signers.push(participant);
|
|
signing_key_shares += shares.0;
|
|
}
|
|
|
|
// Check enough validators participated
|
|
{
|
|
let f = all_key_shares - signing_key_shares;
|
|
if signing_key_shares < ((2 * f) + 1) {
|
|
Err(InvalidTransaction::BadSigner)?;
|
|
}
|
|
}
|
|
|
|
// Verify the signature with the MuSig key of the signers
|
|
if !set.musig_key(&signers).verify(&set.set_keys_message(key_pair), &signature.0.into()) {
|
|
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) = Abstractions::<T>::waiting_for_slash_report(network) else {
|
|
Err(InvalidTransaction::Stale)?
|
|
};
|
|
|
|
if !key.verify(&slashes.report_slashes_message(), &signature.0.into()) {
|
|
Err(InvalidTransaction::BadProof)?;
|
|
}
|
|
|
|
ValidTransaction::with_tag_prefix("ValidatorSets")
|
|
.and_provides((1, key))
|
|
.longevity(KeySharesStruct::MAX_PER_SET_U32.into())
|
|
.propagate(true)
|
|
.build()
|
|
}
|
|
Call::set_embedded_elliptic_curve_keys { .. } |
|
|
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(|_| ())
|
|
}
|
|
}
|
|
|
|
/*
|
|
TODO: Add an intent. While we shouldn't allow `Transfer`, `AddLiquidity` when we're within a
|
|
certain range of the limit, we should still allow swaps.
|
|
*/
|
|
impl<T: Config> AllowMint for Pallet<T> {
|
|
fn is_allowed(balance: &ExternalBalance) -> bool {
|
|
let current_requirement = Self::network_stake_requirement(balance.coin.network());
|
|
let new_requirement = current_requirement.saturating_add(Self::stake_requirement(*balance));
|
|
let staked =
|
|
Abstractions::<T>::stake_for_current_validator_set(balance.coin.network().into())
|
|
.unwrap_or(Amount(0));
|
|
staked.0 >= new_requirement
|
|
}
|
|
}
|
|
}
|
|
pub use pallet::*;
|
|
|
|
sp_api::decl_runtime_apis! {
|
|
#[api_version(1)]
|
|
pub trait ValidatorSetsApi {
|
|
/// Returns the validator set for a given network.
|
|
fn validators(
|
|
network_id: serai_abi::primitives::network_id::NetworkId,
|
|
) -> Vec<serai_abi::primitives::crypto::Public>;
|
|
|
|
/// Returns the external network key for a given external network.
|
|
fn external_network_key(
|
|
network: serai_abi::primitives::network_id::ExternalNetworkId,
|
|
) -> Option<serai_abi::primitives::crypto::ExternalKey>;
|
|
}
|
|
}
|