From 82e1b073e97b7e0a746602dbba923a2811bc6d6b Mon Sep 17 00:00:00 2001 From: UnarbosFour Date: Tue, 11 Aug 2026 15:18:40 -0400 Subject: [PATCH 1/7] Correct SubnetAlphaOut, referine conviction term --- .gitignore | 3 + pallets/alpha-assets/src/lib.rs | 10 ++ pallets/subtensor/src/macros/hooks.rs | 3 + .../migrate_fix_rao_alpha_out_accounting.rs | 130 ++++++++++++++++++ pallets/subtensor/src/migrations/mod.rs | 1 + pallets/subtensor/src/staking/lock.rs | 26 ++-- pallets/subtensor/src/tests/locks.rs | 16 ++- pallets/subtensor/src/tests/migration.rs | 93 +++++++++++++ 8 files changed, 265 insertions(+), 17 deletions(-) create mode 100644 pallets/subtensor/src/migrations/migrate_fix_rao_alpha_out_accounting.rs diff --git a/.gitignore b/.gitignore index 558527ff90..dd1769bbb7 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,6 @@ sdk/python/dist/ .github/scripts/deploy/proxy_proxy_blob.hex .github/scripts/deploy/pending-release.json .github/scripts/deploy/deployment-multisig-proposal.hex + +# Local historical alpha-accounting scanner and PostgreSQL data +.alpha-accounting-scanner/ diff --git a/pallets/alpha-assets/src/lib.rs b/pallets/alpha-assets/src/lib.rs index 6e856975aa..2b478ae46a 100644 --- a/pallets/alpha-assets/src/lib.rs +++ b/pallets/alpha-assets/src/lib.rs @@ -245,6 +245,8 @@ impl Imbalance for NegativeAlphaImbalance { pub trait AlphaAssetsInterface { fn total_alpha_issuance(netuid: NetUid) -> AlphaBalance; + fn alpha_burned(netuid: NetUid) -> AlphaBalance; + fn mint_alpha(netuid: NetUid, amount: AlphaBalance) -> PositiveAlphaImbalance; fn burn_alpha(netuid: NetUid, amount: AlphaBalance) -> AlphaBalance; @@ -257,6 +259,10 @@ impl AlphaAssetsInterface for () { AlphaBalance::ZERO } + fn alpha_burned(_netuid: NetUid) -> AlphaBalance { + AlphaBalance::ZERO + } + fn mint_alpha(netuid: NetUid, amount: AlphaBalance) -> PositiveAlphaImbalance { PositiveAlphaImbalance::new(netuid, amount) } @@ -339,6 +345,10 @@ impl AlphaAssetsInterface for Pallet { TotalAlphaIssuance::::get(netuid) } + fn alpha_burned(netuid: NetUid) -> AlphaBalance { + AlphaBurned::::get(netuid) + } + fn mint_alpha(netuid: NetUid, amount: AlphaBalance) -> PositiveAlphaImbalance { Self::mint_alpha(netuid, amount) } diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index f5323e4140..f1117e9712 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -200,6 +200,9 @@ mod hooks { // rank-32 bar (DefaultEmissionBarRank) applies from the first // recompute after the upgrade instead of the next cadence boundary. .saturating_add(migrations::migrate_reset_emission_gate_bar::migrate_reset_emission_gate_bar::()) + // Repair stabilized SubnetAlphaOut undercounts caused by duplicated RAO-launch + // local dividends and legacy root dividends omitted from the root counter. + .saturating_add(migrations::migrate_fix_rao_alpha_out_accounting::migrate_fix_rao_alpha_out_accounting::()) // Schedule the large storage-GC sweep. Actual work is bounded by the remaining // on_idle weight over subsequent blocks. .saturating_add(migrations::migrate_storage_bloat_v2::kickoff_storage_bloat_cleanup::()); diff --git a/pallets/subtensor/src/migrations/migrate_fix_rao_alpha_out_accounting.rs b/pallets/subtensor/src/migrations/migrate_fix_rao_alpha_out_accounting.rs new file mode 100644 index 0000000000..49b4aa2daa --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_fix_rao_alpha_out_accounting.rs @@ -0,0 +1,130 @@ +use super::*; +use frame_support::{traits::Get, weights::Weight}; +use frame_system::pallet_prelude::BlockNumberFor; +use scale_info::prelude::string::String; +use sp_runtime::traits::Zero; +use subtensor_runtime_common::{AlphaBalance, NetUid}; + +pub(crate) const MIGRATION_NAME: &[u8] = b"migrate_fix_rao_alpha_out_accounting"; + +/// Fixed mainnet corrections generated by an offline historical block scan. +/// +/// Netuid 0 repairs root dividends that were credited directly to root stake +/// without increasing `SubnetAlphaOut(0)`. The other rows repair the RAO launch +/// bug fixed by PR #1321, where the full local dividend and its root portion were +/// both credited while `SubnetAlphaOut` recorded only the intended issuance. +/// +/// Recycled netuids are intentionally absent: their affected historical subnet +/// generation was dissolved, so applying its correction to the current generation +/// would corrupt an unrelated asset. Tests verify these embedded constants against +/// the checked-in reconstruction data. +pub(crate) const ALPHA_OUT_CORRECTIONS: &[(u16, u64)] = &[ + (0, 728_652_620_877_147), + (1, 16_841_481_627_450), + (2, 16_657_607_997_081), + (3, 16_612_384_835_337), + (4, 16_657_531_459_163), + (5, 16_603_538_275_444), + (6, 16_670_388_331_537), + (7, 16_665_154_249_543), + (8, 16_641_858_745_916), + (9, 16_602_494_598_480), + (10, 16_648_982_598_377), + (11, 16_614_884_693_920), + (12, 16_609_506_226_265), + (13, 16_593_293_670_926), + (14, 16_654_855_289_608), + (17, 16_629_648_001_041), + (18, 16_609_953_922_039), + (19, 16_648_375_285_150), + (20, 16_627_174_584_058), + (21, 16_664_619_652_248), + (22, 16_657_589_459_778), + (23, 16_650_031_992_562), + (24, 16_627_624_371_902), + (25, 16_612_520_257_668), + (27, 16_611_935_757_901), + (28, 16_634_666_296_663), + (29, 16_521_483_756_032), + (30, 16_511_202_677_182), + (32, 16_641_878_275_936), + (33, 16_566_858_991_992), + (34, 16_618_182_021_497), + (35, 16_572_673_502_097), + (37, 16_630_032_241_363), + (39, 16_611_788_077_785), + (41, 16_565_029_906_044), + (42, 16_609_212_950_301), + (43, 16_555_071_176_088), + (44, 16_577_716_745_472), + (45, 16_622_137_987_566), + (46, 16_613_515_003_811), + (48, 16_584_343_114_727), + (50, 16_600_577_252_963), + (51, 16_560_623_847_708), + (52, 16_608_149_936_500), + (53, 16_566_475_199_783), + (54, 16_597_835_699_556), + (55, 16_621_722_394_828), + (56, 16_582_983_440_389), + (59, 16_937_046_522_687), + (60, 16_979_801_039_432), + (61, 16_957_520_199_920), + (62, 16_958_464_626_467), + (63, 16_977_368_055_945), + (64, 16_921_598_714_453), + (65, 4_932_684_752_021), + (66, 2_003_518_827_049), +]; + +/// Repairs two stabilized mainnet `SubnetAlphaOut` undercounts using offline +/// reconstructed constants. The migration deliberately does not query or replay +/// historical chain state. +pub fn migrate_fix_rao_alpha_out_accounting() -> Weight { + let migration_name = MIGRATION_NAME.to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{}' already executed - skipping", + String::from_utf8_lossy(MIGRATION_NAME) + ); + return weight; + } + + // These amounts describe mainnet history and must never be applied to + // testnet, devnet, or a local chain. + let genesis_hash = frame_system::Pallet::::block_hash(BlockNumberFor::::zero()); + weight.saturating_accrue(T::DbWeight::get().reads(1)); + let mainnet_genesis = + hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); + let mut applied_corrections = 0; + + if genesis_hash.as_ref() == mainnet_genesis { + for &(netuid, correction) in ALPHA_OUT_CORRECTIONS { + SubnetAlphaOut::::mutate(NetUid::from(netuid), |alpha_out| { + *alpha_out = alpha_out.saturating_add(AlphaBalance::from(correction)); + }); + } + applied_corrections = ALPHA_OUT_CORRECTIONS.len(); + weight.saturating_accrue(T::DbWeight::get().reads_writes( + ALPHA_OUT_CORRECTIONS.len() as u64, + ALPHA_OUT_CORRECTIONS.len() as u64, + )); + } else { + log::info!( + "Migration '{}' skipped outside mainnet", + String::from_utf8_lossy(MIGRATION_NAME) + ); + } + + HasMigrationRun::::insert(&migration_name, true); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{}' completed with {} SubnetAlphaOut corrections", + String::from_utf8_lossy(MIGRATION_NAME), + applied_corrections + ); + weight +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index dfd5714c03..d8c968b2b9 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -25,6 +25,7 @@ pub mod migrate_dynamic_tempo; pub mod migrate_fix_bad_hk_swap; pub mod migrate_fix_childkeys; pub mod migrate_fix_is_network_member; +pub mod migrate_fix_rao_alpha_out_accounting; pub mod migrate_fix_root_claimed_overclaim; pub mod migrate_fix_root_subnet_tao; pub mod migrate_fix_root_tao_and_alpha_in; diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index 6a02d13942..4c92da0a99 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -1,6 +1,7 @@ use super::*; use codec::{Decode, DecodeWithMemTracking, Encode}; use frame_support::weights::WeightMeter; +use pallet_alpha_assets::AlphaAssetsInterface; use safe_math::FixedExt; use scale_info::TypeInfo; use sp_std::collections::btree_map::BTreeMap; @@ -1157,15 +1158,20 @@ impl Pallet { /// is mature enough and enough conviction has accumulated. /// /// Ownership can change only after the subnet is at least [`ONE_YEAR`] old and the - /// total rolled aggregate conviction on the subnet is at least 10% of `SubnetAlphaOut`. - /// If those gates pass, the hotkey with the highest rolled aggregate conviction - /// becomes the subnet owner hotkey, and that hotkey's owning coldkey becomes the - /// subnet owner coldkey. The new owner hotkey's conviction is then progressed to - /// its current locked mass so the new owner starts with full owner conviction. + /// total rolled aggregate conviction on the subnet is at least 10% of + /// `SubnetAlphaOut - SubnetProtocolAlpha - AlphaBurned`. If those gates pass, the + /// hotkey with the highest rolled aggregate conviction becomes the subnet owner + /// hotkey, and that hotkey's owning coldkey becomes the subnet owner coldkey. The new + /// owner hotkey's conviction is then progressed to its current locked mass so the new + /// owner starts with full owner conviction. pub fn change_subnet_owner_if_needed(netuid: NetUid) { - // No outstanding alpha means there is no meaningful 10% conviction threshold. - let subnet_alpha_out = SubnetAlphaOut::::get(netuid); - if subnet_alpha_out.is_zero() { + // Protocol-owned and burned alpha cannot support a challenger, so exclude both + // from the ownership quorum. Saturation keeps inconsistent accounting from + // wrapping the threshold to a very large value. + let eligible_alpha = SubnetAlphaOut::::get(netuid) + .saturating_sub(SubnetProtocolAlpha::::get(netuid)) + .saturating_sub(T::AlphaAssets::alpha_burned(netuid)); + if eligible_alpha.is_zero() { return; } @@ -1176,10 +1182,10 @@ impl Pallet { return; } - // Require total rolled aggregate conviction to be at least 10% of subnet alpha out. + // Require total rolled aggregate conviction to be at least 10% of eligible alpha. let total_conviction = Self::get_total_conviction(netuid); if total_conviction.saturating_mul(U64F64::saturating_from_num(10)) - < U64F64::saturating_from_num(u64::from(subnet_alpha_out)) + < U64F64::saturating_from_num(u64::from(eligible_alpha)) { return; } diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index cbf87a7901..6895e1d93e 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -3155,20 +3155,22 @@ fn test_change_subnet_owner_if_needed_reassigns_to_subnet_king() { &king_hotkey )); - // Make the subnet old enough and set alpha out so 1_000 conviction is exactly - // the 10% minimum required to trigger reassignment. + // Make the subnet old enough. Of 10_000 alpha out, 2_000 is protocol-owned and + // 1_000 is burned, so 700 conviction is exactly the 10% takeover threshold. let now = crate::staking::lock::ONE_YEAR + 1; System::set_block_number(now); NetworkRegisteredAt::::insert(netuid, 1); SubnetAlphaOut::::insert(netuid, AlphaBalance::from(10_000u64)); + SubnetProtocolAlpha::::insert(netuid, AlphaBalance::from(2_000u64)); + pallet_alpha_assets::AlphaBurned::::insert(netuid, AlphaBalance::from(1_000u64)); // Seed matching individual and aggregate lock rows for the future king. - let locked_mass = AlphaBalance::from(1_000u64); + let locked_mass = AlphaBalance::from(700u64); Lock::::insert( (new_owner_coldkey, netuid, king_hotkey), LockState { locked_mass, - conviction: U64F64::from_num(1_000), + conviction: U64F64::from_num(700), last_update: now, }, ); @@ -3177,7 +3179,7 @@ fn test_change_subnet_owner_if_needed_reassigns_to_subnet_king() { king_hotkey, LockState { locked_mass, - conviction: U64F64::from_num(1_000), + conviction: U64F64::from_num(700), last_update: now, }, ); @@ -3190,10 +3192,10 @@ fn test_change_subnet_owner_if_needed_reassigns_to_subnet_king() { // The new owner's aggregate conviction is progressed to locked mass. let owner_lock = Lock::::get((new_owner_coldkey, netuid, king_hotkey)).unwrap(); - assert_eq!(owner_lock.conviction, U64F64::from_num(1_000)); + assert_eq!(owner_lock.conviction, U64F64::from_num(700)); let king_lock = OwnerLock::::get(netuid).unwrap(); - assert_eq!(king_lock.conviction, U64F64::from_num(1_000)); + assert_eq!(king_lock.conviction, U64F64::from_num(700)); }); } diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index ac15ffb07c..212d78a6ae 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -6610,6 +6610,99 @@ fn test_migrate_reset_emission_gate_bar() { }); } +#[test] +fn test_rao_alpha_out_corrections_match_csv() { + use crate::migrations::migrate_fix_rao_alpha_out_accounting::ALPHA_OUT_CORRECTIONS; + + let csv = include_str!("../../../../rao-double-dividend-subnet-alpha-out-correction.csv"); + let mut lines = csv.lines(); + let header: Vec<&str> = lines.next().expect("CSV header").split(',').collect(); + let netuid_index = header.iter().position(|name| *name == "netuid").unwrap(); + let amount_index = header + .iter() + .position(|name| *name == "required_adjustment_rao") + .unwrap(); + let apply_index = header + .iter() + .position(|name| *name == "apply_to_current_generation") + .unwrap(); + let from_csv: Vec<(u16, u64)> = lines + .map(|line| line.split(',').collect::>()) + .filter(|columns| columns[apply_index] == "true") + .map(|columns| { + ( + columns[netuid_index].parse().unwrap(), + columns[amount_index].parse().unwrap(), + ) + }) + .collect(); + + assert_eq!(ALPHA_OUT_CORRECTIONS, from_csv.as_slice()); + assert_eq!( + from_csv + .iter() + .map(|(_, correction)| u128::from(*correction)) + .sum::(), + 1_618_308_219_994_798 + ); +} + +#[test] +fn test_migrate_fix_rao_alpha_out_accounting() { + use crate::migrations::migrate_fix_rao_alpha_out_accounting::{ + ALPHA_OUT_CORRECTIONS, MIGRATION_NAME, migrate_fix_rao_alpha_out_accounting, + }; + + new_test_ext(1).execute_with(|| { + let mainnet_genesis = + hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); + frame_system::BlockHash::::insert(0_u64, H256::from_slice(&mainnet_genesis)); + + for &(netuid, _) in ALPHA_OUT_CORRECTIONS { + SubnetAlphaOut::::insert(NetUid::from(netuid), AlphaBalance::from(10_u64)); + } + + let weight = migrate_fix_rao_alpha_out_accounting::(); + assert!(!weight.is_zero()); + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + for &(netuid, correction) in ALPHA_OUT_CORRECTIONS { + assert_eq!( + SubnetAlphaOut::::get(NetUid::from(netuid)), + AlphaBalance::from(correction + 10) + ); + } + + // The run-once guard prevents a second application. + migrate_fix_rao_alpha_out_accounting::(); + for &(netuid, correction) in ALPHA_OUT_CORRECTIONS { + assert_eq!( + SubnetAlphaOut::::get(NetUid::from(netuid)), + AlphaBalance::from(correction + 10) + ); + } + }); +} + +#[test] +fn test_migrate_fix_rao_alpha_out_accounting_skips_non_mainnet() { + use crate::migrations::migrate_fix_rao_alpha_out_accounting::{ + MIGRATION_NAME, migrate_fix_rao_alpha_out_accounting, + }; + + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1_u16); + SubnetAlphaOut::::insert(netuid, AlphaBalance::from(10_u64)); + + migrate_fix_rao_alpha_out_accounting::(); + + assert_eq!( + SubnetAlphaOut::::get(netuid), + AlphaBalance::from(10_u64) + ); + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + }); +} + #[test] fn test_storage_bloat_cleanup_is_bounded_and_preserves_nonzero_state() { use crate::migrations::migrate_storage_bloat_v2::{ From b747d7a62de132fb026ddf62f2c823bbd3516fa4 Mon Sep 17 00:00:00 2001 From: UnarbosFour Date: Tue, 11 Aug 2026 16:12:56 -0400 Subject: [PATCH 2/7] Fix generation-scoped alpha accounting and safely rebase recycled subnet counters --- pallets/alpha-assets/src/lib.rs | 61 ++++++++ pallets/alpha-assets/src/tests.rs | 33 +++++ pallets/subtensor/src/macros/hooks.rs | 3 + ...te_rebase_recycled_alpha_asset_counters.rs | 89 ++++++++++++ pallets/subtensor/src/migrations/mod.rs | 1 + pallets/subtensor/src/subnets/dissolution.rs | 20 ++- pallets/subtensor/src/subnets/subnet.rs | 7 + pallets/subtensor/src/tests/migration.rs | 133 ++++++++++++++---- pallets/subtensor/src/tests/networks.rs | 51 +++++++ .../subtensor/src/tests/remove_data_tests.rs | 14 +- 10 files changed, 378 insertions(+), 34 deletions(-) create mode 100644 pallets/subtensor/src/migrations/migrate_rebase_recycled_alpha_asset_counters.rs diff --git a/pallets/alpha-assets/src/lib.rs b/pallets/alpha-assets/src/lib.rs index 2b478ae46a..6d8b09896e 100644 --- a/pallets/alpha-assets/src/lib.rs +++ b/pallets/alpha-assets/src/lib.rs @@ -247,6 +247,15 @@ pub trait AlphaAssetsInterface { fn alpha_burned(netuid: NetUid) -> AlphaBalance; + fn clear_alpha_counters(netuid: NetUid); + + fn rebase_alpha_counters( + netuid: NetUid, + issuance_offset: AlphaBalance, + burned_offset: AlphaBalance, + recycled_offset: AlphaBalance, + ); + fn mint_alpha(netuid: NetUid, amount: AlphaBalance) -> PositiveAlphaImbalance; fn burn_alpha(netuid: NetUid, amount: AlphaBalance) -> AlphaBalance; @@ -263,6 +272,16 @@ impl AlphaAssetsInterface for () { AlphaBalance::ZERO } + fn clear_alpha_counters(_netuid: NetUid) {} + + fn rebase_alpha_counters( + _netuid: NetUid, + _issuance_offset: AlphaBalance, + _burned_offset: AlphaBalance, + _recycled_offset: AlphaBalance, + ) { + } + fn mint_alpha(netuid: NetUid, amount: AlphaBalance) -> PositiveAlphaImbalance { PositiveAlphaImbalance::new(netuid, amount) } @@ -306,6 +325,35 @@ pub mod pallet { } impl Pallet { + pub fn clear_alpha_counters(netuid: NetUid) { + TotalAlphaIssuance::::remove(netuid); + AlphaBurned::::remove(netuid); + AlphaRecycled::::remove(netuid); + } + + pub fn rebase_alpha_counters( + netuid: NetUid, + issuance_offset: AlphaBalance, + burned_offset: AlphaBalance, + recycled_offset: AlphaBalance, + ) { + if !issuance_offset.is_zero() { + TotalAlphaIssuance::::mutate(netuid, |issuance| { + *issuance = issuance.saturating_sub(issuance_offset); + }); + } + if !burned_offset.is_zero() { + AlphaBurned::::mutate(netuid, |burned| { + *burned = burned.saturating_sub(burned_offset); + }); + } + if !recycled_offset.is_zero() { + AlphaRecycled::::mutate(netuid, |recycled| { + *recycled = recycled.saturating_sub(recycled_offset); + }); + } + } + pub fn mint_alpha(netuid: NetUid, amount: AlphaBalance) -> PositiveAlphaImbalance { if !amount.is_zero() { TotalAlphaIssuance::::mutate(netuid, |issuance| { @@ -349,6 +397,19 @@ impl AlphaAssetsInterface for Pallet { AlphaBurned::::get(netuid) } + fn clear_alpha_counters(netuid: NetUid) { + Self::clear_alpha_counters(netuid) + } + + fn rebase_alpha_counters( + netuid: NetUid, + issuance_offset: AlphaBalance, + burned_offset: AlphaBalance, + recycled_offset: AlphaBalance, + ) { + Self::rebase_alpha_counters(netuid, issuance_offset, burned_offset, recycled_offset) + } + fn mint_alpha(netuid: NetUid, amount: AlphaBalance) -> PositiveAlphaImbalance { Self::mint_alpha(netuid, amount) } diff --git a/pallets/alpha-assets/src/tests.rs b/pallets/alpha-assets/src/tests.rs index 48608f15a4..7878962556 100644 --- a/pallets/alpha-assets/src/tests.rs +++ b/pallets/alpha-assets/src/tests.rs @@ -57,6 +57,39 @@ fn recycle_alpha_reduces_total_issuance_saturating_at_zero() { }); } +#[test] +fn clear_alpha_counters_removes_previous_generation_state() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(6u16); + + AlphaAssets::mint_alpha(netuid, 100u64.into()); + AlphaAssets::burn_alpha(netuid, 30u64.into()); + AlphaAssets::recycle_alpha(netuid, 20u64.into()); + AlphaAssets::clear_alpha_counters(netuid); + + assert!(!TotalAlphaIssuance::::contains_key(netuid)); + assert!(!AlphaBurned::::contains_key(netuid)); + assert!(!AlphaRecycled::::contains_key(netuid)); + }); +} + +#[test] +fn rebase_alpha_counters_preserves_current_generation_deltas() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(7u16); + + TotalAlphaIssuance::::insert(netuid, AlphaBalance::from(125u64)); + AlphaBurned::::insert(netuid, AlphaBalance::from(47u64)); + AlphaRecycled::::insert(netuid, AlphaBalance::from(31u64)); + + AlphaAssets::rebase_alpha_counters(netuid, 100u64.into(), 40u64.into(), 30u64.into()); + + assert_eq!(TotalAlphaIssuance::::get(netuid), 25u64.into()); + assert_eq!(AlphaBurned::::get(netuid), 7u64.into()); + assert_eq!(AlphaRecycled::::get(netuid), 1u64.into()); + }); +} + #[test] fn positive_imbalance_only_merges_with_same_netuid() { new_test_ext().execute_with(|| { diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index f1117e9712..fbe9b7222f 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -203,6 +203,9 @@ mod hooks { // Repair stabilized SubnetAlphaOut undercounts caused by duplicated RAO-launch // local dividends and legacy root dividends omitted from the root counter. .saturating_add(migrations::migrate_fix_rao_alpha_out_accounting::migrate_fix_rao_alpha_out_accounting::()) + // Remove prior-generation alpha-asset counter offsets from recycled mainnet + // netuids without discarding burns accumulated by their current generations. + .saturating_add(migrations::migrate_rebase_recycled_alpha_asset_counters::migrate_rebase_recycled_alpha_asset_counters::()) // Schedule the large storage-GC sweep. Actual work is bounded by the remaining // on_idle weight over subsequent blocks. .saturating_add(migrations::migrate_storage_bloat_v2::kickoff_storage_bloat_cleanup::()); diff --git a/pallets/subtensor/src/migrations/migrate_rebase_recycled_alpha_asset_counters.rs b/pallets/subtensor/src/migrations/migrate_rebase_recycled_alpha_asset_counters.rs new file mode 100644 index 0000000000..fd58bdfb06 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_rebase_recycled_alpha_asset_counters.rs @@ -0,0 +1,89 @@ +use super::*; +use frame_support::traits::Get; +use frame_system::pallet_prelude::BlockNumberFor; +use pallet_alpha_assets::AlphaAssetsInterface; +use scale_info::prelude::string::String; +use sp_runtime::traits::Zero; +use subtensor_runtime_common::{AlphaBalance, NetUid}; + +pub(crate) const MIGRATION_NAME: &[u8] = b"migrate_rebase_recycled_alpha_asset_counters"; + +/// `(netuid, current_generation_registered_at, issuance, burned, recycled)` offsets +/// inherited from a prior subnet generation. +/// +/// These values were read from mainnet archive state immediately before each current +/// generation was registered. Eight other historically recycled netuids had zeroes for +/// all three counters at their generation boundary and therefore need no correction. +pub(crate) const RECYCLED_ALPHA_COUNTER_OFFSETS: &[(u16, u64, u64, u64, u64)] = &[ + (40, 8_409_860, 126_075_000_000_000, 51_803_789_083_976, 0), + (16, 8_460_646, 176_861_000_000_000, 42_912_779_090_897, 0), + (58, 8_511_017, 227_232_000_000_000, 93_219_350_226_399, 0), +]; + +/// Removes prior-generation offsets from alpha-asset counters for the three affected +/// mainnet netuids while preserving everything accumulated by their current generations. +pub fn migrate_rebase_recycled_alpha_asset_counters() -> Weight { + let migration_name = MIGRATION_NAME.to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{}' already executed - skipping", + String::from_utf8_lossy(MIGRATION_NAME) + ); + return weight; + } + + let genesis_hash = frame_system::Pallet::::block_hash(BlockNumberFor::::zero()); + weight.saturating_accrue(T::DbWeight::get().reads(1)); + let mainnet_genesis = + hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); + let mut corrected = 0u64; + + if genesis_hash.as_ref() == mainnet_genesis { + for &(netuid, expected_registered_at, issuance, burned, recycled) in + RECYCLED_ALPHA_COUNTER_OFFSETS + { + let netuid = NetUid::from(netuid); + let registered_at = NetworkRegisteredAt::::get(netuid); + weight.saturating_accrue(T::DbWeight::get().reads(1)); + + // The constants describe one exact generation. If the slot has moved again, + // leave it untouched rather than applying an offset to the wrong asset. + if registered_at != expected_registered_at { + log::warn!( + "Skipping alpha counter rebase for netuid {:?}: registered_at={}, expected={}", + netuid, + registered_at, + expected_registered_at, + ); + continue; + } + + T::AlphaAssets::rebase_alpha_counters( + netuid, + AlphaBalance::from(issuance), + AlphaBalance::from(burned), + AlphaBalance::from(recycled), + ); + // Every embedded row has non-zero issuance and burned offsets; recycled is zero. + weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); + corrected = corrected.saturating_add(1); + } + } else { + log::info!( + "Migration '{}' skipped outside mainnet", + String::from_utf8_lossy(MIGRATION_NAME) + ); + } + + HasMigrationRun::::insert(&migration_name, true); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{}' completed with {} alpha counter corrections", + String::from_utf8_lossy(MIGRATION_NAME), + corrected, + ); + weight +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index d8c968b2b9..d132973877 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -45,6 +45,7 @@ pub mod migrate_populate_owned_hotkeys; pub mod migrate_rao; pub mod migrate_rate_limit_keys; pub mod migrate_rate_limiting_last_blocks; +pub mod migrate_rebase_recycled_alpha_asset_counters; pub mod migrate_remove_add_stake_burn_rate_limit; pub mod migrate_remove_commitments_rate_limit; pub mod migrate_remove_deprecated_conviction_maps; diff --git a/pallets/subtensor/src/subnets/dissolution.rs b/pallets/subtensor/src/subnets/dissolution.rs index a90ecc49b2..4eb00e2068 100644 --- a/pallets/subtensor/src/subnets/dissolution.rs +++ b/pallets/subtensor/src/subnets/dissolution.rs @@ -1,5 +1,6 @@ use super::*; use frame_support::weights::WeightMeter; +use pallet_alpha_assets::AlphaAssetsInterface; use subtensor_runtime_common::{NetUid, NetUidStorageIndex, clear_prefix_with_meter}; use subtensor_swap_interface::SwapHandler; /// Enum for the dissolve cleanup phase. @@ -65,6 +66,10 @@ pub enum DissolveCleanupPhase { /// possible after that. Appended (not inserted) so in-flight cleanup discriminants /// stay stable. NetworkPendingBasketDeposits, + /// Phase 5.14: Clear generation-scoped alpha-asset counters after every operation + /// that can recycle alpha has completed. Appended so existing in-flight cleanup + /// discriminants remain stable. + NetworkAlphaAssetCounters, } impl Default for DissolveCleanupPhase { @@ -1046,14 +1051,25 @@ impl Pallet { status.last_key.clone(), ); - // if all phases are done, remove the network from the dissolved networks list and emit the event if done { - cleanup_completed = true; + status.set_phase(DissolveCleanupPhase::NetworkAlphaAssetCounters); + status.last_key = None; } else { status.last_key = new_key; } done } + DissolveCleanupPhase::NetworkAlphaAssetCounters => { + let clear_weight = T::DbWeight::get().writes(3); + if !weight_meter.can_consume(clear_weight) { + false + } else { + weight_meter.consume(clear_weight); + T::AlphaAssets::clear_alpha_counters(netuid); + cleanup_completed = true; + true + } + } }; phase_done = done; diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index ad97baac3e..eb4c3940a7 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -1,5 +1,6 @@ use super::*; use frame_support::PalletId; +use pallet_alpha_assets::AlphaAssetsInterface; use safe_math::FixedExt; use sp_core::Get; use sp_runtime::{SaturatedConversion, traits::AccountIdConversion}; @@ -320,6 +321,12 @@ impl Pallet { Self::unlock_network_registration_cost(coldkey, lock_id)?; } + // A netuid identifies a new alpha asset generation after reuse. Clear any + // legacy counters defensively before creating that generation; normal + // dissolution cleanup performs the same reset after its last recycle. + T::AlphaAssets::clear_alpha_counters(netuid_to_register); + weight.saturating_accrue(db_weight.writes(3)); + let default_tempo = DefaultTempo::::get(); weight.saturating_accrue(db_weight.reads(1)); diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index 212d78a6ae..e2cc43ce6c 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -6611,35 +6611,25 @@ fn test_migrate_reset_emission_gate_bar() { } #[test] -fn test_rao_alpha_out_corrections_match_csv() { +fn test_rao_alpha_out_corrections_are_generation_safe() { use crate::migrations::migrate_fix_rao_alpha_out_accounting::ALPHA_OUT_CORRECTIONS; - let csv = include_str!("../../../../rao-double-dividend-subnet-alpha-out-correction.csv"); - let mut lines = csv.lines(); - let header: Vec<&str> = lines.next().expect("CSV header").split(',').collect(); - let netuid_index = header.iter().position(|name| *name == "netuid").unwrap(); - let amount_index = header - .iter() - .position(|name| *name == "required_adjustment_rao") - .unwrap(); - let apply_index = header - .iter() - .position(|name| *name == "apply_to_current_generation") - .unwrap(); - let from_csv: Vec<(u16, u64)> = lines - .map(|line| line.split(',').collect::>()) - .filter(|columns| columns[apply_index] == "true") - .map(|columns| { - ( - columns[netuid_index].parse().unwrap(), - columns[amount_index].parse().unwrap(), - ) - }) - .collect(); + const RECYCLED_NETUIDS: &[u16] = &[15, 16, 26, 31, 36, 38, 40, 47, 49, 57, 58]; - assert_eq!(ALPHA_OUT_CORRECTIONS, from_csv.as_slice()); + assert_eq!(ALPHA_OUT_CORRECTIONS.len(), 56); + assert!( + ALPHA_OUT_CORRECTIONS + .windows(2) + .all(|entries| entries[0].0 < entries[1].0), + "correction netuids must be unique and sorted" + ); + assert!(RECYCLED_NETUIDS.iter().all(|recycled| { + !ALPHA_OUT_CORRECTIONS + .iter() + .any(|(netuid, _)| netuid == recycled) + })); assert_eq!( - from_csv + ALPHA_OUT_CORRECTIONS .iter() .map(|(_, correction)| u128::from(*correction)) .sum::(), @@ -6668,7 +6658,7 @@ fn test_migrate_fix_rao_alpha_out_accounting() { for &(netuid, correction) in ALPHA_OUT_CORRECTIONS { assert_eq!( SubnetAlphaOut::::get(NetUid::from(netuid)), - AlphaBalance::from(correction + 10) + AlphaBalance::from(correction.saturating_add(10)) ); } @@ -6677,7 +6667,7 @@ fn test_migrate_fix_rao_alpha_out_accounting() { for &(netuid, correction) in ALPHA_OUT_CORRECTIONS { assert_eq!( SubnetAlphaOut::::get(NetUid::from(netuid)), - AlphaBalance::from(correction + 10) + AlphaBalance::from(correction.saturating_add(10)) ); } }); @@ -6703,6 +6693,95 @@ fn test_migrate_fix_rao_alpha_out_accounting_skips_non_mainnet() { }); } +#[test] +fn test_migrate_rebases_recycled_alpha_counters_by_generation() { + use crate::migrations::migrate_rebase_recycled_alpha_asset_counters::{ + MIGRATION_NAME, RECYCLED_ALPHA_COUNTER_OFFSETS, + migrate_rebase_recycled_alpha_asset_counters, + }; + + new_test_ext(1).execute_with(|| { + let mainnet_genesis = + hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); + frame_system::BlockHash::::insert(0_u64, H256::from_slice(&mainnet_genesis)); + + for &(netuid, registered_at, issuance, burned, recycled) in RECYCLED_ALPHA_COUNTER_OFFSETS { + let netuid = NetUid::from(netuid); + NetworkRegisteredAt::::insert(netuid, registered_at); + pallet_alpha_assets::TotalAlphaIssuance::::insert( + netuid, + AlphaBalance::from(issuance.saturating_add(11)), + ); + pallet_alpha_assets::AlphaBurned::::insert( + netuid, + AlphaBalance::from(burned.saturating_add(22)), + ); + pallet_alpha_assets::AlphaRecycled::::insert( + netuid, + AlphaBalance::from(recycled.saturating_add(33)), + ); + } + + let weight = migrate_rebase_recycled_alpha_asset_counters::(); + assert!(!weight.is_zero()); + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + + for &(netuid, _, _, _, _) in RECYCLED_ALPHA_COUNTER_OFFSETS { + let netuid = NetUid::from(netuid); + assert_eq!( + pallet_alpha_assets::TotalAlphaIssuance::::get(netuid), + AlphaBalance::from(11) + ); + assert_eq!( + pallet_alpha_assets::AlphaBurned::::get(netuid), + AlphaBalance::from(22) + ); + assert_eq!( + pallet_alpha_assets::AlphaRecycled::::get(netuid), + AlphaBalance::from(33) + ); + } + + // The run-once guard preserves the already-rebased values. + migrate_rebase_recycled_alpha_asset_counters::(); + for &(netuid, _, _, _, _) in RECYCLED_ALPHA_COUNTER_OFFSETS { + assert_eq!( + pallet_alpha_assets::AlphaBurned::::get(NetUid::from(netuid)), + AlphaBalance::from(22) + ); + } + }); +} + +#[test] +fn test_migrate_recycled_alpha_counters_skips_wrong_generation() { + use crate::migrations::migrate_rebase_recycled_alpha_asset_counters::{ + RECYCLED_ALPHA_COUNTER_OFFSETS, migrate_rebase_recycled_alpha_asset_counters, + }; + + new_test_ext(1).execute_with(|| { + let mainnet_genesis = + hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); + frame_system::BlockHash::::insert(0_u64, H256::from_slice(&mainnet_genesis)); + + let (netuid, registered_at, _, burned, _) = RECYCLED_ALPHA_COUNTER_OFFSETS[0]; + let netuid = NetUid::from(netuid); + NetworkRegisteredAt::::insert(netuid, registered_at.saturating_add(1)); + pallet_alpha_assets::AlphaBurned::::insert( + netuid, + AlphaBalance::from(burned.saturating_add(99)), + ); + + migrate_rebase_recycled_alpha_asset_counters::(); + + assert_eq!( + pallet_alpha_assets::AlphaBurned::::get(netuid), + AlphaBalance::from(burned.saturating_add(99)), + "an offset must never be applied to a different subnet generation" + ); + }); +} + #[test] fn test_storage_bloat_cleanup_is_bounded_and_preserves_nonzero_state() { use crate::migrations::migrate_storage_bloat_v2::{ diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 2aafcaf9fc..1fd5dffb93 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -3286,6 +3286,57 @@ fn registered_subnet_counter_survives_dissolve_and_bumps_on_reregistration() { }); } +#[test] +fn alpha_asset_counters_do_not_cross_subnet_generations() { + new_test_ext(1).execute_with(|| { + SubtensorModule::set_max_subnets(2); + + let owner_cold = U256::from(200); + let owner_hot = U256::from(201); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + AlphaAssets::mint_alpha(netuid, AlphaBalance::from(100)); + AlphaAssets::burn_alpha(netuid, AlphaBalance::from(30)); + AlphaAssets::recycle_alpha(netuid, AlphaBalance::from(20)); + assert_eq!( + pallet_alpha_assets::AlphaBurned::::get(netuid), + AlphaBalance::from(30) + ); + + assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + run_block_idle(); + + assert!(!pallet_alpha_assets::TotalAlphaIssuance::::contains_key(netuid)); + assert!(!pallet_alpha_assets::AlphaBurned::::contains_key( + netuid + )); + assert!(!pallet_alpha_assets::AlphaRecycled::::contains_key( + netuid + )); + + // Simulate counters left by a legacy cleanup. Registration is a second + // generation boundary and must clear them before reusing the netuid. + pallet_alpha_assets::TotalAlphaIssuance::::insert(netuid, AlphaBalance::from(1_000)); + pallet_alpha_assets::AlphaBurned::::insert(netuid, AlphaBalance::from(900)); + pallet_alpha_assets::AlphaRecycled::::insert(netuid, AlphaBalance::from(800)); + + let reused_netuid = add_dynamic_network(&owner_hot, &owner_cold); + assert_eq!(reused_netuid, netuid); + assert_eq!( + pallet_alpha_assets::TotalAlphaIssuance::::get(netuid), + AlphaBalance::ZERO + ); + assert_eq!( + pallet_alpha_assets::AlphaBurned::::get(netuid), + AlphaBalance::ZERO + ); + assert_eq!( + pallet_alpha_assets::AlphaRecycled::::get(netuid), + AlphaBalance::ZERO + ); + }); +} + #[test] fn dissolve_clears_voting_power_state_before_netuid_reuse() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/tests/remove_data_tests.rs b/pallets/subtensor/src/tests/remove_data_tests.rs index 9253662c38..7ebfeaf77f 100644 --- a/pallets/subtensor/src/tests/remove_data_tests.rs +++ b/pallets/subtensor/src/tests/remove_data_tests.rs @@ -170,11 +170,15 @@ fn test_remove_data_for_dissolved_networks_all_phases() { PendingBasketDeposits::::get(owner_hot, other_netuid), AlphaBalance::from(777) ); - // Issuance was conserved via recycle (not a silent delete of earned alpha). - assert_eq!( - pallet_alpha_assets::TotalAlphaIssuance::::get(netuid), - issuance_before - ); + // The queued credit is recycled before the completed generation is dropped; + // no alpha-asset counter from it may survive netuid reuse. + assert!(!pallet_alpha_assets::TotalAlphaIssuance::::contains_key(netuid)); + assert!(!pallet_alpha_assets::AlphaBurned::::contains_key( + netuid + )); + assert!(!pallet_alpha_assets::AlphaRecycled::::contains_key( + netuid + )); }); } From d81f1589827a1f3d683896962a4a25e05afd83b6 Mon Sep 17 00:00:00 2001 From: UnarbosFour Date: Tue, 11 Aug 2026 17:52:27 -0400 Subject: [PATCH 3/7] build error --- docs/plans/alpha-imbalance.md | 232 ++++++++++++++++++++++++++++++++++ docs/plans/locks.md | 0 precompiles/src/subnet.rs | 2 + 3 files changed, 234 insertions(+) create mode 100644 docs/plans/alpha-imbalance.md create mode 100644 docs/plans/locks.md diff --git a/docs/plans/alpha-imbalance.md b/docs/plans/alpha-imbalance.md new file mode 100644 index 0000000000..4c597b4c4a --- /dev/null +++ b/docs/plans/alpha-imbalance.md @@ -0,0 +1,232 @@ +--- +title: Alpha imbalance accounting +description: Plan for linear alpha imbalance accounting across issuance and supply-moving paths. +--- + +# Alpha imbalance accounting + +## Goal + +Use linear alpha imbalances across issuance and supply-moving paths in the same +way TAO uses credits. Minted alpha must exist as a short-lived credit until it +is resolved into an accounting bucket or explicitly recycled. This makes +unallocated issuance visible in the type system and prevents silent supply +drift. + +This work does not change root stake accounting. Root stake is TAO-backed and +is outside the per-subnet alpha-assets system. + +## Accounting contract + +At transaction and block boundaries, every live non-root subnet should satisfy: + +```text +TotalAlphaIssuance[netuid] + = SubnetAlphaIn[netuid] + + SubnetAlphaOut[netuid] + + BalancerAlphaReservoir[netuid] +``` + +While an imbalance is alive, it represents the temporary difference: + +```text +TotalAlphaIssuance + = stored alpha + outstanding credits - outstanding debts +``` + +The existing burn semantics are preserved: + +- `AlphaBurned` is cumulative telemetry and a logical subdivision of + `SubnetAlphaOut`; burning does not reduce issuance. +- `AlphaRecycled` is cumulative telemetry for alpha removed from issuance. +- Recycling reduces `TotalAlphaIssuance`. + +All new accounting operations should use checked arithmetic. Saturation must +not conceal an accounting underflow or overflow. + +## Current gaps + +The current alpha imbalance types are not suitable as conservation resources: + +- They implement `Clone` and `Copy`, so a value can be duplicated. +- They can be dropped without finalizing their issuance. +- Their constructors are public. +- They implement a single-currency imbalance trait even though alpha is a + multi-asset system keyed by `netuid`. +- Infallible `merge` and `offset` operations log a netuid mismatch and discard + one side. +- Coinbase currently ignores the pool-side mint record. +- Subnet genesis and registration directly initialize alpha buckets. +- The alpha-assets issuance map is not yet the canonical value used by + `get_alpha_issuance`. + +## Phase 1: Linear imbalance primitive + +Replace the copyable records with a FRAME-style, multi-asset imbalance: + +- Mark it `#[must_use]`. +- Remove `Clone`, `Copy`, codec, metadata, and frozen-structure derives. +- Keep constructors private to alpha-assets. +- Include `netuid` and amount in every imbalance. +- Add drop handlers. Dropping an unresolved credit decreases issuance and + records the amount as recycled; dropping a debt increases issuance. +- Make `merge`, `subsume`, and `offset` fallible when netuids differ. +- Return both original values on a failed combination. +- Add an associated credit type to `AlphaAssetsInterface`. +- Add an explicit settlement operation that consumes an imbalance without + invoking its drop handler after a destination has accepted the alpha. + +For compatibility during the staged rollout, raw `burn_alpha(netuid, amount)` +and `recycle_alpha(netuid, amount)` remain temporarily available. They are +removed after source balances can be withdrawn into credits in later phases. + +No imbalance may be stored on chain or exposed through runtime metadata. + +## Phase 2: Central resolution layer + +Add the alpha equivalent of `spend_tao` in the subtensor pallet: + +```text +resolve_to_alpha_in(credit, amount) +resolve_to_alpha_out(credit, amount) +resolve_to_alpha_reservoir(credit, amount) + +withdraw_from_alpha_in(netuid, amount) -> credit +withdraw_from_alpha_out(netuid, amount) -> credit +withdraw_from_alpha_reservoir(netuid, amount) -> credit +``` + +Each resolver must: + +1. Verify the credit's netuid. +2. Split only the requested amount. +3. update the destination using checked arithmetic. +4. Return the remainder on success. +5. Return the original credit on failure. + +Production code should stop directly mutating `SubnetAlphaIn` and +`SubnetAlphaOut` after this layer is available. + +## Phase 3: Coinbase and balancer reservoir + +Convert the two alpha emission streams: + +- Issue the participant emission as a credit and resolve it into + `SubnetAlphaOut`. +- Issue the pool emission as a credit and divide it between active + `SubnetAlphaIn` and `BalancerAlphaReservoir`. + +Previously deferred reservoir alpha may become active alongside a new +emission. The correct flow is: + +1. Withdraw the old reservoir into a credit. +2. Merge it with the newly issued credit for the same netuid. +3. Resolve the price-active portion into `SubnetAlphaIn`. +4. Resolve the remainder back into the reservoir. + +Every error path must either return the credit to its caller or explicitly +recycle it. + +## Phase 4: Supply-moving operations + +Convert the remaining supply paths: + +- TAO-to-alpha swaps withdraw from `AlphaIn` and resolve into `AlphaOut`. +- Alpha-to-TAO swaps withdraw from `AlphaOut` and resolve into `AlphaIn`. +- Stake transfers withdraw and resolve within `AlphaOut`. +- Recycling withdraws a credit and drops or explicitly recycles it. +- Burning withdraws from an individual stake and resolves into the burned + subdivision of `AlphaOut`, preserving issuance. +- Collateral purchase splitting divides one alpha credit between locked stake + and burn. +- Protocol-owned alpha resolves into `AlphaOut` and its + `SubnetProtocolAlpha` subdivision. +- Basket escrow operations use the same withdrawal and resolution helpers. + +The main implementation sites are: + +- `pallets/subtensor/src/staking/stake_utils.rs` +- `pallets/subtensor/src/staking/recycle_alpha.rs` +- `pallets/subtensor/src/subnets/collateral.rs` +- `pallets/subtensor/src/staking/basket_flush.rs` +- `pallets/subtensor/src/staking/claim_root.rs` + +## Phase 5: Genesis, registration, and dissolution + +New subnet pool alpha must be issued and resolved into `SubnetAlphaIn`, not +inserted independently of alpha-assets. + +Subnet dissolution must: + +1. Withdraw `SubnetAlphaIn`, `SubnetAlphaOut`, and reservoir alpha. +2. Merge and recycle the aggregate credits exactly once. +3. Clear subordinate stake, basket, collateral, and protocol records without + recycling them again. +4. Reset alpha-assets state for the dissolved netuid according to the netuid + reuse policy. + +Genesis builders, benchmark helpers, and test setup utilities must initialize +alpha-assets issuance whenever they seed alpha buckets. + +## Phase 6: Migration and canonical issuance + +Before making the alpha-assets map authoritative, run a storage-versioned +migration over live non-root subnets: + +```text +issuance = AlphaIn + AlphaOut + BalancerAlphaReservoir +``` + +The migration must overwrite the alpha-assets value rather than increment it, +because existing reserve and genesis alpha was not necessarily created through +alpha-assets. + +The migration requires: + +- `pre_upgrade` snapshots of the three accounting buckets. +- A benchmarked bound based on the maximum/live subnet count. +- `post_upgrade` checks for every migrated netuid. +- An explicit policy for stale alpha-assets entries belonging to dissolved + netuids. + +After the migration and all production mutations use the resolution layer, +`get_alpha_issuance` can read +`AlphaAssets::total_alpha_issuance(netuid)` directly. + +## Tests and rollout gates + +### Primitive tests + +- Credits and debts cannot be copied or cloned. +- Dropping an unresolved issued credit reverses issuance. +- Settling a credit preserves issuance. +- Split, extract, merge, subsume, and offset conserve the amount. +- Cross-netuid combinations fail and return both values unchanged. +- Zero imbalances do not touch storage. +- Overflow and underflow cannot be hidden by saturation. + +### Integration tests + +- Coinbase resolves every issued rao. +- Deferred reservoir alpha can become active later without duplication. +- Stake and unstake round trips preserve issuance. +- Burn preserves issuance and recycle reduces it. +- Subnet registration initializes issuance correctly. +- Dissolution leaves zero issuance without double recycling. +- Basket, collateral, protocol-owned alpha, and transaction rollback paths + preserve the boundary invariant. + +### Runtime rollout + +1. Ship the hardened primitive without changing economic calculations. +2. Run the alpha-assets tracker in shadow mode and compare it with the derived + bucket total. +3. Convert coinbase and reservoir handling. +4. Convert swaps, staking, burn, recycle, collateral, and basket paths. +5. Run the reconciliation migration. +6. Make alpha-assets issuance canonical. +7. Remove legacy raw amount APIs and add a CI guard against direct production + mutations of alpha accounting buckets. + +Weights must be regenerated for any extrinsic or hook whose storage accesses +change. diff --git a/docs/plans/locks.md b/docs/plans/locks.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/precompiles/src/subnet.rs b/precompiles/src/subnet.rs index 0f4468ca0c..e138c39241 100644 --- a/precompiles/src/subnet.rs +++ b/precompiles/src/subnet.rs @@ -1322,6 +1322,7 @@ fn dissolution_cleanup_phase_code(phase: &DissolveCleanupPhase) -> u8 { DissolveCleanupPhase::NetworkDecayingLock => 23, DissolveCleanupPhase::SubnetBasketHoldingsToRoot => 24, DissolveCleanupPhase::NetworkPendingBasketDeposits => 25, + DissolveCleanupPhase::NetworkAlphaAssetCounters => 26, } } @@ -2019,6 +2020,7 @@ mod tests { (DissolveCleanupPhase::NetworkDecayingLock, 23), (DissolveCleanupPhase::SubnetBasketHoldingsToRoot, 24), (DissolveCleanupPhase::NetworkPendingBasketDeposits, 25), + (DissolveCleanupPhase::NetworkAlphaAssetCounters, 26), ]; for (phase, expected) in phases { From 03a7e090ada630d9556e67059ee0d8b528afdfe7 Mon Sep 17 00:00:00 2001 From: UnarbosFour Date: Wed, 12 Aug 2026 12:15:33 -0400 Subject: [PATCH 4/7] Add historical reconstruction of alpha burned --- pallets/subtensor/src/macros/hooks.rs | 3 + ...igrate_backfill_historical_alpha_burned.rs | 202 ++++++++++++++++++ pallets/subtensor/src/migrations/mod.rs | 1 + pallets/subtensor/src/tests/migration.rs | 124 +++++++++++ 4 files changed, 330 insertions(+) create mode 100644 pallets/subtensor/src/migrations/migrate_backfill_historical_alpha_burned.rs diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index fbe9b7222f..5c3f7c00a9 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -206,6 +206,9 @@ mod hooks { // Remove prior-generation alpha-asset counter offsets from recycled mainnet // netuids without discarding burns accumulated by their current generations. .saturating_add(migrations::migrate_rebase_recycled_alpha_asset_counters::migrate_rebase_recycled_alpha_asset_counters::()) + // Add pre-tracking burns to the generation-scoped AlphaBurned counters. This + // follows both AlphaOut repair and recycled-generation counter rebasing. + .saturating_add(migrations::migrate_backfill_historical_alpha_burned::migrate_backfill_historical_alpha_burned::()) // Schedule the large storage-GC sweep. Actual work is bounded by the remaining // on_idle weight over subsequent blocks. .saturating_add(migrations::migrate_storage_bloat_v2::kickoff_storage_bloat_cleanup::()); diff --git a/pallets/subtensor/src/migrations/migrate_backfill_historical_alpha_burned.rs b/pallets/subtensor/src/migrations/migrate_backfill_historical_alpha_burned.rs new file mode 100644 index 0000000000..5f228862c0 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_backfill_historical_alpha_burned.rs @@ -0,0 +1,202 @@ +use super::*; +use frame_support::traits::Get; +use frame_system::pallet_prelude::BlockNumberFor; +use pallet_alpha_assets::AlphaAssetsInterface; +use scale_info::prelude::string::String; +use sp_runtime::traits::Zero; +use subtensor_runtime_common::{AlphaBalance, NetUid}; + +pub(crate) const MIGRATION_NAME: &[u8] = b"migrate_backfill_historical_alpha_burned"; + +/// `(netuid, generation_registered_at, historical_burned_alpha)` reconstructed at +/// mainnet block 8,283,784, immediately before `AlphaBurned` tracking began. +/// +/// Before that deployment, explicit alpha burns and miner incentives withheld from +/// owner-associated UIDs removed claims without recording a counter. The amounts are +/// the conservation remainder: +/// +/// `SubnetAlphaOut - TotalHotkeyAlpha - all pending alpha claims` +/// +/// plus the matching `SubnetAlphaOut` correction applied earlier in this upgrade. +/// The reconstruction scanned blocks 4,920,351 through 8,283,784 to verify every burn +/// path and was checked against the new counter after its deployment. Only generations +/// still live at scan block 8,780,303 are embedded; the registration-block guard also +/// prevents applying a row if its netuid is recycled before this migration executes. +pub(crate) const HISTORICAL_ALPHA_BURNED: &[(u16, u64, u64)] = &[ + (1, 1_497_824, 661_707_044_125_477), + (2, 2_734_060, 448_755_318_641_353), + (3, 4_165_565, 578_474_866_822_790), + (4, 1_411_451, 539_567_236_063_726), + (5, 2_491_604, 1_278_433_691_163_126), + (6, 3_219_949, 161_892_134_501_551), + (7, 2_627_691, 36_084_978_602_376), + (8, 1_477_264, 609_870_070_120_154), + (9, 1_489_797, 944_559_342_501_061), + (10, 2_869_647, 495_108_691_195_807), + (11, 2_918_568, 1_087_091_829_288_036), + (12, 2_256_433, 1_299_624_537_206_613), + (13, 1_907_637, 444_081_197_134_941), + (14, 4_848_444, 960_182_196_930_445), + (15, 7_366_897, 301_749_291_195_706), + (17, 2_840_556, 305_407_983_298_724), + (18, 1_604_679, 245_065_757_998_180), + (19, 1_956_072, 801_324_778_843_975), + (20, 1_970_929, 1_094_173_011_650_198), + (21, 3_156_578, 650_713_208_209_082), + (22, 2_009_702, 1_019_276_438_426_431), + (23, 2_063_528, 967_233_605_317_608), + (24, 2_538_424, 505_685_726_352_258), + (25, 2_998_801, 648_845_379_719_658), + (26, 8_123_781, 17_606_271_531_596), + (27, 1_727_132, 1_001_642_315_084_892), + (28, 4_878_363, 1_203_330_815_035_081), + (29, 3_379_782, 610_829_352_749_762), + (30, 3_250_216, 486_388_020_387_918), + (31, 7_173_591, 437_000_151_331_724), + (32, 2_515_294, 17_419_272_873_098), + (33, 2_943_950, 494_543_923_300_457), + (34, 3_493_948, 359_469_273_754_254), + (35, 3_037_158, 266_886_343_998_939), + (36, 7_894_898, 121_987_667_067_834), + (37, 3_212_175, 985_435_365_945_143), + (38, 7_284_230, 345_810_821_435_472), + (39, 3_280_104, 1_287_420_117_604_175), + (41, 3_394_182, 769_681_094_468_115), + (42, 3_613_591, 206_796_478_056_192), + (43, 3_408_582, 678_317_244_908_562), + (44, 3_550_319, 423_455_578_235_596), + (45, 3_633_154, 834_928_101_300_642), + (46, 3_919_107, 780_421_061_400_320), + (47, 7_340_355, 366_716_413_649_807), + (48, 3_856_677, 695_492_461_656_953), + (49, 6_783_158, 573_769_022_103_814), + (50, 4_763_204, 383_336_277_470_646), + (51, 3_966_206, 1_065_224_008_905_985), + (52, 3_989_825, 853_315_981_760_243), + (53, 4_203_869, 355_141_382_485_346), + (54, 4_742_549, 458_319_381_232_759), + (55, 4_703_386, 306_585_844_101_300), + (56, 4_312_927, 627_232_573_084_813), + (57, 8_057_320, 93_750_241_023_900), + (59, 4_401_833, 369_047_970_107_529), + (60, 4_796_992, 507_498_715_394_405), + (61, 4_457_976, 694_963_647_168_562), + (62, 4_474_225, 183_144_981_369_889), + (63, 4_885_578, 725_439_396_179_942), + (64, 4_531_295, 223_211_670_756_295), + (65, 4_950_813, 735_211_819_505_774), + (66, 4_958_013, 817_890_833_634_185), + (67, 7_236_936, 181_452_933_284_774), + (68, 4_972_413, 629_386_445_545_985), + (69, 8_138_182, 57_832_819_758_834), + (70, 7_787_562, 132_569_838_883_663), + (71, 5_048_438, 779_562_258_823_670), + (72, 5_064_327, 161_494_057_515_857), + (73, 5_160_047, 1_245_107_989_914_506), + (74, 5_086_205, 531_791_422_968_235), + (75, 5_102_795, 581_181_547_138_717), + (76, 7_574_784, 296_127_405_249_152), + (77, 5_128_460, 140_886_896_551_370), + (78, 7_966_145, 58_070_016_871_684), + (79, 5_173_967, 380_435_716_169_563), + (80, 7_151_800, 47_626_242_449_288), + (81, 5_203_057, 830_779_100_179_480), + (82, 8_026_517, 50_318_415_917_529), + (83, 5_231_190, 345_172_824_104_195), + (84, 8_085_297, 39_094_022_971_873), + (85, 5_258_781, 395_120_544_109_912), + (87, 7_208_725, 423_704_834_120_304), + (88, 5_299_805, 132_836_998_007_560), + (89, 5_313_364, 416_002_458_400_820), + (91, 7_633_645, 238_851_859_703_207), + (93, 5_370_681, 903_913_959_207_004), + (94, 6_962_737, 463_616_359_168_619), + (95, 5_403_674, 752_120_660_373_289), + (96, 7_692_872, 93_094_905_548_345), + (97, 7_735_450, 46_566_942_010_408), + (98, 5_445_992, 1_079_336_385_082_409), + (100, 6_693_448, 458_140_544_083_257), + (101, 5_481_350, 709_791_313_021_521), + (102, 7_840_965, 67_558_146_161_703), + (104, 5_528_520, 340_321_172_598_343), + (105, 6_841_399, 427_306_744_552_955), + (106, 5_558_480, 694_428_357_403_395), + (107, 7_457_580, 314_712_961_241_019), + (108, 7_105_263, 397_895_877_171_947), + (109, 7_257_480, 403_221_764_503_845), + (110, 5_606_062, 653_602_202_903_777), + (111, 5_615_562, 494_642_104_691_108), + (112, 5_633_022, 752_495_769_344_929), + (113, 7_119_664, 459_198_730_858_941), + (114, 7_312_241, 319_277_386_475_235), + (115, 5_683_635, 660_512_253_515_748), + (117, 5_710_859, 289_424_602_021_989), + (118, 5_724_794, 164_626_458_446_094), + (119, 5_740_660, 649_075_554_396_218), + (120, 5_749_344, 170_312_526_711_417), + (121, 5_766_528, 1_023_076_320_965_598), + (122, 8_238_082, 22_766_972_908_103), + (123, 5_794_330, 149_823_723_964_164), + (124, 5_813_454, 804_691_305_771_930), + (125, 5_834_408, 987_462_933_759_858), + (126, 7_525_773, 288_254_403_748_941), + (127, 5_848_837, 950_782_986_715_338), + (128, 5_856_038, 186_014_794_503_201), +]; + +/// Adds the pre-tracking mainnet burn remainder to `AlphaAssets::AlphaBurned`. +pub fn migrate_backfill_historical_alpha_burned() -> Weight { + let migration_name = MIGRATION_NAME.to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{}' already executed - skipping", + String::from_utf8_lossy(MIGRATION_NAME) + ); + return weight; + } + + let genesis_hash = frame_system::Pallet::::block_hash(BlockNumberFor::::zero()); + weight.saturating_accrue(T::DbWeight::get().reads(1)); + let mainnet_genesis = + hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); + let mut corrected = 0u64; + + if genesis_hash.as_ref() == mainnet_genesis { + for &(netuid, expected_registered_at, historical_burned) in HISTORICAL_ALPHA_BURNED { + let netuid = NetUid::from(netuid); + let registered_at = NetworkRegisteredAt::::get(netuid); + weight.saturating_accrue(T::DbWeight::get().reads(1)); + + if registered_at != expected_registered_at { + log::warn!( + "Skipping historical alpha burn for netuid {:?}: registered_at={}, expected={}", + netuid, + registered_at, + expected_registered_at, + ); + continue; + } + + let _ = T::AlphaAssets::burn_alpha(netuid, AlphaBalance::from(historical_burned)); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + corrected = corrected.saturating_add(1); + } + } else { + log::info!( + "Migration '{}' skipped outside mainnet", + String::from_utf8_lossy(MIGRATION_NAME) + ); + } + + HasMigrationRun::::insert(&migration_name, true); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{}' completed with {} historical alpha burn corrections", + String::from_utf8_lossy(MIGRATION_NAME), + corrected, + ); + weight +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index d132973877..d9ba840018 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -6,6 +6,7 @@ use sp_io::hashing::twox_128; use sp_io::storage::clear_prefix; pub mod migrate_associated_evm_address_index; pub mod migrate_auto_stake_destination; +pub mod migrate_backfill_historical_alpha_burned; pub mod migrate_cleanup_swap_v3; pub mod migrate_clear_deprecated_registration_maps; pub mod migrate_clear_orphan_subnet_identities_v3; diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index e2cc43ce6c..5c64be92d8 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -6782,6 +6782,130 @@ fn test_migrate_recycled_alpha_counters_skips_wrong_generation() { }); } +#[test] +fn test_migrate_backfills_historical_alpha_burns_by_generation() { + use crate::migrations::migrate_backfill_historical_alpha_burned::{ + HISTORICAL_ALPHA_BURNED, MIGRATION_NAME, migrate_backfill_historical_alpha_burned, + }; + + new_test_ext(1).execute_with(|| { + let mainnet_genesis = + hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); + frame_system::BlockHash::::insert(0_u64, H256::from_slice(&mainnet_genesis)); + + for &(netuid, registered_at, _) in HISTORICAL_ALPHA_BURNED { + let netuid = NetUid::from(netuid); + NetworkRegisteredAt::::insert(netuid, registered_at); + pallet_alpha_assets::AlphaBurned::::insert(netuid, AlphaBalance::from(17_u64)); + pallet_alpha_assets::TotalAlphaIssuance::::insert( + netuid, + AlphaBalance::from(31_u64), + ); + pallet_alpha_assets::AlphaRecycled::::insert(netuid, AlphaBalance::from(43_u64)); + } + + let weight = migrate_backfill_historical_alpha_burned::(); + assert!(!weight.is_zero()); + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + + for &(netuid, _, historical_burned) in HISTORICAL_ALPHA_BURNED { + let netuid = NetUid::from(netuid); + assert_eq!( + pallet_alpha_assets::AlphaBurned::::get(netuid), + AlphaBalance::from(historical_burned.saturating_add(17)), + ); + assert_eq!( + pallet_alpha_assets::TotalAlphaIssuance::::get(netuid), + AlphaBalance::from(31_u64), + ); + assert_eq!( + pallet_alpha_assets::AlphaRecycled::::get(netuid), + AlphaBalance::from(43_u64), + ); + } + + migrate_backfill_historical_alpha_burned::(); + for &(netuid, _, historical_burned) in HISTORICAL_ALPHA_BURNED { + assert_eq!( + pallet_alpha_assets::AlphaBurned::::get(NetUid::from(netuid)), + AlphaBalance::from(historical_burned.saturating_add(17)), + ); + } + }); +} + +#[test] +fn test_historical_alpha_burn_reconstruction_is_complete_and_canonical() { + use crate::migrations::migrate_backfill_historical_alpha_burned::HISTORICAL_ALPHA_BURNED; + + assert_eq!(HISTORICAL_ALPHA_BURNED.len(), 119); + assert!( + HISTORICAL_ALPHA_BURNED + .windows(2) + .all(|rows| rows[0].0 < rows[1].0), + "netuids must be unique and sorted" + ); + assert!( + HISTORICAL_ALPHA_BURNED + .iter() + .all(|&(_, registered_at, burned)| registered_at > 0 && burned > 0) + ); + assert_eq!( + HISTORICAL_ALPHA_BURNED + .iter() + .map(|&(_, _, burned)| burned) + .sum::(), + 61_723_043_979_842_021, + ); +} + +#[test] +fn test_migrate_historical_alpha_burns_skips_wrong_generation() { + use crate::migrations::migrate_backfill_historical_alpha_burned::{ + HISTORICAL_ALPHA_BURNED, migrate_backfill_historical_alpha_burned, + }; + + new_test_ext(1).execute_with(|| { + let mainnet_genesis = + hex_literal::hex!("2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03"); + frame_system::BlockHash::::insert(0_u64, H256::from_slice(&mainnet_genesis)); + + let (netuid, registered_at, _) = HISTORICAL_ALPHA_BURNED[0]; + let netuid = NetUid::from(netuid); + NetworkRegisteredAt::::insert(netuid, registered_at.saturating_add(1)); + pallet_alpha_assets::AlphaBurned::::insert(netuid, AlphaBalance::from(99_u64)); + + migrate_backfill_historical_alpha_burned::(); + + assert_eq!( + pallet_alpha_assets::AlphaBurned::::get(netuid), + AlphaBalance::from(99_u64), + ); + }); +} + +#[test] +fn test_migrate_historical_alpha_burns_skips_non_mainnet() { + use crate::migrations::migrate_backfill_historical_alpha_burned::{ + HISTORICAL_ALPHA_BURNED, MIGRATION_NAME, migrate_backfill_historical_alpha_burned, + }; + + new_test_ext(1).execute_with(|| { + let (netuid, registered_at, _) = HISTORICAL_ALPHA_BURNED[0]; + let netuid = NetUid::from(netuid); + NetworkRegisteredAt::::insert(netuid, registered_at); + pallet_alpha_assets::AlphaBurned::::insert(netuid, AlphaBalance::from(77_u64)); + + migrate_backfill_historical_alpha_burned::(); + + assert_eq!( + pallet_alpha_assets::AlphaBurned::::get(netuid), + AlphaBalance::from(77_u64), + ); + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + }); +} + #[test] fn test_storage_bloat_cleanup_is_bounded_and_preserves_nonzero_state() { use crate::migrations::migrate_storage_bloat_v2::{ From 27862a72c006d89787d78655023ca0f0414dee82 Mon Sep 17 00:00:00 2001 From: UnArbosSix Date: Wed, 12 Aug 2026 13:43:49 -0700 Subject: [PATCH 5/7] reduce bloat (#3076) * reduce bloat * bump spec * fix ci --- .github/workflows/check-rust.yml | 2 +- pallets/commitments/src/lib.rs | 5 ++ pallets/commitments/src/tests.rs | 39 ++++++++++++++ .../subtensor/src/coinbase/reveal_commits.rs | 7 +++ pallets/subtensor/src/tests/weights.rs | 54 ++++++++++++++++++- runtime/src/lib.rs | 2 +- sdk/bittensor-core-wasm/tests/smoke.mjs | 2 +- 7 files changed, 106 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check-rust.yml b/.github/workflows/check-rust.yml index f2a64d5051..ea9b859a0f 100644 --- a/.github/workflows/check-rust.yml +++ b/.github/workflows/check-rust.yml @@ -332,7 +332,7 @@ jobs: wasm-pack --version - name: wasm smoke test (node) run: | - wasm-pack build sdk/bittensor-core-wasm --target nodejs --out-dir pkg-node + wasm-pack build sdk/bittensor-core-wasm --target nodejs --out-dir pkg-node --no-opt node sdk/bittensor-core-wasm/tests/smoke.mjs - name: Report wasm compiler cache if: always() diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 986330eba0..92604e5bef 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -473,6 +473,11 @@ impl Pallet { continue; }; + if commit.header.v.len() != 32 || commit.header.w.len() != 32 { + log::warn!("Invalid TLECiphertext header for {who:?}"); + continue; + } + let decrypted_bytes: Vec = tld::(commit, sig) .map_err(|e| { diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index fb001f99ae..3362e9b4b5 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -1,5 +1,6 @@ #![allow(clippy::expect_used, clippy::indexing_slicing)] +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use codec::Encode; use sp_std::prelude::*; use subtensor_runtime_common::{NetUid, TaoBalance}; @@ -21,6 +22,7 @@ use frame_support::{ weights::{Weight, constants::RocksDbWeight}, }; use frame_system::{Pallet as System, RawOrigin}; +use tle::{curves::drand::TinyBLS381, tlock::TLECiphertext}; fn purge_netuid_with_meter(netuid: NetUid, limit: Weight) -> bool { let mut weight_meter = frame_support::weights::WeightMeter::with_limit(limit); @@ -361,6 +363,43 @@ fn reveal_timelocked_commitment_cant_deserialize_ciphertext() { }); } +#[test] +fn reveal_timelocked_commitment_rejects_invalid_ciphertext_header() { + new_test_ext().execute_with(|| { + let who = 42; + let netuid = NetUid::from(9); + let reveal_round = 1000; + let ciphertext = produce_ciphertext(b"Some data", reveal_round); + let mut commit = TLECiphertext::::deserialize_compressed(&ciphertext[..]) + .expect("valid ciphertext"); + commit.header.v.clear(); + commit.header.w = vec![1]; + + let mut malformed = Vec::new(); + commit + .serialize_compressed(&mut malformed) + .expect("serialize malformed ciphertext"); + let data = Data::TimelockEncrypted { + encrypted: malformed.try_into().expect("ciphertext fits"), + reveal_round, + }; + let info = CommitmentInfo { + fields: BoundedVec::try_from(vec![data]).expect("one field fits"), + }; + + assert_ok!(Pallet::::set_commitment( + RuntimeOrigin::signed(who), + netuid, + Box::new(info), + )); + let sig = hex::decode(DRAND_QUICKNET_SIG_HEX).expect("valid signature hex"); + insert_drand_pulse(reveal_round, &sig); + + assert_ok!(Pallet::::reveal_timelocked_commitments()); + assert!(CommitmentOf::::get(netuid, who).is_none()); + }); +} + #[test] fn reveal_timelocked_commitment_bad_signature_skips_decryption() { new_test_ext().execute_with(|| { diff --git a/pallets/subtensor/src/coinbase/reveal_commits.rs b/pallets/subtensor/src/coinbase/reveal_commits.rs index a5cddd6856..720671669b 100644 --- a/pallets/subtensor/src/coinbase/reveal_commits.rs +++ b/pallets/subtensor/src/coinbase/reveal_commits.rs @@ -117,6 +117,13 @@ impl Pallet { } }; + if commit.header.v.len() != 32 || commit.header.w.len() != 32 { + log::trace!( + "Failed to reveal commit for mechanism {netuid_index} submitted by {who:?} due to invalid ciphertext header" + ); + continue; + } + let decrypted_bytes: Vec = match tld::( commit, sig, ) { diff --git a/pallets/subtensor/src/tests/weights.rs b/pallets/subtensor/src/tests/weights.rs index 23318eca4a..b0dae5da3e 100644 --- a/pallets/subtensor/src/tests/weights.rs +++ b/pallets/subtensor/src/tests/weights.rs @@ -22,9 +22,9 @@ use substrate_fixed::types::I32F32; use subtensor_runtime_common::NetUidStorageIndex; use tle::{ curves::drand::TinyBLS381, - ibe::fullident::Identity, + ibe::fullident::{Ciphertext as IBECiphertext, Identity}, stream_ciphers::AESGCMStreamCipherProvider, - tlock::{tld, tle}, + tlock::{TLECiphertext, tld, tle}, }; use w3f_bls::EngineBLS; @@ -4608,6 +4608,56 @@ fn test_reveal_crv3_commits_decryption_failure() { }); } +#[test] +fn test_reveal_crv3_commits_rejects_invalid_ciphertext_header() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let hotkey = U256::from(1); + let reveal_round = 1000; + + add_network(netuid, 5, 0); + register_ok_neuron(netuid, hotkey, U256::from(2), 100_000); + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); + + let public_key = hex::decode("83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a") + .expect("valid public key hex"); + let u = ::PublicKeyGroup::deserialize_compressed(&public_key[..]) + .expect("valid public key"); + let commit = TLECiphertext:: { + header: IBECiphertext { u, v: vec![], w: vec![1] }, + body: vec![], + cipher_suite: vec![], + }; + let mut commit_bytes = Vec::new(); + commit + .serialize_compressed(&mut commit_bytes) + .expect("serialize malformed ciphertext"); + + assert_ok!(SubtensorModule::do_commit_timelocked_weights( + RuntimeOrigin::signed(hotkey), + netuid, + commit_bytes.try_into().expect("ciphertext fits"), + reveal_round, + SubtensorModule::get_commit_reveal_weights_version(), + )); + step_epochs(1, netuid); + + let signature = hex::decode("b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39") + .expect("valid signature hex"); + pallet_drand::Pulses::::insert( + reveal_round, + Pulse { + round: reveal_round, + randomness: vec![0; 32].try_into().expect("randomness fits"), + signature: signature.try_into().expect("signature fits"), + }, + ); + + assert_ok!(SubtensorModule::reveal_crv3_commits_for_subnet(netuid)); + }); +} + // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_reveal_crv3_commits_multiple_commits_some_fail_some_succeed --exact --show-output --nocapture #[test] fn test_reveal_crv3_commits_multiple_commits_some_fail_some_succeed() { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 81cd941f2a..608621afaf 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -235,7 +235,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 444, + spec_version: 445, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/sdk/bittensor-core-wasm/tests/smoke.mjs b/sdk/bittensor-core-wasm/tests/smoke.mjs index f68a6ac35a..b49a31b6c9 100644 --- a/sdk/bittensor-core-wasm/tests/smoke.mjs +++ b/sdk/bittensor-core-wasm/tests/smoke.mjs @@ -1,6 +1,6 @@ // Node smoke test for the wasm binding, driven by the same golden fixtures // the Python SDK tests use. Build first: -// wasm-pack build sdk/bittensor-core-wasm --target nodejs --out-dir pkg-node +// wasm-pack build sdk/bittensor-core-wasm --target nodejs --out-dir pkg-node --no-opt // Then: node sdk/bittensor-core-wasm/tests/smoke.mjs import { readFileSync } from "node:fs"; From d3f40e44bda9019c606aeb0c907bb52ba7fe386c Mon Sep 17 00:00:00 2001 From: UnArbosSix Date: Wed, 12 Aug 2026 13:44:55 -0700 Subject: [PATCH 6/7] Restore miner-burn emission scaling (#3071) * Restore miner-burn emission scaling * Refactor miner-burn share scaling * bump spec and update docs --- docs/concepts/emissions.mdx | 36 +- docs/guides/evm/precompile-design.mdx | 6 +- docs/guides/evm/precompiles/registry.mdx | 4 +- docs/migration.mdx | 13 +- .../subtensor/src/coinbase/run_coinbase.rs | 7 +- .../src/coinbase/subnet_emissions.rs | 45 +- .../subtensor/src/tests/subnet_emissions.rs | 95 ++- sdk/python/bittensor/_generated/calls.py | 2 +- sdk/python/bittensor/_generated/constants.py | 2 +- sdk/python/bittensor/_generated/errors.py | 2 +- .../bittensor/_generated/runtime_apis.py | 2 +- sdk/python/bittensor/_generated/storage.py | 2 +- .../public/catalog/emission-snapshot.json | 661 +++++++++++------- .../scripts/fetch-emission-snapshot.py | 62 +- .../(pages-without-footer)/releases/page.tsx | 10 +- .../{v444-upgrade => v445-upgrade}/page.tsx | 110 +-- .../docs/emission-network-snapshot.tsx | 4 +- .../docs/subnet-emission-share-chart.tsx | 40 +- .../src/lib/emission-math.ts | 21 +- .../src/lib/emission-snapshot.ts | 7 +- 20 files changed, 676 insertions(+), 455 deletions(-) rename website/apps/bittensor-website/src/app/(pages-without-footer)/releases/{v444-upgrade => v445-upgrade}/page.tsx (76%) diff --git a/docs/concepts/emissions.mdx b/docs/concepts/emissions.mdx index 2c3e6ef9ce..5c0e2061c7 100644 --- a/docs/concepts/emissions.mdx +++ b/docs/concepts/emissions.mdx @@ -71,27 +71,37 @@ protocol-owned alpha. ## Subnet emission shares -Each block's TAO emission is divided in two clear steps. First, the chain turns -each eligible subnet's **EMA price** into a share of total demand: +Each block's TAO emission is divided in three steps. First, the chain turns each +eligible subnet's **EMA price** into a share of total demand: ``` demand_share_i = price_ema_i / Σ price_ema ``` -`MinerBurned` does not change this cross-subnet share. It still records what -happened to miner incentive inside the subnet, but one subnet's local burn or -recycle choice does not change another subnet's emission. +Next, the chain scales that price share by `1 − MinerBurned` and renormalizes: -Second, the **emission gate** reduces weak demand before the final shares are -normalized. The gate has a midpoint called `theta`. By default, `theta` is the -32nd-highest positive demand share. It is normally recalculated every 360 -blocks and stays fixed between updates. A subnet at the midpoint passes half -of its demand weight. Subnets well above it pass almost all; subnets well below -it pass much less: +``` +burn_adjusted_share_i = demand_share_i × (1 − miner_burned_i) + / Σ(demand_share × (1 − miner_burned)) +``` + +`MinerBurned` is the proportion of the last tempo's miner incentive withheld +because it was directed to subnet-owner hotkeys. Withheld incentive counts +whether it is recycled or burned, so changing `RecycleOrBurn` cannot bypass the +scaling. If every adjusted weight is zero, the runtime restores the unadjusted +price shares so emission is not stranded. + +Finally, the **emission gate** reduces weak burn-adjusted shares before the final +shares are normalized. The gate has a midpoint called `theta`. By default, +`theta` is the 32nd-highest positive adjusted share. It is normally recalculated +every 360 blocks and stays fixed between updates. A subnet at the midpoint +passes half of its adjusted weight. Subnets well above it pass almost all; +subnets well below it pass much less: ``` -gate_i = 1 / (1 + (theta / demand_share_i)^h) -final_share_i = demand_share_i × gate_i / Σ(demand_share × gate) +gate_i = 1 / (1 + (theta / burn_adjusted_share_i)^h) +final_share_i = burn_adjusted_share_i × gate_i + / Σ(burn_adjusted_share × gate) ``` The default exponent `h` is 3. This makes the gate gradual rather than a hard diff --git a/docs/guides/evm/precompile-design.mdx b/docs/guides/evm/precompile-design.mdx index 570b57244e..fafba2ca1b 100644 --- a/docs/guides/evm/precompile-design.mdx +++ b/docs/guides/evm/precompile-design.mdx @@ -336,7 +336,7 @@ interface IPrecompileRegistry { } ``` -In v444, the fields have the following behavior: +In v445, the fields have the following behavior: | Field | Meaning | |---|---| @@ -346,7 +346,7 @@ In v444, the fields have the following behavior: | `newSelector` | Reserved; always `0x00000000`. | | `message` | Reserved; always empty. | -The `selector` parameter is also reserved and is not interpreted in v444. A +The `selector` parameter is also reserved and is not interpreted in v445. A response therefore does not prove that a selector exists or describe its lifecycle. Tooling must use the canonical Solidity interfaces and JSON ABIs to discover supported selectors. @@ -455,6 +455,6 @@ Precompiles are a long-lived contract between Subtensor and deployed EVM code. Keep addresses and released selectors stable, version functions additively, preserve old semantics whenever possible, and replace raw storage access with typed views that insulate callers from storage layouts. Use deprecation to guide -migration and reversible disablement to handle operational risk. The v444 +migration and reversible disablement to handle operational risk. The v445 registry reports whole-precompile disablement; function-level lifecycle status remains a future extension. diff --git a/docs/guides/evm/precompiles/registry.mdx b/docs/guides/evm/precompiles/registry.mdx index 68b9fb0513..d7166136d0 100644 --- a/docs/guides/evm/precompiles/registry.mdx +++ b/docs/guides/evm/precompiles/registry.mdx @@ -10,7 +10,7 @@ description: Registry for precompile availability. | Address | `0x0000000000000000000000000000000000000813` | | Status | Deployed | -In v444, the registry reports the current operational availability of a whole +In v445, the registry reports the current operational availability of a whole precompile through `isDisabled`. It does not report whether an individual selector exists, is deprecated, or has a replacement. @@ -35,7 +35,7 @@ interface IPrecompileRegistry { The `selector` parameter and the `isDeprecated`, `newPrecompile`, `newSelector`, and `message` result fields are reserved for future selector-lifecycle support. -In v444 those fields are not populated, and the selector is not interpreted. +In v445 those fields are not populated, and the selector is not interpreted. Do not use this call to test whether a selector is supported; use the canonical Solidity interfaces and JSON ABIs for selector discovery. diff --git a/docs/migration.mdx b/docs/migration.mdx index 7600537c53..80ea04be70 100644 --- a/docs/migration.mdx +++ b/docs/migration.mdx @@ -513,7 +513,7 @@ if not result.success: The v11 package ships against a runtime that also changed behavior. These are not rename mappings — scripts that still "work" can fail or mis-account after the upgrade. See the release notes for -[V431](/releases/v431-upgrade) and [V444](/releases/v444-upgrade). +[V431](/releases/v431-upgrade) and [V445](/releases/v445-upgrade). ### Ownership and emissions @@ -521,12 +521,11 @@ the upgrade. See the release notes for to the highest-conviction hotkey when total conviction reaches 10% of `SubnetAlphaOut`. Existing conviction counts toward this threshold. See [Conviction](/docs/guides/conviction). -- **Cross-subnet emissions.** V444 allocation uses each subnet's EMA price, - followed by the emission gate. `MinerBurned` no longer changes a subnet's - cross-network share. [`root_proportion`](/code/pallets/subtensor/src/coinbase/block_step.rs#L74-L84) - is also not part of the inter-subnet split; it still applies inside each - subnet for injection caps and root dividends. Update emission calculations - and forecasts. See [Emissions](/docs/concepts/emissions). +- **Cross-subnet emissions.** Allocation continues to use each subnet's EMA + price scaled by `1 − MinerBurned`, followed by the emission gate. + [`root_proportion`](/code/pallets/subtensor/src/coinbase/block_step.rs#L74-L84) + is not part of the inter-subnet split; it still applies inside each subnet + for injection caps and root dividends. See [Emissions](/docs/concepts/emissions). ### Proxies and coldkeys diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 1421d37e0f..644c36a489 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -737,10 +737,9 @@ impl Pallet { "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 recorded - // withheld proportion so the metric is independent of the subnet's - // RecycleOrBurn configuration. The proportion is informational only and - // does not affect the subnet's emission share. + // 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::::try_get(netuid) { diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 8fc77e52db..f98c23f3fc 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -347,19 +347,52 @@ impl Pallet { offset_flows } - // Price-based emission shares: each subnet's share is its EMA price normalized - // by the sum of EMA prices, passed through the emission gate. Emit-disabled - // subnets are zeroed and their share redistributed to enabled subnets in - // `get_subnet_block_emissions`, so the effective emission is - // e_i = gate(s_i) * s_i / sum(gate(s_j) * s_j) over emit-enabled subnets. + // Price-based emission shares: normalize EMA prices, reweight each share by + // (1 - MinerBurned), renormalize, then pass the result through the emission + // gate. Emit-disabled subnets are zeroed and their share redistributed to + // enabled subnets in `get_subnet_block_emissions`. pub(crate) fn get_shares(subnets_to_emit_to: &[NetUid]) -> BTreeMap { - let mut shares = Self::get_shares_price_ema(subnets_to_emit_to); + let price_shares = Self::get_shares_price_ema(subnets_to_emit_to); + let mut shares = Self::scale_price_shares_by_miner_burn(price_shares); Self::maybe_update_emission_gate_bar(&shares); Self::apply_emission_gate(&mut shares); shares } + /// Scales normalized EMA-price shares by `(1 - MinerBurned)` and + /// renormalizes them. If every resulting weight is zero, returns the + /// original price shares so emission is not stranded. + fn scale_price_shares_by_miner_burn( + price_shares: BTreeMap, + ) -> BTreeMap { + let zero = U64F64::saturating_from_num(0); + let one = U64F64::saturating_from_num(1); + let weighted: BTreeMap = price_shares + .iter() + .map(|(netuid, share)| { + let burned = U64F64::saturating_from_num(MinerBurned::::get(netuid)).min(one); + let factor = one.saturating_sub(burned); + + (*netuid, share.saturating_mul(factor)) + }) + .collect(); + + let total_weight = weighted + .values() + .copied() + .fold(zero, |acc, weight| acc.saturating_add(weight)); + + if total_weight > zero { + weighted + .into_iter() + .map(|(netuid, weight)| (netuid, weight.safe_div(total_weight))) + .collect() + } else { + price_shares + } + } + /// Blocks between emission gate bar recomputations. Matching the standard /// tempo keeps the bar steady between epochs so per-block price wiggles /// cannot flap emission at the boundary. diff --git a/pallets/subtensor/src/tests/subnet_emissions.rs b/pallets/subtensor/src/tests/subnet_emissions.rs index 756bfbe8c0..b0d3d6efd1 100644 --- a/pallets/subtensor/src/tests/subnet_emissions.rs +++ b/pallets/subtensor/src/tests/subnet_emissions.rs @@ -192,36 +192,73 @@ fn get_shares_ignores_root_prop_storage_when_prices_and_burns_match() { }); } -/// Regression: `get_shares` must be independent of `MinerBurned`. The removed -/// formula weighted each price share by (1 - miner_burned) and renormalized, -/// so under it a burn split of 0 : 1 yields shares 1 : 0 and a split of -/// 0.25 : 0.75 yields 0.75 : 0.25. This test fails if that weighting is -/// reintroduced. +/// Miner-burn scaling is applied before the emission gate. Equal EMA prices +/// with burns 0.25 : 0.75 produce pre-gate shares 0.75 : 0.25. With the +/// default h=3 gate and theta=0.25, the final shares are 81/95 : 14/95. #[test] -fn get_shares_independent_of_miner_burned() { - // (burn_n1, burn_n2): boundary values and a non-boundary split. - for (burn_n1, burn_n2) in [(0.0, 1.0), (1.0, 0.0), (0.25, 0.75)] { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(90); - let owner_coldkey = U256::from(91); - let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); - let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); - - // Equal prices; only the miner-burn values differ. - SubnetMovingPrice::::insert(n1, i96f32(1.0)); - SubnetMovingPrice::::insert(n2, i96f32(1.0)); - MinerBurned::::insert(n1, U96F32::saturating_from_num(burn_n1)); - MinerBurned::::insert(n2, U96F32::saturating_from_num(burn_n2)); - - let shares = SubtensorModule::get_shares(&[n1, n2]); - let s1 = shares.get(&n1).copied().unwrap().to_num::(); - let s2 = shares.get(&n2).copied().unwrap().to_num::(); - - assert_abs_diff_eq!(s1, 0.5_f64, epsilon = 1e-9); - assert_abs_diff_eq!(s2, 0.5_f64, epsilon = 1e-9); - assert_abs_diff_eq!(s1 + s2, 1.0_f64, epsilon = 1e-9); - }); - } +fn get_shares_scales_by_miner_burn_before_gate() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(90); + let owner_coldkey = U256::from(91); + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + SubnetMovingPrice::::insert(n1, i96f32(1.0)); + SubnetMovingPrice::::insert(n2, i96f32(1.0)); + MinerBurned::::insert(n1, U96F32::saturating_from_num(0.25)); + MinerBurned::::insert(n2, U96F32::saturating_from_num(0.75)); + + let shares = SubtensorModule::get_shares(&[n1, n2]); + let s1 = shares.get(&n1).copied().unwrap().to_num::(); + let s2 = shares.get(&n2).copied().unwrap().to_num::(); + + assert_abs_diff_eq!( + EmissionGateBar::::get().to_num::(), + 0.25, + epsilon = 1e-9 + ); + assert_abs_diff_eq!(s1, 81.0 / 95.0, epsilon = 1e-6); + assert_abs_diff_eq!(s2, 14.0 / 95.0, epsilon = 1e-6); + assert_abs_diff_eq!(s1 + s2, 1.0, epsilon = 1e-9); + }); +} + +#[test] +fn get_shares_full_miner_burn_gets_zero() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(92); + let owner_coldkey = U256::from(93); + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + SubnetMovingPrice::::insert(n1, i96f32(1.0)); + SubnetMovingPrice::::insert(n2, i96f32(1.0)); + MinerBurned::::insert(n1, U96F32::saturating_from_num(1.0)); + MinerBurned::::insert(n2, U96F32::saturating_from_num(0.0)); + + let shares = SubtensorModule::get_shares(&[n1, n2]); + assert_abs_diff_eq!(shares[&n1].to_num::(), 0.0, epsilon = 1e-9); + assert_abs_diff_eq!(shares[&n2].to_num::(), 1.0, epsilon = 1e-9); + }); +} + +#[test] +fn get_shares_all_full_miner_burn_falls_back_to_price_shares() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(94); + let owner_coldkey = U256::from(95); + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + SubnetMovingPrice::::insert(n1, i96f32(1.0)); + SubnetMovingPrice::::insert(n2, i96f32(1.0)); + MinerBurned::::insert(n1, U96F32::saturating_from_num(1.0)); + MinerBurned::::insert(n2, U96F32::saturating_from_num(1.0)); + + let shares = SubtensorModule::get_shares(&[n1, n2]); + assert_abs_diff_eq!(shares[&n1].to_num::(), 0.5, epsilon = 1e-9); + assert_abs_diff_eq!(shares[&n2].to_num::(), 0.5, epsilon = 1e-9); + }); } /// Empty candidate set: no panic, empty map, bar stays unset. diff --git a/sdk/python/bittensor/_generated/calls.py b/sdk/python/bittensor/_generated/calls.py index 0ac4793935..6003448427 100644 --- a/sdk/python/bittensor/_generated/calls.py +++ b/sdk/python/bittensor/_generated/calls.py @@ -1,7 +1,7 @@ """Generated from runtime metadata by codegen. DO NOT EDIT BY HAND. Regenerate with: python -m codegen -Spec version: 444 +Spec version: 445 """ from typing import Any, NamedTuple diff --git a/sdk/python/bittensor/_generated/constants.py b/sdk/python/bittensor/_generated/constants.py index 27276d9970..70c8ad6fd9 100644 --- a/sdk/python/bittensor/_generated/constants.py +++ b/sdk/python/bittensor/_generated/constants.py @@ -1,7 +1,7 @@ """Generated from runtime metadata by codegen. DO NOT EDIT BY HAND. Regenerate with: python -m codegen -Spec version: 444 +Spec version: 445 Pallet constant descriptors: unpack into substrate.constant. """ diff --git a/sdk/python/bittensor/_generated/errors.py b/sdk/python/bittensor/_generated/errors.py index 4417a3fe36..7bde2904e9 100644 --- a/sdk/python/bittensor/_generated/errors.py +++ b/sdk/python/bittensor/_generated/errors.py @@ -1,7 +1,7 @@ """Generated from runtime metadata by codegen. DO NOT EDIT BY HAND. Regenerate with: python -m codegen -Spec version: 444 +Spec version: 445 """ from dataclasses import dataclass diff --git a/sdk/python/bittensor/_generated/runtime_apis.py b/sdk/python/bittensor/_generated/runtime_apis.py index 0408cfabc3..835765baac 100644 --- a/sdk/python/bittensor/_generated/runtime_apis.py +++ b/sdk/python/bittensor/_generated/runtime_apis.py @@ -1,7 +1,7 @@ """Generated from runtime metadata by codegen. DO NOT EDIT BY HAND. Regenerate with: python -m codegen -Spec version: 444 +Spec version: 445 Runtime API method descriptors: unpack into substrate.runtime_call. """ diff --git a/sdk/python/bittensor/_generated/storage.py b/sdk/python/bittensor/_generated/storage.py index a38f662944..b823af6953 100644 --- a/sdk/python/bittensor/_generated/storage.py +++ b/sdk/python/bittensor/_generated/storage.py @@ -1,7 +1,7 @@ """Generated from runtime metadata by codegen. DO NOT EDIT BY HAND. Regenerate with: python -m codegen -Spec version: 444 +Spec version: 445 Storage item descriptors: unpack into substrate.query/query_map. Each carries its VALUE's type identity (value_type_ident) so normalization can key on the runtime's own type names without a node round-trip. """ diff --git a/website/apps/bittensor-website/public/catalog/emission-snapshot.json b/website/apps/bittensor-website/public/catalog/emission-snapshot.json index 680c5a28cd..bf2afd3503 100644 --- a/website/apps/bittensor-website/public/catalog/emission-snapshot.json +++ b/website/apps/bittensor-website/public/catalog/emission-snapshot.json @@ -1,844 +1,997 @@ { - "fetchedAt": "2026-08-10T19:50:58.619291Z", + "fetchedAt": "2026-08-11T16:37:11.729288Z", "network": "finney", "chainSpecVersion": 443, - "emissionMode": "v444_price_ema_hill_gate", - "emissionGateSource": "v444_defaults_recomputed", + "emissionMode": "price_ema_miner_burn_hill_gate", + "emissionGateSource": "v445_defaults_recomputed", "dataSource": { "subnets": "taomarketcap.com", "chain": "finney", "tmcEndpoint": "https://api.taomarketcap.com/public/v1/subnets/" }, "blockEmissionTao": 0.5, - "totalIssuanceTao": 11219409.114, - "totalIssuanceRao": 11219409114200811, - "rootTao": 5413546.52, - "emaPriceSum": 1.3084, + "totalIssuanceTao": 11222524.602, + "totalIssuanceRao": 11222524602355523, + "rootTao": 5411665.86, + "emaPriceSum": 1.3036, "rootDividendGateOpen": true, "taoWeight": 0.18, "emissionGateRank": 32, "emissionGateQuantile": 0.61, "emissionGateExponent": 3.0, - "emissionGateBar": 0.00733118, + "emissionGateBar": 0.00794042, "emissionInputs": [ { "netuid": 1, - "emaPrice": 0.008054, + "emaPrice": 0.008041, + "minerBurned": 0.52743466, "emissionEnabled": true }, { "netuid": 2, - "emaPrice": 0.004216, + "emaPrice": 0.004181, + "minerBurned": 0.82661404, "emissionEnabled": true }, { "netuid": 3, - "emaPrice": 0.02563, + "emaPrice": 0.025466, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 4, - "emaPrice": 0.057038, + "emaPrice": 0.056618, + "minerBurned": 0.07230859, "emissionEnabled": true }, { "netuid": 5, - "emaPrice": 0.013702, + "emaPrice": 0.013659, + "minerBurned": 0.42237093, "emissionEnabled": true }, { "netuid": 6, - "emaPrice": 0.002964, + "emaPrice": 0.003066, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 7, - "emaPrice": 0.003321, + "emaPrice": 0.003367, + "minerBurned": 0.90417399, "emissionEnabled": true }, { "netuid": 8, - "emaPrice": 0.029832, + "emaPrice": 0.029893, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 9, - "emaPrice": 0.033401, + "emaPrice": 0.033205, + "minerBurned": 1.0, "emissionEnabled": true }, { "netuid": 10, - "emaPrice": 0.006398, + "emaPrice": 0.006447, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 11, - "emaPrice": 0.007657, + "emaPrice": 0.007319, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 12, - "emaPrice": 0.004999, + "emaPrice": 0.004997, + "minerBurned": 0.99407092, "emissionEnabled": false }, { "netuid": 13, - "emaPrice": 0.006294, + "emaPrice": 0.006284, + "minerBurned": 0.70894014, "emissionEnabled": true }, { "netuid": 14, - "emaPrice": 0.010195, + "emaPrice": 0.010257, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 15, - "emaPrice": 0.021038, + "emaPrice": 0.022328, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 16, - "emaPrice": 0.003094, + "emaPrice": 0.002972, + "minerBurned": 0.0, "emissionEnabled": false }, { "netuid": 17, - "emaPrice": 0.009592, + "emaPrice": 0.009435, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 18, - "emaPrice": 0.00515, + "emaPrice": 0.005063, + "minerBurned": 0.04716244, "emissionEnabled": true }, { "netuid": 19, - "emaPrice": 0.010228, + "emaPrice": 0.010212, + "minerBurned": 0.38279278, "emissionEnabled": true }, { "netuid": 20, - "emaPrice": 0.002892, + "emaPrice": 0.002888, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 21, - "emaPrice": 0.003186, + "emaPrice": 0.00317, + "minerBurned": 0.45150375, "emissionEnabled": true }, { "netuid": 22, - "emaPrice": 0.003353, + "emaPrice": 0.003352, + "minerBurned": 0.52455545, "emissionEnabled": true }, { "netuid": 23, - "emaPrice": 0.004562, + "emaPrice": 0.00455, + "minerBurned": 1.0, "emissionEnabled": true }, { "netuid": 24, - "emaPrice": 0.004405, + "emaPrice": 0.004442, + "minerBurned": 1.0, "emissionEnabled": true }, { "netuid": 25, - "emaPrice": 0.010332, + "emaPrice": 0.010057, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 26, - "emaPrice": 0.004317, + "emaPrice": 0.004172, + "minerBurned": 0.5016723, "emissionEnabled": false }, { "netuid": 27, - "emaPrice": 0.002477, + "emaPrice": 0.002441, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 28, - "emaPrice": 0.016706, + "emaPrice": 0.016987, + "minerBurned": 0.10477993, "emissionEnabled": true }, { "netuid": 29, "emaPrice": 0.003131, + "minerBurned": 0.0, "emissionEnabled": false }, { "netuid": 30, - "emaPrice": 0.004021, + "emaPrice": 0.004025, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 31, - "emaPrice": 0.005064, + "emaPrice": 0.004764, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 32, - "emaPrice": 0.003187, + "emaPrice": 0.003171, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 33, - "emaPrice": 0.005635, + "emaPrice": 0.005672, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 34, "emaPrice": 0.012238, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 35, - "emaPrice": 0.002785, + "emaPrice": 0.002724, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 36, - "emaPrice": 0.00167, + "emaPrice": 0.001521, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 37, - "emaPrice": 0.003596, + "emaPrice": 0.00366, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 38, - "emaPrice": 0.011619, + "emaPrice": 0.011583, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 39, - "emaPrice": 0.006459, + "emaPrice": 0.006456, + "minerBurned": 1.0, "emissionEnabled": true }, { "netuid": 40, - "emaPrice": 0.006692, + "emaPrice": 0.006581, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 41, - "emaPrice": 0.005026, + "emaPrice": 0.005016, + "minerBurned": 0.72833058, "emissionEnabled": true }, { "netuid": 42, - "emaPrice": 0.002774, + "emaPrice": 0.002767, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 43, - "emaPrice": 0.005369, + "emaPrice": 0.005353, + "minerBurned": 0.79643628, "emissionEnabled": false }, { "netuid": 44, - "emaPrice": 0.042633, + "emaPrice": 0.041827, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 45, - "emaPrice": 0.002634, + "emaPrice": 0.002625, + "minerBurned": 0.68900336, "emissionEnabled": false }, { "netuid": 46, - "emaPrice": 0.004547, + "emaPrice": 0.004772, + "minerBurned": 1.0, "emissionEnabled": true }, { "netuid": 47, - "emaPrice": 0.002858, + "emaPrice": 0.002951, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 48, - "emaPrice": 0.004478, + "emaPrice": 0.004481, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 49, - "emaPrice": 0.007034, + "emaPrice": 0.00701, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 50, - "emaPrice": 0.004823, + "emaPrice": 0.004831, + "minerBurned": 1.08e-06, "emissionEnabled": true }, { "netuid": 51, - "emaPrice": 0.073255, + "emaPrice": 0.073811, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 52, - "emaPrice": 0.00678, + "emaPrice": 0.00676, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 53, - "emaPrice": 0.03318, + "emaPrice": 0.031652, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 54, - "emaPrice": 0.004743, + "emaPrice": 0.004638, + "minerBurned": 0.30939764, "emissionEnabled": true }, { "netuid": 55, - "emaPrice": 0.002857, + "emaPrice": 0.002796, + "minerBurned": 0.02000552, "emissionEnabled": true }, { "netuid": 56, - "emaPrice": 0.016981, + "emaPrice": 0.017013, + "minerBurned": 0.72379724, "emissionEnabled": true }, { "netuid": 57, - "emaPrice": 0.005359, + "emaPrice": 0.005288, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 58, - "emaPrice": 0.008788, + "emaPrice": 0.008798, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 59, - "emaPrice": 0.002019, + "emaPrice": 0.001975, + "minerBurned": 1.0, "emissionEnabled": true }, { "netuid": 60, - "emaPrice": 0.004075, + "emaPrice": 0.003998, + "minerBurned": 0.5, "emissionEnabled": true }, { "netuid": 61, - "emaPrice": 0.008461, + "emaPrice": 0.008466, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 62, - "emaPrice": 0.012264, + "emaPrice": 0.012144, + "minerBurned": 0.13338149, "emissionEnabled": true }, { "netuid": 63, - "emaPrice": 0.008203, + "emaPrice": 0.008208, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 64, - "emaPrice": 0.086481, + "emaPrice": 0.086255, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 65, - "emaPrice": 0.002534, + "emaPrice": 0.00251, + "minerBurned": 1.0, "emissionEnabled": true }, { "netuid": 66, - "emaPrice": 0.003033, + "emaPrice": 0.002983, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 67, - "emaPrice": 0.006518, + "emaPrice": 0.006471, + "minerBurned": 0.03652611, "emissionEnabled": true }, { "netuid": 68, - "emaPrice": 0.024419, + "emaPrice": 0.024017, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 69, - "emaPrice": 0.009912, + "emaPrice": 0.009892, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 70, - "emaPrice": 0.001371, + "emaPrice": 0.00134, + "minerBurned": 0.0, "emissionEnabled": false }, { "netuid": 71, - "emaPrice": 0.003849, + "emaPrice": 0.0038, + "minerBurned": 1.0, "emissionEnabled": true }, { "netuid": 72, - "emaPrice": 0.002701, + "emaPrice": 0.002626, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 73, - "emaPrice": 0.003274, + "emaPrice": 0.003278, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 74, - "emaPrice": 0.003809, + "emaPrice": 0.003794, + "minerBurned": 0.62999936, "emissionEnabled": true }, { "netuid": 75, - "emaPrice": 0.019342, + "emaPrice": 0.018814, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 76, - "emaPrice": 0.002632, + "emaPrice": 0.00261, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 77, - "emaPrice": 0.006135, + "emaPrice": 0.00612, + "minerBurned": 0.0, "emissionEnabled": false }, { "netuid": 78, - "emaPrice": 0.002605, + "emaPrice": 0.002534, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 79, - "emaPrice": 0.007527, + "emaPrice": 0.007579, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 80, - "emaPrice": 0.014918, + "emaPrice": 0.014729, + "minerBurned": 0.90650646, "emissionEnabled": true }, { "netuid": 81, - "emaPrice": 0.007893, + "emaPrice": 0.007889, + "minerBurned": 0.01844684, "emissionEnabled": true }, { "netuid": 82, - "emaPrice": 0.004499, + "emaPrice": 0.004648, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 83, - "emaPrice": 0.010613, + "emaPrice": 0.010587, + "minerBurned": 0.0, "emissionEnabled": false }, { "netuid": 84, - "emaPrice": 0.002521, + "emaPrice": 0.002544, + "minerBurned": 0.0, "emissionEnabled": false }, { "netuid": 85, - "emaPrice": 0.005342, + "emaPrice": 0.005324, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 87, - "emaPrice": 0.003704, + "emaPrice": 0.00365, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 88, - "emaPrice": 0.004539, + "emaPrice": 0.005168, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 89, - "emaPrice": 0.003664, + "emaPrice": 0.003655, + "minerBurned": 0.2, "emissionEnabled": true }, { "netuid": 90, - "emaPrice": 0.025303, + "emaPrice": 0.025993, + "minerBurned": 0.86008526, "emissionEnabled": false }, { "netuid": 91, - "emaPrice": 0.009309, + "emaPrice": 0.009029, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 92, - "emaPrice": 0.003914, + "emaPrice": 0.003814, + "minerBurned": 0.0, "emissionEnabled": false }, { "netuid": 93, - "emaPrice": 0.00976, + "emaPrice": 0.009616, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 94, - "emaPrice": 0.00375, + "emaPrice": 0.003652, + "minerBurned": 1.0, "emissionEnabled": true }, { "netuid": 95, - "emaPrice": 0.057053, + "emaPrice": 0.057009, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 96, - "emaPrice": 0.008541, + "emaPrice": 0.008529, + "minerBurned": 0.41741, "emissionEnabled": true }, { "netuid": 97, - "emaPrice": 0.025306, + "emaPrice": 0.024897, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 98, - "emaPrice": 0.002965, + "emaPrice": 0.002966, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 99, - "emaPrice": 0.004781, + "emaPrice": 0.004726, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 100, - "emaPrice": 0.005732, + "emaPrice": 0.006412, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 101, - "emaPrice": 0.004636, + "emaPrice": 0.004631, + "minerBurned": 0.90102437, "emissionEnabled": true }, { "netuid": 102, - "emaPrice": 0.008691, + "emaPrice": 0.00926, + "minerBurned": 0.25060051, "emissionEnabled": true }, { "netuid": 103, - "emaPrice": 0.009532, + "emaPrice": 0.009905, + "minerBurned": 0.0, "emissionEnabled": false }, { "netuid": 104, - "emaPrice": 0.004384, + "emaPrice": 0.004367, + "minerBurned": 0.9639356, "emissionEnabled": false }, { "netuid": 105, - "emaPrice": 0.007274, + "emaPrice": 0.007393, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 106, - "emaPrice": 0.003108, + "emaPrice": 0.003041, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 107, - "emaPrice": 0.06158, + "emaPrice": 0.060843, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 108, - "emaPrice": 0.004023, + "emaPrice": 0.003638, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 109, - "emaPrice": 0.003304, + "emaPrice": 0.003345, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 110, - "emaPrice": 0.00737, + "emaPrice": 0.007125, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 111, - "emaPrice": 0.004958, + "emaPrice": 0.00529, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 112, - "emaPrice": 0.002535, + "emaPrice": 0.002502, + "minerBurned": 0.80094412, "emissionEnabled": true }, { "netuid": 113, - "emaPrice": 0.00273, + "emaPrice": 0.002727, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 114, - "emaPrice": 0.011642, + "emaPrice": 0.011322, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 115, - "emaPrice": 0.003533, + "emaPrice": 0.003086, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 116, - "emaPrice": 0.007126, + "emaPrice": 0.006622, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 117, - "emaPrice": 0.002574, + "emaPrice": 0.002566, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 118, - "emaPrice": 0.009272, + "emaPrice": 0.009178, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 119, "emaPrice": 0.003091, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 120, - "emaPrice": 0.059987, + "emaPrice": 0.060463, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 121, - "emaPrice": 0.004739, + "emaPrice": 0.004741, + "minerBurned": 1.0, "emissionEnabled": true }, { "netuid": 122, - "emaPrice": 0.004685, + "emaPrice": 0.004615, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 123, - "emaPrice": 0.002703, + "emaPrice": 0.002672, + "minerBurned": 0.41245638, "emissionEnabled": true }, { "netuid": 124, - "emaPrice": 0.010121, + "emaPrice": 0.010072, + "minerBurned": 0.0, "emissionEnabled": true }, { "netuid": 125, - "emaPrice": 0.00328, + "emaPrice": 0.003267, + "minerBurned": 1.0, "emissionEnabled": false }, { "netuid": 126, - "emaPrice": 0.005165, + "emaPrice": 0.005009, + "minerBurned": 0.3047695, "emissionEnabled": true }, { "netuid": 127, - "emaPrice": 0.002883, + "emaPrice": 0.002904, + "minerBurned": 0.70639378, "emissionEnabled": true }, { "netuid": 128, - "emaPrice": 0.002938, + "emaPrice": 0.002916, + "minerBurned": 0.0, "emissionEnabled": false } ], "featuredSubnet": { "netuid": 4, "name": "Targon", - "spotPrice": 0.056784, - "emaPrice": 0.057038, + "spotPrice": 0.056624, + "emaPrice": 0.056618, + "minerBurned": 0.07230859, "emissionEnabled": true, - "taoIn": 136777.6, - "alphaIn": 2408742.63, - "alphaOut": 3498348.62, - "demandShare": 0.04359424, - "gateFactor": 0.99526659, - "taoShare": 0.07641434, - "taoPerBlock": 0.03820717 + "taoIn": 136634.82, + "alphaIn": 2413022.19, + "alphaOut": 3501181.98, + "demandShare": 0.04343117, + "burnAdjustedShare": 0.05698358, + "gateFactor": 0.99730159, + "taoShare": 0.07542274, + "taoPerBlock": 0.03771137 }, "topSubnets": [ { "netuid": 64, "name": "Chutes", - "spotPrice": 0.085869, - "emaPrice": 0.086481, + "spotPrice": 0.085763, + "emaPrice": 0.086255, + "minerBurned": 0.0, "emissionEnabled": true, - "taoIn": 220039.08, - "alphaIn": 2562495.19, - "alphaOut": 3327621.07, - "demandShare": 0.06609757, - "gateFactor": 0.99863738, - "taoShare": 0.11625179, - "taoPerBlock": 0.0581259 + "taoIn": 219979.27, + "alphaIn": 2564964.24, + "alphaOut": 3332267.14, + "demandShare": 0.06616545, + "burnAdjustedShare": 0.09357848, + "gateFactor": 0.99938943, + "taoShare": 0.12411856, + "taoPerBlock": 0.06205928 }, { "netuid": 51, "name": "lium.io", - "spotPrice": 0.073558, - "emaPrice": 0.073255, + "spotPrice": 0.074515, + "emaPrice": 0.073811, + "minerBurned": 0.0, "emissionEnabled": true, - "taoIn": 140987.41, - "alphaIn": 1916689.46, - "alphaOut": 3668935.54, - "demandShare": 0.05598891, - "gateFactor": 0.99776003, - "taoShare": 0.09838627, - "taoPerBlock": 0.04919314 + "taoIn": 141970.5, + "alphaIn": 1905258.42, + "alphaOut": 3687515.95, + "demandShare": 0.05661977, + "burnAdjustedShare": 0.08007792, + "gateFactor": 0.99902598, + "taoShare": 0.10617336, + "taoPerBlock": 0.05308668 }, { "netuid": 107, "name": "Minos", - "spotPrice": 0.061349, - "emaPrice": 0.06158, + "spotPrice": 0.060403, + "emaPrice": 0.060843, + "minerBurned": 0.0, "emissionEnabled": true, - "taoIn": 19184.4, - "alphaIn": 312709.51, - "alphaOut": 1406061.78, - "demandShare": 0.04706569, - "gateFactor": 0.99623495, - "taoShare": 0.08257957, - "taoPerBlock": 0.04128978 + "taoIn": 19172.35, + "alphaIn": 317408.35, + "alphaOut": 1409812.54, + "demandShare": 0.04667213, + "burnAdjustedShare": 0.06600887, + "gateFactor": 0.99826233, + "taoShare": 0.08745266, + "taoPerBlock": 0.04372633 }, { "netuid": 120, "name": "Affine", - "spotPrice": 0.060743, - "emaPrice": 0.059987, + "spotPrice": 0.059335, + "emaPrice": 0.060463, + "minerBurned": 0.0, "emissionEnabled": true, - "taoIn": 84377.64, - "alphaIn": 1389096.16, - "alphaOut": 2556991.84, - "demandShare": 0.04584816, - "gateFactor": 0.99592822, - "taoShare": 0.08041856, - "taoPerBlock": 0.04020928 + "taoIn": 83468.17, + "alphaIn": 1406725.51, + "alphaOut": 2546641.18, + "demandShare": 0.04638063, + "burnAdjustedShare": 0.06559661, + "gateFactor": 0.99822942, + "taoShare": 0.0869036, + "taoPerBlock": 0.0434518 }, { "netuid": 4, "name": "Targon", - "spotPrice": 0.056784, - "emaPrice": 0.057038, + "spotPrice": 0.056624, + "emaPrice": 0.056618, + "minerBurned": 0.07230859, "emissionEnabled": true, - "taoIn": 136777.6, - "alphaIn": 2408742.63, - "alphaOut": 3498348.62, - "demandShare": 0.04359424, - "gateFactor": 0.99526659, - "taoShare": 0.07641434, - "taoPerBlock": 0.03820717 + "taoIn": 136634.82, + "alphaIn": 2413022.19, + "alphaOut": 3501181.98, + "demandShare": 0.04343117, + "burnAdjustedShare": 0.05698358, + "gateFactor": 0.99730159, + "taoShare": 0.07542274, + "taoPerBlock": 0.03771137 }, { "netuid": 44, "name": "Score", - "spotPrice": 0.042135, - "emaPrice": 0.042633, - "emissionEnabled": true, - "taoIn": 69596.55, - "alphaIn": 1651741.69, - "alphaOut": 3998290.27, - "demandShare": 0.03258447, - "gateFactor": 0.98873914, - "taoShare": 0.05674123, - "taoPerBlock": 0.02837062 - }, - { - "netuid": 9, - "name": "iota", - "spotPrice": 0.03345, - "emaPrice": 0.033401, + "spotPrice": 0.042201, + "emaPrice": 0.041827, + "minerBurned": 0.0, "emissionEnabled": true, - "taoIn": 60926.17, - "alphaIn": 1821422.27, - "alphaOut": 3951121.46, - "demandShare": 0.02552844, - "gateFactor": 0.97686426, - "taoShare": 0.04392025, - "taoPerBlock": 0.02196013 + "taoIn": 69689.46, + "alphaIn": 1651357.73, + "alphaOut": 4005821.33, + "demandShare": 0.03208512, + "burnAdjustedShare": 0.04537832, + "gateFactor": 0.99467077, + "taoShare": 0.05990372, + "taoPerBlock": 0.02995186 }, { "netuid": 53, "name": "engy", - "spotPrice": 0.032, - "emaPrice": 0.03318, + "spotPrice": 0.033562, + "emaPrice": 0.031652, + "minerBurned": 0.0, "emissionEnabled": true, - "taoIn": 35107.27, - "alphaIn": 1097096.66, - "alphaOut": 4580364.57, - "demandShare": 0.02535953, - "gateFactor": 0.97640986, - "taoShare": 0.04360936, - "taoPerBlock": 0.02180468 + "taoIn": 35983.37, + "alphaIn": 1072141.54, + "alphaOut": 4612463.02, + "demandShare": 0.02427997, + "burnAdjustedShare": 0.03433941, + "gateFactor": 0.9877872, + "taoShare": 0.04501759, + "taoPerBlock": 0.0225088 }, { "netuid": 8, "name": "Vanta", - "spotPrice": 0.029842, - "emaPrice": 0.029832, + "spotPrice": 0.029939, + "emaPrice": 0.029893, + "minerBurned": 0.0, "emissionEnabled": true, - "taoIn": 81218.94, - "alphaIn": 2721615.73, - "alphaOut": 3019607.15, - "demandShare": 0.02280065, - "gateFactor": 0.96782796, - "taoShare": 0.03886437, - "taoPerBlock": 0.01943219 + "taoIn": 81377.2, + "alphaIn": 2718128.48, + "alphaOut": 3030229.05, + "demandShare": 0.02293066, + "burnAdjustedShare": 0.03243107, + "gateFactor": 0.98553499, + "taoShare": 0.04241889, + "taoPerBlock": 0.02120944 }, { "netuid": 3, "name": "deprecated", - "spotPrice": 0.025426, - "emaPrice": 0.02563, + "spotPrice": 0.025225, + "emaPrice": 0.025466, + "minerBurned": 0.0, "emissionEnabled": true, - "taoIn": 74149.25, - "alphaIn": 2916243.4, - "alphaOut": 2819141.61, - "demandShare": 0.01958905, - "gateFactor": 0.95019266, - "taoShare": 0.0327817, - "taoPerBlock": 0.01639085 + "taoIn": 73878.81, + "alphaIn": 2928735.47, + "alphaOut": 2813772.82, + "demandShare": 0.01953474, + "burnAdjustedShare": 0.02762819, + "gateFactor": 0.97681092, + "taoShare": 0.03581698, + "taoPerBlock": 0.01790849 }, { "netuid": 97, "name": "Albedo", - "spotPrice": 0.024872, - "emaPrice": 0.025306, + "spotPrice": 0.024758, + "emaPrice": 0.024897, + "minerBurned": 0.0, "emissionEnabled": true, - "taoIn": 11057.13, - "alphaIn": 444567.2, - "alphaOut": 932144.63, - "demandShare": 0.01934142, - "gateFactor": 0.94835504, - "taoShare": 0.03230469, - "taoPerBlock": 0.01615235 + "taoIn": 11095.78, + "alphaIn": 448165.46, + "alphaOut": 936842.91, + "demandShare": 0.01909827, + "burnAdjustedShare": 0.02701088, + "gateFactor": 0.9752247, + "taoShare": 0.03495984, + "taoPerBlock": 0.01747992 }, { "netuid": 68, "name": "NOVA", - "spotPrice": 0.02442, - "emaPrice": 0.024419, + "spotPrice": 0.023941, + "emaPrice": 0.024017, + "minerBurned": 0.0, + "emissionEnabled": true, + "taoIn": 45722.1, + "alphaIn": 1909747.31, + "alphaOut": 3596531.4, + "demandShare": 0.01842323, + "burnAdjustedShare": 0.02605616, + "gateFactor": 0.97247808, + "taoShare": 0.03362918, + "taoPerBlock": 0.01681459 + }, + { + "netuid": 15, + "name": "ORO", + "spotPrice": 0.02277, + "emaPrice": 0.022328, + "minerBurned": 0.0, "emissionEnabled": true, - "taoIn": 46153.92, - "alphaIn": 1890019.99, - "alphaOut": 3609349.49, - "demandShare": 0.01866348, - "gateFactor": 0.94285369, - "taoShare": 0.03099155, - "taoPerBlock": 0.01549578 + "taoIn": 10754.4, + "alphaIn": 472315.91, + "alphaOut": 1283562.57, + "demandShare": 0.01712761, + "burnAdjustedShare": 0.02422376, + "gateFactor": 0.965977, + "taoShare": 0.0310552, + "taoPerBlock": 0.0155276 } ] } diff --git a/website/apps/bittensor-website/scripts/fetch-emission-snapshot.py b/website/apps/bittensor-website/scripts/fetch-emission-snapshot.py index 502703e4ca..bec55dac86 100644 --- a/website/apps/bittensor-website/scripts/fetch-emission-snapshot.py +++ b/website/apps/bittensor-website/scripts/fetch-emission-snapshot.py @@ -1,12 +1,12 @@ """Refresh public/catalog/emission-snapshot.json from TaoMarketCap + Finney. Subnet prices and EMA sums use TMC's public API. Eligibility, emission-enabled -flags, total issuance, and root pool TAO come from Finney storage. The output -models the v444 pure-price EMA allocation and Hill gate. On spec 444 or later, -the gate settings and cadence-held midpoint come from chain storage. Before the -upgrade, the output previews the first v444 calculation with the upgrade -defaults. It does not use the deprecated ``BlockEmission`` or ``MinerBurned`` -storage items. +flags, MinerBurned proportions, total issuance, and root pool TAO come from +Finney storage. The output models price EMA with miner-burn scaling and the +Hill gate. On spec 445 or later, the gate settings and cadence-held midpoint +come from chain storage. Before the upgrade, the output previews the v445 gate +settings while retaining miner-burn scaling. It does not use the deprecated +``BlockEmission`` storage item. Usage (from bittensor-website/, with the bittensor SDK venv active): @@ -47,6 +47,15 @@ def fixed_u64f64_num(value) -> float: return float(value) +def fixed_u96f32_num(value) -> float: + """Convert the SDK's U96F32 representation to a float.""" + if isinstance(value, dict) and "bits" in value: + return int(value["bits"]) / 2**32 + if isinstance(value, int): + return value / 2**32 + return float(value) + + def tmc_num(val) -> float: if val is None: return 0.0 @@ -99,7 +108,7 @@ def subnet_name(row: dict) -> str: return identities.get("subnetName") or f"SN{row['netuid']}" -def row_from_tmc(row: dict, emission_enabled: bool) -> dict: +def row_from_tmc(row: dict, emission_enabled: bool, miner_burned: float) -> dict: snap = row.get("latest_snapshot") or {} netuid = int(row["netuid"]) spot = tmc_num(snap.get("price")) @@ -112,6 +121,7 @@ def row_from_tmc(row: dict, emission_enabled: bool) -> dict: "name": subnet_name(row), "spotPrice": round(spot, 6), "emaPrice": round(ema, 6), + "minerBurned": round(min(max(miner_burned, 0.0), 1.0), 8), "emissionEnabled": emission_enabled, "taoIn": round(tao_in, 2), "alphaIn": round(alpha_in, 2), @@ -140,17 +150,19 @@ async def chain_fields(client: bt.Client, netuids: list[int]) -> dict: root_tao = int(await view.query(st.SubtensorModule.SubnetTAO, [0])) tao_weight_raw = int(await view.query(st.SubtensorModule.TaoWeight)) - async def subnet_flags(netuid: int) -> tuple[int, dict[str, bool]]: + async def subnet_flags(netuid: int) -> tuple[int, dict[str, bool | float]]: ( first_emission, subtoken_enabled, registration_allowed, emission_enabled, + miner_burned, ) = await asyncio.gather( view.query(st.SubtensorModule.FirstEmissionBlockNumber, [netuid]), view.query(st.SubtensorModule.SubtokenEnabled, [netuid]), view.query(st.SubtensorModule.NetworkRegistrationAllowed, [netuid]), view.query(st.SubtensorModule.SubnetEmissionEnabled, [netuid]), + view.query(st.SubtensorModule.MinerBurned, [netuid]), ) return netuid, { "eligible": ( @@ -159,12 +171,13 @@ async def subnet_flags(netuid: int) -> tuple[int, dict[str, bool]]: and bool(registration_allowed) ), "emissionEnabled": bool(emission_enabled), + "minerBurned": fixed_u96f32_num(miner_burned), } flags = dict(await asyncio.gather(*(subnet_flags(netuid) for netuid in netuids))) gate = None - if spec_version >= 444: + if spec_version >= 445: rank, quantile, exponent, bar = await asyncio.gather( view.query(st.SubtensorModule.EmissionBarRank), view.query(st.SubtensorModule.EmissionBarQuantile), @@ -197,7 +210,7 @@ def select_emission_gate_bar( rank: int = DEFAULT_EMISSION_BAR_RANK, quantile: float = DEFAULT_EMISSION_BAR_QUANTILE, ) -> float: - """Mirror ``maybe_update_emission_gate_bar`` for the v444 snapshot.""" + """Mirror ``maybe_update_emission_gate_bar`` for the v445 snapshot.""" positive = sorted((share for share in demand_shares if share > 0), reverse=True) if not positive: return 0.0 @@ -219,18 +232,28 @@ def apply_shares( gate_bar: float | None = None, gate_exponent: float = DEFAULT_EMISSION_GATE_EXPONENT, ) -> float: - """Apply v444 pure-price demand shares, the Hill gate, and enable flags.""" + """Apply price shares, miner-burn scaling, the Hill gate, and enable flags.""" price_sum = sum(max(subnet["emaPrice"], 0.0) for subnet in subnets) demand_shares = [ max(subnet["emaPrice"], 0.0) / price_sum if price_sum > 0 else 0.0 for subnet in subnets ] + burn_weights = [ + share * (1.0 - min(max(subnet["minerBurned"], 0.0), 1.0)) + for subnet, share in zip(subnets, demand_shares) + ] + burn_weight_sum = sum(burn_weights) + burn_adjusted_shares = ( + [weight / burn_weight_sum for weight in burn_weights] + if burn_weight_sum > 0 + else demand_shares + ) if gate_bar is None: - gate_bar = select_emission_gate_bar(demand_shares) + gate_bar = select_emission_gate_bar(burn_adjusted_shares) gate_factors = [] gated_weights = [] - for share in demand_shares: + for share in burn_adjusted_shares: gate = ( 1.0 / (1.0 + (gate_bar / share) ** gate_exponent) if share > 0 and gate_bar > 0 @@ -240,15 +263,15 @@ def apply_shares( gated_weights.append(share * gate) if sum(gated_weights) == 0: - gated_weights = demand_shares + gated_weights = burn_adjusted_shares enabled_total = sum( weight for subnet, weight in zip(subnets, gated_weights) if subnet["emissionEnabled"] ) - for subnet, demand_share, gate, weight in zip( - subnets, demand_shares, gate_factors, gated_weights + for subnet, demand_share, burn_adjusted_share, gate, weight in zip( + subnets, demand_shares, burn_adjusted_shares, gate_factors, gated_weights ): share = ( weight / enabled_total @@ -256,6 +279,7 @@ def apply_shares( else 0.0 ) subnet["demandShare"] = round(demand_share, 8) + subnet["burnAdjustedShare"] = round(burn_adjusted_share, 8) subnet["gateFactor"] = round(gate, 8) subnet["taoShare"] = round(share, 8) subnet["taoPerBlock"] = round(block_emission * share, 8) @@ -283,6 +307,7 @@ async def build_snapshot() -> dict: row_from_tmc( row, chain["subnetFlags"][int(row["netuid"])]["emissionEnabled"], + chain["subnetFlags"][int(row["netuid"])]["minerBurned"], ) for row in eligible_rows ] @@ -291,7 +316,7 @@ async def build_snapshot() -> dict: "quantile": DEFAULT_EMISSION_BAR_QUANTILE, "exponent": DEFAULT_EMISSION_GATE_EXPONENT, "bar": None, - "source": "v444_defaults_recomputed", + "source": "v445_defaults_recomputed", } gate_bar = apply_shares( subnets, @@ -312,7 +337,7 @@ async def build_snapshot() -> dict: "fetchedAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), "network": "finney", "chainSpecVersion": chain["specVersion"], - "emissionMode": "v444_price_ema_hill_gate", + "emissionMode": "price_ema_miner_burn_hill_gate", "emissionGateSource": gate["source"], "dataSource": { "subnets": "taomarketcap.com", @@ -334,6 +359,7 @@ async def build_snapshot() -> dict: { "netuid": subnet["netuid"], "emaPrice": subnet["emaPrice"], + "minerBurned": subnet["minerBurned"], "emissionEnabled": subnet["emissionEnabled"], } for subnet in subnets diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.tsx index 019c139935..1e0aa12c5c 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.tsx @@ -23,14 +23,14 @@ type Release = { // Newest first. Add new releases to the top. const releases: Release[] = [ { - tag: 'v444', + tag: 'v445', date: 'August 2026', - title: 'Pure Price Emissions', + title: 'EVM, btcli, and Reliability', summary: - 'Subnet emission returns to pure price EMA through the gate. The release also completes ' + - 'the typed EVM surface, makes multisigs first-class btcli wallets, adds human-readable ' + + 'This release completes the typed EVM surface, makes multisigs first-class btcli wallets, ' + + 'recycles transaction fees, adds human-readable ' + 'Ledger orders, and lands a broad reliability pass.', - href: '/releases/v444-upgrade', + href: '/releases/v445-upgrade', }, { tag: 'v441', diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v444-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v445-upgrade/page.tsx similarity index 76% rename from website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v444-upgrade/page.tsx rename to website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v445-upgrade/page.tsx index 089af21603..b9e7816f02 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v444-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v445-upgrade/page.tsx @@ -6,12 +6,11 @@ import {Suspense} from 'react'; import styles from '../v436-upgrade/page.module.css'; export const metadata: Metadata = { - title: 'The V444 Upgrade — Pure Price Emissions', + title: 'The V445 Upgrade — EVM, btcli, and Reliability', description: - 'Subnet emission returns to pure price EMA through the emission gate, while v444 ' + - 'completes the EVM surface, makes btcli safer for multisigs and automation, adds ' + + 'V445 completes the EVM surface, makes btcli safer for multisigs and automation, adds ' + 'human-readable Ledger orders, and lands a broad reliability pass.', - alternates: {canonical: '/releases/v444-upgrade'}, + alternates: {canonical: '/releases/v445-upgrade'}, }; const DocLink = ({href, children}: {href: string; children: React.ReactNode}) => ( @@ -25,87 +24,23 @@ const page = () => { }>
-

