Skip to content

Commit 1962dcd

Browse files
ppolewiczclaude
andcommitted
Add dividend-efficiency utilization, scaling, and hard cap
- Change compute_and_store_effective_root_prop to use dividend-efficiency metric instead of binary active/inactive. Returns U96F32 utilization. - Move ERP computation from distribute_dividends_and_incentives to distribute_emission; add utilization scaling (< 1.0) and hard cap (< 0.5 recycles all root dividends, sets ERP to 0). - Add 555k unstaked TAO to test setup_test(). - Add 3 new tests: unstaked_tao_does_not_affect_utilization, half_weights_to_validator, half_weights_no_minor_root. - Update existing test assertions for hard cap behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4afd191 commit 1962dcd

File tree

3 files changed

+443
-57
lines changed

3 files changed

+443
-57
lines changed

pallets/subtensor/src/coinbase/run_coinbase.rs

Lines changed: 130 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -508,18 +508,6 @@ impl<T: Config> Pallet<T> {
508508
alpha_dividends: BTreeMap<T::AccountId, U96F32>,
509509
root_alpha_dividends: BTreeMap<T::AccountId, U96F32>,
510510
) {
511-
// Compute and store EffectiveRootProp for the NEXT round before distributing.
512-
// This intentionally computes the effective root proportion for the next epoch based on
513-
// the current epoch's dividend distribution (using raw, pre-distribution dividend values).
514-
// It is calculated once per epoch from the actual dividend proportions that occurred.
515-
// Exploitation via temporary stake placement before this calculation is mitigated because
516-
// apply_effective_root_prop_scaling uses min(EffectiveRootProp, RootProp), which caps the
517-
// value at the protocol-level RootProp setting.
518-
Self::compute_and_store_effective_root_prop(
519-
netuid,
520-
&alpha_dividends,
521-
&root_alpha_dividends,
522-
);
523511

524512
// Distribute the owner cut.
525513
if let Ok(owner_coldkey) = SubnetOwner::<T>::try_get(netuid)
@@ -651,25 +639,26 @@ impl<T: Config> Pallet<T> {
651639
}
652640
}
653641

