diff --git a/pallets/admin-utils/src/benchmarking.rs b/pallets/admin-utils/src/benchmarking.rs index 7b8124144d..48ed450d48 100644 --- a/pallets/admin-utils/src/benchmarking.rs +++ b/pallets/admin-utils/src/benchmarking.rs @@ -645,5 +645,17 @@ mod benchmarks { ); /* sudo_set_min_non_immune_uids() */ } + #[benchmark] + fn sudo_set_emissions_disabled() { + pallet_subtensor::Pallet::::set_admin_freeze_window(0); + pallet_subtensor::Pallet::::init_new_network( + 1u16.into(), /*netuid*/ + 1u16, /*tempo*/ + ); + + #[extrinsic_call] + _(RawOrigin::Root, 1u16.into()/*netuid*/, true/*disabled*/)/*sudo_set_emissions_disabled*/; + } + //impl_benchmark_test_suite!(AdminUtils, crate::mock::new_test_ext(), crate::mock::Test); } diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index eebefcf3e9..03ed2f5c06 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -2240,6 +2240,40 @@ pub mod pallet { pallet_subtensor::Pallet::::set_min_non_immune_uids(netuid, min); Ok(()) } + + /// Enables or disables emissions for a specific subnet. + /// It is only callable by the root account (sudo). + /// When emissions are disabled, the subnet will not receive any TAO emissions. + #[pallet::call_index(85)] + #[pallet::weight(( + Weight::from_parts(17_230_000, 0) + .saturating_add(::DbWeight::get().reads(2)) + .saturating_add(::DbWeight::get().writes(1)), + DispatchClass::Operational, + Pays::Yes + ))] + pub fn sudo_set_emissions_disabled( + origin: OriginFor, + netuid: NetUid, + disabled: bool, + ) -> DispatchResult { + ensure_root(origin)?; + ensure!( + pallet_subtensor::Pallet::::if_subnet_exist(netuid), + Error::::SubnetDoesNotExist + ); + + let current_value = pallet_subtensor::Pallet::::get_emissions_disabled(netuid); + if current_value == disabled { + return Ok(()); + } + + pallet_subtensor::Pallet::::set_emissions_disabled(netuid, disabled); + log::debug!( + "sudo_set_emissions_disabled( netuid: {netuid:?}, disabled: {disabled:?} ) " + ); + Ok(()) + } } } diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index 024871e60f..e6e35f2004 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -2887,3 +2887,94 @@ fn test_sudo_set_min_non_immune_uids() { assert_eq!(SubtensorModule::get_min_non_immune_uids(netuid), to_be_set); }); } + +#[test] +fn test_sudo_set_emissions_disabled() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 10); + + // Check default value is false (emissions enabled) + let init_value: bool = SubtensorModule::get_emissions_disabled(netuid); + assert!(!init_value); + + // Non-root cannot call + assert_eq!( + AdminUtils::sudo_set_emissions_disabled( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + true + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!(SubtensorModule::get_emissions_disabled(netuid), init_value); + + // Root can disable emissions + assert_ok!(AdminUtils::sudo_set_emissions_disabled( + <::RuntimeOrigin>::root(), + netuid, + true + )); + assert!(SubtensorModule::get_emissions_disabled(netuid)); + + // Root can re-enable emissions + assert_ok!(AdminUtils::sudo_set_emissions_disabled( + <::RuntimeOrigin>::root(), + netuid, + false + )); + assert!(!SubtensorModule::get_emissions_disabled(netuid)); + }); +} + +#[test] +fn test_sudo_set_emissions_disabled_subnet_not_exist() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(99); + + // Subnet does not exist + assert_err!( + AdminUtils::sudo_set_emissions_disabled( + <::RuntimeOrigin>::root(), + netuid, + true + ), + Error::::SubnetDoesNotExist + ); + }); +} + +#[test] +fn test_sudo_set_emissions_disabled_same_value() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 10); + + // Default value is false + assert!(!SubtensorModule::get_emissions_disabled(netuid)); + + // Setting to same value (false) should succeed without changing anything + assert_ok!(AdminUtils::sudo_set_emissions_disabled( + <::RuntimeOrigin>::root(), + netuid, + false + )); + assert!(!SubtensorModule::get_emissions_disabled(netuid)); + + // Now disable emissions + assert_ok!(AdminUtils::sudo_set_emissions_disabled( + <::RuntimeOrigin>::root(), + netuid, + true + )); + assert!(SubtensorModule::get_emissions_disabled(netuid)); + + // Setting to same value (true) should succeed without changing anything + assert_ok!(AdminUtils::sudo_set_emissions_disabled( + <::RuntimeOrigin>::root(), + netuid, + true + )); + assert!(SubtensorModule::get_emissions_disabled(netuid)); + }); +} diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index d508a0162b..ea505b2f43 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -314,6 +314,7 @@ impl Pallet { // --- 15. Mechanism step / emissions bookkeeping. FirstEmissionBlockNumber::::remove(netuid); + EmissionsDisabled::::remove(netuid); PendingValidatorEmission::::remove(netuid); PendingServerEmission::::remove(netuid); PendingRootAlphaDivs::::remove(netuid); diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 477a678864..31c8d9d710 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -8,11 +8,13 @@ impl Pallet { pub fn get_subnets_to_emit_to(subnets: &[NetUid]) -> Vec { // Filter out root subnet. // Filter out subnets with no first emission block number. + // Filter out subnets with emissions disabled. subnets .iter() .filter(|netuid| !netuid.is_root()) .filter(|netuid| FirstEmissionBlockNumber::::get(*netuid).is_some()) .filter(|netuid| SubtokenEnabled::::get(*netuid)) + .filter(|netuid| !EmissionsDisabled::::get(*netuid)) .filter(|&netuid| { // Only emit TAO if the subnetwork allows registration. Self::get_network_registration_allowed(*netuid) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index ce0781b536..c1373264c2 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1587,6 +1587,11 @@ pub mod pallet { pub type FirstEmissionBlockNumber = StorageMap<_, Identity, NetUid, u64, OptionQuery>; + /// --- MAP ( netuid ) --> emissions_disabled | Whether emissions are disabled for this subnet + #[pallet::storage] + pub type EmissionsDisabled = + StorageMap<_, Identity, NetUid, bool, ValueQuery, DefaultFalse>; + /// --- MAP ( netuid ) --> subnet mechanism #[pallet::storage] pub type SubnetMechanism = diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index a06e035d86..053bbf1e10 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -131,6 +131,8 @@ mod events { RegistrationAllowed(NetUid, bool), /// POW registration is allowed/disallowed for a subnet. PowRegistrationAllowed(NetUid, bool), + /// emissions are enabled/disabled for a subnet. + EmissionsDisabledSet(NetUid, bool), /// setting tempo on a network TempoSet(NetUid, u16), /// setting the RAO recycled for registration. diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index f6c92c8079..7bdb3e9948 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -4016,3 +4016,138 @@ fn test_get_subnet_terms_alpha_emissions_cap() { assert_eq!(alpha_in.get(&netuid).copied().unwrap(), tao_block_emission); }); } + +#[test] +fn test_get_subnets_to_emit_to_filters_emissions_disabled() { + new_test_ext(1).execute_with(|| { + let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); + let netuid1 = add_dynamic_network(&U256::from(3), &U256::from(4)); + + // Both subnets should be in the list initially + let subnets_to_emit_to_0 = SubtensorModule::get_subnets_to_emit_to(&[netuid0, netuid1]); + assert_eq!(subnets_to_emit_to_0.len(), 2); + assert!(subnets_to_emit_to_0.contains(&netuid0)); + assert!(subnets_to_emit_to_0.contains(&netuid1)); + + // Disable emissions for netuid0 + EmissionsDisabled::::insert(netuid0, true); + + // Check that netuid0 is not in the list + let subnets_to_emit_to_1 = SubtensorModule::get_subnets_to_emit_to(&[netuid0, netuid1]); + assert_eq!(subnets_to_emit_to_1.len(), 1); + assert!(!subnets_to_emit_to_1.contains(&netuid0)); + assert!(subnets_to_emit_to_1.contains(&netuid1)); + + // Re-enable emissions for netuid0 + EmissionsDisabled::::insert(netuid0, false); + + // Check that netuid0 is back in the list + let subnets_to_emit_to_2 = SubtensorModule::get_subnets_to_emit_to(&[netuid0, netuid1]); + assert_eq!(subnets_to_emit_to_2.len(), 2); + assert!(subnets_to_emit_to_2.contains(&netuid0)); + assert!(subnets_to_emit_to_2.contains(&netuid1)); + }); +} + +/// Test that disabled emissions do NOT accrue retroactively when re-enabled. +/// This is critical to ensure that turning emissions back on doesn't magically +/// give the subnet all the emissions it would have received while disabled. +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_emissions_disabled_no_retroactive_accrual --exact --show-output --nocapture +#[test] +fn test_emissions_disabled_no_retroactive_accrual() { + new_test_ext(1).execute_with(|| { + let netuid1 = NetUid::from(1); + let netuid2 = NetUid::from(2); + let emission_per_block: u64 = 1_000_000; + + // Set up two networks + add_network(netuid1, 1, 0); + add_network(netuid2, 1, 0); + + // Make subnets dynamic + SubnetMechanism::::insert(netuid1, 1); + SubnetMechanism::::insert(netuid2, 1); + + // Set up equal initial state for both subnets + let initial: u64 = 1_000_000; + SubnetTAO::::insert(netuid1, TaoCurrency::from(initial)); + SubnetAlphaIn::::insert(netuid1, AlphaCurrency::from(initial)); + SubnetTAO::::insert(netuid2, TaoCurrency::from(initial)); + SubnetAlphaIn::::insert(netuid2, AlphaCurrency::from(initial)); + + // Set equal TAO flows so emissions are split equally + SubnetTaoFlow::::insert(netuid1, 100_000_000_i64); + SubnetTaoFlow::::insert(netuid2, 100_000_000_i64); + + // Run coinbase once - both subnets should receive emissions + SubtensorModule::run_coinbase(U96F32::from_num(emission_per_block)); + + let alpha_after_first_emission_netuid1 = SubnetAlphaIn::::get(netuid1); + let alpha_after_first_emission_netuid2 = SubnetAlphaIn::::get(netuid2); + + // Both should have received equal emissions + assert_eq!(alpha_after_first_emission_netuid1, alpha_after_first_emission_netuid2); + assert!(u64::from(alpha_after_first_emission_netuid1) > initial); + + // Now disable emissions for netuid1 + EmissionsDisabled::::insert(netuid1, true); + + // Run coinbase multiple times while netuid1 is disabled + // netuid2 should continue to receive emissions, netuid1 should NOT + for _ in 0..5 { + SubtensorModule::run_coinbase(U96F32::from_num(emission_per_block)); + } + + let alpha_after_disabled_period_netuid1 = SubnetAlphaIn::::get(netuid1); + let alpha_after_disabled_period_netuid2 = SubnetAlphaIn::::get(netuid2); + + // netuid1 should NOT have received any additional emissions while disabled + assert_eq!( + alpha_after_disabled_period_netuid1, + alpha_after_first_emission_netuid1, + "netuid1 should not have received emissions while disabled" + ); + + // netuid2 should have received 5 more blocks of emissions + assert!( + u64::from(alpha_after_disabled_period_netuid2) > u64::from(alpha_after_first_emission_netuid2), + "netuid2 should have continued receiving emissions" + ); + + // Record the state before re-enabling + let alpha_before_reenable_netuid1 = SubnetAlphaIn::::get(netuid1); + + // Re-enable emissions for netuid1 + EmissionsDisabled::::insert(netuid1, false); + + // Run coinbase once after re-enabling + SubtensorModule::run_coinbase(U96F32::from_num(emission_per_block)); + + let alpha_after_reenable_netuid1 = SubnetAlphaIn::::get(netuid1); + + // netuid1 should have received ONLY the current block's emission share, + // NOT any retroactive emissions from the 5 blocks it was disabled + let emission_received_after_reenable = + u64::from(alpha_after_reenable_netuid1) - u64::from(alpha_before_reenable_netuid1); + + // The emission received should be roughly equal to one block's worth of emissions + // (accounting for the emission split between subnets) + // It should definitely NOT be 5x or 6x the normal emission + let one_block_emission_estimate = emission_per_block / 2; // Split between 2 subnets + + // Allow some tolerance for rounding, but ensure it's not retroactive + // If retroactive, it would be ~5x the normal emission + assert!( + emission_received_after_reenable < one_block_emission_estimate * 2, + "Emission after re-enable ({}) should be roughly one block's worth ({}), not retroactive", + emission_received_after_reenable, + one_block_emission_estimate + ); + + // Also verify it's not zero - the subnet should receive current emissions + assert!( + emission_received_after_reenable > 0, + "netuid1 should receive emissions after being re-enabled" + ); + }); +} diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 10fc0535f0..0174554c93 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -596,6 +596,15 @@ impl Pallet { Self::deposit_event(Event::PowRegistrationAllowed(netuid, registration_allowed)); } + // Emissions Disabled utils + pub fn get_emissions_disabled(netuid: NetUid) -> bool { + EmissionsDisabled::::get(netuid) + } + pub fn set_emissions_disabled(netuid: NetUid, disabled: bool) { + EmissionsDisabled::::insert(netuid, disabled); + Self::deposit_event(Event::EmissionsDisabledSet(netuid, disabled)); + } + pub fn get_target_registrations_per_interval(netuid: NetUid) -> u16 { TargetRegistrationsPerInterval::::get(netuid) }