From 1d17819d8b7a69d11af541c70ea65a4cc91e9432 Mon Sep 17 00:00:00 2001 From: Ankan Date: Thu, 26 Mar 2026 08:01:37 +0100 Subject: [PATCH 01/11] frontrunning traits --- polkadot/runtime/westend/src/lib.rs | 1 + polkadot/runtime/westend/src/tests.rs | 1 + substrate/frame/staking/src/lib.rs | 29 +--- substrate/frame/staking/src/mock.rs | 2 + substrate/frame/staking/src/pallet/impls.rs | 5 +- substrate/frame/staking/src/pallet/mod.rs | 5 +- substrate/primitives/staking/src/lib.rs | 149 +++++++++++++++++++- 7 files changed, 163 insertions(+), 29 deletions(-) diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 7929d6b531d00..576c425aa21ca 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -709,6 +709,7 @@ impl pallet_bags_list::Config for Runtime { } pub struct EraPayout; +#[allow(deprecated)] impl pallet_staking::EraPayout for EraPayout { fn era_payout( _total_staked: Balance, diff --git a/polkadot/runtime/westend/src/tests.rs b/polkadot/runtime/westend/src/tests.rs index c2acd70b4bed3..6a4ad4223df79 100644 --- a/polkadot/runtime/westend/src/tests.rs +++ b/polkadot/runtime/westend/src/tests.rs @@ -27,6 +27,7 @@ use frame_support::{ WhitelistedStorageKeys, }, }; +#[allow(deprecated)] use pallet_staking::EraPayout; use sp_core::{crypto::Ss58Codec, hexdisplay::HexDisplay}; use sp_keyring::Sr25519Keyring::{self, Alice}; diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index cb264e3d0d274..6cfc6b32b7c56 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -331,7 +331,8 @@ use sp_staking::{ offence::{Offence, OffenceError, OffenceSeverity, ReportOffence}, EraIndex, ExposurePage, OnStakingUpdate, Page, PagedExposureMetadata, SessionIndex, }; -pub use sp_staking::{Exposure, IndividualExposure, StakerStatus}; +#[allow(deprecated)] +pub use sp_staking::{EraPayout, Exposure, IndividualExposure, StakerStatus}; pub use weights::WeightInfo; pub use pallet::{pallet::*, UseNominatorsAndValidatorsMap, UseValidatorsMap}; @@ -977,33 +978,11 @@ 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 +#[allow(deprecated)] +impl sp_staking::EraPayout for ConvertCurve where Balance: AtLeast32BitUnsigned + Clone + Copy, T: Get<&'static PiecewiseLinear<'static>>, diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index 0826cec3b4057..f159c0b148913 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -686,6 +686,7 @@ pub(crate) fn start_active_era(era_index: EraIndex) { assert_eq!(current_era(), active_era()); } +#[allow(deprecated)] pub(crate) fn current_total_payout_for_duration(duration: u64) -> Balance { let (payout, _rest) = ::EraPayout::era_payout( pallet_staking::ErasTotalStake::::get(active_era()), @@ -696,6 +697,7 @@ pub(crate) fn current_total_payout_for_duration(duration: u64) -> Balance { payout } +#[allow(deprecated)] pub(crate) fn maximum_payout_for_duration(duration: u64) -> Balance { let (payout, rest) = ::EraPayout::era_payout( pallet_staking::ErasTotalStake::::get(active_era()), diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 2562594c9eb6f..af8da5daa4732 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -41,6 +41,8 @@ use sp_runtime::{ }, ArithmeticError, DispatchResult, Perbill, Percent, }; +#[allow(deprecated)] +use sp_staking::EraPayout; use sp_staking::{ currency_to_vote::CurrencyToVote, offence::{OffenceDetails, OnOffenceHandler}, @@ -51,7 +53,7 @@ use sp_staking::{ 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, }; @@ -578,6 +580,7 @@ impl Pallet { let staked = ErasTotalStake::::get(&active_era.index); let issuance = asset::total_issuance::(); + #[allow(deprecated)] let (validator_payout, remainder) = T::EraPayout::era_payout(staked, issuance, era_duration); diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index a4f49d3082499..29812211236d3 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,8 @@ 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>; + #[allow(deprecated)] + 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/lib.rs b/substrate/primitives/staking/src/lib.rs index 42d35f8ed3b33..389befb3024b7 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -29,7 +29,7 @@ use core::ops::{Add, AddAssign, Sub, SubAssign}; use scale_info::TypeInfo; use sp_runtime::{ traits::{AtLeast32BitUnsigned, Zero}, - Debug, DispatchError, DispatchResult, Perbill, Saturating, + BoundedVec, Debug, DispatchError, DispatchResult, Perbill, Saturating, }; pub mod offence; @@ -726,6 +726,153 @@ pub trait DelegationMigrator { fn force_kill_agent(agent: Agent); } +/// Allocation breakdown for era rewards among stakers. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EraRewardAllocation { + /// Amount allocated to stakers (nominators + validator stake rewards). + pub staker_rewards: Balance, + /// Amount allocated to validator self-stake incentive. + pub validator_incentive: Balance, +} + +/// Trait for receiving unclaimed staking rewards. +/// +/// When era pot accounts are cleaned up, any remaining balance is deposited into the sink. +/// The implementor handles both the transfer and any bookkeeping (e.g. deactivation). +pub trait UnclaimedRewardSink { + /// Transfer unclaimed rewards from `source` to the sink. + fn deposit(source: &AccountId, amount: Balance) -> DispatchResult; +} + +impl UnclaimedRewardSink for () { + fn deposit(_source: &AccountId, _amount: Balance) -> DispatchResult { + Ok(()) + } +} + +/// Handler for determining how much of a balance should be paid out on the current era. +/// +/// Used by `pallet-staking` (legacy). New code should use [`IssuanceCurve`] instead, +/// which decouples issuance from staking state. +#[deprecated(note = "Use `IssuanceCurve` instead, which decouples issuance 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); +} + +#[allow(deprecated)] +impl EraPayout for () { + fn era_payout( + _total_staked: Balance, + _total_issuance: Balance, + _era_duration_millis: u64, + ) -> (Balance, Balance) { + (Default::default(), Default::default()) + } +} + +/// 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, buffer) 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 [`EraPayout`], this trait does not depend on staking state (`total_staked`). +/// 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. + /// + /// # Parameters + /// - `total_issuance`: Current total token supply + /// - `elapsed_millis`: Time elapsed since last issuance drip, in milliseconds + 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())); )* ); + v + } +} + +/// Result of staker reward calculation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StakerRewardResult { + /// Total payout for the validator (staking reward + commission) + pub validator_payout: Balance, + /// Total payout for all nominators (to be split proportionally by caller) + pub nominator_payout: Balance, +} + +/// Trait for calculating staking rewards including validator incentives. +/// +/// This trait allows runtimes to customize both: +/// - How validator self-stake is weighted for self-stake incentive rewards +/// - How staking rewards are distributed between validators and nominators +pub trait StakerRewardCalculator { + /// Calculate the reward weight for a validator's self-stake. + /// + /// Used for distributing validator self-stake incentive rewards proportionally. + /// The implementation defines the curve shape and reads any parameters it needs + /// (e.g. optimum, cap, slope) from its own configuration. + fn calculate_validator_incentive_weight(self_stake: Balance) -> Balance; + + /// Calculate how staking rewards are distributed between validator and nominators. + /// + /// Implements the standard staking reward distribution: + /// 1. Apply validator commission + /// 2. Split remaining between validator and nominators based on stake + 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"), $); From c3dc7e8cfa1a11a02b712a23c0b58b42b825cc46 Mon Sep 17 00:00:00 2001 From: Ankan Date: Fri, 27 Mar 2026 06:22:30 +0100 Subject: [PATCH 02/11] dont deprecate yet --- polkadot/runtime/westend/src/lib.rs | 1 - polkadot/runtime/westend/src/tests.rs | 1 - substrate/frame/staking/src/lib.rs | 2 -- substrate/frame/staking/src/mock.rs | 2 -- substrate/frame/staking/src/pallet/impls.rs | 4 +--- substrate/frame/staking/src/pallet/mod.rs | 1 - substrate/primitives/staking/src/lib.rs | 21 ++------------------- 7 files changed, 3 insertions(+), 29 deletions(-) diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 576c425aa21ca..7929d6b531d00 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -709,7 +709,6 @@ impl pallet_bags_list::Config for Runtime { } pub struct EraPayout; -#[allow(deprecated)] impl pallet_staking::EraPayout for EraPayout { fn era_payout( _total_staked: Balance, diff --git a/polkadot/runtime/westend/src/tests.rs b/polkadot/runtime/westend/src/tests.rs index 6a4ad4223df79..c2acd70b4bed3 100644 --- a/polkadot/runtime/westend/src/tests.rs +++ b/polkadot/runtime/westend/src/tests.rs @@ -27,7 +27,6 @@ use frame_support::{ WhitelistedStorageKeys, }, }; -#[allow(deprecated)] use pallet_staking::EraPayout; use sp_core::{crypto::Ss58Codec, hexdisplay::HexDisplay}; use sp_keyring::Sr25519Keyring::{self, Alice}; diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 6cfc6b32b7c56..c510f3952a5cc 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -331,7 +331,6 @@ use sp_staking::{ offence::{Offence, OffenceError, OffenceSeverity, ReportOffence}, EraIndex, ExposurePage, OnStakingUpdate, Page, PagedExposureMetadata, SessionIndex, }; -#[allow(deprecated)] pub use sp_staking::{EraPayout, Exposure, IndividualExposure, StakerStatus}; pub use weights::WeightInfo; @@ -981,7 +980,6 @@ impl SessionInterface for () { /// Adaptor to turn a `PiecewiseLinear` curve definition into an `EraPayout` impl, used for /// backwards compatibility. pub struct ConvertCurve(core::marker::PhantomData); -#[allow(deprecated)] impl sp_staking::EraPayout for ConvertCurve where Balance: AtLeast32BitUnsigned + Clone + Copy, diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index f159c0b148913..0826cec3b4057 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -686,7 +686,6 @@ pub(crate) fn start_active_era(era_index: EraIndex) { assert_eq!(current_era(), active_era()); } -#[allow(deprecated)] pub(crate) fn current_total_payout_for_duration(duration: u64) -> Balance { let (payout, _rest) = ::EraPayout::era_payout( pallet_staking::ErasTotalStake::::get(active_era()), @@ -697,7 +696,6 @@ pub(crate) fn current_total_payout_for_duration(duration: u64) -> Balance { payout } -#[allow(deprecated)] pub(crate) fn maximum_payout_for_duration(duration: u64) -> Balance { let (payout, rest) = ::EraPayout::era_payout( pallet_staking::ErasTotalStake::::get(active_era()), diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index af8da5daa4732..5db28da42ab2c 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -41,9 +41,8 @@ use sp_runtime::{ }, ArithmeticError, DispatchResult, Perbill, Percent, }; -#[allow(deprecated)] -use sp_staking::EraPayout; use sp_staking::{ + EraPayout, currency_to_vote::CurrencyToVote, offence::{OffenceDetails, OnOffenceHandler}, EraIndex, OnStakingUpdate, Page, SessionIndex, Stake, @@ -580,7 +579,6 @@ impl Pallet { let staked = ErasTotalStake::::get(&active_era.index); let issuance = asset::total_issuance::(); - #[allow(deprecated)] let (validator_payout, remainder) = T::EraPayout::era_payout(staked, issuance, era_duration); diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 29812211236d3..b890fc0bc0237 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -231,7 +231,6 @@ pub mod pallet { /// The payout for validators and the system for the current era. /// See [Era payout](./index.html#era-payout). #[pallet::no_default] - #[allow(deprecated)] type EraPayout: sp_staking::EraPayout>; /// Something that can estimate the next session change, accurately or as a best effort diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 389befb3024b7..fda410921f5cc 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -735,26 +735,10 @@ pub struct EraRewardAllocation { pub validator_incentive: Balance, } -/// Trait for receiving unclaimed staking rewards. -/// -/// When era pot accounts are cleaned up, any remaining balance is deposited into the sink. -/// The implementor handles both the transfer and any bookkeeping (e.g. deactivation). -pub trait UnclaimedRewardSink { - /// Transfer unclaimed rewards from `source` to the sink. - fn deposit(source: &AccountId, amount: Balance) -> DispatchResult; -} - -impl UnclaimedRewardSink for () { - fn deposit(_source: &AccountId, _amount: Balance) -> DispatchResult { - Ok(()) - } -} - /// Handler for determining how much of a balance should be paid out on the current era. /// -/// Used by `pallet-staking` (legacy). New code should use [`IssuanceCurve`] instead, -/// which decouples issuance from staking state. -#[deprecated(note = "Use `IssuanceCurve` instead, which decouples issuance from staking state")] +/// [`IssuanceCurve`] is the successor to this trait, decoupling issuance computation from +/// staking state. pub trait EraPayout { /// Determine the payout for this era. /// @@ -767,7 +751,6 @@ pub trait EraPayout { ) -> (Balance, Balance); } -#[allow(deprecated)] impl EraPayout for () { fn era_payout( _total_staked: Balance, From 6f2742bf63efd19e748d8bcc79f5eb9f85d79f9c Mon Sep 17 00:00:00 2001 From: Ankan Date: Fri, 27 Mar 2026 06:23:10 +0100 Subject: [PATCH 03/11] fmt --- substrate/frame/staking/src/pallet/impls.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 5db28da42ab2c..df5951dbaf859 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -42,10 +42,9 @@ use sp_runtime::{ ArithmeticError, DispatchResult, Perbill, Percent, }; use sp_staking::{ - EraPayout, currency_to_vote::CurrencyToVote, offence::{OffenceDetails, OnOffenceHandler}, - EraIndex, OnStakingUpdate, Page, SessionIndex, Stake, + EraIndex, EraPayout, OnStakingUpdate, Page, SessionIndex, Stake, StakingAccount::{self, Controller, Stash}, StakingInterface, }; From 14e3aa996595c7826811308e4da425413b3235ea Mon Sep 17 00:00:00 2001 From: Ankan Date: Fri, 27 Mar 2026 06:50:32 +0100 Subject: [PATCH 04/11] remov EraPayout from staking async --- substrate/frame/staking-async/src/lib.rs | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) 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( From ddd01ecc76e5bf0de71c552e3402f17dd835e191 Mon Sep 17 00:00:00 2001 From: Ankan Date: Fri, 27 Mar 2026 06:59:45 +0100 Subject: [PATCH 05/11] remove EraRewardAllocation. It belongs in staking async --- substrate/primitives/staking/src/lib.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index fda410921f5cc..cabb9a3b539ad 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -726,15 +726,6 @@ pub trait DelegationMigrator { fn force_kill_agent(agent: Agent); } -/// Allocation breakdown for era rewards among stakers. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct EraRewardAllocation { - /// Amount allocated to stakers (nominators + validator stake rewards). - pub staker_rewards: Balance, - /// Amount allocated to validator self-stake incentive. - pub validator_incentive: Balance, -} - /// Handler for determining how much of a balance should be paid out on the current era. /// /// [`IssuanceCurve`] is the successor to this trait, decoupling issuance computation from From e14a38800abd140f3aee7fea237cab6593045b4c Mon Sep 17 00:00:00 2001 From: Ankan Date: Fri, 27 Mar 2026 07:20:54 +0100 Subject: [PATCH 06/11] move budget stuff to budget module --- substrate/primitives/staking/src/budget.rs | 81 ++++++++++++++++++++ substrate/primitives/staking/src/lib.rs | 87 +++------------------- 2 files changed, 90 insertions(+), 78 deletions(-) create mode 100644 substrate/primitives/staking/src/budget.rs diff --git a/substrate/primitives/staking/src/budget.rs b/substrate/primitives/staking/src/budget.rs new file mode 100644 index 0000000000000..09ed8fa966899 --- /dev/null +++ b/substrate/primitives/staking/src/budget.rs @@ -0,0 +1,81 @@ +// 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). They are used by `pallet-dap` to drive +//! inflation drip and budget allocation. + +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())); )* ); + v + } +} diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index cabb9a3b539ad..c14ed683f247d 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -29,9 +29,10 @@ use core::ops::{Add, AddAssign, Sub, SubAssign}; use scale_info::TypeInfo; use sp_runtime::{ traits::{AtLeast32BitUnsigned, Zero}, - BoundedVec, Debug, DispatchError, DispatchResult, Perbill, Saturating, + Debug, DispatchError, DispatchResult, Perbill, Saturating, }; +pub mod budget; pub mod offence; pub mod currency_to_vote; @@ -728,8 +729,8 @@ pub trait DelegationMigrator { /// Handler for determining how much of a balance should be paid out on the current era. /// -/// [`IssuanceCurve`] is the successor to this trait, decoupling issuance computation from -/// staking state. +/// [`budget::IssuanceCurve`] is the successor to this trait, decoupling issuance computation +/// from staking state. pub trait EraPayout { /// Determine the payout for this era. /// @@ -752,93 +753,23 @@ impl EraPayout for () { } } -/// 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, buffer) 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 [`EraPayout`], this trait does not depend on staking state (`total_staked`). -/// 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. - /// - /// # Parameters - /// - `total_issuance`: Current total token supply - /// - `elapsed_millis`: Time elapsed since last issuance drip, in milliseconds - 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())); )* ); - v - } -} - /// Result of staker reward calculation. #[derive(Debug, Clone, PartialEq, Eq)] pub struct StakerRewardResult { - /// Total payout for the validator (staking reward + commission) + /// Total payout for the validator (staking reward + commission). pub validator_payout: Balance, - /// Total payout for all nominators (to be split proportionally by caller) + /// Total payout for all nominators (to be split proportionally by caller). pub nominator_payout: Balance, } -/// Trait for calculating staking rewards including validator incentives. -/// -/// This trait allows runtimes to customize both: -/// - How validator self-stake is weighted for self-stake incentive rewards -/// - How staking rewards are distributed between validators and nominators +/// Calculates staking rewards and validator incentive weights. pub trait StakerRewardCalculator { - /// Calculate the reward weight for a validator's self-stake. + /// Calculate the reward weight for a validator based on their self-stake. /// /// Used for distributing validator self-stake incentive rewards proportionally. - /// The implementation defines the curve shape and reads any parameters it needs - /// (e.g. optimum, cap, slope) from its own configuration. fn calculate_validator_incentive_weight(self_stake: Balance) -> Balance; - /// Calculate how staking rewards are distributed between validator and nominators. - /// - /// Implements the standard staking reward distribution: - /// 1. Apply validator commission - /// 2. Split remaining between validator and nominators based on stake + /// Calculate how staking rewards are split between validator and nominators. fn calculate_staker_reward( validator_total_reward: Balance, validator_commission: Perbill, From 187aa7f717217f5425f92a7d92f66ed9fe35ea28 Mon Sep 17 00:00:00 2001 From: Ankan Date: Fri, 27 Mar 2026 07:29:40 +0100 Subject: [PATCH 07/11] remove AccountID from StakerRewardCalcualtor --- substrate/primitives/staking/src/budget.rs | 3 +-- substrate/primitives/staking/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/substrate/primitives/staking/src/budget.rs b/substrate/primitives/staking/src/budget.rs index 09ed8fa966899..b943f5d2b1e5d 100644 --- a/substrate/primitives/staking/src/budget.rs +++ b/substrate/primitives/staking/src/budget.rs @@ -18,8 +18,7 @@ //! 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). They are used by `pallet-dap` to drive -//! inflation drip and budget allocation. +//! (e.g. staker rewards, validator incentives). use alloc::vec::Vec; use sp_runtime::BoundedVec; diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index c14ed683f247d..3a9afaa711f7e 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -763,7 +763,7 @@ pub struct StakerRewardResult { } /// Calculates staking rewards and validator incentive weights. -pub trait StakerRewardCalculator { +pub trait StakerRewardCalculator { /// Calculate the reward weight for a validator based on their self-stake. /// /// Used for distributing validator self-stake incentive rewards proportionally. From 4571f1fadf8935c38ee0d1d1975a36bfbc337d34 Mon Sep 17 00:00:00 2001 From: Ankan Date: Fri, 27 Mar 2026 07:42:03 +0100 Subject: [PATCH 08/11] prdoc --- prdoc/pr_11513.prdoc | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 prdoc/pr_11513.prdoc 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 From 946ff6a580cad7ee04b3666757ddc8323672f839 Mon Sep 17 00:00:00 2001 From: Ankan Date: Mon, 30 Mar 2026 12:20:21 +0200 Subject: [PATCH 09/11] improve trait docs --- substrate/primitives/staking/src/lib.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 3a9afaa711f7e..d58aa4b52c814 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -753,23 +753,33 @@ impl EraPayout for () { } } -/// Result of staker reward calculation. +/// 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 (staking reward + commission). + /// Total payout for the validator (commission + proportional stake reward). pub validator_payout: Balance, - /// Total payout for all nominators (to be split proportionally by caller). + /// Total payout for all nominators, to be split proportionally by the caller. pub nominator_payout: Balance, } -/// Calculates staking rewards and validator incentive weights. +/// 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 { - /// Calculate the reward weight for a validator based on their self-stake. + /// Compute a weight for this validator's share of the validator incentive pot. /// - /// Used for distributing validator self-stake incentive rewards proportionally. + /// 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; - /// Calculate how staking rewards are split between validator and nominators. + /// Split a validator's staking reward into validator and nominator portions. fn calculate_staker_reward( validator_total_reward: Balance, validator_commission: Perbill, From bb15c4e72b1652f90f71253fc7730dadcf31b3b6 Mon Sep 17 00:00:00 2001 From: Ankan Date: Mon, 30 Mar 2026 12:40:57 +0200 Subject: [PATCH 10/11] no duplicate assertion + test for budget id --- substrate/primitives/staking/src/budget.rs | 54 ++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/substrate/primitives/staking/src/budget.rs b/substrate/primitives/staking/src/budget.rs index b943f5d2b1e5d..6af52899f9258 100644 --- a/substrate/primitives/staking/src/budget.rs +++ b/substrate/primitives/staking/src/budget.rs @@ -75,6 +75,60 @@ 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(); + } +} From 2df7d7135849029e328eb9bdf223996f1c535e51 Mon Sep 17 00:00:00 2001 From: Ankan Date: Mon, 30 Mar 2026 12:50:16 +0200 Subject: [PATCH 11/11] fmt --- substrate/primitives/staking/src/budget.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/substrate/primitives/staking/src/budget.rs b/substrate/primitives/staking/src/budget.rs index 6af52899f9258..c8c7caf8633df 100644 --- a/substrate/primitives/staking/src/budget.rs +++ b/substrate/primitives/staking/src/budget.rs @@ -75,11 +75,14 @@ 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"); + 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 } } @@ -128,7 +131,6 @@ mod tests { #[test] #[should_panic(expected = "Duplicate BudgetRecipient key detected")] fn duplicate_keys_panics() { - let _ = - <(RecipientA, RecipientDuplicate) as BudgetRecipientList>::recipients(); + let _ = <(RecipientA, RecipientDuplicate) as BudgetRecipientList>::recipients(); } }