diff --git a/prdoc/pr_11513.prdoc b/prdoc/pr_11513.prdoc new file mode 100644 index 0000000000000..8ec2c4706182f --- /dev/null +++ b/prdoc/pr_11513.prdoc @@ -0,0 +1,18 @@ +title: Add issuance/budget traits +doc: +- audience: Runtime Dev + description: |- + Moves `EraPayout` trait from `pallet-staking` and `pallet-staking-async` to `sp-staking`, + eliminating duplicate definitions. Adds a new `budget` module with stake independent + traits for issuance computation (`IssuanceCurve`) and budget distribution + (`BudgetRecipient`, `BudgetRecipientList`). Also adds `StakerRewardCalculator` trait for + customizing validator incentive weights and staker reward splits. + + No behavior changes. Existing `EraPayout` re-exports from both staking pallets are preserved. +crates: +- name: sp-staking + bump: minor +- name: pallet-staking + bump: patch +- name: pallet-staking-async + bump: patch diff --git a/substrate/frame/staking-async/src/lib.rs b/substrate/frame/staking-async/src/lib.rs index 171ec3e440912..9aac50b542639 100644 --- a/substrate/frame/staking-async/src/lib.rs +++ b/substrate/frame/staking-async/src/lib.rs @@ -489,28 +489,7 @@ impl NominationsQuota for FixedNominationsQuot } } -/// Handler for determining how much of a balance should be paid out on the current era. -pub trait EraPayout { - /// Determine the payout for this era. - /// - /// Returns the amount to be paid to stakers in this era, as well as whatever else should be - /// paid out ("the rest"). - fn era_payout( - total_staked: Balance, - total_issuance: Balance, - era_duration_millis: u64, - ) -> (Balance, Balance); -} - -impl EraPayout for () { - fn era_payout( - _total_staked: Balance, - _total_issuance: Balance, - _era_duration_millis: u64, - ) -> (Balance, Balance) { - (Default::default(), Default::default()) - } -} +pub use sp_staking::EraPayout; /// Mode of era-forcing. #[derive( diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index cb264e3d0d274..c510f3952a5cc 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -331,7 +331,7 @@ use sp_staking::{ offence::{Offence, OffenceError, OffenceSeverity, ReportOffence}, EraIndex, ExposurePage, OnStakingUpdate, Page, PagedExposureMetadata, SessionIndex, }; -pub use sp_staking::{Exposure, IndividualExposure, StakerStatus}; +pub use sp_staking::{EraPayout, Exposure, IndividualExposure, StakerStatus}; pub use weights::WeightInfo; pub use pallet::{pallet::*, UseNominatorsAndValidatorsMap, UseValidatorsMap}; @@ -977,33 +977,10 @@ impl SessionInterface for () { } } -/// Handler for determining how much of a balance should be paid out on the current era. -pub trait EraPayout { - /// Determine the payout for this era. - /// - /// Returns the amount to be paid to stakers in this era, as well as whatever else should be - /// paid out ("the rest"). - fn era_payout( - total_staked: Balance, - total_issuance: Balance, - era_duration_millis: u64, - ) -> (Balance, Balance); -} - -impl EraPayout for () { - fn era_payout( - _total_staked: Balance, - _total_issuance: Balance, - _era_duration_millis: u64, - ) -> (Balance, Balance) { - (Default::default(), Default::default()) - } -} - /// Adaptor to turn a `PiecewiseLinear` curve definition into an `EraPayout` impl, used for /// backwards compatibility. pub struct ConvertCurve(core::marker::PhantomData); -impl EraPayout for ConvertCurve +impl sp_staking::EraPayout for ConvertCurve where Balance: AtLeast32BitUnsigned + Clone + Copy, T: Get<&'static PiecewiseLinear<'static>>, diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 2562594c9eb6f..df5951dbaf859 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -44,14 +44,14 @@ use sp_runtime::{ use sp_staking::{ currency_to_vote::CurrencyToVote, offence::{OffenceDetails, OnOffenceHandler}, - EraIndex, OnStakingUpdate, Page, SessionIndex, Stake, + EraIndex, EraPayout, OnStakingUpdate, Page, SessionIndex, Stake, StakingAccount::{self, Controller, Stash}, StakingInterface, }; use crate::{ asset, election_size_tracker::StaticTracker, log, slashing, weights::WeightInfo, ActiveEraInfo, - BalanceOf, EraInfo, EraPayout, Exposure, Forcing, IndividualExposure, LedgerIntegrityState, + BalanceOf, EraInfo, Exposure, Forcing, IndividualExposure, LedgerIntegrityState, MaxNominationsOf, MaxWinnersOf, Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface, StakingLedger, UnlockChunk, ValidatorPrefs, STAKING_ID, }; diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index a4f49d3082499..b890fc0bc0237 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -50,7 +50,7 @@ mod impls; pub use impls::*; use crate::{ - asset, slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraPayout, + asset, slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraRewardPoints, Exposure, ExposurePage, Forcing, LedgerIntegrityState, MaxNominationsOf, NegativeImbalanceOf, Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk, ValidatorPrefs, @@ -231,7 +231,7 @@ pub mod pallet { /// The payout for validators and the system for the current era. /// See [Era payout](./index.html#era-payout). #[pallet::no_default] - type EraPayout: EraPayout>; + type EraPayout: sp_staking::EraPayout>; /// Something that can estimate the next session change, accurately or as a best effort /// guess. diff --git a/substrate/primitives/staking/src/budget.rs b/substrate/primitives/staking/src/budget.rs new file mode 100644 index 0000000000000..c8c7caf8633df --- /dev/null +++ b/substrate/primitives/staking/src/budget.rs @@ -0,0 +1,136 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Traits for inflation issuance and budget distribution. +//! +//! These traits define how new tokens are minted and distributed among budget recipients +//! (e.g. staker rewards, validator incentives). + +use alloc::vec::Vec; +use sp_runtime::BoundedVec; + +/// Maximum length of a budget key identifier. +pub const MAX_BUDGET_KEY_LEN: u32 = 32; + +/// Identifier for a budget category in the inflation distribution system. +/// +/// Each budget recipient (e.g. staker rewards, validator incentive) is identified +/// by a unique key. Keys are bounded to [`MAX_BUDGET_KEY_LEN`] bytes. +pub type BudgetKey = BoundedVec>; + +/// Computes new token issuance for a given time period. +/// +/// Unlike [`super::EraPayout`], this trait does not depend on staking state. Issuance is +/// purely a function of total supply and elapsed time. +pub trait IssuanceCurve { + /// Compute how much new tokens to mint for the given period. + fn issue(total_issuance: Balance, elapsed_millis: u64) -> Balance; +} + +/// A recipient of inflation budget. +/// +/// Pallets that want a share of inflation implement this trait, providing a unique key +/// and a pot account where minted funds are deposited. +pub trait BudgetRecipient { + /// Unique identifier for this budget category. + fn budget_key() -> BudgetKey; + /// The account that receives minted inflation funds. + fn pot_account() -> AccountId; +} + +/// Aggregates multiple [`BudgetRecipient`]s into a list. +/// +/// Implemented for tuples of `BudgetRecipient` types, allowing runtime configuration like: +/// ```ignore +/// type BudgetRecipients = (StakerRewardRecipient, ValidatorIncentiveRecipient); +/// ``` +pub trait BudgetRecipientList { + /// Collect all registered recipients as `(key, account)` pairs. + fn recipients() -> Vec<(BudgetKey, AccountId)>; +} + +impl BudgetRecipientList for () { + fn recipients() -> Vec<(BudgetKey, AccountId)> { + Vec::new() + } +} + +#[impl_trait_for_tuples::impl_for_tuples(1, 10)] +#[tuple_types_custom_trait_bound(BudgetRecipient)] +impl BudgetRecipientList for Tuple { + fn recipients() -> Vec<(BudgetKey, AccountId)> { + let mut v = Vec::new(); + for_tuples!( #( v.push((Tuple::budget_key(), Tuple::pot_account())); )* ); + debug_assert!( + { + let mut keys: Vec<_> = v.iter().map(|(k, _)| k.clone()).collect(); + keys.sort(); + keys.windows(2).all(|w| w[0] != w[1]) + }, + "Duplicate BudgetRecipient key detected" + ); + v + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct RecipientA; + impl BudgetRecipient for RecipientA { + fn budget_key() -> BudgetKey { + BudgetKey::truncate_from(b"alpha".to_vec()) + } + fn pot_account() -> u64 { + 1 + } + } + + struct RecipientB; + impl BudgetRecipient for RecipientB { + fn budget_key() -> BudgetKey { + BudgetKey::truncate_from(b"beta".to_vec()) + } + fn pot_account() -> u64 { + 2 + } + } + + // Duplicate key: same as RecipientA. + struct RecipientDuplicate; + impl BudgetRecipient for RecipientDuplicate { + fn budget_key() -> BudgetKey { + BudgetKey::truncate_from(b"alpha".to_vec()) + } + fn pot_account() -> u64 { + 3 + } + } + + #[test] + fn unique_keys_work() { + let recipients = <(RecipientA, RecipientB) as BudgetRecipientList>::recipients(); + assert_eq!(recipients.len(), 2); + } + + #[test] + #[should_panic(expected = "Duplicate BudgetRecipient key detected")] + fn duplicate_keys_panics() { + let _ = <(RecipientA, RecipientDuplicate) as BudgetRecipientList>::recipients(); + } +} diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 42d35f8ed3b33..d58aa4b52c814 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -32,6 +32,7 @@ use sp_runtime::{ Debug, DispatchError, DispatchResult, Perbill, Saturating, }; +pub mod budget; pub mod offence; pub mod currency_to_vote; @@ -726,6 +727,67 @@ pub trait DelegationMigrator { fn force_kill_agent(agent: Agent); } +/// Handler for determining how much of a balance should be paid out on the current era. +/// +/// [`budget::IssuanceCurve`] is the successor to this trait, decoupling issuance computation +/// from staking state. +pub trait EraPayout { + /// Determine the payout for this era. + /// + /// Returns the amount to be paid to stakers in this era, as well as whatever else should be + /// paid out ("the rest"). + fn era_payout( + total_staked: Balance, + total_issuance: Balance, + era_duration_millis: u64, + ) -> (Balance, Balance); +} + +impl EraPayout for () { + fn era_payout( + _total_staked: Balance, + _total_issuance: Balance, + _era_duration_millis: u64, + ) -> (Balance, Balance) { + (Default::default(), Default::default()) + } +} + +/// Result of splitting a validator's staking reward between the validator and their nominators. +/// +/// Produced by [`StakerRewardCalculator::calculate_staker_reward`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StakerRewardResult { + /// Total payout for the validator (commission + proportional stake reward). + pub validator_payout: Balance, + /// Total payout for all nominators, to be split proportionally by the caller. + pub nominator_payout: Balance, +} + +/// Handles two independent reward calculations: +/// +/// 1. **Staker reward split** ([`Self::calculate_staker_reward`]) — determines how a validator's +/// staking reward is divided between the validator and their nominators. +/// +/// 2. **Validator incentive weight** ([`Self::calculate_validator_incentive_weight`]) — determines +/// a validator's relative share of a separate validator incentive pot, based on self-stake. This +/// incentive pot is validator-only; nominators do not receive from it. +pub trait StakerRewardCalculator { + /// Compute a weight for this validator's share of the validator incentive pot. + /// + /// Called once per validator during era planning. All validators' weights are summed, and + /// each validator's incentive payout is proportional to `their_weight / total_weight`. + fn calculate_validator_incentive_weight(self_stake: Balance) -> Balance; + + /// Split a validator's staking reward into validator and nominator portions. + fn calculate_staker_reward( + validator_total_reward: Balance, + validator_commission: Perbill, + validator_own_stake: Balance, + total_stake: Balance, + ) -> StakerRewardResult; +} + sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $); sp_core::generate_feature_enabled_macro!(std_or_benchmarks_enabled, any(feature = "std", feature = "runtime-benchmarks"), $);