The V444 Upgrade

+

The V445 Upgrade

- Pure Price Emissions · August 2026 + EVM, btcli, and Reliability · August 2026

Introduction

- The market signal becomes simple again in spec 444: a subnet's - share of network emission is determined by its moving price, passed through the emission - gate. The share is no longer reduced when a subnet directs miner incentive to an owner - or burn hotkey. Recycling and burning still do exactly what the subnet chose locally; - they no longer change its standing against every other subnet. -

-

- The release also makes the chain substantially easier to use from every external - surface. Solidity contracts gain 31 functions across five new Bittensor precompiles, - plus 135 additions to existing interfaces. A saved multisig now behaves like a wallet - throughout btcli. Automated dry runs carry enough information to approve - and replay a transaction safely. Ledger users can read the actual fields of a limit - order before signing it. Underneath those interfaces, v444 corrects transaction-pool - validation, proxy charging, commitment cleanup, storage growth, and GRANDPA finality. -

-
- -
-

Price is the signal

-

- The emission gate introduced in v440{' '} - starts with each subnet's share of price EMA, then suppresses emission below the - market-set bar. A second factor had been applied before that gate:{' '} - 1 − MinerBurned. That meant two subnets with the same demand could receive - different cross-network emission solely because one withheld miner incentive for - recycling or burning. -

