From d66a7ee43ee03d6c8afe81c122bd3238b333a66a Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sun, 22 Oct 2023 03:59:21 -0400 Subject: [PATCH] Remove the staking pallet for validator-sets alone The staking pallet is an indirection which offered no practical benefit yet increased the overhead of every call. --- Cargo.lock | 18 +- Cargo.toml | 2 - deny.toml | 2 - substrate/runtime/Cargo.toml | 2 - substrate/runtime/src/lib.rs | 8 +- substrate/staking/pallet/Cargo.toml | 54 ---- substrate/staking/pallet/LICENSE | 15 -- substrate/staking/pallet/src/lib.rs | 233 ----------------- substrate/validator-sets/pallet/Cargo.toml | 4 + substrate/validator-sets/pallet/src/lib.rs | 289 +++++++++++++-------- 10 files changed, 193 insertions(+), 434 deletions(-) delete mode 100644 substrate/staking/pallet/Cargo.toml delete mode 100644 substrate/staking/pallet/LICENSE delete mode 100644 substrate/staking/pallet/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 70ea10de..d3ecf82f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8567,7 +8567,6 @@ dependencies = [ "serai-in-instructions-pallet", "serai-primitives", "serai-signals-pallet", - "serai-staking-pallet", "serai-validator-sets-pallet", "sp-api", "sp-authority-discovery", @@ -8599,22 +8598,6 @@ dependencies = [ "sp-io", ] -[[package]] -name = "serai-staking-pallet" -version = "0.1.0" -dependencies = [ - "frame-support", - "frame-system", - "pallet-session", - "parity-scale-codec", - "scale-info", - "serai-coins-pallet", - "serai-primitives", - "serai-validator-sets-pallet", - "sp-runtime", - "sp-std", -] - [[package]] name = "serai-validator-sets-pallet" version = "0.1.0" @@ -8624,6 +8607,7 @@ dependencies = [ "pallet-session", "parity-scale-codec", "scale-info", + "serai-coins-pallet", "serai-primitives", "serai-validator-sets-primitives", "sp-application-crypto", diff --git a/Cargo.toml b/Cargo.toml index c2acc6f0..ac7882b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,8 +46,6 @@ members = [ "substrate/validator-sets/primitives", "substrate/validator-sets/pallet", - "substrate/staking/pallet", - "substrate/signals/pallet", "substrate/runtime", diff --git a/deny.toml b/deny.toml index 7fb696fd..f846c761 100644 --- a/deny.toml +++ b/deny.toml @@ -60,8 +60,6 @@ exceptions = [ { allow = ["AGPL-3.0"], name = "serai-validator-sets-pallet" }, - { allow = ["AGPL-3.0"], name = "serai-staking-pallet" }, - { allow = ["AGPL-3.0"], name = "serai-signals-pallet" }, { allow = ["AGPL-3.0"], name = "serai-runtime" }, diff --git a/substrate/runtime/Cargo.toml b/substrate/runtime/Cargo.toml index c576fc2e..58563245 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -48,7 +48,6 @@ pallet-transaction-payment = { git = "https://github.com/serai-dex/substrate", d coins-pallet = { package = "serai-coins-pallet", path = "../coins/pallet", default-features = false } validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../validator-sets/pallet", default-features = false } -staking-pallet = { package = "serai-staking-pallet", path = "../staking/pallet", default-features = false } pallet-session = { git = "https://github.com/serai-dex/substrate", default-features = false } in-instructions-pallet = { package = "serai-in-instructions-pallet", path = "../in-instructions/pallet", default-features = false } @@ -103,7 +102,6 @@ std = [ "coins-pallet/std", "validator-sets-pallet/std", - "staking-pallet/std", "pallet-session/std", "in-instructions-pallet/std", diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index 4daae391..0ea3c246 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -18,7 +18,6 @@ pub use pallet_transaction_payment as transaction_payment; pub use coins_pallet as coins; -pub use staking_pallet as staking; pub use validator_sets_pallet as validator_sets; pub use pallet_session as session; @@ -169,7 +168,6 @@ impl Contains for CallFilter { // All of these pallets are our own, and all of their written calls are intended to be called RuntimeCall::Coins(call) => !matches!(call, coins::Call::__Ignore(_, _)), RuntimeCall::ValidatorSets(call) => !matches!(call, validator_sets::Call::__Ignore(_, _)), - RuntimeCall::Staking(call) => !matches!(call, staking::Call::__Ignore(_, _)), RuntimeCall::InInstructions(call) => !matches!(call, in_instructions::Call::__Ignore(_, _)), RuntimeCall::Signals(call) => !matches!(call, signals::Call::__Ignore(_, _)), @@ -248,9 +246,6 @@ impl coins::Config for Runtime { impl validator_sets::Config for Runtime { type RuntimeEvent = RuntimeEvent; } -impl staking::Config for Runtime { - type RuntimeEvent = RuntimeEvent; -} pub struct IdentityValidatorIdOf; impl Convert> for IdentityValidatorIdOf { @@ -265,7 +260,7 @@ impl session::Config for Runtime { type ValidatorIdOf = IdentityValidatorIdOf; type ShouldEndSession = Babe; type NextSessionRotation = Babe; - type SessionManager = Staking; + type SessionManager = ValidatorSets; type SessionHandler = ::KeyTypeIdProviders; type Keys = SessionKeys; type WeightInfo = session::weights::SubstrateWeight; @@ -349,7 +344,6 @@ construct_runtime!( Coins: coins, ValidatorSets: validator_sets, - Staking: staking, Session: session, InInstructions: in_instructions, diff --git a/substrate/staking/pallet/Cargo.toml b/substrate/staking/pallet/Cargo.toml deleted file mode 100644 index 16b24cf8..00000000 --- a/substrate/staking/pallet/Cargo.toml +++ /dev/null @@ -1,54 +0,0 @@ -[package] -name = "serai-staking-pallet" -version = "0.1.0" -description = "Staking pallet for Serai" -license = "AGPL-3.0-only" -repository = "https://github.com/serai-dex/serai/tree/develop/substrate/staking/pallet" -authors = ["Luke Parker "] -edition = "2021" - -[package.metadata.docs.rs] -all-features = true -rustdoc-args = ["--cfg", "docsrs"] - -[dependencies] -parity-scale-codec = { version = "3", default-features = false, features = ["derive"] } -scale-info = { version = "2", default-features = false, features = ["derive"] } - -sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false } -sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false } - -frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false } -frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false } - -serai-primitives = { path = "../../primitives", default-features = false } - -coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false } - -validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false } - -pallet-session = { git = "https://github.com/serai-dex/substrate", default-features = false } - -[features] -std = [ - "frame-system/std", - "frame-support/std", - - "sp-std/std", - "sp-runtime/std", - - "serai-primitives/std", - - "coins-pallet/std", - - "validator-sets-pallet/std", - - "pallet-session/std", -] - -runtime-benchmarks = [ - "frame-system/runtime-benchmarks", - "frame-support/runtime-benchmarks", -] - -default = ["std"] diff --git a/substrate/staking/pallet/LICENSE b/substrate/staking/pallet/LICENSE deleted file mode 100644 index c425427c..00000000 --- a/substrate/staking/pallet/LICENSE +++ /dev/null @@ -1,15 +0,0 @@ -AGPL-3.0-only license - -Copyright (c) 2022-2023 Luke Parker - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License Version 3 as -published by the Free Software Foundation. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . diff --git a/substrate/staking/pallet/src/lib.rs b/substrate/staking/pallet/src/lib.rs deleted file mode 100644 index 80a6c885..00000000 --- a/substrate/staking/pallet/src/lib.rs +++ /dev/null @@ -1,233 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] - -#[frame_support::pallet] -pub mod pallet { - use sp_runtime::traits::TrailingZeroInput; - use sp_std::vec::Vec; - - use frame_system::pallet_prelude::*; - use frame_support::pallet_prelude::*; - - use serai_primitives::*; - - use coins_pallet::{Config as CoinsConfig, Pallet as Coins}; - - use validator_sets_pallet::{ - primitives::{Session, ValidatorSet}, - Config as VsConfig, Pallet as VsPallet, - }; - use pallet_session::{Config as SessionConfig, SessionManager}; - - #[pallet::error] - pub enum Error { - StakeUnavilable, - NoDeallocation, - } - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - Staked { - validator: T::AccountId, - amount: Amount, - }, - Unstaked { - validator: T::AccountId, - amount: Amount, - }, - ImmediateDeallocation { - validator: T::AccountId, - network: NetworkId, - amount: Amount, - }, - DeallocationClaimed { - validator: T::AccountId, - network: NetworkId, - session: Session, - amount: Amount, - }, - } - - #[pallet::config] - pub trait Config: - frame_system::Config + CoinsConfig + VsConfig + SessionConfig - { - type RuntimeEvent: IsType<::RuntimeEvent> + From>; - } - - #[pallet::pallet] - pub struct Pallet(PhantomData); - - /// The amount of funds this account has staked. - #[pallet::storage] - #[pallet::getter(fn staked)] - pub type Staked = StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>; - - /// The amount of stake this account has allocated to validator sets. - #[pallet::storage] - #[pallet::getter(fn allocated)] - pub type Allocated = StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>; - - impl Pallet { - fn account() -> T::AccountId { - // Substrate has a pattern of using simply using 8-bytes (as a PalletId) directly as an - // AccountId. This replicates its internals to remove the 8-byte limit - T::AccountId::decode(&mut TrailingZeroInput::new(b"staking")).unwrap() - } - - fn add_stake(account: T::AccountId, amount: u64) { - Staked::::mutate(account, |staked| *staked += amount); - Self::deposit_event(Event::Staked { validator: account, amount: Amount(amount) }); - } - - fn remove_stake(account: T::AccountId, amount: u64) -> Result<(), Error> { - Staked::::mutate(account, |staked| { - let available = *staked - Self::allocated(account); - if available < amount { - Err(Error::::StakeUnavilable)?; - } - *staked -= amount; - Self::deposit_event(Event::Unstaked { validator: account, amount: Amount(amount) }); - Ok::<_, Error>(()) - }) - } - - fn allocate_internal(account: &T::AccountId, amount: u64) -> Result<(), Error> { - Allocated::::try_mutate(account, |allocated| { - let available = Self::staked(account) - *allocated; - if available < amount { - Err(Error::::StakeUnavilable)?; - } - *allocated += amount; - Ok(()) - }) - } - - fn deallocate_internal(account: &T::AccountId, amount: u64) -> Result<(), Error> { - Allocated::::try_mutate(account, |allocated| { - if *allocated < amount { - Err(Error::::StakeUnavilable)?; - } - *allocated -= amount; - Ok(()) - }) - } - } - - #[pallet::call] - impl Pallet { - /// Stake funds from this account. - #[pallet::call_index(0)] - #[pallet::weight((0, DispatchClass::Operational))] // TODO - pub fn stake(origin: OriginFor, #[pallet::compact] amount: u64) -> DispatchResult { - let signer = ensure_signed(origin)?; - let balance = Balance { coin: Coin::Serai, amount: Amount(amount) }; - Coins::::transfer_internal(signer, Self::account(), balance)?; - Self::add_stake(signer, amount); - Ok(()) - } - - /// Unstake funds from this account. Only unallocated funds may be unstaked. - #[pallet::call_index(1)] - #[pallet::weight((0, DispatchClass::Operational))] // TODO - pub fn unstake(origin: OriginFor, #[pallet::compact] amount: u64) -> DispatchResult { - let signer = ensure_signed(origin)?; - Self::remove_stake(signer, amount)?; - let balance = Balance { coin: Coin::Serai, amount: Amount(amount) }; - Coins::::transfer_internal(Self::account(), signer, balance)?; - Ok(()) - } - - /// Allocate `amount` to a given validator set. - #[pallet::call_index(2)] - #[pallet::weight((0, DispatchClass::Operational))] // TODO - pub fn allocate( - origin: OriginFor, - network: NetworkId, - #[pallet::compact] amount: u64, - ) -> DispatchResult { - let account = ensure_signed(origin)?; - - // add to amount allocated - Self::allocate_internal(&account, amount)?; - // This does not emit an event as the validator-sets pallet will - - // increase allocation for participant in validator set - VsPallet::::increase_allocation(network, account, Amount(amount))?; - Ok(()) - } - - /// Deallocate `amount` from a given validator set. - #[pallet::call_index(3)] - #[pallet::weight((0, DispatchClass::Operational))] // TODO - pub fn deallocate( - origin: OriginFor, - network: NetworkId, - #[pallet::compact] amount: u64, - ) -> DispatchResult { - let account = ensure_signed(origin)?; - - // decrease allocation in validator set - let can_immediately_deallocate = - VsPallet::::decrease_allocation(network, account, Amount(amount))?; - if can_immediately_deallocate { - Self::deallocate_internal(&account, amount)?; - Self::deposit_event(Event::ImmediateDeallocation { - validator: account, - network, - amount: Amount(amount), - }); - } - - Ok(()) - } - - #[pallet::call_index(4)] - #[pallet::weight((0, DispatchClass::Operational))] // TODO - pub fn claim_deallocation( - origin: OriginFor, - network: NetworkId, - session: Session, - ) -> DispatchResult { - let account = ensure_signed(origin)?; - let Some(amount) = VsPallet::::take_deallocatable_amount(network, session, account) else { - Err(Error::::NoDeallocation)? - }; - Self::deallocate_internal(&account, amount.0)?; - Self::deposit_event(Event::DeallocationClaimed { - validator: account, - network, - session, - amount, - }); - Ok(()) - } - } - - // Call order is end_session(i - 1) -> start_session(i) -> new_session(i + 1) - // new_session(i + 1) is called immediately after start_session(i) - // then we wait until the session ends then get a call to end_session(i) and so on. - impl SessionManager for Pallet { - fn new_session(_new_index: u32) -> Option> { - VsPallet::::new_session(); - // TODO: Where do we return their stake? - Some(VsPallet::::select_validators(NetworkId::Serai)) - } - - fn new_session_genesis(_: u32) -> Option> { - // TODO: Because we don't call new_session here, we don't emit NewSet { Serai, session: 1 } - Some(VsPallet::::select_validators(NetworkId::Serai)) - } - - fn end_session(end_index: u32) { - VsPallet::::retire_set(ValidatorSet { - network: NetworkId::Serai, - session: Session(end_index), - }) - } - - fn start_session(_start_index: u32) {} - } -} - -pub use pallet::*; diff --git a/substrate/validator-sets/pallet/Cargo.toml b/substrate/validator-sets/pallet/Cargo.toml index 745cf087..110679f8 100644 --- a/substrate/validator-sets/pallet/Cargo.toml +++ b/substrate/validator-sets/pallet/Cargo.toml @@ -29,6 +29,8 @@ pallet-session = { git = "https://github.com/serai-dex/substrate", default-featu serai-primitives = { path = "../../primitives", default-features = false } validator-sets-primitives = { package = "serai-validator-sets-primitives", path = "../primitives", default-features = false } +coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false } + [features] std = [ "scale/std", @@ -47,6 +49,8 @@ std = [ "serai-primitives/std", "validator-sets-primitives/std", + + "coins-pallet/std", ] runtime-benchmarks = [ diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index 7cb3f8c9..72183b46 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -16,9 +16,12 @@ pub mod pallet { pub use validator_sets_primitives as primitives; use primitives::*; + use coins_pallet::Pallet as Coins; + #[pallet::config] pub trait Config: frame_system::Config + + coins_pallet::Config + pallet_session::Config + TypeInfo { @@ -245,37 +248,6 @@ pub mod pallet { } } - impl Pallet { - 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 SortedAllocationsIter::::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 }; - - // key_shares may be over MAX_KEY_SHARES_PER_SET, which will cause an off-chain 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.into()) - } - } - /// Pending deallocations, keyed by the Session they become unlocked on. #[pallet::storage] type PendingDeallocations = @@ -312,6 +284,11 @@ pub mod pallet { amount: Amount, delayed_until: Option, }, + DeallocationClaimed { + validator: T::AccountId, + network: NetworkId, + session: Session, + }, SetRetired { set: ValidatorSet, }, @@ -383,6 +360,8 @@ pub mod pallet { DeallocationWouldRemoveParticipant, /// Deallocation would cause the validator set to no longer achieve fault tolerance. DeallocationWouldRemoveFaultTolerance, + /// Deallocation to be claimed doesn't exist. + NonExistentDeallocation, /// Validator Set already generated keys. AlreadyGeneratedKeys, /// An invalid MuSig signature was provided. @@ -426,78 +405,41 @@ pub mod pallet { } } - #[pallet::call] impl Pallet { - #[pallet::call_index(0)] - #[pallet::weight(0)] // TODO - pub fn set_keys( - origin: OriginFor, - network: NetworkId, - key_pair: KeyPair, - 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; - - let session = Session(pallet_session::Pallet::::current_index()); - - let set = ValidatorSet { session, network }; - - Keys::::set(set, Some(key_pair.clone())); - Self::deposit_event(Event::KeyGen { set, key_pair }); - - Ok(()) + fn account() -> T::AccountId { + system_address(b"validator-sets").into() } - } - #[pallet::validate_unsigned] - impl ValidateUnsigned for Pallet { - type Call = Call; + fn is_bft(network: NetworkId) -> bool { + let allocation_per_key_share = AllocationPerKeyShare::::get(network).unwrap().0; - fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity { - // Match to be exhaustive - let (network, key_pair, signature) = match call { - Call::set_keys { network, ref key_pair, ref signature } => (network, key_pair, signature), - Call::__Ignore(_, _) => unreachable!(), - }; + let mut validators_len = 0; + let mut top = None; + let mut key_shares = 0; + for (_, amount) in SortedAllocationsIter::::new(network) { + validators_len += 1; - let session = Session(pallet_session::Pallet::::current_index()); + key_shares += amount.0 / allocation_per_key_share; + if top.is_none() { + top = Some(key_shares); + } - let set = ValidatorSet { session, network: *network }; - match Self::verify_signature(set, key_pair, signature) { - Err(Error::AlreadyGeneratedKeys) => Err(InvalidTransaction::Stale)?, - Err(Error::NonExistentValidatorSet) | - Err(Error::InsufficientAllocation) | - Err(Error::NotEnoughAllocated) | - Err(Error::AllocationWouldRemoveFaultTolerance) | - Err(Error::DeallocationWouldRemoveParticipant) | - Err(Error::DeallocationWouldRemoveFaultTolerance) | - Err(Error::NonExistentValidator) | - Err(Error::BadSignature) => Err(InvalidTransaction::BadProof)?, - Err(Error::__Ignore(_, _)) => unreachable!(), - Ok(()) => (), + if key_shares > u64::from(MAX_KEY_SHARES_PER_SET) { + break; + } } - ValidTransaction::with_tag_prefix("validator-sets") - .and_provides(set) - // Set a 10 block longevity, though this should be included in the next block - .longevity(10) - .propagate(true) - .build() + let Some(top) = top else { return false }; + + // key_shares may be over MAX_KEY_SHARES_PER_SET, which will cause an off-chain 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.into()) } - // Explicitly provide a pre-dispatch which calls validate_unsigned - fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> { - Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ()).map_err(Into::into) - } - } - - impl Pallet { - #[frame_support::transactional] - pub fn increase_allocation( + fn increase_allocation( network: NetworkId, account: T::AccountId, amount: Amount, @@ -548,8 +490,7 @@ pub mod pallet { /// doesn't become used (preventing deallocation). /// /// Returns if the amount is immediately eligible for deallocation. - #[frame_support::transactional] - pub fn decrease_allocation( + fn decrease_allocation( network: NetworkId, account: T::AccountId, amount: Amount, @@ -609,7 +550,7 @@ pub mod pallet { return Ok(true); } - // Set it to PendingDeallocations, letting the staking pallet release it on a future session + // Set it to PendingDeallocations, letting it be released upon a future session // This unwrap should be fine as this account is active, meaning a session has occurred let mut to_unlock_on = Self::session(network).unwrap(); if network == NetworkId::Serai { @@ -665,7 +606,7 @@ pub mod pallet { true } - pub fn new_session() { + 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 }; @@ -677,10 +618,6 @@ pub mod pallet { } } - pub fn select_validators(network: NetworkId) -> Vec { - Self::participants(network).into() - } - pub fn retire_set(set: ValidatorSet) { MuSigKeys::::remove(set); Keys::::remove(set); @@ -690,7 +627,7 @@ pub mod pallet { /// Take the amount deallocatable. /// /// `session` refers to the Session the stake becomes deallocatable on. - pub fn take_deallocatable_amount( + fn take_deallocatable_amount( network: NetworkId, session: Session, key: Public, @@ -702,6 +639,154 @@ pub mod pallet { PendingDeallocations::::take((network, session, key)) } } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(0)] // TODO + pub fn set_keys( + origin: OriginFor, + network: NetworkId, + key_pair: KeyPair, + 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; + + let session = Session(pallet_session::Pallet::::current_index()); + + let set = ValidatorSet { session, network }; + + Keys::::set(set, Some(key_pair.clone())); + Self::deposit_event(Event::KeyGen { set, key_pair }); + + Ok(()) + } + + #[pallet::call_index(1)] + #[pallet::weight(0)] // TODO + pub fn allocate(origin: OriginFor, network: NetworkId, amount: Amount) -> DispatchResult { + let validator = ensure_signed(origin)?; + Coins::::transfer_internal( + validator, + Self::account(), + Balance { coin: Coin::Serai, amount }, + )?; + Self::increase_allocation(network, validator, amount) + } + + #[pallet::call_index(2)] + #[pallet::weight(0)] // TODO + pub fn deallocate(origin: OriginFor, 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::::transfer_internal( + Self::account(), + account, + Balance { coin: Coin::Serai, amount }, + )?; + } + + Ok(()) + } + + #[pallet::call_index(3)] + #[pallet::weight((0, DispatchClass::Operational))] // TODO + pub fn claim_deallocation( + origin: OriginFor, + network: NetworkId, + session: Session, + ) -> DispatchResult { + let account = ensure_signed(origin)?; + let Some(amount) = Self::take_deallocatable_amount(network, session, account) else { + Err(Error::::NonExistentDeallocation)? + }; + Coins::::transfer_internal( + Self::account(), + account, + Balance { coin: Coin::Serai, amount }, + )?; + Self::deposit_event(Event::DeallocationClaimed { + validator: account, + network, + session, + }); + Ok(()) + } + } + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + + fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity { + // Match to be exhaustive + let (network, key_pair, signature) = match call { + Call::set_keys { network, ref key_pair, ref signature } => (network, key_pair, signature), + Call::allocate { .. } | Call::deallocate { .. } | Call::claim_deallocation { .. } => { + Err(InvalidTransaction::Call)? + } + Call::__Ignore(_, _) => unreachable!(), + }; + + let session = Session(pallet_session::Pallet::::current_index()); + + let set = ValidatorSet { session, network: *network }; + match Self::verify_signature(set, key_pair, signature) { + Err(Error::AlreadyGeneratedKeys) => Err(InvalidTransaction::Stale)?, + Err(Error::NonExistentValidatorSet) | + Err(Error::InsufficientAllocation) | + Err(Error::NotEnoughAllocated) | + Err(Error::AllocationWouldRemoveFaultTolerance) | + Err(Error::DeallocationWouldRemoveParticipant) | + Err(Error::DeallocationWouldRemoveFaultTolerance) | + Err(Error::NonExistentDeallocation) | + Err(Error::NonExistentValidator) | + Err(Error::BadSignature) => Err(InvalidTransaction::BadProof)?, + Err(Error::__Ignore(_, _)) => unreachable!(), + Ok(()) => (), + } + + ValidTransaction::with_tag_prefix("validator-sets") + .and_provides(set) + // Set a 10 block longevity, though this should be included in the next block + .longevity(10) + .propagate(true) + .build() + } + + // Explicitly provide a pre-dispatch which calls validate_unsigned + fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> { + Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ()).map_err(Into::into) + } + } + + // Call order is end_session(i - 1) -> start_session(i) -> new_session(i + 1) + // new_session(i + 1) is called immediately after start_session(i) + // then we wait until the session ends then get a call to end_session(i) and so on. + impl pallet_session::SessionManager for Pallet { + fn new_session(_new_index: u32) -> Option> { + Self::new_session(); + // TODO: Where do we return their stake? + Some(Self::participants(NetworkId::Serai).into()) + } + + fn new_session_genesis(_: u32) -> Option> { + // TODO: Because we don't call new_session here, we don't emit NewSet { Serai, session: 1 } + Some(Self::participants(NetworkId::Serai).into()) + } + + fn end_session(end_index: u32) { + Self::retire_set(ValidatorSet { network: NetworkId::Serai, session: Session(end_index) }) + } + + fn start_session(_start_index: u32) {} + } } pub use pallet::*;