Skip to content
Open
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
470 changes: 235 additions & 235 deletions pallets/admin-utils/src/weights.rs

Large diffs are not rendered by default.

218 changes: 107 additions & 111 deletions pallets/proxy/src/weights.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pallets/subtensor/src/coinbase/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,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
17 changes: 17 additions & 0 deletions pallets/subtensor/src/coinbase/run_coinbase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -669,14 +669,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 @@ -716,6 +726,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
37 changes: 36 additions & 1 deletion pallets/subtensor/src/coinbase/subnet_emissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,42 @@ impl<T: Config> Pallet<T> {
// 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_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
}
}

// Implementation of shares that uses subnet EMA prices (SubnetMovingPrice),
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 @@ -67,6 +67,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 @@ -1190,6 +1193,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 @@ -1926,6 +1934,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
26 changes: 26 additions & 0 deletions pallets/subtensor/src/macros/dispatches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2625,5 +2625,31 @@ mod dispatches {
pub fn trigger_epoch(origin: OriginFor<T>, netuid: NetUid) -> DispatchResult {
Self::do_trigger_epoch(origin, netuid)
}

/// 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(142)]
#[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 @@ -317,5 +317,7 @@ mod errors {
/// an out-of-band epoch would desync the CRv3 reveal window from the wall-clock
/// Drand schedule and silently drop committed weights.
DynamicTempoBlockedByCommitReveal,
/// 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 @@ -669,5 +669,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,
},
}
}
82 changes: 60 additions & 22 deletions pallets/subtensor/src/staking/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,29 @@ impl<T: Config> Pallet<T> {
LockingColdkeys::<T>::remove((netuid, hotkey, coldkey));
}

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 @@ -1359,6 +1382,10 @@ 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();

Expand All @@ -1367,15 +1394,8 @@ impl<T: Config> Pallet<T> {
locks_to_transfer.push((netuid, hotkey, lock));
}

for (netuid, decaying) in decaying_locks_to_transfer.iter() {
DecayingLock::<T>::insert(new_coldkey, *netuid, *decaying);
}

// 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);
Expand All @@ -1387,32 +1407,49 @@ impl<T: Config> Pallet<T> {
Self::is_subnet_owner_hotkey(netuid, &hotkey),
perpetual_lock,
);
Self::ensure_can_receive_locked_alpha_with_flag(
new_coldkey_rejects_locked_alpha,
old_lock.0.locked_mass,
)?;
rolled_locks_to_transfer.push((netuid, hotkey, old_lock.0, 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::maybe_remove_locking_coldkey(hotkey, *netuid, old_coldkey);
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.0.clone(),
old_lock.clone(),
now,
unlock_rate,
maturity_rate,
Self::is_subnet_owner_hotkey(netuid, &hotkey),
perpetual_lock,
)
.0;
Lock::<T>::remove((old_coldkey.clone(), netuid, hotkey.clone()));
Self::maybe_remove_locking_coldkey(&hotkey, netuid, old_coldkey);
Self::reduce_aggregate_lock(
old_coldkey,
&hotkey,
netuid,
old_lock.0.locked_mass,
old_lock.0.conviction,
);
Self::insert_lock_state(new_coldkey, netuid, &hotkey, new_lock.clone());
Self::add_aggregate_lock(new_coldkey, &hotkey, netuid, new_lock);
}

for (netuid, _) in decaying_locks_to_transfer {
DecayingLock::<T>::remove(old_coldkey, netuid);
}

Ok(())
}

Expand Down Expand Up @@ -1838,6 +1875,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
40 changes: 40 additions & 0 deletions pallets/subtensor/src/tests/coinbase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2894,6 +2894,46 @@ fn test_run_coinbase_not_started_start_after() {
});
}

/// Test that coinbase updates protocol liquidity accounting.
/// cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_v3_liquidity_update --exact --show-output
#[test]
fn test_coinbase_v3_liquidity_update() {
new_test_ext(1).execute_with(|| {
let owner_hotkey = U256::from(1);
let owner_coldkey = U256::from(2);

// add network
let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey);

// Force the swap to initialize
SubtensorModule::swap_tao_for_alpha(
netuid,
TaoBalance::ZERO,
1_000_000_000_000_u64.into(),
false,
)
.unwrap();

let tao_before = SubnetTAO::<Test>::get(netuid);
let alpha_in_before = SubnetAlphaIn::<Test>::get(netuid);

// Enable emissions and run coinbase (which will adjust protocol liquidity)
let emission: u64 = 1_234_567;
let emission_credit = SubtensorModule::mint_tao(emission.into());
// Price-based emission shares require a non-zero moving price.
SubnetMovingPrice::<Test>::insert(netuid, I96F32::from_num(1));
// Keep root_proportion ~1 so the injection cap does not bind.
set_full_injection_root_stake();
FirstEmissionBlockNumber::<Test>::insert(netuid, 0);
SubtensorModule::run_coinbase(emission_credit);

assert!(!SubnetTaoInEmission::<Test>::get(netuid).is_zero());
assert!(!SubnetAlphaInEmission::<Test>::get(netuid).is_zero());
assert!(SubnetTAO::<Test>::get(netuid) > tao_before);
assert!(SubnetAlphaIn::<Test>::get(netuid) > alpha_in_before);
});
}

// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_alpha_childkey_parentkey_with_burn --exact --show-output --nocapture
#[test]
fn test_drain_alpha_childkey_parentkey_with_burn() {
Expand Down
Loading
Loading