From 9ef3f6023979b202def86a52004714dc3f877580 Mon Sep 17 00:00:00 2001 From: open-junius Date: Mon, 13 Apr 2026 16:07:48 +0800 Subject: [PATCH 01/10] fix staking approval e2e test --- contract-tests/test/staking.precompile.approval.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contract-tests/test/staking.precompile.approval.test.ts b/contract-tests/test/staking.precompile.approval.test.ts index 372e1ac661..20614ab5fe 100644 --- a/contract-tests/test/staking.precompile.approval.test.ts +++ b/contract-tests/test/staking.precompile.approval.test.ts @@ -11,6 +11,7 @@ import { forceSetBalanceToEthAddress, forceSetBalanceToSs58Address, addNewSubnetwork, burnedRegister, sendProxyCall, startCall, + getStake, } from "../src/subtensor" import { ETH_LOCAL_URL } from "../src/config"; import { ISTAKING_ADDRESS, ISTAKING_V2_ADDRESS, IStakingABI, IStakingV2ABI } from "../src/contracts/staking" @@ -67,7 +68,7 @@ describe("Test approval in staking precompile", () => { ); assert.ok(stakeFromContract > stakeBefore) - const stakeAfter = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), stakeNetuid) + const stakeAfter = await getStake(api, convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), stakeNetuid) assert.ok(stakeAfter > stakeBefore) } }) From f95609899bc4ac5798d84a041c852f37c6afb8bc Mon Sep 17 00:00:00 2001 From: Evgeny Svirsky Date: Wed, 15 Apr 2026 15:30:54 +0200 Subject: [PATCH 02/10] introduce netuid generation for storage cleaning --- pallets/subtensor/src/coinbase/root.rs | 3 ++ pallets/subtensor/src/lib.rs | 12 +++++ pallets/subtensor/src/subnets/subnet.rs | 1 + pallets/subtensor/src/tests/networks.rs | 67 +++++++++++++++++++++++++ precompiles/src/staking.rs | 47 ++++++++++++----- 5 files changed, 117 insertions(+), 13 deletions(-) diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index a4f3c0df8c..87782fe1be 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -534,6 +534,9 @@ impl Pallet { pub fn get_network_registered_block(netuid: NetUid) -> u64 { NetworkRegisteredAt::::get(netuid) } + pub fn get_netuid_generation(netuid: NetUid) -> u64 { + NetuidGeneration::::get(netuid) + } pub fn get_network_immunity_period() -> u64 { NetworkImmunityPeriod::::get() } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 94a18d7d16..6f36f3d580 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1693,6 +1693,18 @@ pub mod pallet { pub type NetworkRegisteredAt = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultNetworkRegisteredAt>; + /// --- MAP ( netuid ) --> registration_generation + /// + /// Monotonic counter incremented on every successful `do_register_network` + /// for a given netuid. Consumers that persist per-netuid state keyed by + /// `(user, netuid)` (e.g. the staking precompile `AllowancesStorage`) can + /// mix the current generation into their storage key so that entries + /// written under a previous registration of the same netuid become + /// unreachable after the netuid is re-registered, without requiring + /// unbounded storage iteration on deregistration. + #[pallet::storage] + pub type NetuidGeneration = StorageMap<_, Identity, NetUid, u64, ValueQuery>; + /// --- MAP ( netuid ) --> pending_server_emission #[pallet::storage] pub type PendingServerEmission = diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index 2f2869d4ec..77ff63a20b 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -204,6 +204,7 @@ impl Pallet { // --- 15. Set the creation terms. NetworkRegisteredAt::::insert(netuid_to_register, current_block); + NetuidGeneration::::mutate(netuid_to_register, |g| *g = g.saturating_add(1)); // --- 16. Set the symbol. let symbol = Self::get_next_available_symbol(netuid_to_register); diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 0edb743e5a..cce168c052 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -2683,3 +2683,70 @@ fn register_network_non_associated_hotkey_does_not_withdraw_or_write_owner_alpha ); }); } + +#[test] +fn netuid_generation_bumps_on_first_registration() { + new_test_ext(1).execute_with(|| { + let cold = U256::from(1); + let hot = U256::from(2); + + let netuid = add_dynamic_network(&hot, &cold); + + assert_eq!( + SubtensorModule::get_netuid_generation(netuid), + 1, + "first registration of a netuid must leave generation == 1" + ); + }); +} + +#[test] +fn netuid_generation_is_independent_per_netuid() { + new_test_ext(1).execute_with(|| { + let n1 = add_dynamic_network(&U256::from(10), &U256::from(11)); + let n2 = add_dynamic_network(&U256::from(20), &U256::from(21)); + + assert_ne!(n1, n2); + assert_eq!(SubtensorModule::get_netuid_generation(n1), 1); + assert_eq!(SubtensorModule::get_netuid_generation(n2), 1); + }); +} + +#[test] +fn netuid_generation_survives_dissolve_and_bumps_on_reregistration() { + new_test_ext(1).execute_with(|| { + // Force reuse of the same netuid on re-registration by pinning the + // active subnet cap so the next registration must prune. + SubtensorModule::set_max_subnets(2); + + let owner_cold = U256::from(100); + let owner_hot = U256::from(101); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + assert_eq!(SubtensorModule::get_netuid_generation(netuid), 1); + + // Dissolve: generation is intentionally *not* cleared — stale + // consumers can still detect the pre-dereg lifetime if they stored + // the generation they observed at approval time. + assert_ok!(SubtensorModule::do_dissolve_network(netuid)); + assert!(!SubtensorModule::if_subnet_exist(netuid)); + assert_eq!( + SubtensorModule::get_netuid_generation(netuid), + 1, + "dissolve must not clear or reset the generation" + ); + + // Re-register. With the cap pinned, the prune selector reuses the + // freed netuid; the generation bumps to 2 so that any state still + // keyed to gen=1 becomes unreachable under the new registration. + let reg_netuid = add_dynamic_network(&owner_hot, &owner_cold); + assert_eq!( + reg_netuid, netuid, + "the pruned netuid should be reused under the subnet cap" + ); + assert_eq!( + SubtensorModule::get_netuid_generation(netuid), + 2, + "re-registration must bump generation" + ); + }); +} diff --git a/precompiles/src/staking.rs b/precompiles/src/staking.rs index 30d28aaa13..0b6580fd31 100644 --- a/precompiles/src/staking.rs +++ b/precompiles/src/staking.rs @@ -66,9 +66,10 @@ pub type AllowancesStorage = StorageDoubleMap< // For each approver (EVM address as only EVM-natives need the precompile) Blake2_128Concat, H160, - // For each pair of (spender, netuid) (EVM address as only EVM-natives need the precompile) + // For each (spender, netuid, generation) triple — the generation tag invalidates + // entries written under a previous registration of the same netuid. Blake2_128Concat, - (H160, u16), + (H160, u16, u64), // Allowed amount U256, ValueQuery, @@ -480,6 +481,13 @@ where Ok(stake.to_u64().into()) } + /// Current registration generation for `netuid`, used as part of the + /// `AllowancesStorage` secondary key to invalidate approvals granted + /// for a previous registration of the same netuid. + fn current_netuid_generation(netuid: u16) -> u64 { + pallet_subtensor::Pallet::::get_netuid_generation(netuid.into()) + } + #[precompile::public("approve(address,uint256,uint256)")] fn approve( handle: &mut impl PrecompileHandle, @@ -487,17 +495,19 @@ where origin_netuid: U256, amount_alpha: U256, ) -> EvmResult<()> { - // AllowancesStorage write + // AllowancesStorage write + NetuidGeneration read + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; let approver = handle.context().caller; let spender = spender_address.0; let netuid = try_u16_from_u256(origin_netuid)?; + let generation = Self::current_netuid_generation(netuid); if amount_alpha.is_zero() { - AllowancesStorage::remove(approver, (spender, netuid)); + AllowancesStorage::remove(approver, (spender, netuid, generation)); } else { - AllowancesStorage::insert(approver, (spender, netuid), amount_alpha); + AllowancesStorage::insert(approver, (spender, netuid, generation), amount_alpha); } Ok(()) @@ -511,13 +521,18 @@ where spender_address: Address, origin_netuid: U256, ) -> EvmResult { - // AllowancesStorage read + // AllowancesStorage read + NetuidGeneration read + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; let spender = spender_address.0; let netuid = try_u16_from_u256(origin_netuid)?; + let generation = Self::current_netuid_generation(netuid); - Ok(AllowancesStorage::get(source_address.0, (spender, netuid))) + Ok(AllowancesStorage::get( + source_address.0, + (spender, netuid, generation), + )) } #[precompile::public("increaseAllowance(address,uint256,uint256)")] @@ -531,15 +546,17 @@ where return Ok(()); } - // AllowancesStorage read + write + // AllowancesStorage read + write + NetuidGeneration read + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; let approver = handle.context().caller; let spender = spender_address.0; let netuid = try_u16_from_u256(origin_netuid)?; + let generation = Self::current_netuid_generation(netuid); - let approval_key = (spender, netuid); + let approval_key = (spender, netuid, generation); let current_amount = AllowancesStorage::get(approver, approval_key); let new_amount = current_amount.saturating_add(amount_alpha_increase); @@ -560,15 +577,17 @@ where return Ok(()); } - // AllowancesStorage read + write + // AllowancesStorage read + write + NetuidGeneration read + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; let approver = handle.context().caller; let spender = spender_address.0; let netuid = try_u16_from_u256(origin_netuid)?; + let generation = Self::current_netuid_generation(netuid); - let approval_key = (spender, netuid); + let approval_key = (spender, netuid, generation); let current_amount = AllowancesStorage::get(approver, approval_key); let new_amount = current_amount.saturating_sub(amount_alpha_decrease); @@ -593,11 +612,13 @@ where return Ok(()); } - // AllowancesStorage read + write + // AllowancesStorage read + write + NetuidGeneration read + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; - let approval_key = (spender, netuid); + let generation = Self::current_netuid_generation(netuid); + let approval_key = (spender, netuid, generation); let current_amount = AllowancesStorage::get(approver, approval_key); let Some(new_amount) = current_amount.checked_sub(amount) else { From ad49ae15a79221e917fd1abbc61fa5b1e614d0f9 Mon Sep 17 00:00:00 2001 From: Evgeny Svirsky Date: Wed, 15 Apr 2026 18:34:24 +0200 Subject: [PATCH 03/10] Rename to RegisteredSubnetCounter --- pallets/subtensor/src/coinbase/root.rs | 4 +-- pallets/subtensor/src/lib.rs | 7 +++-- pallets/subtensor/src/subnets/subnet.rs | 4 ++- pallets/subtensor/src/tests/networks.rs | 34 ++++++++++----------- precompiles/src/staking.rs | 40 ++++++++++++------------- 5 files changed, 46 insertions(+), 43 deletions(-) diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 87782fe1be..ac157b2b30 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -534,8 +534,8 @@ impl Pallet { pub fn get_network_registered_block(netuid: NetUid) -> u64 { NetworkRegisteredAt::::get(netuid) } - pub fn get_netuid_generation(netuid: NetUid) -> u64 { - NetuidGeneration::::get(netuid) + pub fn get_registered_subnet_counter(netuid: NetUid) -> u64 { + RegisteredSubnetCounter::::get(netuid) } pub fn get_network_immunity_period() -> u64 { NetworkImmunityPeriod::::get() diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 6f36f3d580..003c94ef30 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1693,17 +1693,18 @@ pub mod pallet { pub type NetworkRegisteredAt = StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultNetworkRegisteredAt>; - /// --- MAP ( netuid ) --> registration_generation + /// --- MAP ( netuid ) --> registered_subnet_counter /// /// Monotonic counter incremented on every successful `do_register_network` /// for a given netuid. Consumers that persist per-netuid state keyed by /// `(user, netuid)` (e.g. the staking precompile `AllowancesStorage`) can - /// mix the current generation into their storage key so that entries + /// mix the current counter value into their storage key so that entries /// written under a previous registration of the same netuid become /// unreachable after the netuid is re-registered, without requiring /// unbounded storage iteration on deregistration. #[pallet::storage] - pub type NetuidGeneration = StorageMap<_, Identity, NetUid, u64, ValueQuery>; + pub type RegisteredSubnetCounter = + StorageMap<_, Identity, NetUid, u64, ValueQuery>; /// --- MAP ( netuid ) --> pending_server_emission #[pallet::storage] diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index 77ff63a20b..a7437e6884 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -204,7 +204,9 @@ impl Pallet { // --- 15. Set the creation terms. NetworkRegisteredAt::::insert(netuid_to_register, current_block); - NetuidGeneration::::mutate(netuid_to_register, |g| *g = g.saturating_add(1)); + RegisteredSubnetCounter::::mutate(netuid_to_register, |c| { + *c = c.saturating_add(1) + }); // --- 16. Set the symbol. let symbol = Self::get_next_available_symbol(netuid_to_register); diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index cce168c052..c5107cffc5 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -2685,7 +2685,7 @@ fn register_network_non_associated_hotkey_does_not_withdraw_or_write_owner_alpha } #[test] -fn netuid_generation_bumps_on_first_registration() { +fn registered_subnet_counter_bumps_on_first_registration() { new_test_ext(1).execute_with(|| { let cold = U256::from(1); let hot = U256::from(2); @@ -2693,27 +2693,27 @@ fn netuid_generation_bumps_on_first_registration() { let netuid = add_dynamic_network(&hot, &cold); assert_eq!( - SubtensorModule::get_netuid_generation(netuid), + SubtensorModule::get_registered_subnet_counter(netuid), 1, - "first registration of a netuid must leave generation == 1" + "first registration of a netuid must leave counter == 1" ); }); } #[test] -fn netuid_generation_is_independent_per_netuid() { +fn registered_subnet_counter_is_independent_per_netuid() { new_test_ext(1).execute_with(|| { let n1 = add_dynamic_network(&U256::from(10), &U256::from(11)); let n2 = add_dynamic_network(&U256::from(20), &U256::from(21)); assert_ne!(n1, n2); - assert_eq!(SubtensorModule::get_netuid_generation(n1), 1); - assert_eq!(SubtensorModule::get_netuid_generation(n2), 1); + assert_eq!(SubtensorModule::get_registered_subnet_counter(n1), 1); + assert_eq!(SubtensorModule::get_registered_subnet_counter(n2), 1); }); } #[test] -fn netuid_generation_survives_dissolve_and_bumps_on_reregistration() { +fn registered_subnet_counter_survives_dissolve_and_bumps_on_reregistration() { new_test_ext(1).execute_with(|| { // Force reuse of the same netuid on re-registration by pinning the // active subnet cap so the next registration must prune. @@ -2722,31 +2722,31 @@ fn netuid_generation_survives_dissolve_and_bumps_on_reregistration() { let owner_cold = U256::from(100); let owner_hot = U256::from(101); let netuid = add_dynamic_network(&owner_hot, &owner_cold); - assert_eq!(SubtensorModule::get_netuid_generation(netuid), 1); + assert_eq!(SubtensorModule::get_registered_subnet_counter(netuid), 1); - // Dissolve: generation is intentionally *not* cleared — stale - // consumers can still detect the pre-dereg lifetime if they stored - // the generation they observed at approval time. + // Dissolve: counter is intentionally *not* cleared — stale consumers + // can still detect the pre-dereg lifetime if they stored the counter + // value they observed at approval time. assert_ok!(SubtensorModule::do_dissolve_network(netuid)); assert!(!SubtensorModule::if_subnet_exist(netuid)); assert_eq!( - SubtensorModule::get_netuid_generation(netuid), + SubtensorModule::get_registered_subnet_counter(netuid), 1, - "dissolve must not clear or reset the generation" + "dissolve must not clear or reset the counter" ); // Re-register. With the cap pinned, the prune selector reuses the - // freed netuid; the generation bumps to 2 so that any state still - // keyed to gen=1 becomes unreachable under the new registration. + // freed netuid; the counter bumps to 2 so that any state still keyed + // to the prior value becomes unreachable under the new registration. let reg_netuid = add_dynamic_network(&owner_hot, &owner_cold); assert_eq!( reg_netuid, netuid, "the pruned netuid should be reused under the subnet cap" ); assert_eq!( - SubtensorModule::get_netuid_generation(netuid), + SubtensorModule::get_registered_subnet_counter(netuid), 2, - "re-registration must bump generation" + "re-registration must bump counter" ); }); } diff --git a/precompiles/src/staking.rs b/precompiles/src/staking.rs index 0b6580fd31..3392de468e 100644 --- a/precompiles/src/staking.rs +++ b/precompiles/src/staking.rs @@ -66,7 +66,7 @@ pub type AllowancesStorage = StorageDoubleMap< // For each approver (EVM address as only EVM-natives need the precompile) Blake2_128Concat, H160, - // For each (spender, netuid, generation) triple — the generation tag invalidates + // For each (spender, netuid, counter) triple — the counter tag invalidates // entries written under a previous registration of the same netuid. Blake2_128Concat, (H160, u16, u64), @@ -481,11 +481,11 @@ where Ok(stake.to_u64().into()) } - /// Current registration generation for `netuid`, used as part of the + /// Current registration counter for `netuid`, used as part of the /// `AllowancesStorage` secondary key to invalidate approvals granted /// for a previous registration of the same netuid. - fn current_netuid_generation(netuid: u16) -> u64 { - pallet_subtensor::Pallet::::get_netuid_generation(netuid.into()) + fn current_subnet_counter(netuid: u16) -> u64 { + pallet_subtensor::Pallet::::get_registered_subnet_counter(netuid.into()) } #[precompile::public("approve(address,uint256,uint256)")] @@ -495,19 +495,19 @@ where origin_netuid: U256, amount_alpha: U256, ) -> EvmResult<()> { - // AllowancesStorage write + NetuidGeneration read + // AllowancesStorage write + RegisteredSubnetCounter read handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; let approver = handle.context().caller; let spender = spender_address.0; let netuid = try_u16_from_u256(origin_netuid)?; - let generation = Self::current_netuid_generation(netuid); + let counter = Self::current_subnet_counter(netuid); if amount_alpha.is_zero() { - AllowancesStorage::remove(approver, (spender, netuid, generation)); + AllowancesStorage::remove(approver, (spender, netuid, counter)); } else { - AllowancesStorage::insert(approver, (spender, netuid, generation), amount_alpha); + AllowancesStorage::insert(approver, (spender, netuid, counter), amount_alpha); } Ok(()) @@ -521,17 +521,17 @@ where spender_address: Address, origin_netuid: U256, ) -> EvmResult { - // AllowancesStorage read + NetuidGeneration read + // AllowancesStorage read + RegisteredSubnetCounter read handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; let spender = spender_address.0; let netuid = try_u16_from_u256(origin_netuid)?; - let generation = Self::current_netuid_generation(netuid); + let counter = Self::current_subnet_counter(netuid); Ok(AllowancesStorage::get( source_address.0, - (spender, netuid, generation), + (spender, netuid, counter), )) } @@ -546,7 +546,7 @@ where return Ok(()); } - // AllowancesStorage read + write + NetuidGeneration read + // AllowancesStorage read + write + RegisteredSubnetCounter read handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; @@ -554,9 +554,9 @@ where let approver = handle.context().caller; let spender = spender_address.0; let netuid = try_u16_from_u256(origin_netuid)?; - let generation = Self::current_netuid_generation(netuid); + let counter = Self::current_subnet_counter(netuid); - let approval_key = (spender, netuid, generation); + let approval_key = (spender, netuid, counter); let current_amount = AllowancesStorage::get(approver, approval_key); let new_amount = current_amount.saturating_add(amount_alpha_increase); @@ -577,7 +577,7 @@ where return Ok(()); } - // AllowancesStorage read + write + NetuidGeneration read + // AllowancesStorage read + write + RegisteredSubnetCounter read handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; @@ -585,9 +585,9 @@ where let approver = handle.context().caller; let spender = spender_address.0; let netuid = try_u16_from_u256(origin_netuid)?; - let generation = Self::current_netuid_generation(netuid); + let counter = Self::current_subnet_counter(netuid); - let approval_key = (spender, netuid, generation); + let approval_key = (spender, netuid, counter); let current_amount = AllowancesStorage::get(approver, approval_key); let new_amount = current_amount.saturating_sub(amount_alpha_decrease); @@ -612,13 +612,13 @@ where return Ok(()); } - // AllowancesStorage read + write + NetuidGeneration read + // AllowancesStorage read + write + RegisteredSubnetCounter read handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; - let generation = Self::current_netuid_generation(netuid); - let approval_key = (spender, netuid, generation); + let counter = Self::current_subnet_counter(netuid); + let approval_key = (spender, netuid, counter); let current_amount = AllowancesStorage::get(approver, approval_key); let Some(new_amount) = current_amount.checked_sub(amount) else { From de76ba9012a793ce2a5b876804b233b87de063e6 Mon Sep 17 00:00:00 2001 From: Evgeny Svirsky Date: Wed, 15 Apr 2026 18:34:50 +0200 Subject: [PATCH 04/10] fmt --- pallets/subtensor/src/lib.rs | 3 +-- pallets/subtensor/src/subnets/subnet.rs | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 003c94ef30..a1ae6cc3bb 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1703,8 +1703,7 @@ pub mod pallet { /// unreachable after the netuid is re-registered, without requiring /// unbounded storage iteration on deregistration. #[pallet::storage] - pub type RegisteredSubnetCounter = - StorageMap<_, Identity, NetUid, u64, ValueQuery>; + pub type RegisteredSubnetCounter = StorageMap<_, Identity, NetUid, u64, ValueQuery>; /// --- MAP ( netuid ) --> pending_server_emission #[pallet::storage] diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index a7437e6884..fd8b61b5dc 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -204,9 +204,7 @@ impl Pallet { // --- 15. Set the creation terms. NetworkRegisteredAt::::insert(netuid_to_register, current_block); - RegisteredSubnetCounter::::mutate(netuid_to_register, |c| { - *c = c.saturating_add(1) - }); + RegisteredSubnetCounter::::mutate(netuid_to_register, |c| *c = c.saturating_add(1)); // --- 16. Set the symbol. let symbol = Self::get_next_available_symbol(netuid_to_register); From 8d82dc68e32383a7a29ccb9a085349465b17396d Mon Sep 17 00:00:00 2001 From: Evgeny Svirsky Date: Fri, 17 Apr 2026 15:54:43 +0200 Subject: [PATCH 05/10] Fix for e2e test --- contract-tests/test/staking.precompile.approval.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract-tests/test/staking.precompile.approval.test.ts b/contract-tests/test/staking.precompile.approval.test.ts index 20614ab5fe..2717b90e5b 100644 --- a/contract-tests/test/staking.precompile.approval.test.ts +++ b/contract-tests/test/staking.precompile.approval.test.ts @@ -58,7 +58,7 @@ describe("Test approval in staking precompile", () => { stakeNetuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 // the unit in V2 is RAO, not ETH let stakeBalance = tao(20) - const stakeBefore = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), stakeNetuid) + const stakeBefore = await getStake(api, convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), stakeNetuid) const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1); const tx = await contract.addStake(hotkey.publicKey, stakeBalance.toString(), stakeNetuid) await tx.wait() From a2ab174c80cf2e7dac81081ed386a7d433636804 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 16 Apr 2026 17:16:03 +0200 Subject: [PATCH 06/10] Port address mapping precompile tests to rust --- .../test/addressMapping.precompile.test.ts | 87 ------------------- runtime/tests/precompiles.rs | 66 ++++++++++++++ 2 files changed, 66 insertions(+), 87 deletions(-) delete mode 100644 contract-tests/test/addressMapping.precompile.test.ts diff --git a/contract-tests/test/addressMapping.precompile.test.ts b/contract-tests/test/addressMapping.precompile.test.ts deleted file mode 100644 index 4f316fc57a..0000000000 --- a/contract-tests/test/addressMapping.precompile.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as assert from "assert"; -import { ethers } from "ethers"; -import { generateRandomEthersWallet } from "../src/utils"; -import { IADDRESS_MAPPING_ADDRESS, IAddressMappingABI } from "../src/contracts/addressMapping"; -import { convertH160ToPublicKey } from "../src/address-utils"; -import { u8aToHex } from "@polkadot/util"; - -describe("Test address mapping precompile", () => { - const wallet1 = generateRandomEthersWallet(); - const wallet2 = generateRandomEthersWallet(); - - it("Address mapping converts H160 to AccountId32 correctly", async () => { - const contract = new ethers.Contract( - IADDRESS_MAPPING_ADDRESS, - IAddressMappingABI, - wallet1 - ); - - // Test with wallet1's address - const evmAddress = wallet1.address; - const accountId32 = await contract.addressMapping(evmAddress); - const expectedAcccountId32 = convertH160ToPublicKey(evmAddress); - - // Verify the result is a valid bytes32 (32 bytes) - assert.ok(accountId32.length === 66, "AccountId32 should be 32 bytes (66 hex chars with 0x)"); - assert.ok(accountId32.startsWith("0x"), "AccountId32 should start with 0x"); - - // Verify it's not all zeros - assert.notEqual( - accountId32, - "0x0000000000000000000000000000000000000000000000000000000000000000", - "AccountId32 should not be all zeros" - ); - - console.log("accountId32: {}", accountId32); - console.log("expectedAcccountId32: {}", expectedAcccountId32); - - assert.equal(accountId32, u8aToHex(expectedAcccountId32), "AccountId32 should be the same as the expected AccountId32"); - }); - - it("Address mapping works with different addresses", async () => { - const contract = new ethers.Contract( - IADDRESS_MAPPING_ADDRESS, - IAddressMappingABI, - wallet1 - ); - - // Test with wallet2's address - const evmAddress1 = wallet1.address; - const evmAddress2 = wallet2.address; - - const accountId1 = await contract.addressMapping(evmAddress1); - const accountId2 = await contract.addressMapping(evmAddress2); - - // Different addresses should map to different AccountIds - assert.notEqual( - accountId1, - accountId2, - "Different EVM addresses should map to different AccountIds" - ); - - // Both should be valid bytes32 - assert.ok(accountId1.length === 66, "AccountId1 should be 32 bytes"); - assert.ok(accountId2.length === 66, "AccountId2 should be 32 bytes"); - }); - - it("Address mapping is deterministic", async () => { - const contract = new ethers.Contract( - IADDRESS_MAPPING_ADDRESS, - IAddressMappingABI, - wallet1 - ); - - const evmAddress = wallet1.address; - - // Call multiple times with the same address - const accountId1 = await contract.addressMapping(evmAddress); - const accountId2 = await contract.addressMapping(evmAddress); - - // All calls should return the same result - assert.equal( - accountId1, - accountId2, - "First and second calls should return the same AccountId" - ); - }); -}); diff --git a/runtime/tests/precompiles.rs b/runtime/tests/precompiles.rs index 815d055ab7..44caeb661b 100644 --- a/runtime/tests/precompiles.rs +++ b/runtime/tests/precompiles.rs @@ -99,6 +99,72 @@ mod address_mapping { .execute_returns_raw(expected_output.to_vec()); }); } + + #[test] + fn address_mapping_precompile_maps_distinct_addresses_to_distinct_accounts() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(1); + let first_address = addr_from_index(0x1234); + let second_address = addr_from_index(0x5678); + let precompile_addr = addr_from_index(AddressMappingPrecompile::::INDEX); + + let first_output = execute_precompile( + &Precompiles::::new(), + precompile_addr, + caller, + address_mapping_call_data(first_address), + U256::zero(), + ) + .expect("expected precompile mapping call to be routed to a precompile") + .expect("address mapping call should succeed") + .output; + let second_output = execute_precompile( + &Precompiles::::new(), + precompile_addr, + caller, + address_mapping_call_data(second_address), + U256::zero(), + ) + .expect("expected precompile mapping call to be routed to a precompile") + .expect("address mapping call should succeed") + .output; + + assert_ne!(first_output, second_output); + }); + } + + #[test] + fn address_mapping_precompile_is_deterministic() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(1); + let target_address = addr_from_index(0x1234); + let precompile_addr = addr_from_index(AddressMappingPrecompile::::INDEX); + let input = address_mapping_call_data(target_address); + + let first_output = execute_precompile( + &Precompiles::::new(), + precompile_addr, + caller, + input.clone(), + U256::zero(), + ) + .expect("expected precompile mapping call to be routed to a precompile") + .expect("address mapping call should succeed") + .output; + let second_output = execute_precompile( + &Precompiles::::new(), + precompile_addr, + caller, + input, + U256::zero(), + ) + .expect("expected precompile mapping call to be routed to a precompile") + .expect("address mapping call should succeed") + .output; + + assert_eq!(first_output, second_output); + }); + } } mod balance_transfer { From 9fcab70bcf3e43a1b35fbbea634070a54d4480c3 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 16 Apr 2026 18:07:20 +0200 Subject: [PATCH 07/10] Port alpha precompile tests to rust --- contract-tests/test/alpha.precompile.test.ts | 443 ------------------- runtime/tests/precompiles.rs | 403 ++++++++++++++++- 2 files changed, 395 insertions(+), 451 deletions(-) delete mode 100644 contract-tests/test/alpha.precompile.test.ts diff --git a/contract-tests/test/alpha.precompile.test.ts b/contract-tests/test/alpha.precompile.test.ts deleted file mode 100644 index 9c1a5daa8e..0000000000 --- a/contract-tests/test/alpha.precompile.test.ts +++ /dev/null @@ -1,443 +0,0 @@ -import * as assert from "assert"; - -import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" -import { getPublicClient } from "../src/utils"; -import { ETH_LOCAL_URL } from "../src/config"; -import { devnet } from "@polkadot-api/descriptors" -import { PublicClient } from "viem"; -import { TypedApi } from "polkadot-api"; -import { toViemAddress, convertPublicKeyToSs58 } from "../src/address-utils" -import { IAlphaABI, IALPHA_ADDRESS } from "../src/contracts/alpha" -import { forceSetBalanceToSs58Address, addNewSubnetwork, startCall } from "../src/subtensor"; -describe("Test Alpha Precompile", () => { - // init substrate part - const hotkey = getRandomSubstrateKeypair(); - const coldkey = getRandomSubstrateKeypair(); - let publicClient: PublicClient; - - let api: TypedApi; - - // init other variable - let subnetId = 0; - - before(async () => { - // init variables got from await and async - publicClient = await getPublicClient(ETH_LOCAL_URL) - api = await getDevnetApi() - - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) - - let netuid = await addNewSubnetwork(api, hotkey, coldkey) - await startCall(api, netuid, coldkey) - - }) - - describe("Alpha Price Functions", () => { - it("getAlphaPrice returns valid price for subnet", async () => { - const alphaPrice = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getAlphaPrice", - args: [subnetId] - }) - - assert.ok(alphaPrice !== undefined, "Alpha price should be defined"); - assert.ok(typeof alphaPrice === 'bigint', "Alpha price should be a bigint"); - assert.ok(alphaPrice >= BigInt(0), "Alpha price should be non-negative"); - }); - - it("getMovingAlphaPrice returns valid moving price for subnet", async () => { - const movingAlphaPrice = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getMovingAlphaPrice", - args: [subnetId] - }) - - assert.ok(movingAlphaPrice !== undefined, "Moving alpha price should be defined"); - assert.ok(typeof movingAlphaPrice === 'bigint', "Moving alpha price should be a bigint"); - assert.ok(movingAlphaPrice >= BigInt(0), "Moving alpha price should be non-negative"); - }); - - it("alpha prices are consistent for same subnet", async () => { - const alphaPrice = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getAlphaPrice", - args: [subnetId] - }) - - const movingAlphaPrice = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getMovingAlphaPrice", - args: [subnetId] - }) - - // Both should be defined and valid - assert.ok(alphaPrice !== undefined && movingAlphaPrice !== undefined); - }); - - it("Tao in / Alpha in / Alpha out are consistent for same subnet", async () => { - const taoInEmission = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getTaoInEmission", - args: [subnetId] - }) - - const alphaInEmission = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getAlphaInEmission", - args: [subnetId] - }) - - const alphaOutEmission = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getAlphaOutEmission", - args: [subnetId] - }) - - // all should be defined and valid - assert.ok(taoInEmission !== undefined && alphaInEmission !== undefined && alphaOutEmission !== undefined); - }); - - it("getSumAlphaPrice returns valid sum of alpha prices", async () => { - const sumAlphaPrice = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getSumAlphaPrice", - args: [] - }) - - assert.ok(sumAlphaPrice !== undefined, "Sum alpha price should be defined"); - }) - }); - - describe("Pool Data Functions", () => { - it("getTaoInPool returns valid TAO amount", async () => { - const taoInPool = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getTaoInPool", - args: [subnetId] - }) - - assert.ok(taoInPool !== undefined, "TAO in pool should be defined"); - assert.ok(typeof taoInPool === 'bigint', "TAO in pool should be a bigint"); - assert.ok(taoInPool >= BigInt(0), "TAO in pool should be non-negative"); - }); - - it("getAlphaInPool returns valid Alpha amount", async () => { - const alphaInPool = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getAlphaInPool", - args: [subnetId] - }) - - assert.ok(alphaInPool !== undefined, "Alpha in pool should be defined"); - assert.ok(typeof alphaInPool === 'bigint', "Alpha in pool should be a bigint"); - assert.ok(alphaInPool >= BigInt(0), "Alpha in pool should be non-negative"); - }); - - it("getAlphaOutPool returns valid Alpha out amount", async () => { - const alphaOutPool = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getAlphaOutPool", - args: [subnetId] - }) - - assert.ok(alphaOutPool !== undefined, "Alpha out pool should be defined"); - assert.ok(typeof alphaOutPool === 'bigint', "Alpha out pool should be a bigint"); - assert.ok(alphaOutPool >= BigInt(0), "Alpha out pool should be non-negative"); - }); - - it("getAlphaIssuance returns valid issuance amount", async () => { - const alphaIssuance = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getAlphaIssuance", - args: [subnetId] - }) - - assert.ok(alphaIssuance !== undefined, "Alpha issuance should be defined"); - assert.ok(typeof alphaIssuance === 'bigint', "Alpha issuance should be a bigint"); - assert.ok(alphaIssuance >= BigInt(0), "Alpha issuance should be non-negative"); - }); - - it("getCKBurn returns valid CK burn rate", async () => { - const ckBurn = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getCKBurn", - args: [] - }) - - const ckBurnOnChain = await api.query.SubtensorModule.CKBurn.getValue() - - assert.strictEqual(ckBurn, ckBurnOnChain, "CK burn should match on chain"); - assert.ok(ckBurn !== undefined, "CK burn should be defined"); - const ckBurnPercentage = BigInt(ckBurn) * BigInt(100) / BigInt(2 ** 64 - 1) - assert.ok(ckBurnPercentage >= BigInt(0), "CK burn percentage should be non-negative"); - assert.ok(ckBurnPercentage <= BigInt(100), "CK burn percentage should be less than or equal to 100"); - assert.ok(typeof ckBurn === 'bigint', "CK burn should be a bigint"); - }); - }); - - describe("Global Functions", () => { - it("getTaoWeight returns valid TAO weight", async () => { - const taoWeight = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getTaoWeight", - args: [] - }) - - assert.ok(taoWeight !== undefined, "TAO weight should be defined"); - assert.ok(typeof taoWeight === 'bigint', "TAO weight should be a bigint"); - assert.ok(taoWeight >= BigInt(0), "TAO weight should be non-negative"); - }); - - it("getRootNetuid returns correct root netuid", async () => { - const rootNetuid = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getRootNetuid", - args: [] - }) - - assert.ok(rootNetuid !== undefined, "Root netuid should be defined"); - assert.ok(typeof rootNetuid === 'number', "Root netuid should be a number"); - assert.strictEqual(rootNetuid, 0, "Root netuid should be 0"); - }); - }); - - describe("Swap Simulation Functions", () => { - it("simSwapTaoForAlpha returns valid simulation", async () => { - const taoAmount = BigInt(1000000000); // 1 TAO in RAO - const simulatedAlpha = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "simSwapTaoForAlpha", - args: [subnetId, taoAmount] - }) - - assert.ok(simulatedAlpha !== undefined, "Simulated alpha should be defined"); - assert.ok(typeof simulatedAlpha === 'bigint', "Simulated alpha should be a bigint"); - assert.ok(simulatedAlpha >= BigInt(0), "Simulated alpha should be non-negative"); - }); - - it("simSwapAlphaForTao returns valid simulation", async () => { - const alphaAmount = BigInt(1000000000); // 1 Alpha - const simulatedTao = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "simSwapAlphaForTao", - args: [subnetId, alphaAmount] - }) - - assert.ok(simulatedTao !== undefined, "Simulated tao should be defined"); - assert.ok(typeof simulatedTao === 'bigint', "Simulated tao should be a bigint"); - assert.ok(simulatedTao >= BigInt(0), "Simulated tao should be non-negative"); - }); - - it("swap simulations handle zero amounts", async () => { - const zeroTaoForAlpha = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "simSwapTaoForAlpha", - args: [subnetId, BigInt(0)] - }) - - const zeroAlphaForTao = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "simSwapAlphaForTao", - args: [subnetId, BigInt(0)] - }) - - assert.strictEqual(zeroTaoForAlpha, BigInt(0), "Zero TAO should result in zero Alpha"); - assert.strictEqual(zeroAlphaForTao, BigInt(0), "Zero Alpha should result in zero TAO"); - }); - - it("swap simulations are internally consistent", async () => { - const taoAmount = BigInt(1000000000); // 1 TAO - - // Simulate TAO -> Alpha - const simulatedAlpha = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "simSwapTaoForAlpha", - args: [subnetId, taoAmount] - }) - - // If we got alpha, simulate Alpha -> TAO - if ((simulatedAlpha as bigint) > BigInt(0)) { - const simulatedTao = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "simSwapAlphaForTao", - args: [subnetId, simulatedAlpha] - }) - - // Check if simulated values are reasonably close (allowing for rounding/fees) - if ((simulatedTao as bigint) > BigInt(0)) { - const ratio = Number(taoAmount) / Number(simulatedTao); - assert.ok(ratio >= 0.5 && ratio <= 2.0, "Swap simulation should be within reasonable bounds"); - } - } - }); - }); - - describe("Subnet Configuration Functions", () => { - it("getSubnetMechanism returns valid mechanism", async () => { - const mechanism = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getSubnetMechanism", - args: [subnetId] - }) - - assert.ok(mechanism !== undefined, "Subnet mechanism should be defined"); - assert.ok(typeof mechanism === 'number', "Subnet mechanism should be a number"); - assert.ok(mechanism >= 0, "Subnet mechanism should be non-negative"); - }); - - it("getEMAPriceHalvingBlocks returns valid halving period", async () => { - const halvingBlocks = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getEMAPriceHalvingBlocks", - args: [subnetId] - }) - - assert.ok(halvingBlocks !== undefined, "EMA price halving blocks should be defined"); - assert.ok(typeof halvingBlocks === 'bigint', "EMA halving blocks should be a bigint"); - assert.ok(halvingBlocks >= BigInt(0), "EMA halving blocks should be non-negative"); - }); - - it("getSubnetVolume returns valid volume data", async () => { - const subnetVolume = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getSubnetVolume", - args: [subnetId] - }) - - assert.ok(subnetVolume !== undefined, "Subnet volume should be defined"); - assert.ok(typeof subnetVolume === 'bigint', "Subnet volume should be a bigint"); - assert.ok(subnetVolume >= BigInt(0), "Subnet volume should be non-negative"); - }); - }); - - describe("Data Consistency with Pallet", () => { - it("precompile data matches pallet values", async () => { - // Get TAO in pool from precompile - const taoInPool = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getTaoInPool", - args: [subnetId] - }) - - // Get TAO in pool directly from the pallet - const taoInPoolFromPallet = await api.query.SubtensorModule.SubnetTAO.getValue(subnetId); - - // Compare values - assert.strictEqual(taoInPool as bigint, taoInPoolFromPallet, "TAO in pool values should match"); - - // Get Alpha in pool from precompile - const alphaInPool = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getAlphaInPool", - args: [subnetId] - }) - - // Get Alpha in pool directly from the pallet - const alphaInPoolFromPallet = await api.query.SubtensorModule.SubnetAlphaIn.getValue(subnetId); - - // Compare values - assert.strictEqual(alphaInPool as bigint, alphaInPoolFromPallet, "Alpha in pool values should match"); - - // Get Alpha out pool from precompile - const alphaOutPool = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getAlphaOutPool", - args: [subnetId] - }) - - // Get Alpha out pool directly from the pallet - const alphaOutPoolFromPallet = await api.query.SubtensorModule.SubnetAlphaOut.getValue(subnetId); - - // Compare values - assert.strictEqual(alphaOutPool as bigint, alphaOutPoolFromPallet, "Alpha out pool values should match"); - }); - - it("subnet volume data is consistent", async () => { - const subnetVolume = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getSubnetVolume", - args: [subnetId] - }) - - const subnetVolumeFromPallet = await api.query.SubtensorModule.SubnetVolume.getValue(subnetId); - - assert.strictEqual(subnetVolume as bigint, subnetVolumeFromPallet, "Subnet volume values should match"); - }); - }); - - describe("Edge Cases and Error Handling", () => { - it("handles non-existent subnet gracefully", async () => { - const nonExistentSubnet = 9999; - - // These should not throw but return default values - const alphaPrice = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getAlphaPrice", - args: [nonExistentSubnet] - }) - - const taoInPool = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "getTaoInPool", - args: [nonExistentSubnet] - }) - - // Should return default values, not throw - assert.ok(alphaPrice !== undefined, "Should handle non-existent subnet gracefully"); - assert.ok(taoInPool !== undefined, "Should handle non-existent subnet gracefully"); - }); - - it("simulation functions handle large amounts", async () => { - const largeAmount = BigInt("1000000000000000000"); // Very large amount - - const simulatedAlpha = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "simSwapTaoForAlpha", - args: [subnetId, largeAmount] - }) - - const simulatedTao = await publicClient.readContract({ - abi: IAlphaABI, - address: toViemAddress(IALPHA_ADDRESS), - functionName: "simSwapAlphaForTao", - args: [subnetId, largeAmount] - }) - - // Should handle large amounts without throwing - assert.ok(simulatedAlpha !== undefined, "Should handle large TAO amounts"); - assert.ok(simulatedTao !== undefined, "Should handle large Alpha amounts"); - }); - }); -}); diff --git a/runtime/tests/precompiles.rs b/runtime/tests/precompiles.rs index 44caeb661b..5a7ebf35fe 100644 --- a/runtime/tests/precompiles.rs +++ b/runtime/tests/precompiles.rs @@ -10,9 +10,13 @@ use pallet_evm::{AddressMapping, BalanceConverter, PrecompileSet}; use precompile_utils::testing::{MockHandle, PrecompileTesterExt}; use sp_core::{H160, H256, U256}; use sp_runtime::traits::Hash; +use substrate_fixed::types::{I96F32, U96F32}; use subtensor_precompiles::{ - AddressMappingPrecompile, BalanceTransferPrecompile, PrecompileExt, Precompiles, + AddressMappingPrecompile, AlphaPrecompile, BalanceTransferPrecompile, PrecompileExt, + Precompiles, }; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_swap_interface::{Order, SwapHandler}; type AccountId = ::AccountId; @@ -54,6 +58,53 @@ fn addr_from_index(index: u64) -> H160 { H160::from_low_u64_be(index) } +/// Appends one 32-byte ABI word to manually encoded precompile input. +fn push_abi_word(input: &mut Vec, value: U256) { + input.extend_from_slice(&value.to_big_endian()); +} + +/// Encodes one 32-byte ABI output word for exact raw return checks. +fn abi_word(value: U256) -> Vec { + value.to_big_endian().to_vec() +} + +/// Builds a 4-byte Solidity selector from a function signature. +fn selector(signature: &str) -> [u8; 4] { + let hash = sp_io::hashing::keccak_256(signature.as_bytes()); + [hash[0], hash[1], hash[2], hash[3]] +} + +/// Encodes a selector-only call with no arguments. +fn call_data_no_args(signature: &str) -> Vec { + selector(signature).to_vec() +} + +/// Encodes a selector plus one uint16 ABI argument. +fn call_data_u16(signature: &str, value: u16) -> Vec { + let mut input = Vec::with_capacity(4 + 32); + input.extend_from_slice(&selector(signature)); + push_abi_word(&mut input, U256::from(value)); + input +} + +/// Encodes a selector plus `(uint16,uint64)` ABI arguments. +fn call_data_u16_u64(signature: &str, first: u16, second: u64) -> Vec { + let mut input = Vec::with_capacity(4 + 64); + input.extend_from_slice(&selector(signature)); + push_abi_word(&mut input, U256::from(first)); + push_abi_word(&mut input, U256::from(second)); + input +} + +/// Matches the alpha precompile conversion from fixed-point price to EVM `uint256`. +fn alpha_price_to_evm(price: U96F32) -> U256 { + let scaled_price = price.saturating_mul(U96F32::from_num(1_000_000_000)); + let scaled_price = scaled_price.saturating_to_num::(); + ::BalanceConverter::into_evm_balance(scaled_price.into()) + .expect("runtime balance conversion should work for alpha price") + .into_u256() +} + #[test] fn precompile_registry_addresses_are_unique() { new_test_ext().execute_with(|| { @@ -67,11 +118,8 @@ mod address_mapping { use super::*; fn address_mapping_call_data(target: H160) -> Vec { - // Solidity selector for addressMapping(address). - let selector = sp_io::hashing::keccak_256(b"addressMapping(address)"); let mut input = Vec::with_capacity(4 + 32); - // First 4 bytes of keccak256(function_signature): ABI function selector. - input.extend_from_slice(&selector[..4]); + input.extend_from_slice(&selector("addressMapping(address)")); // Left-pad the 20-byte address argument to a 32-byte ABI word. input.extend_from_slice(&[0u8; 12]); // The 20-byte address payload (right-aligned in the 32-byte ABI word). @@ -167,14 +215,353 @@ mod address_mapping { } } +mod alpha { + use super::*; + + const DYNAMIC_NETUID_U16: u16 = 1; + const SUM_PRICE_NETUID_U16: u16 = 2; + const TAO_WEIGHT: u64 = 444; + const CK_BURN: u64 = 555; + const EMA_HALVING_BLOCKS: u64 = 777; + const SUBNET_VOLUME: u128 = 888; + const TAO_IN_EMISSION: u64 = 111; + const ALPHA_IN_EMISSION: u64 = 222; + const ALPHA_OUT_EMISSION: u64 = 333; + + fn dynamic_netuid() -> NetUid { + NetUid::from(DYNAMIC_NETUID_U16) + } + + fn sum_price_netuid() -> NetUid { + NetUid::from(SUM_PRICE_NETUID_U16) + } + + fn seed_alpha_test_state() { + let dynamic_netuid = dynamic_netuid(); + let sum_price_netuid = sum_price_netuid(); + + pallet_subtensor::TaoWeight::::put(TAO_WEIGHT); + pallet_subtensor::CKBurn::::put(CK_BURN); + + pallet_subtensor::NetworksAdded::::insert(dynamic_netuid, true); + pallet_subtensor::SubnetMechanism::::insert(dynamic_netuid, 1); + pallet_subtensor::SubnetTAO::::insert( + dynamic_netuid, + TaoBalance::from(20_000_000_000_u64), + ); + pallet_subtensor::SubnetAlphaIn::::insert( + dynamic_netuid, + AlphaBalance::from(10_000_000_000_u64), + ); + pallet_subtensor::SubnetAlphaOut::::insert( + dynamic_netuid, + AlphaBalance::from(3_000_000_000_u64), + ); + pallet_subtensor::SubnetTaoInEmission::::insert( + dynamic_netuid, + TaoBalance::from(TAO_IN_EMISSION), + ); + pallet_subtensor::SubnetAlphaInEmission::::insert( + dynamic_netuid, + AlphaBalance::from(ALPHA_IN_EMISSION), + ); + pallet_subtensor::SubnetAlphaOutEmission::::insert( + dynamic_netuid, + AlphaBalance::from(ALPHA_OUT_EMISSION), + ); + pallet_subtensor::SubnetVolume::::insert(dynamic_netuid, SUBNET_VOLUME); + pallet_subtensor::EMAPriceHalvingBlocks::::insert( + dynamic_netuid, + EMA_HALVING_BLOCKS, + ); + pallet_subtensor::SubnetMovingPrice::::insert( + dynamic_netuid, + I96F32::from_num(3.0 / 2.0), + ); + + pallet_subtensor::NetworksAdded::::insert(sum_price_netuid, true); + pallet_subtensor::SubnetMechanism::::insert(sum_price_netuid, 1); + pallet_subtensor::SubnetTAO::::insert( + sum_price_netuid, + TaoBalance::from(5_000_000_000_u64), + ); + pallet_subtensor::SubnetAlphaIn::::insert( + sum_price_netuid, + AlphaBalance::from(10_000_000_000_u64), + ); + } + + fn assert_static_call( + precompiles: &Precompiles, + caller: H160, + precompile_addr: H160, + input: Vec, + expected: U256, + ) { + precompiles + .prepare_test(caller, precompile_addr, input) + .with_static_call(true) + .execute_returns_raw(abi_word(expected)); + } + + #[test] + fn alpha_precompile_matches_runtime_values_for_dynamic_subnet() { + new_test_ext().execute_with(|| { + seed_alpha_test_state(); + + let precompiles = Precompiles::::new(); + let caller = addr_from_index(1); + let precompile_addr = addr_from_index(AlphaPrecompile::::INDEX); + + let dynamic_netuid = dynamic_netuid(); + let alpha_price = + as SwapHandler>::current_alpha_price( + dynamic_netuid, + ); + let moving_alpha_price = + pallet_subtensor::Pallet::::get_moving_alpha_price(dynamic_netuid); + + assert!(alpha_price > U96F32::from_num(1)); + assert!(moving_alpha_price > U96F32::from_num(1)); + + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16("getAlphaPrice(uint16)", DYNAMIC_NETUID_U16), + alpha_price_to_evm(alpha_price), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16("getMovingAlphaPrice(uint16)", DYNAMIC_NETUID_U16), + alpha_price_to_evm(moving_alpha_price), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16("getTaoInPool(uint16)", DYNAMIC_NETUID_U16), + U256::from(pallet_subtensor::SubnetTAO::::get(dynamic_netuid).to_u64()), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16("getAlphaInPool(uint16)", DYNAMIC_NETUID_U16), + U256::from(u64::from(pallet_subtensor::SubnetAlphaIn::::get( + dynamic_netuid, + ))), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16("getAlphaOutPool(uint16)", DYNAMIC_NETUID_U16), + U256::from(u64::from(pallet_subtensor::SubnetAlphaOut::::get( + dynamic_netuid, + ))), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16("getAlphaIssuance(uint16)", DYNAMIC_NETUID_U16), + U256::from(u64::from( + pallet_subtensor::Pallet::::get_alpha_issuance(dynamic_netuid), + )), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16("getSubnetMechanism(uint16)", DYNAMIC_NETUID_U16), + U256::from(pallet_subtensor::SubnetMechanism::::get( + dynamic_netuid, + )), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16("getEMAPriceHalvingBlocks(uint16)", DYNAMIC_NETUID_U16), + U256::from(pallet_subtensor::EMAPriceHalvingBlocks::::get( + dynamic_netuid, + )), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16("getSubnetVolume(uint16)", DYNAMIC_NETUID_U16), + U256::from(pallet_subtensor::SubnetVolume::::get( + dynamic_netuid, + )), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16("getTaoInEmission(uint16)", DYNAMIC_NETUID_U16), + U256::from( + pallet_subtensor::SubnetTaoInEmission::::get(dynamic_netuid).to_u64(), + ), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16("getAlphaInEmission(uint16)", DYNAMIC_NETUID_U16), + U256::from( + pallet_subtensor::SubnetAlphaInEmission::::get(dynamic_netuid) + .to_u64(), + ), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16("getAlphaOutEmission(uint16)", DYNAMIC_NETUID_U16), + U256::from( + pallet_subtensor::SubnetAlphaOutEmission::::get(dynamic_netuid) + .to_u64(), + ), + ); + }); + } + + #[test] + fn alpha_precompile_matches_runtime_global_values() { + new_test_ext().execute_with(|| { + seed_alpha_test_state(); + + let precompiles = Precompiles::::new(); + let caller = addr_from_index(1); + let precompile_addr = addr_from_index(AlphaPrecompile::::INDEX); + + let mut sum_alpha_price = U96F32::from_num(0); + for (netuid, _) in pallet_subtensor::NetworksAdded::::iter() { + if netuid.is_root() { + continue; + } + let price = + as SwapHandler>::current_alpha_price( + netuid, + ); + if price < U96F32::from_num(1) { + sum_alpha_price = sum_alpha_price.saturating_add(price); + } + } + + assert!(sum_alpha_price > U96F32::from_num(0)); + + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_no_args("getCKBurn()"), + U256::from(pallet_subtensor::CKBurn::::get()), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_no_args("getTaoWeight()"), + U256::from(pallet_subtensor::TaoWeight::::get()), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_no_args("getRootNetuid()"), + U256::from(u16::from(NetUid::ROOT)), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_no_args("getSumAlphaPrice()"), + alpha_price_to_evm(sum_alpha_price), + ); + }); + } + + #[test] + fn alpha_precompile_matches_runtime_swap_simulations() { + new_test_ext().execute_with(|| { + seed_alpha_test_state(); + + let precompiles = Precompiles::::new(); + let caller = addr_from_index(1); + let precompile_addr = addr_from_index(AlphaPrecompile::::INDEX); + + let tao_amount = 1_000_000_000_u64; + let alpha_amount = 1_000_000_000_u64; + let expected_alpha = as SwapHandler>::sim_swap( + dynamic_netuid(), + pallet_subtensor::GetAlphaForTao::::with_amount(tao_amount), + ) + .expect("tao-for-alpha simulation should succeed") + .amount_paid_out + .to_u64(); + let expected_tao = as SwapHandler>::sim_swap( + dynamic_netuid(), + pallet_subtensor::GetTaoForAlpha::::with_amount(alpha_amount), + ) + .expect("alpha-for-tao simulation should succeed") + .amount_paid_out + .to_u64(); + + assert!(expected_alpha > 0); + assert!(expected_tao > 0); + + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16_u64( + "simSwapTaoForAlpha(uint16,uint64)", + DYNAMIC_NETUID_U16, + tao_amount, + ), + U256::from(expected_alpha), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16_u64( + "simSwapAlphaForTao(uint16,uint64)", + DYNAMIC_NETUID_U16, + alpha_amount, + ), + U256::from(expected_tao), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16_u64("simSwapTaoForAlpha(uint16,uint64)", DYNAMIC_NETUID_U16, 0), + U256::zero(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + call_data_u16_u64("simSwapAlphaForTao(uint16,uint64)", DYNAMIC_NETUID_U16, 0), + U256::zero(), + ); + }); + } +} + mod balance_transfer { use super::*; fn balance_transfer_call_data(target: H256) -> Vec { - // Solidity selector for transfer(bytes32). - let selector = sp_io::hashing::keccak_256(b"transfer(bytes32)"); let mut input = Vec::with_capacity(4 + 32); - input.extend_from_slice(&selector[..4]); + input.extend_from_slice(&selector("transfer(bytes32)")); input.extend_from_slice(target.as_bytes()); input } From d76b40b4889a1e58954ed2fb61dbdf4866b48d70 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 16 Apr 2026 18:26:21 +0200 Subject: [PATCH 08/10] Fix lints --- runtime/tests/precompiles.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/runtime/tests/precompiles.rs b/runtime/tests/precompiles.rs index 5a7ebf35fe..8f906a09dc 100644 --- a/runtime/tests/precompiles.rs +++ b/runtime/tests/precompiles.rs @@ -1,5 +1,6 @@ #![allow(clippy::unwrap_used)] #![allow(clippy::expect_used)] +#![allow(clippy::arithmetic_side_effects)] use core::iter::IntoIterator; use std::collections::BTreeSet; @@ -98,8 +99,7 @@ fn call_data_u16_u64(signature: &str, first: u16, second: u64) -> Vec { /// Matches the alpha precompile conversion from fixed-point price to EVM `uint256`. fn alpha_price_to_evm(price: U96F32) -> U256 { - let scaled_price = price.saturating_mul(U96F32::from_num(1_000_000_000)); - let scaled_price = scaled_price.saturating_to_num::(); + let scaled_price = (price * U96F32::from_num(1_000_000_000)).to_num::(); ::BalanceConverter::into_evm_balance(scaled_price.into()) .expect("runtime balance conversion should work for alpha price") .into_u256() @@ -450,7 +450,7 @@ mod alpha { netuid, ); if price < U96F32::from_num(1) { - sum_alpha_price = sum_alpha_price.saturating_add(price); + sum_alpha_price += price; } } From 34a0140aafd4fd524815d6d4b78d864d6f3fbc15 Mon Sep 17 00:00:00 2001 From: greatjourney589 Date: Fri, 17 Apr 2026 04:23:47 -0700 Subject: [PATCH 09/10] fix: remove unconditional inplace_mask_diag that overrides owner self-weight exception --- pallets/subtensor/src/epoch/run_epoch.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/pallets/subtensor/src/epoch/run_epoch.rs b/pallets/subtensor/src/epoch/run_epoch.rs index 962c5bbbb4..f1230e496e 100644 --- a/pallets/subtensor/src/epoch/run_epoch.rs +++ b/pallets/subtensor/src/epoch/run_epoch.rs @@ -301,8 +301,6 @@ impl Pallet { } else { inplace_mask_diag(&mut weights); } - - inplace_mask_diag(&mut weights); log::trace!("W (permit+diag): {:?}", &weights); // Mask outdated weights: remove weights referring to deregistered neurons. From 7d0fa9e6cdee1c301abe4572837eb2734d56129e Mon Sep 17 00:00:00 2001 From: greatjourney589 Date: Tue, 21 Apr 2026 07:15:50 -0700 Subject: [PATCH 10/10] test: add regression test for owner self-weight in dense epoch --- pallets/subtensor/src/tests/epoch.rs | 66 ++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/pallets/subtensor/src/tests/epoch.rs b/pallets/subtensor/src/tests/epoch.rs index 5c516f9f30..b65b5c957a 100644 --- a/pallets/subtensor/src/tests/epoch.rs +++ b/pallets/subtensor/src/tests/epoch.rs @@ -2559,6 +2559,72 @@ fn test_can_set_self_weight_as_subnet_owner() { }); } +/// Regression test: the dense epoch must preserve the subnet owner's self-weight. +/// +/// Before the fix, `epoch_dense_mechanism` applied `inplace_mask_diag_except_index` +/// to keep the owner's self-weight, then immediately re-zeroed the full diagonal +/// with a second unconditional `inplace_mask_diag` — silently negating the +/// owner-exception path. This test fails on the buggy code because the owner's +/// uid receives zero incentive. +#[test] +fn test_dense_epoch_preserves_owner_self_weight() { + new_test_ext(1).execute_with(|| { + let owner_cold: U256 = U256::from(1); + let owner_hot: U256 = U256::from(1 + 456); + let other_hot: U256 = U256::from(2); + + let stake = 5_000_000_000_000_u64; + let to_emit: u64 = 1_000_000_000_u64; + + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + register_ok_neuron(netuid, other_hot, owner_cold, 0); + + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &owner_hot, + &owner_cold, + netuid, + stake.into(), + ); + + // Only the owner has a validator permit — owner's weights drive consensus. + ValidatorPermit::::insert(netuid, vec![true, false]); + + // Owner weights: 50% self, 50% other. If the owner self-weight is masked, + // uid 0 gets no incentive and the two emissions diverge. + let half: u16 = u16::MAX / 2; + Weights::::insert( + NetUidStorageIndex::from(netuid), + 0, + vec![(0, half), (1, half)], + ); + + step_block(1); + LastUpdate::::insert(NetUidStorageIndex::from(netuid), vec![2, 0]); + + let emissions = SubtensorModule::epoch_dense(netuid, to_emit.into()); + + assert_eq!(emissions.len(), 2); + let owner_incentive = emissions + .iter() + .find(|(hk, _, _)| *hk == owner_hot) + .map(|(_, inc, _)| *inc) + .expect("owner hotkey missing from emissions"); + let other_incentive = emissions + .iter() + .find(|(hk, _, _)| *hk == other_hot) + .map(|(_, inc, _)| *inc) + .expect("other hotkey missing from emissions"); + + // Owner must still receive incentive — its self-weight was preserved. + assert!( + owner_incentive > 0.into(), + "owner self-weight was zeroed in dense epoch" + ); + // With an equal split and symmetric treatment, both should be equal. + assert_eq!(owner_incentive, other_incentive); + }); +} + #[test] fn test_epoch_outputs_single_staker_registered_no_weights() { new_test_ext(1).execute_with(|| {