- -

- V444 removes that extra multiplier. The on-chain value remains as an informational - measure called MinerBurned. The miner-incentive path still recycles or - burns according to the subnet's configuration. What changes is the boundary between - local token policy and network allocation: demand determines how much emission a subnet - earns; the subnet determines what it does with the miner portion after that. -

- - - - - - - - - - - - - - - - - - - - - - - - - -
Miner incentive policyEffect on subnet's network share in v444Local effect
Paid to minersPrice EMA through the gateMiner alpha is distributed
Withheld and recycledPrice EMA through the gateValue returns through the recycle path
Withheld and burnedPrice EMA through the gateValue is removed by the burn path
-

- Subnet teams do not need to change a setting. Forecasting software should remove the - miner-burn factor from cross-subnet share calculations and retain it only where it - describes the subnet's own incentive accounting. + Spec 445 makes the chain substantially easier to use from every + external surface. Solidity contracts gain 31 functions across five new Bittensor + precompiles, plus 135 additions to existing interfaces. A saved multisig now behaves + like a wallet throughout btcli. Automated dry runs carry enough information + to approve and replay a transaction safely. Ledger users can read the actual fields of a + limit order before signing it. Underneath those interfaces, v445 recycles transaction + fees and corrects transaction-pool validation, proxy charging, commitment cleanup, + storage growth, and GRANDPA finality.