654-
/// Computes and stores the EffectiveRootProp for a subnet.
642+
/// Computes and stores the EffectiveRootProp for a subnet. Returns the utilization value.
655643
///
656644
/// EffectiveRootProp = raw_root_prop * utilization
657645
///
658646
/// Where:
659647
/// raw_root_prop = sum(root_alpha_dividends) / (sum(alpha_dividends) + sum(root_alpha_dividends))
660-
/// utilization = active_root_stake / total_root_stake
648+
/// utilization = sum(root_stake_i * efficiency_i) / total_root_stake
649+
/// efficiency_i = min(actual_share_i / expected_share_i, 1.0)
650+
/// expected_share_i = root_stake_i / total_root_stake
651+
/// actual_share_i = root_alpha_dividends[i] / sum(root_alpha_dividends)
661652
///
662-
/// active_root_stake is the root stake of validators that earned root dividends this epoch.
663-
/// total_root_stake is the root stake of ALL validators registered on the subnet.
664-
///
665-
/// This weighting ensures that subnets where most root stake is idle (validators not setting
666-
/// weights) get a much lower EffectiveRootProp than subnets where all root stake is active.
653+
/// Only root stake of validators with UIDs on this subnet is counted.
654+
/// TotalIssuance, unstaked TAO, and root stake on other subnets are irrelevant.
667655
pub fn compute_and_store_effective_root_prop(
668656
netuid: NetUid,
669657
alpha_dividends: &BTreeMap<T::AccountId, U96F32>,
670658
root_alpha_dividends: &BTreeMap<T::AccountId, U96F32>,
671-
) {
659+
) -> U96F32 {
672660
let zero = U96F32::saturating_from_num(0);
661+
let one = U96F32::saturating_from_num(1);
673662

674663
let total_alpha_divs: U96F32 = alpha_dividends
675664
.values()
@@ -687,42 +676,62 @@ impl<T: Config> Pallet<T> {
687676
zero
688677
};
689678

690-
// Compute root stake utilization: fraction of total root stake that actively earns dividends.
691-
// Iterate all UIDs on the subnet and sum their root stakes. Hotkeys that appear in
692-
// root_alpha_dividends with a nonzero value are considered "active".
679+
// Compute dividend-efficiency-based utilization.
680+
// For each root-staked validator registered on this subnet:
681+
// expected_share = root_stake_i / total_root_stake
682+
// actual_share = root_dividends_i / total_root_divs
683+
// efficiency = min(actual_share / expected_share, 1.0)
684+
// utilization = sum(root_stake_i * efficiency_i) / total_root_stake
693685
let n = SubnetworkN::<T>::get(netuid);
694686
let mut total_root_stake = zero;
695-
let mut active_root_stake = zero;
696687

688+
// First pass: compute total root stake on this subnet
689+
let mut hotkey_root_stakes: Vec<(T::AccountId, U96F32)> = Vec::new();
697690
for uid in 0..n {
698691
if let Ok(hotkey) = Keys::<T>::try_get(netuid, uid) {
699692
let root_stake = Self::get_stake_for_hotkey_on_subnet(&hotkey, NetUid::ROOT);
700693
let rs = U96F32::saturating_from_num(root_stake.to_u64());
701694
total_root_stake = total_root_stake.saturating_add(rs);
702-
if root_alpha_dividends
703-
.get(&hotkey)
704-
.is_some_and(|v| *v > zero)
705-
{
706-
active_root_stake = active_root_stake.saturating_add(rs);
695+
if rs > zero {
696+
hotkey_root_stakes.push((hotkey, rs));
707697
}
708698
}
709699
}
710700

711-
let utilization = if total_root_stake > zero {
712-
active_root_stake
701+
let utilization = if total_root_stake > zero && total_root_divs > zero {
702+
// Second pass: compute weighted efficiency
703+
let mut weighted_efficiency_sum = zero;
704+
for (hotkey, rs) in &hotkey_root_stakes {
705+
let expected_share = rs.checked_div(total_root_stake).unwrap_or(zero);
706+
let actual_div = root_alpha_dividends.get(hotkey).copied().unwrap_or(zero);
707+
let actual_share = actual_div.checked_div(total_root_divs).unwrap_or(zero);
708+
let efficiency = if expected_share > zero {
709+
let raw_eff = actual_share.checked_div(expected_share).unwrap_or(zero);
710+
raw_eff.min(one)
711+
} else {
712+
zero
713+
};
714+
weighted_efficiency_sum =
715+
weighted_efficiency_sum.saturating_add(rs.saturating_mul(efficiency));
716+
}
717+
weighted_efficiency_sum
713718
.checked_div(total_root_stake)
714719
.unwrap_or(zero)
720+
} else if total_root_stake > zero {
721+
// No root dividends at all → utilization = 0
722+
zero
715723
} else {
716724
zero
717725
};
718726

719727
let effective_root_prop = raw_root_prop.saturating_mul(utilization);
720728

721729
log::debug!(
722-
"EffectiveRootProp for netuid {netuid:?}: {effective_root_prop:?} (raw: {raw_root_prop:?}, utilization: {utilization:?}, active_root_stake: {active_root_stake:?}, total_root_stake: {total_root_stake:?})"
730+
"EffectiveRootProp for netuid {netuid:?}: {effective_root_prop:?} (raw: {raw_root_prop:?}, utilization: {utilization:?}, total_root_stake: {total_root_stake:?})"
723731
);
724732

725733
EffectiveRootProp::<T>::insert(netuid, effective_root_prop);
734+
utilization
726735
}
727736

728737
pub fn get_stake_map(
@@ -811,7 +820,7 @@ impl<T: Config> Pallet<T> {
811820
let root_alpha = pending_root_alpha;
812821
let owner_cut = pending_owner_cut;
813822

814-
let (incentives, (alpha_dividends, root_alpha_dividends)) =
823+
let (incentives, (mut alpha_dividends, mut root_alpha_dividends)) =
815824
Self::calculate_dividend_and_incentive_distribution(
816825
netuid,
817826
root_alpha,
@@ -820,6 +829,94 @@ impl<T: Config> Pallet<T> {
820829
tao_weight,
821830
);
822831

832+
// Compute and store EffectiveRootProp, getting back utilization for scaling.
833+
let utilization = Self::compute_and_store_effective_root_prop(
834+
netuid,
835+
&alpha_dividends,
836+
&root_alpha_dividends,
837+
);
838+
839+
let half = U96F32::saturating_from_num(0.5);
840+
let one = U96F32::saturating_from_num(1);
841+
let zero = U96F32::saturating_from_num(0);
842+
843+
if utilization < half {
844+
// Hard cap: recycle ALL root alpha dividends
845+
let total_root: U96F32 = root_alpha_dividends
846+
.values()
847+
.fold(zero, |acc, v| acc.saturating_add(*v));
848+
Self::recycle_subnet_alpha(netuid, AlphaCurrency::from(tou64!(total_root)));
849+
root_alpha_dividends.clear();
850+
851+
// Zero root-staked portion of alpha_dividends
852+
for (_hotkey, alpha_div) in alpha_dividends.iter_mut() {
853+
let root_stake = Self::get_stake_for_hotkey_on_subnet(_hotkey, NetUid::ROOT);
854+
let root_stake_f = asfloat!(root_stake.to_u64());
855+
if root_stake_f > zero {
856+
let root_alpha_weighted = root_stake_f.saturating_mul(tao_weight);
857+
let alpha_stake =
858+
Self::get_stake_for_hotkey_on_subnet(_hotkey, netuid);
859+
let alpha_stake_f = asfloat!(alpha_stake.to_u64());
860+
let total_stake = alpha_stake_f.saturating_add(root_alpha_weighted);
861+
if total_stake > zero {
862+
let root_fraction =
863+
root_alpha_weighted.checked_div(total_stake).unwrap_or(zero);
864+
let recycle_amount = (*alpha_div).saturating_mul(root_fraction);
865+
*alpha_div = (*alpha_div).saturating_sub(recycle_amount);
866+
Self::recycle_subnet_alpha(
867+
netuid,
868+
AlphaCurrency::from(tou64!(recycle_amount)),
869+
);
870+
}
871+
}
872+
}
873+
874+
// Overwrite EffectiveRootProp to 0
875+
EffectiveRootProp::<T>::insert(netuid, U96F32::saturating_from_num(0));
876+
877+
log::debug!(
878+
"Hard cap triggered for netuid {netuid:?}: utilization {utilization:?} < 0.5, all root dividends recycled"
879+
);
880+
} else if utilization < one {
881+
// Scale root_alpha_dividends by utilization
882+
for (_hotkey, root_div) in root_alpha_dividends.iter_mut() {
883+
let scaled = (*root_div).saturating_mul(utilization);
884+
let reduction = (*root_div).saturating_sub(scaled);
885+
*root_div = scaled;
886+
Self::recycle_subnet_alpha(netuid, AlphaCurrency::from(tou64!(reduction)));
887+
}
888+
889+
// Scale root-staked portion of alpha_dividends by utilization
890+
for (_hotkey, alpha_div) in alpha_dividends.iter_mut() {
891+
let root_stake = Self::get_stake_for_hotkey_on_subnet(_hotkey, NetUid::ROOT);
892+
let root_stake_f = asfloat!(root_stake.to_u64());
893+
if root_stake_f > zero {
894+
let root_alpha_weighted = root_stake_f.saturating_mul(tao_weight);
895+
let alpha_stake =
896+
Self::get_stake_for_hotkey_on_subnet(_hotkey, netuid);
897+
let alpha_stake_f = asfloat!(alpha_stake.to_u64());
898+
let total_stake = alpha_stake_f.saturating_add(root_alpha_weighted);
899+
if total_stake > zero {
900+
let root_fraction =
901+
root_alpha_weighted.checked_div(total_stake).unwrap_or(zero);
902+
let root_portion = (*alpha_div).saturating_mul(root_fraction);
903+
let reduction =
904+
root_portion.saturating_mul(one.saturating_sub(utilization));
905+
*alpha_div = (*alpha_div).saturating_sub(reduction);
906+
Self::recycle_subnet_alpha(
907+
netuid,
908+
AlphaCurrency::from(tou64!(reduction)),
909+
);
910+
}
911+
}
912+
}
913+
914+
log::debug!(
915+
"Utilization scaling for netuid {netuid:?}: utilization {utilization:?}, dividends scaled"
916+
);
917+
}
918+
// else: utilization >= 1.0, no scaling needed
919+
823920
Self::distribute_dividends_and_incentives(
824921
netuid,
825922
owner_cut,

pallets/subtensor/src/tests/subnet_emissions.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,13 @@ fn test_effective_root_prop_no_root_dividends() {
516516

517517
#[test]
518518
fn test_effective_root_prop_all_root_dividends() {
519-
// When there are only root alpha dividends, EffectiveRootProp should be 1.0
519+
// When there are only root alpha dividends with equal root stakes but unequal dividends,
520+
// efficiency-based utilization < 1.0 because the validator with less dividends than expected
521+
// has efficiency < 1.0.
522+
// hotkey1: expected_share=0.5, actual_share=1/3 → efficiency=2/3
523+
// hotkey2: expected_share=0.5, actual_share=2/3 → efficiency=1.0 (capped)
524+
// utilization = (1000*2/3 + 1000*1.0) / 2000 ≈ 0.8333
525+
// raw_root_prop = 1.0 (all root divs), so ERP ≈ 0.8333
520526
new_test_ext(1).execute_with(|| {
521527
let netuid = NetUid::from(1);
522528
let hotkey1 = U256::from(100);
@@ -537,14 +543,17 @@ fn test_effective_root_prop_all_root_dividends() {
537543
root_alpha_dividends.insert(hotkey1, U96F32::from_num(1000));
538544
root_alpha_dividends.insert(hotkey2, U96F32::from_num(2000));
539545

540-
SubtensorModule::compute_and_store_effective_root_prop(
546+
let utilization = SubtensorModule::compute_and_store_effective_root_prop(
541547
netuid,
542548
&alpha_dividends,
543549
&root_alpha_dividends,
544550
);
545551

552+
assert_abs_diff_eq!(utilization.to_num::<f64>(), 0.8333, epsilon = 1e-3);
553+
546554
let prop = EffectiveRootProp::<Test>::get(netuid);
547-
assert_abs_diff_eq!(prop.to_num::<f64>(), 1.0, epsilon = 1e-12);
555+
// raw_root_prop = 1.0, utilization ≈ 0.8333
556+
assert_abs_diff_eq!(prop.to_num::<f64>(), 0.8333, epsilon = 1e-3);
548557
});
549558
}
550559

0 commit comments

Comments
 (0)