diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index a910f7c517..db25a9df68 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -356,7 +356,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = 10; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); - pub const MaxEpochsPerBlock: u32 = 32; + pub const MaxEpochsPerBlock: u8 = 32; } impl pallet_subtensor::Config for Test { @@ -440,7 +440,7 @@ impl pallet_subtensor::Config for Test { type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; - type MaxEpochsPerBlock = MaxEpochsPerBlock; + type InitialMaxEpochsPerBlock = MaxEpochsPerBlock; type WeightInfo = (); } diff --git a/eco-tests/src/mock.rs b/eco-tests/src/mock.rs index e4fc547789..d605f47a65 100644 --- a/eco-tests/src/mock.rs +++ b/eco-tests/src/mock.rs @@ -244,7 +244,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = 10; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); - pub const MaxEpochsPerBlock: u32 = 32; + pub const MaxEpochsPerBlock: u8 = 32; } impl pallet_subtensor::Config for Test { @@ -327,7 +327,7 @@ impl pallet_subtensor::Config for Test { type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; - type MaxEpochsPerBlock = MaxEpochsPerBlock; + type InitialMaxEpochsPerBlock = MaxEpochsPerBlock; type WeightInfo = (); type AlphaAssets = AlphaAssets; } diff --git a/pallets/admin-utils/src/benchmarking.rs b/pallets/admin-utils/src/benchmarking.rs index 0f4928237c..d1a373d791 100644 --- a/pallets/admin-utils/src/benchmarking.rs +++ b/pallets/admin-utils/src/benchmarking.rs @@ -689,5 +689,16 @@ mod benchmarks { ); /* sudo_set_min_non_immune_uids() */ } + #[benchmark] + fn sudo_set_max_epochs_per_block() { + #[extrinsic_call] + _(RawOrigin::Root, 8u8); + + assert_eq!( + pallet_subtensor::Pallet::::get_max_epochs_per_block(), + 8u8 + ); + } + impl_benchmark_test_suite!(AdminUtils, mock::new_test_ext(), mock::Test); } diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index ac9f239f3c..0e63a1f05a 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -2279,6 +2279,20 @@ pub mod pallet { Ok(()) } + + /// Sets the per-block cap on subnet epochs (dynamic tempo throttle). + #[pallet::call_index(96)] + #[pallet::weight(::WeightInfo::sudo_set_max_epochs_per_block())] + pub fn sudo_set_max_epochs_per_block( + origin: OriginFor, + max_epochs_per_block: u8, + ) -> DispatchResult { + ensure_root(origin)?; + ensure!(max_epochs_per_block >= 1, Error::::ValueNotInBounds); + pallet_subtensor::Pallet::::set_max_epochs_per_block(max_epochs_per_block); + + Ok(()) + } } } diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 2534c719f4..e3b658a0a8 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -165,7 +165,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = 0; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); - pub const MaxEpochsPerBlock: u32 = 32; + pub const MaxEpochsPerBlock: u8 = 32; } impl pallet_subtensor::Config for Test { @@ -249,7 +249,7 @@ impl pallet_subtensor::Config for Test { type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; - type MaxEpochsPerBlock = MaxEpochsPerBlock; + type InitialMaxEpochsPerBlock = MaxEpochsPerBlock; type WeightInfo = (); } diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index de49e7651d..735e784ad8 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -1498,6 +1498,79 @@ fn test_sudo_set_coldkey_swap_reannouncement_delay() { }); } +#[test] +fn test_sudo_set_max_epochs_per_block() { + new_test_ext().execute_with(|| { + let root = RuntimeOrigin::root(); + let non_root = RuntimeOrigin::signed(U256::from(1)); + let init_value = SubtensorModule::get_max_epochs_per_block(); + let to_be_set: u8 = init_value.saturating_add(3); + + // Non-root is rejected and leaves the value untouched. + assert_noop!( + AdminUtils::sudo_set_max_epochs_per_block(non_root, to_be_set), + DispatchError::BadOrigin + ); + assert_eq!(SubtensorModule::get_max_epochs_per_block(), init_value); + + // Zero is rejected by the `>= 1` guard (a zero cap would halt all subnet epochs). + assert_noop!( + AdminUtils::sudo_set_max_epochs_per_block(root.clone(), 0u8), + Error::::ValueNotInBounds + ); + assert_eq!(SubtensorModule::get_max_epochs_per_block(), init_value); + + // Root succeeds: storage is updated and the event is emitted. + assert_ok!(AdminUtils::sudo_set_max_epochs_per_block(root, to_be_set)); + assert_eq!(SubtensorModule::get_max_epochs_per_block(), to_be_set); + System::assert_last_event(Event::MaxEpochsPerBlockSet(to_be_set).into()); + }); +} + +#[test] +fn test_sudo_set_max_epochs_per_block_changes_deferrals() { + new_test_ext().execute_with(|| { + let root = RuntimeOrigin::root(); + + // Create several subnets and force each to be "due this block". + let created: u16 = 4; + for i in 0..created { + let netuid = NetUid::from(i + 1); + add_network(netuid, 100 /*tempo*/); + pallet_subtensor::PendingEpochAt::::insert(netuid, 1); + } + + let block = SubtensorModule::get_current_block_as_u64(); + let subnets: Vec = SubtensorModule::get_all_subnet_netuids() + .into_iter() + .filter(|x| *x != NetUid::ROOT) + .collect(); + let due = subnets + .iter() + .filter(|n| SubtensorModule::should_run_epoch(**n, block)) + .count(); + assert!(due >= created as usize); + + // Tight cap (1): every due subnet beyond the first is deferred. + assert_ok!(AdminUtils::sudo_set_max_epochs_per_block(root.clone(), 1u8)); + let deferred_tight = SubtensorModule::epochs_deferred_this_block(&subnets, block).len(); + assert_eq!(deferred_tight, due.saturating_sub(1)); + + // Raising the cap above the due count clears all deferrals — proving the + // admin-set cap directly drives which epochs are deferred. + assert_ok!(AdminUtils::sudo_set_max_epochs_per_block( + root, + (due as u8).saturating_add(2) + )); + let deferred_loose = SubtensorModule::epochs_deferred_this_block(&subnets, block).len(); + assert_eq!(deferred_loose, 0); + assert!( + deferred_loose < deferred_tight, + "raising MaxEpochsPerBlock must defer fewer epochs" + ); + }); +} + #[test] fn test_sudo_set_dissolve_network_schedule_duration() { new_test_ext().execute_with(|| { diff --git a/pallets/admin-utils/src/weights.rs b/pallets/admin-utils/src/weights.rs index 8320052fe1..c248b2eb57 100644 --- a/pallets/admin-utils/src/weights.rs +++ b/pallets/admin-utils/src/weights.rs @@ -66,6 +66,7 @@ pub trait WeightInfo { fn sudo_set_commit_reveal_weights_enabled() -> Weight; fn sudo_set_commit_reveal_version() -> Weight; fn sudo_set_tx_rate_limit() -> Weight; + fn sudo_set_max_epochs_per_block() -> Weight; fn sudo_set_total_issuance() -> Weight; fn sudo_set_rao_recycled() -> Weight; fn sudo_set_stake_threshold() -> Weight; @@ -575,6 +576,16 @@ impl WeightInfo for SubstrateWeight { Weight::from_parts(5_500_000, 0) .saturating_add(T::DbWeight::get().writes(1_u64)) } + /// Storage: `SubtensorModule::MaxEpochsPerBlock` (r:0 w:1) + /// Proof: `SubtensorModule::MaxEpochsPerBlock` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn sudo_set_max_epochs_per_block() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 4_000_000 picoseconds. + Weight::from_parts(4_000_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } fn sudo_set_total_issuance() -> Weight { // Proof Size summary in bytes: // Measured: `0` @@ -1407,6 +1418,16 @@ impl WeightInfo for () { Weight::from_parts(5_500_000, 0) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + /// Storage: `SubtensorModule::MaxEpochsPerBlock` (r:0 w:1) + /// Proof: `SubtensorModule::MaxEpochsPerBlock` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn sudo_set_max_epochs_per_block() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 4_000_000 picoseconds. + Weight::from_parts(4_000_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } fn sudo_set_total_issuance() -> Weight { // Proof Size summary in bytes: // Measured: `0` diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index d42f90ec98..ccdde46320 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -322,7 +322,7 @@ impl Pallet { /// Subnets whose epoch slot is due *this* block but is deferred by the per-block /// cap (`MaxEpochsPerBlock`). pub fn epochs_deferred_this_block(subnets: &[NetUid], current_block: u64) -> BTreeSet { - let cap = T::MaxEpochsPerBlock::get(); + let cap = Self::get_max_epochs_per_block() as u32; let mut deferred: BTreeSet = BTreeSet::new(); let mut epochs_run_this_block: u32 = 0; @@ -353,6 +353,7 @@ impl Pallet { > = BTreeMap::new(); // Per-block cap on number of epochs that may run; the rest are deferred 1 block forward // by setting `PendingEpochAt`. + let max_epochs_per_block = Self::get_max_epochs_per_block() as u32; let mut epochs_run_this_block: u32 = 0; for &netuid in subnets.iter() { @@ -364,7 +365,7 @@ impl Pallet { } // Per-block cap — defer if already at limit. - if epochs_run_this_block >= T::MaxEpochsPerBlock::get() { + if epochs_run_this_block >= max_epochs_per_block { let next_block = current_block.saturating_add(1); PendingEpochAt::::insert(netuid, next_block); Self::deposit_event(Event::EpochDeferred { diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 410e3bef16..bd0b4ab974 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -923,6 +923,12 @@ pub mod pallet { // T::InitialHotkeyEmissionTempo::get() // } (DEPRECATED) + /// Default per-block epoch cap, seeded from the runtime-configured initial value. + #[pallet::type_value] + pub fn DefaultMaxEpochsPerBlock() -> u8 { + T::InitialMaxEpochsPerBlock::get() + } + /// Default value for rate limiting #[pallet::type_value] pub fn DefaultTxRateLimit() -> u64 { @@ -2130,6 +2136,10 @@ pub mod pallet { DefaultRAORecycledForRegistration, >; + /// --- ITEM ( max_epochs_per_block ) + #[pallet::storage] + pub type MaxEpochsPerBlock = StorageValue<_, u8, ValueQuery, DefaultMaxEpochsPerBlock>; + /// --- ITEM ( tx_rate_limit ) #[pallet::storage] pub type TxRateLimit = StorageValue<_, u64, ValueQuery, DefaultTxRateLimit>; diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index 553451ab12..ad372d1e0e 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -282,9 +282,10 @@ mod config { /// Burn account ID #[pallet::constant] type BurnAccountId: Get; - /// Per-block cap on number of subnet epochs that may execute in a single - /// `block_step`; the rest are deferred 1 block forward via `PendingEpochAt`. + /// Initial default per-block cap on number of subnet epochs that may + /// execute in a single `block_step`; the rest are deferred 1 block forward via + /// `PendingEpochAt`. #[pallet::constant] - type MaxEpochsPerBlock: Get; + type InitialMaxEpochsPerBlock: Get; } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index b5bea2c186..adcd97e0cf 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -109,6 +109,8 @@ mod events { MaxBurnSet(NetUid, TaoBalance), /// setting min burn on a network. MinBurnSet(NetUid, TaoBalance), + /// setting the per-block epoch cap (dynamic tempo throttle). + MaxEpochsPerBlockSet(u8), /// setting the transaction rate limit. TxRateLimitSet(u64), /// setting the delegate take transaction rate limit. diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 02d1865905..4f3da7d6bb 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -4272,7 +4272,7 @@ fn test_get_subnet_terms_alpha_emissions_cap() { #[test] fn test_epochs_deferred_this_block_respects_cap() { new_test_ext(1).execute_with(|| { - let cap = ::MaxEpochsPerBlock::get() as usize; + let cap = SubtensorModule::get_max_epochs_per_block() as usize; let n = cap + 2; for i in 0..n { @@ -4311,7 +4311,7 @@ fn test_epochs_deferred_this_block_respects_cap() { #[test] fn test_reveal_crv3_defers_with_capped_epoch() { new_test_ext(1).execute_with(|| { - let cap = ::MaxEpochsPerBlock::get() as usize; + let cap = SubtensorModule::get_max_epochs_per_block() as usize; let n = cap + 2; let mec0 = subtensor_runtime_common::MechId::from(0); diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 49ec0c1638..8a813fac8d 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -257,7 +257,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = 10; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); - pub const MaxEpochsPerBlock: u32 = 32; + pub const MaxEpochsPerBlock: u8 = 32; } impl crate::Config for Test { @@ -341,7 +341,7 @@ impl crate::Config for Test { type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; - type MaxEpochsPerBlock = MaxEpochsPerBlock; + type InitialMaxEpochsPerBlock = MaxEpochsPerBlock; type WeightInfo = (); } diff --git a/pallets/subtensor/src/tests/mock_high_ed.rs b/pallets/subtensor/src/tests/mock_high_ed.rs index cb381a3f81..ac20b4131a 100644 --- a/pallets/subtensor/src/tests/mock_high_ed.rs +++ b/pallets/subtensor/src/tests/mock_high_ed.rs @@ -217,7 +217,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = 10; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); - pub const MaxEpochsPerBlock: u32 = 32; + pub const MaxEpochsPerBlock: u8 = 32; } impl crate::Config for Test { @@ -301,7 +301,7 @@ impl crate::Config for Test { type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; - type MaxEpochsPerBlock = MaxEpochsPerBlock; + type InitialMaxEpochsPerBlock = MaxEpochsPerBlock; type WeightInfo = (); } diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index a2b0fa2627..7e62e4c7ec 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -377,6 +377,15 @@ impl Pallet { // ========= Sudo ========= // ======================== + // Per-block epoch cap (dynamic tempo throttle) + pub fn get_max_epochs_per_block() -> u8 { + MaxEpochsPerBlock::::get() + } + pub fn set_max_epochs_per_block(max_epochs_per_block: u8) { + MaxEpochsPerBlock::::put(max_epochs_per_block); + Self::deposit_event(Event::MaxEpochsPerBlockSet(max_epochs_per_block)); + } + // Configure tx rate limiting pub fn get_tx_rate_limit() -> u64 { TxRateLimit::::get() diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index 703ed27ba2..7d8508c5c0 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -235,7 +235,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = 0; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); - pub const MaxEpochsPerBlock: u32 = 32; + pub const MaxEpochsPerBlock: u8 = 32; } impl pallet_subtensor::Config for Test { @@ -319,7 +319,7 @@ impl pallet_subtensor::Config for Test { type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; - type MaxEpochsPerBlock = MaxEpochsPerBlock; + type InitialMaxEpochsPerBlock = MaxEpochsPerBlock; type WeightInfo = (); } diff --git a/precompiles/src/mock.rs b/precompiles/src/mock.rs index cb7275e47c..ed8e407f88 100644 --- a/precompiles/src/mock.rs +++ b/precompiles/src/mock.rs @@ -157,7 +157,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = 0; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); - pub const MaxEpochsPerBlock: u32 = 32; + pub const MaxEpochsPerBlock: u8 = 32; } #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] @@ -483,7 +483,7 @@ impl pallet_subtensor::Config for Runtime { type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; - type MaxEpochsPerBlock = MaxEpochsPerBlock; + type InitialMaxEpochsPerBlock = MaxEpochsPerBlock; type WeightInfo = (); } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index f18065f75c..542e896369 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -234,7 +234,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: 420, + spec_version: 421, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -1032,7 +1032,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = EVM_KEY_ASSOCIATE_RATELIMIT; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); - pub const SubtensorMaxEpochsPerBlock: u32 = prod_or_fast!(2, 32); + pub const SubtensorMaxEpochsPerBlock: u8 = prod_or_fast!(2, 32); } impl pallet_subtensor::Config for Runtime { @@ -1116,7 +1116,7 @@ impl pallet_subtensor::Config for Runtime { type AuthorshipProvider = BlockAuthorFromAura; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; - type MaxEpochsPerBlock = SubtensorMaxEpochsPerBlock; + type InitialMaxEpochsPerBlock = SubtensorMaxEpochsPerBlock; type WeightInfo = pallet_subtensor::weights::SubstrateWeight; }