//! Per-subnet emission weight vector ("etao"). //! //! Every subnet carries a five-slot vector of `u64` parts out of //! [`EMISSION_WEIGHT_ONE`] that sums to exactly one and says how each block's //! alpha emission is divided: //! //! | slot | role | v1 rule | //! |-----:|-----------|-------------------------------------------| //! | 0 | burn | must be 0 (not yet enabled) | //! | 1 | recycle | must be 0 (not yet enabled) | //! | 2 | owner | `0 ≤ owner ≤ 33%`; leased subnets `≥ 18%` | //! | 3 | validator | `≤ 50%`, and must keep root's share | //! | 4 | miner | `≥ 33%` | //! //! Root holders are paid out of the validator slot in proportion to their //! TAO-weighted stake. So that a subnet cannot cut root's income by shrinking //! the validator slot, every subnet carries its own effective TAO weight //! `w_s` ([`SubnetTaoWeight`]). A subnet with no entry follows the global //! `TaoWeight`. Every accepted set re-solves `w_s` **once**, at set time, so //! that root's share of the subnet's emission equals a *fresh baseline*: what //! root would earn on this subnet at this block with the default validator //! share `V0` and the global `TaoWeight` (see //! [`Pallet::baseline_root_emission_share`]). The baseline never looks at the //! old vector or the old `w_s`, so an owner cannot ratchet root's share up or //! down by changing the validator slot in steps. Afterwards `w_s` is left //! alone until the next set. Setting the validator share back to `V0` clears //! the entry so the subnet follows the global knob again. The call is rejected //! when no `w_s ≤ 1` can reach the baseline. use super::*; use frame_support::{BoundedVec, traits::ConstU32}; use safe_math::*; use substrate_fixed::types::U96F32; use subtensor_runtime_common::{AlphaBalance, NetUid}; /// Number of slots in the emission weight vector. pub const EMISSION_WEIGHT_SLOTS: u32 = 5; /// Denominator of every emission weight: weights are `u64` parts out of `u64::MAX`. pub const EMISSION_WEIGHT_ONE: u64 = u64::MAX; /// Storage shape of a subnet's emission weight vector. pub type EmissionWeightsVec = BoundedVec>; /// In-memory shape of a subnet's emission weight vector. pub type EmissionWeights = [u64; EMISSION_WEIGHT_SLOTS as usize]; /// `num / den` expressed in parts of [`EMISSION_WEIGHT_ONE`]. pub const fn emission_fraction(num: u64, den: u64) -> u64 { match (EMISSION_WEIGHT_ONE as u128) .saturating_mul(num as u128) .checked_div(den as u128) { Some(parts) => parts as u64, None => 0, } } /// Owner slot may be at most 33% of emission. pub const MAX_OWNER_EMISSION: u64 = emission_fraction(33, 100); /// Validator slot may be at most 50% of emission. pub const MAX_VALIDATOR_EMISSION: u64 = emission_fraction(50, 100); /// Miner slot must be at least 33% of emission. pub const MIN_MINER_EMISSION: u64 = emission_fraction(33, 100); /// Distinct storage items [`Pallet::read_subnet_emission_weights_view`] reads: /// `SubnetEmissionWeights` and `SubnetCreatedViaLease`. pub const EMISSION_WEIGHTS_VIEW_READS: u64 = 2; /// A TAO weight in u64 parts of [`EMISSION_WEIGHT_ONE`] as a fraction in `[0, 1]`. pub fn normalize_tao_weight(parts: u64) -> U96F32 { U96F32::saturating_from_num(parts).safe_div(U96F32::saturating_from_num(u64::MAX)) } /// A TAO weight in `[0, 1]` as u64 parts of [`EMISSION_WEIGHT_ONE`]. pub fn tao_weight_to_parts(weight: U96F32) -> u64 { weight .saturating_mul(U96F32::saturating_from_num(EMISSION_WEIGHT_ONE)) .saturating_to_num::() } /// Root's share of a subnet's emission: `V · x / (x + A)` with /// `x = root_tao · tao_weight` and `A` the subnet's alpha issuance. This is /// the shape `consensus_root_proportion` applies to the validator slot each /// block. pub fn root_emission_share( validator: U96F32, tao_weight: U96F32, root_tao: U96F32, alpha_issuance: U96F32, ) -> U96F32 { let weighted_root_tao = root_tao.saturating_mul(tao_weight); weighted_root_tao .checked_div(weighted_root_tao.saturating_add(alpha_issuance)) .unwrap_or(U96F32::saturating_from_num(0)) .saturating_mul(validator) } /// Solve the TAO weight that gives root exactly the share `root_share` of /// the emission once the validator slot is `validator`: /// `x = r · A / (V − r)`, `w = x / root_tao`. `None` when no `w ≤ 1` exists, /// i.e. `V ≤ r` or root TAO would have to count more than 1:1. pub fn solve_tao_weight_for_root_share( root_share: U96F32, validator: U96F32, root_tao: U96F32, alpha_issuance: U96F32, ) -> Option { if validator <= root_share { return None; } let headroom = validator.saturating_sub(root_share); let weighted_root_tao = root_share .saturating_mul(alpha_issuance) .checked_div(headroom)?; let tao_weight = weighted_root_tao.checked_div(root_tao)?; (tao_weight <= U96F32::saturating_from_num(1)).then_some(tao_weight) } /// What an accepted set does to a subnet's [`SubnetTaoWeight`] entry. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum SubnetTaoWeightUpdate { /// Leave the stored entry (or its absence) as it is: there is nothing to /// preserve (no root TAO or no alpha issuance). Keep, /// Remove the entry so the subnet follows the global `TaoWeight` again: /// the validator share is back at the default `V0`. FollowGlobal, /// Freeze this weight (u64 parts) so root's share equals the baseline. Freeze(u64), } /// Snapshot of the storage items behind a subnet's emission weight view, each /// read once. A caller that has paid for [`EMISSION_WEIGHTS_VIEW_READS`] /// reads owes nothing more. #[derive(Clone, Debug, PartialEq, Eq)] pub struct EmissionWeightsView { /// `[burn, recycle, owner, validator, miner]`. pub weights: EmissionWeights, /// Whether the subnet was registered through a lease. pub created_via_lease: bool, } /// Named slot of the emission weight vector. #[derive( Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, )] #[repr(u16)] pub enum EmissionIndex { /// Alpha destroyed outright. Reserved: must be zero in v1. Burn = 0, /// Alpha burned and un-minted from issuance. Reserved: must be zero in v1. Recycle = 1, /// Subnet owner cut. Owner = 2, /// Validator dividends (root alpha is carved out of this slot). Validator = 3, /// Miner incentive (split across mechanisms by `MechanismEmissionSplit`). Miner = 4, } impl EmissionIndex { /// Every slot, in vector order. pub const ALL: [EmissionIndex; EMISSION_WEIGHT_SLOTS as usize] = [ EmissionIndex::Burn, EmissionIndex::Recycle, EmissionIndex::Owner, EmissionIndex::Validator, EmissionIndex::Miner, ]; /// Decode a wire index; `None` for reserved / unknown slots. pub fn from_u16(value: u16) -> Option { match value { 0 => Some(Self::Burn), 1 => Some(Self::Recycle), 2 => Some(Self::Owner), 3 => Some(Self::Validator), 4 => Some(Self::Miner), _ => None, } } /// Position of this slot in the vector. pub const fn position(self) -> usize { self as u16 as usize } } impl Pallet { // --------------------------------------------------------------------- // Defaults // --------------------------------------------------------------------- /// Scale a legacy `u16 / u16::MAX` fraction to `u64 / u64::MAX` parts. pub fn scale_u16_fraction_to_emission_weight(value: u16) -> u64 { (value as u128) .saturating_mul(EMISSION_WEIGHT_ONE as u128) .safe_div(u16::MAX as u128) as u64 } /// Canonical owner share for subnets that never set a vector: the legacy /// `InitialSubnetOwnerCut` (18% on mainnet) scaled to u64 parts. pub fn default_owner_emission() -> u64 { Self::scale_u16_fraction_to_emission_weight(T::InitialSubnetOwnerCut::get()) } /// Canonical validator share: half of what remains after the owner cut /// (41% on mainnet), matching the pre-etao 50/50 split. pub fn default_validator_emission() -> u64 { EMISSION_WEIGHT_ONE.saturating_sub(Self::default_owner_emission()) / 2 } /// Lowest owner share a lease-created subnet may set: the canonical /// default, so lease dividends can never be squeezed below launch terms. pub fn leased_subnet_min_owner_emission() -> u64 { Self::default_owner_emission() } /// Build the canonical vector `[0, 0, owner, validator, miner]`; the miner /// slot absorbs any rounding so the vector sums to exactly one. pub fn default_emission_weights() -> EmissionWeights { let owner = Self::default_owner_emission(); let validator = Self::default_validator_emission(); let miner = EMISSION_WEIGHT_ONE .saturating_sub(owner) .saturating_sub(validator); [0, 0, owner, validator, miner] } /// `[0, 0, owner, validator, miner]` with the non-owner remainder split /// evenly between validators and miners (miner absorbs rounding). pub fn emission_weights_with_owner(owner: u64) -> EmissionWeights { let validator = EMISSION_WEIGHT_ONE.saturating_sub(owner) / 2; let miner = EMISSION_WEIGHT_ONE .saturating_sub(owner) .saturating_sub(validator); [0, 0, owner, validator, miner] } pub(crate) fn emission_weights_to_vec(weights: EmissionWeights) -> EmissionWeightsVec { EmissionWeightsVec::truncate_from(weights.to_vec()) } // --------------------------------------------------------------------- // Reads // --------------------------------------------------------------------- /// The subnet's full emission weight vector. pub fn get_subnet_emission_weights(netuid: NetUid) -> EmissionWeights { let stored = SubnetEmissionWeights::::get(netuid); let mut weights = [0u64; EMISSION_WEIGHT_SLOTS as usize]; for (slot, value) in weights.iter_mut().zip(stored.iter()) { *slot = *value; } weights } /// One slot of the subnet's emission weight vector. pub fn get_subnet_emission_weight(netuid: NetUid, index: EmissionIndex) -> u64 { Self::get_subnet_emission_weights(netuid) .get(index.position()) .copied() .unwrap_or_default() } /// Burn share (slot 0). Always zero in v1. pub fn get_subnet_burn_emission(netuid: NetUid) -> u64 { Self::get_subnet_emission_weight(netuid, EmissionIndex::Burn) } /// Recycle share (slot 1). Always zero in v1. pub fn get_subnet_recycle_emission(netuid: NetUid) -> u64 { Self::get_subnet_emission_weight(netuid, EmissionIndex::Recycle) } /// Owner cut (slot 2) in u64 parts. pub fn get_subnet_owner_emission_cut(netuid: NetUid) -> u64 { Self::get_subnet_emission_weight(netuid, EmissionIndex::Owner) } /// Validator share (slot 3) in u64 parts. pub fn get_subnet_validator_emission_cut(netuid: NetUid) -> u64 { Self::get_subnet_emission_weight(netuid, EmissionIndex::Validator) } /// Miner share (slot 4) in u64 parts. pub fn get_subnet_miner_emission_cut(netuid: NetUid) -> u64 { Self::get_subnet_emission_weight(netuid, EmissionIndex::Miner) } /// Convert u64 parts to a `[0, 1]` fixed-point fraction. pub fn emission_weight_to_fixed(weight: u64) -> U96F32 { U96F32::saturating_from_num(weight) .safe_div(U96F32::saturating_from_num(EMISSION_WEIGHT_ONE)) } /// Owner cut as a `[0, 1]` fraction. pub fn get_float_subnet_owner_emission_cut(netuid: NetUid) -> U96F32 { Self::emission_weight_to_fixed(Self::get_subnet_owner_emission_cut(netuid)) } /// Validator share as a `[0, 1]` fraction. pub fn get_float_subnet_validator_emission_cut(netuid: NetUid) -> U96F32 { Self::emission_weight_to_fixed(Self::get_subnet_validator_emission_cut(netuid)) } /// Miner share as a `[0, 1]` fraction. pub fn get_float_subnet_miner_emission_cut(netuid: NetUid) -> U96F32 { Self::emission_weight_to_fixed(Self::get_subnet_miner_emission_cut(netuid)) } /// Whether the owner receives any emission cut (legacy `OwnerCutEnabled` /// view: true iff the owner slot is non-zero). pub fn get_owner_cut_enabled(netuid: NetUid) -> bool { Self::get_subnet_owner_emission_cut(netuid) > 0 } /// Effective TAO weight `w_s` on `netuid` as a `[0, 1]` fraction. Consensus-side /// only: it feeds the root carve-out of the validator slot, dividend and /// child/parent contribution splits, and the stake weighting in Yuma /// Consensus. The pool side (`tao_in`, `alpha_in`, the injection cap, /// `RootProp`) uses the global [`Pallet::get_tao_weight`] and must never /// read this. A subnet that has never changed its validator share reads /// the global `TaoWeight`. pub fn get_subnet_tao_weight(netuid: NetUid) -> U96F32 { normalize_tao_weight(Self::get_subnet_tao_weight_parts(netuid)) } /// [`Self::get_subnet_tao_weight`] in u64 parts, for RPC. Falls back to the /// global `TaoWeight` while the subnet has no frozen weight of its own. pub fn get_subnet_tao_weight_parts(netuid: NetUid) -> u64 { SubnetTaoWeight::::get(netuid).unwrap_or_else(TaoWeight::::get) } /// Whether the subnet was registered through a lease (persists after the /// lease ends). pub fn get_subnet_created_via_lease(netuid: NetUid) -> bool { SubnetCreatedViaLease::::get(netuid) } /// Every storage item behind the emission weight view, each read exactly /// once. Views that meter storage (the EVM precompile) charge /// [`EMISSION_WEIGHTS_VIEW_READS`] reads and derive the rest from this. pub fn read_subnet_emission_weights_view(netuid: NetUid) -> EmissionWeightsView { EmissionWeightsView { weights: Self::get_subnet_emission_weights(netuid), created_via_lease: Self::get_subnet_created_via_lease(netuid), } } /// Root's current share of `netuid`'s emission under its stored state: /// `V · x / (x + A)`, see [`root_emission_share`]. pub fn get_root_emission_share(netuid: NetUid) -> U96F32 { root_emission_share( Self::get_float_subnet_validator_emission_cut(netuid), Self::get_subnet_tao_weight(netuid), U96F32::saturating_from_num(Self::get_subnet_tao(NetUid::ROOT)), U96F32::saturating_from_num(Self::get_alpha_issuance(netuid)), ) } /// Root's *baseline* share of `netuid`'s emission at this block: what root /// would earn with the default validator share `V0` /// ([`Self::default_validator_emission`]) and the global `TaoWeight`, /// `V0 · x_g / (x_g + A)` with `x_g = root_tao · TaoWeight`. This is the /// target every set re-solves `w_s` for. It reads neither the subnet's /// current vector nor its frozen weight, so a sequence of sets cannot move /// root's share away from what an untouched subnet would pay. pub fn baseline_root_emission_share(netuid: NetUid) -> U96F32 { root_emission_share( Self::emission_weight_to_fixed(Self::default_validator_emission()), Self::get_tao_weight(), U96F32::saturating_from_num(Self::get_subnet_tao(NetUid::ROOT)), U96F32::saturating_from_num(Self::get_alpha_issuance(netuid)), ) } // --------------------------------------------------------------------- // Writes // --------------------------------------------------------------------- /// Seed a freshly registered subnet with the canonical vector and reset any /// per-subnet state a previous occupant of this netuid may have left /// (effective TAO weight, lease marker, accrued owner cut). With no /// `SubnetTaoWeight` entry the subnet follows the global `TaoWeight`. The /// accrued owner cut is written as an explicit zero so that an *absent* /// entry always means "pre-dates the seed migration" (see /// [`Self::owner_cut_alpha_for_lock_refund`]). Lease registration sets the /// marker after this runs. pub fn seed_subnet_emission_weights(netuid: NetUid) { if netuid.is_root() { return; } let weights = Self::default_emission_weights(); SubnetEmissionWeights::::insert(netuid, Self::emission_weights_to_vec(weights)); SubnetTaoWeight::::remove(netuid); SubnetCreatedViaLease::::remove(netuid); SubnetOwnerCutAccrued::::insert(netuid, AlphaBalance::ZERO); } /// The owner α the pre-emission-weights lock refund deducted: /// `alpha_issuance × legacy_owner_cut / u16::MAX`, floored. `legacy_owner_cut` /// is the frozen global `SubnetOwnerCut` (its setter is deprecated). pub fn legacy_owner_cut_accrued(netuid: NetUid, legacy_owner_cut: u16) -> AlphaBalance { let issuance: u64 = Self::get_alpha_issuance(netuid).to_u64(); let fraction = U96F32::saturating_from_num(legacy_owner_cut) .safe_div(U96F32::saturating_from_num(u16::MAX)); U96F32::saturating_from_num(issuance) .saturating_mul(fraction) .floor() .saturating_to_num::() .into() } /// Owner α to deduct from a legacy lock refund. The accrued history /// (`SubnetOwnerCutAccrued`) when the subnet has an entry; otherwise the /// pre-upgrade formula. Every subnet registered or migrated after the seed /// migration has an entry (possibly zero), so the fallback only fires for a /// subnet that was already dissolving when the migration ran and was not /// seeded. It can never make the deduction silently zero. pub fn owner_cut_alpha_for_lock_refund(netuid: NetUid) -> AlphaBalance { match SubnetOwnerCutAccrued::::try_get(netuid) { Ok(accrued) => accrued, Err(()) => Self::legacy_owner_cut_accrued(netuid, Self::get_subnet_owner_cut()), } } /// Write a vector verbatim: no rule checks, no TAO-weight solve, no /// event. Migration and test plumbing only; extrinsics must go through /// [`Self::do_set_subnet_emission_weights`]. pub fn set_subnet_emission_weights_unchecked(netuid: NetUid, weights: EmissionWeights) { SubnetEmissionWeights::::insert(netuid, Self::emission_weights_to_vec(weights)); } /// Apply a sparse `(uids, values)` update on top of `current`. Omitted /// slots keep their current value; burn and recycle are always treated as /// zero when omitted. pub fn merge_emission_weight_updates( current: EmissionWeights, uids: &[u16], values: &[u64], ) -> Result> { ensure!( uids.len() == values.len(), Error::::EmissionWeightsLengthMismatch ); let mut merged = current; for reserved in [EmissionIndex::Burn, EmissionIndex::Recycle] { if let Some(target) = merged.get_mut(reserved.position()) { *target = 0; } } let mut seen = [false; EMISSION_WEIGHT_SLOTS as usize]; for (uid, value) in uids.iter().zip(values.iter()) { let index = EmissionIndex::from_u16(*uid).ok_or(Error::::EmissionWeightIndexOutOfRange)?; let slot = index.position(); ensure!( !seen.get(slot).copied().unwrap_or(true), Error::::EmissionWeightIndexDuplicated ); if let Some(flag) = seen.get_mut(slot) { *flag = true; } if let Some(target) = merged.get_mut(slot) { *target = *value; } } Ok(merged) } /// Check every v1 rule against a full vector. Each failure names the rule /// so callers learn the constraints from the error alone. pub fn validate_subnet_emission_weights( netuid: NetUid, weights: &EmissionWeights, ) -> DispatchResult { let slot = |index: EmissionIndex| weights.get(index.position()).copied().unwrap_or(0); ensure!( slot(EmissionIndex::Burn) == 0, Error::::BurnEmissionMustBeZero ); ensure!( slot(EmissionIndex::Recycle) == 0, Error::::RecycleEmissionMustBeZero ); let owner = slot(EmissionIndex::Owner); ensure!( owner <= MAX_OWNER_EMISSION, Error::::OwnerEmissionAboveMaximum ); if Self::get_subnet_created_via_lease(netuid) { ensure!( owner >= Self::leased_subnet_min_owner_emission(), Error::::LeasedSubnetOwnerCutBelowMinimum ); } let validator = slot(EmissionIndex::Validator); ensure!( validator <= MAX_VALIDATOR_EMISSION, Error::::ValidatorEmissionAboveMaximum ); ensure!( slot(EmissionIndex::Miner) >= MIN_MINER_EMISSION, Error::::MinerEmissionBelowMinimum ); let total: u128 = weights.iter().map(|w| *w as u128).sum(); ensure!( total == EMISSION_WEIGHT_ONE as u128, Error::::EmissionWeightsDoNotSumToOne ); Ok(()) } /// What `netuid`'s effective TAO weight must become so that, with the /// validator slot at `new_validator`, root's share of emission equals the /// fresh baseline [`Self::baseline_root_emission_share`] at this block. /// Only `new_validator` and live chain values go in: the old vector and /// the old weight are never consulted, so repeated sets cannot ratchet /// root's share in either direction. `new_validator == V0` clears the /// entry (the solve would land on the global weight anyway); no root TAO /// or no alpha issuance keeps whatever is stored. Fails with /// [`Error::ValidatorEmissionTooLowToPreserveRoot`] when `V_new ≤ r` or the /// solved weight would exceed one (root TAO never counts more than 1:1). pub fn tao_weight_preserving_root( netuid: NetUid, new_validator: u64, ) -> Result> { let root_tao = U96F32::saturating_from_num(Self::get_subnet_tao(NetUid::ROOT)); let alpha_issuance = U96F32::saturating_from_num(Self::get_alpha_issuance(netuid)); let zero = U96F32::saturating_from_num(0); if root_tao == zero || alpha_issuance == zero { return Ok(SubnetTaoWeightUpdate::Keep); } if new_validator == Self::default_validator_emission() { return Ok(SubnetTaoWeightUpdate::FollowGlobal); } let tao_weight = solve_tao_weight_for_root_share( Self::baseline_root_emission_share(netuid), Self::emission_weight_to_fixed(new_validator), root_tao, alpha_issuance, ) .ok_or(Error::::ValidatorEmissionTooLowToPreserveRoot)?; Ok(SubnetTaoWeightUpdate::Freeze(tao_weight_to_parts( tao_weight, ))) } /// Merge, validate and store a sparse emission weight update, re-anchor /// the subnet's effective TAO weight to the baseline for the new validator /// slot, and emit [`Event::SubnetEmissionWeightsSet`]. pub fn do_set_subnet_emission_weights( netuid: NetUid, uids: Vec, values: Vec, ) -> DispatchResult { ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); ensure!( !netuid.is_root(), Error::::EmissionWeightsNotSettableOnRoot ); let current = Self::get_subnet_emission_weights(netuid); let merged = Self::merge_emission_weight_updates(current, &uids, &values)?; Self::validate_subnet_emission_weights(netuid, &merged)?; let new_validator = merged .get(EmissionIndex::Validator.position()) .copied() .unwrap_or(0); let tao_weight = Self::tao_weight_preserving_root(netuid, new_validator)?; SubnetEmissionWeights::::insert(netuid, Self::emission_weights_to_vec(merged)); match tao_weight { SubnetTaoWeightUpdate::Keep => {} SubnetTaoWeightUpdate::FollowGlobal => SubnetTaoWeight::::remove(netuid), SubnetTaoWeightUpdate::Freeze(parts) => SubnetTaoWeight::::insert(netuid, parts), } Self::deposit_event(Event::SubnetEmissionWeightsSet { netuid, weights: merged.to_vec(), }); Ok(()) } }