@@ -197,13 +132,13 @@ IRuntimeConfiguration config = uint256 chainId = config.getEvmChainId();`} />

- In v444, only the registry's isDisabled field is populated. Its + In v445, only the registry's isDisabled field is populated. Its selector parameter and selector-lifecycle fields are reserved for a future extension and must not be used to infer whether a selector exists.

Canonical Solidity interfaces, JSON ABIs, generated Python ABI copies, documentation, - gas accounting, and tests ship together. Integrators should use the v444 copies rather + gas accounting, and tests ship together. Integrators should use the v445 copies rather than reconstructing selectors from release notes. The new maintainer documentation also fixes the rules for ABI versioning, state exposure, lifecycle metadata, coverage, and backwards compatibility so later runtime releases can extend this surface without @@ -273,7 +208,7 @@ btcli wallet transfer --dest 5F... --amount-tao 10 -w team-treasury`}

Read the order before signing it

V438 let Ledger and compatible signers - authorize limit orders by signing a wrapped order hash. V444 adds an alternative + authorize limit orders by signing a wrapped order hash. V445 adds an alternative clear-signing form: one canonical printable message containing the order type, amount, subnet, limit or trigger price, expiry, hotkey, relayer policy, fee, slippage, chain ID, partial-fill policy, and signer. The hardware wallet displays those fields before @@ -310,7 +245,7 @@ partial fills , signer `} Area - Change in v444 + Change in v445 @@ -378,20 +313,15 @@ partial fills , signer `}

