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
14 changes: 14 additions & 0 deletions pallets/admin-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2134,6 +2134,20 @@ pub mod pallet {
log::trace!("ColdkeySwapReannouncementDelaySet( duration: {duration:?} )");
Ok(())
}

/// Set the number of top subnets that will receive emission
/// If the subnet is not within this number in the list of subnets sorted by emission,
/// it will receive no emission
#[pallet::call_index(89)]
#[pallet::weight(Weight::from_parts(5_420_000, 0)
.saturating_add(<T as frame_system::Config>::DbWeight::get().reads(0_u64))
.saturating_add(<T as frame_system::Config>::DbWeight::get().writes(1_u64)))]
pub fn sudo_set_subnet_emission_cap(origin: OriginFor<T>, cap: u16) -> DispatchResult {
ensure_root(origin)?;
pallet_subtensor::Pallet::<T>::set_subnet_emission_cap(cap);
log::debug!("SubnetEmissionCap set to {}", cap);
Ok(())
}
}
}

Expand Down
56 changes: 53 additions & 3 deletions pallets/subtensor/src/coinbase/subnet_emissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,56 @@ impl<T: Config> Pallet<T> {
offset_flows
}

// Combines ema price method and tao flow method linearly over FlowHalfLife blocks
// Return emission shares
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 topk = SubnetEmissionCap::<T>::get() as usize;
let shares = Self::get_shares_flow(subnets_to_emit_to);

// Start with all requested subnets present in the output and assigned
// a zero share.
//
// This guarantees that:
// 1. every input NetUid is present in the returned map
// 2. non-selected subnets remain at zero
// 3. if normalization is impossible, all shares stay at zero
let mut normalized: BTreeMap<NetUid, U64F64> = subnets_to_emit_to
.iter()
.map(|netuid| (*netuid, U64F64::from_num(0)))
.collect();

// If there is no capacity or no computed shares, return the all-zero map.
if topk == 0 || shares.is_empty() {
return normalized;
}

// Collect into a vector so we can sort by share descending.
let mut top_shares: Vec<(NetUid, U64F64)> = shares.into_iter().collect();

// Sort by:
// 1. larger share first
// 2. smaller NetUid first as a deterministic tie-breaker
top_shares.sort_unstable_by(|(netuid_a, share_a), (netuid_b, share_b)| {
share_b.cmp(share_a).then_with(|| netuid_a.cmp(netuid_b))
});

// Keep only the top-k shares. If topk is larger than the number of shares,
// all shares are kept.
top_shares.truncate(topk);

// Sum the selected shares so we can re-normalize them to add up to 1.0.
let total_selected_share: U64F64 = top_shares.iter().map(|(_, share)| *share).sum();

// If normalization is possible, write normalized values for the selected
// top-k subnets. All other subnets remain at zero.
//
// If normalization is not possible (sum == 0), return the all-zero map.
if total_selected_share != U64F64::from_num(0) {
for (netuid, share) in top_shares {
normalized.insert(netuid, share.safe_div(total_selected_share));
}
}

normalized
}

// DEPRECATED: Implementation of shares that uses EMA prices will be gradually deprecated
Expand Down Expand Up @@ -246,4 +292,8 @@ impl<T: Config> Pallet<T> {
})
.collect::<BTreeMap<NetUid, U64F64>>()
}

pub fn set_subnet_emission_cap(cap: u16) {
SubnetEmissionCap::<T>::set(cap);
}
}
10 changes: 10 additions & 0 deletions pallets/subtensor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,12 @@ pub mod pallet {
0.into()
}

/// Default maximum childkey take.
#[pallet::type_value]
pub fn DefaultSubnetEmissionCap<T: Config>() -> u16 {
256_u16
}

/// Default value for blocks since last step.
#[pallet::type_value]
pub fn DefaultBlocksSinceLastStep<T: Config>() -> u64 {
Expand Down Expand Up @@ -1634,6 +1640,10 @@ pub mod pallet {
pub type PendingServerEmission<T> =
StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha<T>>;

/// --- ITEM ( subnet_emission_cap )
#[pallet::storage]
pub type SubnetEmissionCap<T> = StorageValue<_, u16, ValueQuery, DefaultSubnetEmissionCap<T>>;

/// --- MAP ( netuid ) --> pending_validator_emission
#[pallet::storage]
pub type PendingValidatorEmission<T> =
Expand Down
52 changes: 52 additions & 0 deletions pallets/subtensor/src/tests/subnet_emissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,55 @@ fn seed_price_and_flow(n1: NetUid, n2: NetUid, price1: f64, price2: f64, flow1:
// assert_abs_diff_eq!(s1 + s2 + s3, 1.0, epsilon = 1e-9);
// });
// }

// cargo test --package pallet-subtensor --lib -- tests::subnet_emissions::get_shares_respects_subnet_emission_cap_and_keeps_zero_entries --exact --nocapture
#[test]
fn get_shares_respects_subnet_emission_cap_and_keeps_zero_entries() {
new_test_ext(1).execute_with(|| {
let owner_hotkey = U256::from(60);
let owner_coldkey = U256::from(61);

let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey);
let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey);
let n3 = add_dynamic_network(&owner_hotkey, &owner_coldkey);

// Keep the math simple and deterministic.
FlowNormExponent::<Test>::set(u64f64(1.0));
let block_num = FlowHalfLife::<Test>::get();
System::set_block_number(block_num);

// Only the top 2 subnets should receive non-zero emission.
SubnetEmissionCap::<Test>::set(2);

// Neutral prices so ordering comes from flows.
SubnetMovingPrice::<Test>::insert(n1, i96f32(1.0));
SubnetMovingPrice::<Test>::insert(n2, i96f32(1.0));
SubnetMovingPrice::<Test>::insert(n3, i96f32(1.0));

// Positive, strictly ordered flows.
SubnetEmaTaoFlow::<Test>::insert(n1, (block_num, i64f64(1_000.0)));
SubnetEmaTaoFlow::<Test>::insert(n2, (block_num, i64f64(3_000.0)));
SubnetEmaTaoFlow::<Test>::insert(n3, (block_num, i64f64(6_000.0)));

let shares = SubtensorModule::get_shares(&[n1, n2, n3]);

// All requested subnets must be present in the returned map.
assert_eq!(shares.len(), 3);
assert!(shares.contains_key(&n1));
assert!(shares.contains_key(&n2));
assert!(shares.contains_key(&n3));

let s1 = shares.get(&n1).unwrap().to_num::<f64>();
let s2 = shares.get(&n2).unwrap().to_num::<f64>();
let s3 = shares.get(&n3).unwrap().to_num::<f64>();

// The lowest-share subnet should be kept in the map but receive zero.
assert_abs_diff_eq!(s1, 0.0, epsilon = 1e-18);

// The top-2 subnets should be re-normalized to sum to 1.
assert!(s2 > 0.0, "expected n2 to receive non-zero share, got {s2}");
assert!(s3 > 0.0, "expected n3 to receive non-zero share, got {s3}");
assert!(s3 > s2, "expected n3 > n2; got s2={s2}, s3={s3}");
assert_abs_diff_eq!(s1 + s2 + s3, 1.0_f64, epsilon = 1e-9);
});
}
Loading