Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pallets/subtensor/src/coinbase/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ impl<T: Config> Pallet<T> {
PendingServerEmission::<T>::remove(netuid);
PendingRootAlphaDivs::<T>::remove(netuid);
PendingOwnerCut::<T>::remove(netuid);
MinerBurned::<T>::remove(netuid);
BlocksSinceLastStep::<T>::remove(netuid);
LastMechansimStepBlock::<T>::remove(netuid);
LastAdjustmentBlock::<T>::remove(netuid);
Expand Down
31 changes: 25 additions & 6 deletions pallets/subtensor/src/coinbase/run_coinbase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,6 @@ impl<T: Config> Pallet<T> {
let mut alpha_in: BTreeMap<NetUid, U96F32> = BTreeMap::new();
let mut alpha_out: BTreeMap<NetUid, U96F32> = BTreeMap::new();
let mut excess_tao: BTreeMap<NetUid, U96F32> = BTreeMap::new();
let tao_block_emission: U96F32 = U96F32::saturating_from_num(
Self::calculate_block_emission()
.unwrap_or(TaoBalance::ZERO)
.to_u64(),
);

// Only calculate for subnets that we are emitting to.
for (&netuid_i, &tao_emission_i) in subnet_emissions.iter() {
Expand All @@ -210,7 +205,14 @@ impl<T: Config> Pallet<T> {
let alpha_out_i: U96F32 = alpha_emission_i;
let mut alpha_in_i: U96F32 = tao_emission_i.safe_div_or(price_i, U96F32::from_num(0.0));

let alpha_injection_cap: U96F32 = alpha_emission_i.min(tao_block_emission);
// Cap alpha injection by the subnet's root proportion of its alpha emission.
// root_proportion = tao_weight / (tao_weight + alpha_issuance), so as a subnet
// ages its alpha issuance grows, root_proportion shrinks, and the injection cap
// falls. The TAO emission that can no longer be injected as liquidity becomes
// excess TAO and is routed into chain buys instead. This is what transitions
// older subnets from liquidity injection to chain buys over time.
let root_proportion_i: U96F32 = Self::root_proportion(netuid_i);
let alpha_injection_cap: U96F32 = root_proportion_i.saturating_mul(alpha_emission_i);
if alpha_in_i > alpha_injection_cap {
alpha_in_i = alpha_injection_cap;
tao_in_i = alpha_in_i.saturating_mul(price_i);
Expand Down Expand Up @@ -611,14 +613,24 @@ impl<T: Config> Pallet<T> {
let subnet_owner_coldkey = SubnetOwner::<T>::get(netuid);
let owner_hotkeys = Self::get_owner_hotkeys(netuid, &subnet_owner_coldkey);
log::debug!("incentives: owner hotkeys: {owner_hotkeys:?}");
// Track total miner emission vs the portion withheld from miners this tempo
// (directed to an owner/immune hotkey) to record the withheld proportion.
let mut total_incentive: AlphaBalance = AlphaBalance::ZERO;
let mut withheld_incentive: AlphaBalance = AlphaBalance::ZERO;
for (hotkey, incentive) in incentives {
log::debug!("incentives: hotkey: {incentive:?}");
total_incentive = total_incentive.saturating_add(incentive);

// Skip/burn miner-emission for immune keys
if owner_hotkeys.contains(&hotkey) {
log::debug!(
"incentives: hotkey: {hotkey:?} is SN owner hotkey or associated hotkey, skipping {incentive:?}"
);
// Miner emission directed to an owner (immune) hotkey is withheld from
// miners whether it is recycled or burned. Count both toward the withheld
// proportion so the emission penalty cannot be dodged by choosing Recycle
// and an unset RecycleOrBurn config is not uniquely penalized.
withheld_incentive = withheld_incentive.saturating_add(incentive);
// Check if we should recycle or burn the incentive
match RecycleOrBurn::<T>::try_get(netuid) {
Ok(RecycleOrBurnEnum::Recycle) => {
Expand Down Expand Up @@ -658,6 +670,13 @@ impl<T: Config> Pallet<T> {
);
}

// Record the proportion of this tempo's miner emission that was withheld from
// miners (directed to owner/immune hotkeys, whether recycled or burned).
let withheld_proportion: U96F32 = U96F32::saturating_from_num(withheld_incentive.to_u64())
.checked_div(U96F32::saturating_from_num(total_incentive.to_u64()))
.unwrap_or_else(|| U96F32::saturating_from_num(0));
MinerBurned::<T>::insert(netuid, withheld_proportion);

// Distribute alpha divs.
let _ = AlphaDividendsPerSubnet::<T>::clear_prefix(netuid, u32::MAX, None);
for (hotkey, mut alpha_divs) in alpha_dividends {
Expand Down
47 changes: 42 additions & 5 deletions pallets/subtensor/src/coinbase/subnet_emissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,14 +329,51 @@ impl<T: Config> Pallet<T> {
offset_flows
}

// Combines ema price method and tao flow method linearly over FlowHalfLife blocks
// Price-based emission shares: each subnet's share is its EMA price normalized
// by the sum of EMA prices. Emit-disabled subnets are zeroed and their share
// redistributed to enabled subnets in `get_subnet_block_emissions`, so the
// effective emission is e_i = p_i / sum(p_j) over emit-enabled subnets.
pub(crate) fn get_shares(subnets_to_emit_to: &[NetUid]) -> BTreeMap<NetUid, U64F64> {
Self::get_shares_flow(subnets_to_emit_to)
// Self::get_shares_price_ema(subnets_to_emit_to)
let price_shares = Self::get_shares_price_ema(subnets_to_emit_to);

// Weight each subnet's price share by root_proportion * (1 - miner_burned), then
// renormalize. The effective emission is therefore proportional to
// root_proportion_i * price_i * (1 - miner_burned_i).
// - root_proportion shrinks as a subnet's alpha issuance grows, so emission is
// reallocated away from older subnets toward newer ones (easier entrance).
// - (1 - miner_burned) reallocates away from subnets that withhold miner emission.
let zero = U64F64::saturating_from_num(0);
let one = U64F64::saturating_from_num(1);
let weighted: BTreeMap<NetUid, U64F64> = price_shares
.iter()
.map(|(netuid, share)| {
let burned = U64F64::saturating_from_num(MinerBurned::<T>::get(netuid)).min(one);
let root_prop = U64F64::saturating_from_num(Self::root_proportion(*netuid));
let factor = root_prop.saturating_mul(one.saturating_sub(burned));
(*netuid, share.saturating_mul(factor))
})
.collect();

let total_weight = weighted
.values()
.copied()
.fold(zero, |acc, w| acc.saturating_add(w));

if total_weight > zero {
weighted
.into_iter()
.map(|(netuid, w)| (netuid, w.safe_div(total_weight)))
.collect()
} else {
// The combined weight zeroes out for every subnet (e.g. no root stake, or
// every subnet burning all of its miner emission); fall back to the
// unweighted price shares so the block's emission is not stranded.
price_shares
}
}

// DEPRECATED: Implementation of shares that uses EMA prices will be gradually deprecated
#[allow(dead_code)]
// Implementation of shares that uses subnet EMA prices (SubnetMovingPrice),
// not the active/spot alpha price.
fn get_shares_price_ema(subnets_to_emit_to: &[NetUid]) -> BTreeMap<NetUid, U64F64> {
// Get sum of alpha moving prices
let total_moving_prices = subnets_to_emit_to
Expand Down
22 changes: 22 additions & 0 deletions pallets/subtensor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ pub const MAX_SUBNET_CLAIMS: usize = 5;

pub const MAX_ROOT_CLAIM_THRESHOLD: u64 = 10_000_000;

/// Account flag bit that opts into receiving locked alpha transfers.
pub const ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA: u128 = 1u128 << 0;

#[allow(deprecated)]
#[deny(missing_docs)]
#[import_section(errors::errors)]
Expand Down Expand Up @@ -1186,6 +1189,11 @@ pub mod pallet {
pub type Owner<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, T::AccountId, ValueQuery, DefaultAccount<T>>;

/// MAP ( coldkey ) --> flags | Account-level flags. Defaults to zero.
#[pallet::storage]
pub type AccountFlags<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, u128, ValueQuery>;

/// MAP ( hot ) --> take | Returns the hotkey delegation take. And signals that this key is open for delegation
#[pallet::storage]
pub type Delegates<T: Config> =
Expand Down Expand Up @@ -1875,6 +1883,20 @@ pub mod pallet {
pub type PendingOwnerCut<T> =
StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha<T>>;

/// Default miner-burned proportion.
#[pallet::type_value]
pub fn DefaultMinerBurned<T: Config>() -> U96F32 {
U96F32::saturating_from_num(0.0)
}
/// --- MAP ( netuid ) --> miner_burned | Proportion (0..1) of this tempo's miner
/// (incentive) emission that was withheld from miners during emission distribution
/// because the recipient hotkey is owned by the subnet owner (immune key). Counts
/// emission that is either recycled or burned, so the value is independent of the
/// subnet's RecycleOrBurn configuration.
#[pallet::storage]
pub type MinerBurned<T> =
StorageMap<_, Identity, NetUid, U96F32, ValueQuery, DefaultMinerBurned<T>>;

/// --- MAP ( netuid ) --> blocks_since_last_step
#[pallet::storage]
pub type BlocksSinceLastStep<T> =
Expand Down
25 changes: 25 additions & 0 deletions pallets/subtensor/src/macros/dispatches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2593,5 +2593,30 @@ mod dispatches {
let coldkey = ensure_signed(origin)?;
Self::do_set_perpetual_lock(&coldkey, netuid, enabled)
}
/// Sets or clears whether the caller rejects incoming locked alpha.
///
/// Coldkeys reject locked alpha by default. Passing `false` opts the
/// caller into receiving locked alpha from stake transfers or coldkey
/// swaps.
#[pallet::call_index(139)]
#[pallet::weight((
<T as frame_system::Config>::DbWeight::get().reads_writes(1, 1),
DispatchClass::Normal,
Pays::Yes
))]
pub fn set_reject_locked_alpha(origin: OriginFor<T>, enabled: bool) -> DispatchResult {
let coldkey = ensure_signed(origin)?;
AccountFlags::<T>::mutate_exists(&coldkey, |maybe_flags| {
let mut flags = maybe_flags.unwrap_or_default();
if enabled {
flags &= !crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA;
} else {
flags |= crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA;
}
*maybe_flags = if flags == 0 { None } else { Some(flags) };
});
Self::deposit_event(Event::RejectLockedAlphaUpdated { coldkey, enabled });
Ok(())
}
}
}
2 changes: 2 additions & 0 deletions pallets/subtensor/src/macros/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,5 +305,7 @@ mod errors {
CannotUseSystemAccount,
/// Trying to unlock more than locked
UnlockAmountTooHigh,
/// The destination coldkey rejects incoming locked alpha.
AccountRejectsLockedAlpha,
}
}
8 changes: 8 additions & 0 deletions pallets/subtensor/src/macros/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -631,5 +631,13 @@ mod events {
/// Whether this coldkey's locks are now perpetual.
enabled: bool,
},

/// A coldkey's reject locked alpha account flag was updated.
RejectLockedAlphaUpdated {
/// The coldkey whose flag changed.
coldkey: T::AccountId,
/// Whether this coldkey rejects incoming locked alpha.
enabled: bool,
},
}
}
79 changes: 65 additions & 14 deletions pallets/subtensor/src/staking/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,29 @@ impl ConvictionModel {
}

impl<T: Config> Pallet<T> {
pub fn account_rejects_locked_alpha(coldkey: &T::AccountId) -> bool {
AccountFlags::<T>::get(coldkey) & crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA != 1
}

pub fn ensure_can_receive_locked_alpha(
coldkey: &T::AccountId,
amount: AlphaBalance,
) -> DispatchResult {
let rejects_locked_alpha = Self::account_rejects_locked_alpha(coldkey);
Self::ensure_can_receive_locked_alpha_with_flag(rejects_locked_alpha, amount)
}

fn ensure_can_receive_locked_alpha_with_flag(
rejects_locked_alpha: bool,
amount: AlphaBalance,
) -> DispatchResult {
if amount.is_zero() {
return Ok(());
}
ensure!(!rejects_locked_alpha, Error::<T>::AccountRejectsLockedAlpha);
Ok(())
}

pub fn insert_lock_state(
coldkey: &T::AccountId,
netuid: NetUid,
Expand Down Expand Up @@ -1331,40 +1354,67 @@ impl<T: Config> Pallet<T> {
Self::ensure_no_active_locks(new_coldkey)?;

let mut locks_to_transfer: Vec<(NetUid, T::AccountId, LockState)> = Vec::new();
let now = Self::get_current_block_as_u64();
let unlock_rate = UnlockRate::<T>::get();
let maturity_rate = MaturityRate::<T>::get();
let new_coldkey_rejects_locked_alpha = Self::account_rejects_locked_alpha(new_coldkey);
let decaying_locks_to_transfer: Vec<(NetUid, bool)> =
DecayingLock::<T>::iter_prefix(old_coldkey).collect();

// Gather locks for old coldkey
for ((netuid, hotkey), lock) in Lock::<T>::iter_prefix((old_coldkey,)) {
locks_to_transfer.push((netuid, hotkey, lock));
}

// Remove locks for old coldkey and insert for new
let mut rolled_locks_to_transfer: Vec<(NetUid, T::AccountId, LockState, bool)> = Vec::new();
for (netuid, hotkey, lock) in locks_to_transfer {
let now = Self::get_current_block_as_u64();
let unlock_rate = UnlockRate::<T>::get();
let maturity_rate = MaturityRate::<T>::get();
let perpetual_lock = decaying_locks_to_transfer
.iter()
.any(|(decaying_netuid, decaying)| *decaying_netuid == netuid && !*decaying);
let old_lock = ConvictionModel::roll_forward_lock(
lock,
now,
unlock_rate,
maturity_rate,
Self::is_subnet_owner_hotkey(netuid, &hotkey),
Self::is_perpetual_lock(old_coldkey, netuid),
perpetual_lock,
);
Self::ensure_can_receive_locked_alpha_with_flag(
new_coldkey_rejects_locked_alpha,
old_lock.locked_mass,
)?;
rolled_locks_to_transfer.push((netuid, hotkey, old_lock, perpetual_lock));
}

// Remove old locks and reduce old aggregate buckets before moving the
// perpetual-lock flags; aggregate selection depends on the old flag.
for (netuid, hotkey, old_lock, _) in rolled_locks_to_transfer.iter() {
Lock::<T>::remove((old_coldkey.clone(), *netuid, hotkey.clone()));
Self::reduce_aggregate_lock(
old_coldkey,
hotkey,
*netuid,
old_lock.locked_mass,
old_lock.conviction,
);
}

for (netuid, _) in decaying_locks_to_transfer {
if let Some(decaying) = DecayingLock::<T>::take(old_coldkey, netuid) {
DecayingLock::<T>::insert(new_coldkey, netuid, decaying);
}
}

// Insert locks for the new coldkey and add to the destination aggregate
// buckets after the flags have moved.
for (netuid, hotkey, old_lock, perpetual_lock) in rolled_locks_to_transfer {
let new_lock = ConvictionModel::roll_forward_lock(
old_lock.clone(),
now,
unlock_rate,
maturity_rate,
Self::is_subnet_owner_hotkey(netuid, &hotkey),
Self::is_perpetual_lock(new_coldkey, netuid),
);
Lock::<T>::remove((old_coldkey.clone(), netuid, hotkey.clone()));
Self::reduce_aggregate_lock(
old_coldkey,
&hotkey,
netuid,
old_lock.locked_mass,
old_lock.conviction,
perpetual_lock,
);
Self::insert_lock_state(new_coldkey, netuid, &hotkey, new_lock.clone());
Self::add_aggregate_lock(new_coldkey, &hotkey, netuid, new_lock);
Expand Down Expand Up @@ -1780,6 +1830,7 @@ impl<T: Config> Pallet<T> {
.conviction
.saturating_add(conviction_transfer);
}
Self::ensure_can_receive_locked_alpha(destination_coldkey, locked_transfer)?;

source_lock = ConvictionModel::roll_forward_lock(
source_lock,
Expand Down
14 changes: 9 additions & 5 deletions pallets/subtensor/src/tests/claim_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1139,11 +1139,15 @@ fn test_claim_root_coinbase_distribution() {
run_to_block(2);

let alpha_issuance = SubtensorModule::get_alpha_issuance(netuid);
// We went two blocks so we should have 2x the alpha emissions
assert_eq!(
initial_alpha_issuance + alpha_emissions.saturating_mul(2.into()),
alpha_issuance
);
// Net issuance grows by the block alpha emission (alpha_out) plus the
// root-proportion-capped alpha injection. Chain buys move alpha between the
// pool reserve and outstanding supply without changing net issuance, and with
// this subnet's small root proportion the injection is well under a second
// full emission.
let issuance_growth =
u64::from(alpha_issuance).saturating_sub(u64::from(initial_alpha_issuance));
assert!(issuance_growth >= u64::from(alpha_emissions));
assert!(issuance_growth < u64::from(alpha_emissions.saturating_mul(2.into())));

let root_prop = initial_tao as f64 / (u64::from(alpha_issuance) + initial_tao) as f64;
let root_validators_share = 0.5f64;
Expand Down
Loading
Loading