What to do

    -
  • - Subnet teams and analysts: remove 1 − MinerBurned from - cross-subnet emission forecasts. The emission gate remains active and miner recycling - or burning remains a local policy. -
  • EVM integrators: refresh the complete canonical ABI set before using - v444 selectors. Add 0x…080f through 0x…0813 only from the + v445 selectors. Add 0x…080f through 0x…0813 only from the published interfaces, and use the registry to inspect whole-precompile availability. Re-estimate aggregate staking reads and do not rely on fixed gas stipends.
  • SDK and CLI users: older clients that read current chain metadata can - keep using existing commands. To use the new v444 features, upgrade to{' '} + keep using existing commands. To use the new v445 features, upgrade to{' '} bittensor 11.1.0 and bittensor-core 0.1.3 alongside the runtime upgrade. Existing wallet files remain usable, and saved multisigs can now be passed directly as -w. Rebuild any offline signing payload prepared @@ -405,7 +335,7 @@ partial fills , signer `}

Signers: after the release train proposes, use{' '} - btcli upgrade sign --url <v444 release URL> -w <wallet>. + btcli upgrade sign --url <v445 release URL> -w <wallet>.

diff --git a/website/apps/bittensor-website/src/components/docs/emission-network-snapshot.tsx b/website/apps/bittensor-website/src/components/docs/emission-network-snapshot.tsx index 55fb849a0a..61364612d0 100644 --- a/website/apps/bittensor-website/src/components/docs/emission-network-snapshot.tsx +++ b/website/apps/bittensor-website/src/components/docs/emission-network-snapshot.tsx @@ -48,8 +48,8 @@ export function EmissionNetworkSnapshot() {

This preview starts with each eligible subnet's{' '} SubnetMovingPrice (EMA of spot alpha price, - capped at 1.0), turns it into a share of the total, then applies the emission gate.{' '} - MinerBurned is not part of this cross-subnet allocation. Top modeled recipient:{' '} + capped at 1.0), scales it by 1 − MinerBurned, then applies the emission gate. + Top modeled recipient:{' '} SN{snapshot.topSubnets[0]?.netuid} {snapshot.topSubnets[0]?.name} {' '} diff --git a/website/apps/bittensor-website/src/components/docs/subnet-emission-share-chart.tsx b/website/apps/bittensor-website/src/components/docs/subnet-emission-share-chart.tsx index f6143e7915..b7e2008648 100644 --- a/website/apps/bittensor-website/src/components/docs/subnet-emission-share-chart.tsx +++ b/website/apps/bittensor-website/src/components/docs/subnet-emission-share-chart.tsx @@ -34,6 +34,7 @@ export function SubnetEmissionShareChart() { const {snapshot, loading} = useEmissionSnapshot(); const [selectedIdx, setSelectedIdx] = useState(2); const [whatIfEma, setWhatIfEma] = useState(null); + const [whatIfBurn, setWhatIfBurn] = useState(null); const rows = useMemo(() => snapshot.topSubnets.slice(0, 8), [snapshot.topSubnets]); const selected = rows[selectedIdx] ?? rows[0]; @@ -43,10 +44,13 @@ export function SubnetEmissionShareChart() { ...input, emaPrice: input.netuid === selected?.netuid && whatIfEma !== null ? whatIfEma : input.emaPrice, + minerBurned: + input.netuid === selected?.netuid && whatIfBurn !== null ? whatIfBurn : input.minerBurned, })); const result = subnetEmissionShares( inputs.map((input) => input.emaPrice), { + minerBurned: inputs.map((input) => input.minerBurned), emissionEnabled: inputs.map((input) => input.emissionEnabled), rank: snapshot.emissionGateRank, quantile: snapshot.emissionGateQuantile, @@ -56,8 +60,9 @@ export function SubnetEmissionShareChart() { ); const indexByNetuid = new Map(inputs.map((input, index) => [input.netuid, index])); const priceByNetuid = new Map(inputs.map((input) => [input.netuid, input.emaPrice])); - return {indexByNetuid, priceByNetuid, ...result}; - }, [selected?.netuid, snapshot, whatIfEma]); + const burnByNetuid = new Map(inputs.map((input) => [input.netuid, input.minerBurned])); + return {indexByNetuid, priceByNetuid, burnByNetuid, ...result}; + }, [selected?.netuid, snapshot, whatIfBurn, whatIfEma]); const shares = useMemo( () => rows.map((row) => { @@ -132,6 +137,7 @@ export function SubnetEmissionShareChart() { `${ctx.parsed.x.toFixed(1)}% of ${formatTao(blockEmission)}/block`, `${formatTao(tao, 4)}/block`, `Price EMA ${calculation.priceByNetuid.get(row.netuid)?.toFixed(4) ?? '0.0000'}`, + `MinerBurned ${formatPct(calculation.burnByNetuid.get(row.netuid) ?? 0)}`, ]; }, }, @@ -158,6 +164,7 @@ export function SubnetEmissionShareChart() { if (elements[0]) { setSelectedIdx(elements[0].index); setWhatIfEma(null); + setWhatIfBurn(null); } }, }), @@ -165,16 +172,17 @@ export function SubnetEmissionShareChart() { ); const displayEma = whatIfEma ?? selected?.emaPrice ?? 0; + const displayBurn = whatIfBurn ?? selected?.minerBurned ?? 0; const selectedInputIdx = selected ? calculation.indexByNetuid.get(selected.netuid) : undefined; - const selectedDemandShare = - selectedInputIdx === undefined ? 0 : calculation.demandShares[selectedInputIdx]; + const selectedBurnAdjustedShare = + selectedInputIdx === undefined ? 0 : calculation.burnAdjustedShares[selectedInputIdx]; const selectedGateFactor = selectedInputIdx === undefined ? 0 : calculation.gateFactors[selectedInputIdx]; return (

{loading ? ( @@ -188,7 +196,7 @@ export function SubnetEmissionShareChart() { {selected && ( <> -
+
+
-
+
setWhatIfEma(v)} /> + setWhatIfBurn(v)} + />
)} diff --git a/website/apps/bittensor-website/src/lib/emission-math.ts b/website/apps/bittensor-website/src/lib/emission-math.ts index baea8d5e4f..18ad5d4924 100644 --- a/website/apps/bittensor-website/src/lib/emission-math.ts +++ b/website/apps/bittensor-website/src/lib/emission-math.ts @@ -47,6 +47,7 @@ export function halvingThresholdsTao(count = 8): number[] { export type SubnetEmissionResult = { demandShares: number[]; + burnAdjustedShares: number[]; gateFactors: number[]; shares: number[]; gateBar: number; @@ -70,10 +71,11 @@ export function selectEmissionGateBar( return positive[positive.length - 1]; } -/** v444 `get_shares` plus emission-enabled redistribution in subnet_emissions.rs */ +/** `get_shares` plus emission-enabled redistribution in subnet_emissions.rs */ export function subnetEmissionShares( prices: number[], options: { + minerBurned?: number[]; emissionEnabled?: boolean[]; rank?: number; quantile?: number; @@ -84,18 +86,25 @@ export function subnetEmissionShares( const safePrices = prices.map((price) => Math.max(price, 0)); const priceSum = safePrices.reduce((sum, price) => sum + price, 0); const demandShares = safePrices.map((price) => (priceSum > 0 ? price / priceSum : 0)); + const minerBurned = options.minerBurned ?? prices.map(() => 0); + const burnWeights = demandShares.map( + (share, index) => share * (1 - Math.min(Math.max(minerBurned[index] ?? 0, 0), 1)), + ); + const burnWeightSum = burnWeights.reduce((sum, weight) => sum + weight, 0); + const burnAdjustedShares = + burnWeightSum > 0 ? burnWeights.map((weight) => weight / burnWeightSum) : demandShares; const gateBar = - options.gateBar ?? selectEmissionGateBar(demandShares, options.rank, options.quantile); + options.gateBar ?? selectEmissionGateBar(burnAdjustedShares, options.rank, options.quantile); const exponent = options.exponent ?? DEFAULT_EMISSION_GATE_EXPONENT; - const gateFactors = demandShares.map((share) => { + const gateFactors = burnAdjustedShares.map((share) => { if (share <= 0) return 0; if (gateBar <= 0) return 1; return 1 / (1 + (gateBar / share) ** exponent); }); - let gatedWeights = demandShares.map((share, index) => share * gateFactors[index]); + let gatedWeights = burnAdjustedShares.map((share, index) => share * gateFactors[index]); if (gatedWeights.reduce((sum, weight) => sum + weight, 0) === 0) { - gatedWeights = demandShares; + gatedWeights = burnAdjustedShares; } const emissionEnabled = options.emissionEnabled ?? prices.map(() => true); @@ -107,7 +116,7 @@ export function subnetEmissionShares( emissionEnabled[index] !== false && enabledTotal > 0 ? weight / enabledTotal : 0, ); - return {demandShares, gateFactors, shares, gateBar}; + return {demandShares, burnAdjustedShares, gateFactors, shares, gateBar}; } /** `update_moving_price` smoothing factor in stake_utils.rs */ diff --git a/website/apps/bittensor-website/src/lib/emission-snapshot.ts b/website/apps/bittensor-website/src/lib/emission-snapshot.ts index 53abdb2b74..112d7468d8 100644 --- a/website/apps/bittensor-website/src/lib/emission-snapshot.ts +++ b/website/apps/bittensor-website/src/lib/emission-snapshot.ts @@ -6,11 +6,13 @@ export type SubnetEmissionRow = { name: string; spotPrice: number; emaPrice: number; + minerBurned: number; emissionEnabled: boolean; taoIn: number; alphaIn: number; alphaOut: number; demandShare: number; + burnAdjustedShare: number; gateFactor: number; taoShare: number; taoPerBlock: number; @@ -19,6 +21,7 @@ export type SubnetEmissionRow = { export type EmissionInput = { netuid: number; emaPrice: number; + minerBurned: number; emissionEnabled: boolean; }; @@ -26,8 +29,8 @@ export type EmissionSnapshot = { fetchedAt: string; network: string; chainSpecVersion: number; - emissionMode: 'v444_price_ema_hill_gate' | string; - emissionGateSource: 'chain_storage' | 'v444_defaults_recomputed'; + emissionMode: 'price_ema_miner_burn_hill_gate' | string; + emissionGateSource: 'chain_storage' | 'v445_defaults_recomputed'; blockEmissionTao: number; totalIssuanceTao: number; totalIssuanceRao?: number; From 9d162dc10a93e0cb36b63597fdb18340e21eb48d Mon Sep 17 00:00:00 2001 From: UnarbosFour Date: Wed, 12 Aug 2026 17:03:13 -0400 Subject: [PATCH 7/7] Handle registration counters for subnets 116, 92, 40, 16, 58, 99, 90, 86, 103, 70 --- ...te_rebase_recycled_alpha_asset_counters.rs | 52 +++++++++++++++---- pallets/subtensor/src/tests/migration.rs | 20 +++++++ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/pallets/subtensor/src/migrations/migrate_rebase_recycled_alpha_asset_counters.rs b/pallets/subtensor/src/migrations/migrate_rebase_recycled_alpha_asset_counters.rs index fd58bdfb06..7b2910d3f1 100644 --- a/pallets/subtensor/src/migrations/migrate_rebase_recycled_alpha_asset_counters.rs +++ b/pallets/subtensor/src/migrations/migrate_rebase_recycled_alpha_asset_counters.rs @@ -11,17 +11,44 @@ pub(crate) const MIGRATION_NAME: &[u8] = b"migrate_rebase_recycled_alpha_asset_c /// `(netuid, current_generation_registered_at, issuance, burned, recycled)` offsets /// inherited from a prior subnet generation. /// -/// These values were read from mainnet archive state immediately before each current -/// generation was registered. Eight other historically recycled netuids had zeroes for -/// all three counters at their generation boundary and therefore need no correction. +/// These values were read from mainnet archive state at the end of the block in which +/// each current generation was registered. This captures old-generation emission and +/// recycling performed earlier in that block, while the registration itself did not +/// reset these counters. The registration-block guard prevents applying an offset after +/// the netuid is recycled again. pub(crate) const RECYCLED_ALPHA_COUNTER_OFFSETS: &[(u16, u64, u64, u64, u64)] = &[ - (40, 8_409_860, 126_075_000_000_000, 51_803_789_083_976, 0), - (16, 8_460_646, 176_861_000_000_000, 42_912_779_090_897, 0), - (58, 8_511_017, 227_232_000_000_000, 93_219_350_226_399, 0), + (116, 8_294_730, 10_946_000_000_000, 0, 0), + (92, 8_352_006, 68_575_136_222_730, 0, 0), + (40, 8_409_860, 126_076_000_000_000, 51_803_789_083_976, 0), + (16, 8_460_646, 176_862_000_000_000, 42_912_779_090_897, 0), + (58, 8_511_017, 227_233_000_000_000, 93_219_350_226_399, 0), + ( + 99, + 8_572_056, + 317_241_269_686_137, + 591_633_301_496, + 1_518_370_122_169, + ), + ( + 90, + 8_618_670, + 345_144_192_991_428, + 116_392_269_522_858, + 80_950_014_273, + ), + (86, 8_693_284, 415_723_558_271_648, 167_757_356_198_720, 0), + (103, 8_762_380, 529_768_402_624_963, 78_741_954_591_065, 0), + ( + 70, + 8_825_571, + 589_757_917_904_445, + 148_169_094_182, + 3_904_269_338_445, + ), ]; -/// Removes prior-generation offsets from alpha-asset counters for the three affected -/// mainnet netuids while preserving everything accumulated by their current generations. +/// Removes prior-generation offsets from alpha-asset counters for affected mainnet +/// netuids while preserving everything accumulated by their current generations. pub fn migrate_rebase_recycled_alpha_asset_counters() -> Weight { let migration_name = MIGRATION_NAME.to_vec(); let mut weight = T::DbWeight::get().reads(1); @@ -66,8 +93,13 @@ pub fn migrate_rebase_recycled_alpha_asset_counters() -> Weight { AlphaBalance::from(burned), AlphaBalance::from(recycled), ); - // Every embedded row has non-zero issuance and burned offsets; recycled is zero. - weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); + let updated_counters = [issuance, burned, recycled] + .into_iter() + .filter(|offset| *offset != 0) + .count() as u64; + weight.saturating_accrue( + T::DbWeight::get().reads_writes(updated_counters, updated_counters), + ); corrected = corrected.saturating_add(1); } } else { diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index 5c64be92d8..82e1261a9a 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -6782,6 +6782,26 @@ fn test_migrate_recycled_alpha_counters_skips_wrong_generation() { }); } +#[test] +fn test_recycled_alpha_counter_offsets_cover_current_registrations() { + use crate::migrations::migrate_rebase_recycled_alpha_asset_counters::RECYCLED_ALPHA_COUNTER_OFFSETS; + + assert_eq!(RECYCLED_ALPHA_COUNTER_OFFSETS.len(), 10); + assert!( + RECYCLED_ALPHA_COUNTER_OFFSETS + .windows(2) + .all(|rows| rows[0].1 < rows[1].1), + "counter offsets must be ordered by registration block" + ); + assert!(RECYCLED_ALPHA_COUNTER_OFFSETS.contains(&( + 90, + 8_618_670, + 345_144_192_991_428, + 116_392_269_522_858, + 80_950_014_273, + ))); +} + #[test] fn test_migrate_backfills_historical_alpha_burns_by_generation() { use crate::migrations::migrate_backfill_historical_alpha_burned::{