From dfabe1c188f89fc072e53ad846e6f26915cd7957 Mon Sep 17 00:00:00 2001 From: open-junius Date: Mon, 13 Apr 2026 16:07:48 +0800 Subject: [PATCH 01/21] 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 eea8d411f595ed46b59dc48d3273dad30b36a794 Mon Sep 17 00:00:00 2001 From: Evgeny Svirsky Date: Wed, 15 Apr 2026 15:30:54 +0200 Subject: [PATCH 02/21] 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 4eaaf7700e..af62316574 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 216c256a75..ca072f1dcb 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -2632,3 +2632,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 ff2920fb210ab4461ff1b354bb7bec6a44f6d6b9 Mon Sep 17 00:00:00 2001 From: Evgeny Svirsky Date: Wed, 15 Apr 2026 18:34:24 +0200 Subject: [PATCH 03/21] 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 af62316574..91c6f9d917 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 ca072f1dcb..43c76b2929 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -2634,7 +2634,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); @@ -2642,27 +2642,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. @@ -2671,31 +2671,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 3792f26f48bbae4f3b9c8ce440187a9a57cd35b8 Mon Sep 17 00:00:00 2001 From: Evgeny Svirsky Date: Wed, 15 Apr 2026 18:34:50 +0200 Subject: [PATCH 04/21] 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 91c6f9d917..1f37cb19d2 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 2b5155dae21c95d96f1090f41d4601c220b3f0d0 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 16 Apr 2026 17:16:03 +0200 Subject: [PATCH 05/21] 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 3604202ffade00fb8b7cb9c18149b785329e87da Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 16 Apr 2026 18:07:20 +0200 Subject: [PATCH 06/21] 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 d6b65a19c982b94b7d1d4a7782d9b9dc5380db77 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Thu, 16 Apr 2026 18:26:21 +0200 Subject: [PATCH 07/21] 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 f6471044103b8a20d4912b417f0a23849b406b67 Mon Sep 17 00:00:00 2001 From: Evgeny Svirsky Date: Fri, 17 Apr 2026 15:54:43 +0200 Subject: [PATCH 08/21] Fix for e2e test --- contract-tests/test/staking.precompile.approval.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contract-tests/test/staking.precompile.approval.test.ts b/contract-tests/test/staking.precompile.approval.test.ts index 372e1ac661..2717b90e5b 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" @@ -57,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() @@ -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 e7af694d09d83eb84577a4971397ab51372cb3ce Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 20 Apr 2026 16:07:03 +0200 Subject: [PATCH 09/21] Port ed25519 precompile tests --- .../test/ed25519.precompile.verify.test.ts | 122 ------------------ runtime/tests/precompiles.rs | 70 +++++++++- 2 files changed, 67 insertions(+), 125 deletions(-) delete mode 100644 contract-tests/test/ed25519.precompile.verify.test.ts diff --git a/contract-tests/test/ed25519.precompile.verify.test.ts b/contract-tests/test/ed25519.precompile.verify.test.ts deleted file mode 100644 index fcd79ec9d7..0000000000 --- a/contract-tests/test/ed25519.precompile.verify.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { IED25519VERIFY_ADDRESS, IEd25519VerifyABI, ETH_LOCAL_URL } from '../src/config' -import { getPublicClient } from "../src/utils"; -import { toHex, toBytes, keccak256, PublicClient } from 'viem' -import { Keyring } from "@polkadot/keyring"; -import * as assert from "assert"; - -describe("Verfication of ed25519 signature", () => { - // init eth part - let ethClient: PublicClient; - - before(async () => { - ethClient = await getPublicClient(ETH_LOCAL_URL); - }); - - it("Verification of ed25519 works", async () => { - const keyring = new Keyring({ type: "ed25519" }); - const alice = keyring.addFromUri("//Alice"); - - // Use this example: https://github.com/gztensor/evm-demo/blob/main/docs/ed25519verify-precompile.md - // const keyring = new Keyring({ type: "ed25519" }); - // const myAccount = keyring.addFromUri("//Alice"); - - ////////////////////////////////////////////////////////////////////// - // Generate a signature - - // Your message to sign - const message = "Sign this message"; - const messageU8a = new TextEncoder().encode(message); - const messageHex = toHex(messageU8a); // Convert message to hex string - const messageHash = keccak256(messageHex); // Hash the message to fit into bytes32 - console.log(`messageHash = ${messageHash}`); - const hashedMessageBytes = toBytes(messageHash); - console.log(`hashedMessageBytes = ${hashedMessageBytes}`); - - // Sign the message - const signature = await alice.sign(hashedMessageBytes); - console.log(`Signature: ${toHex(signature)}`); - - // Verify the signature locally - const isValid = alice.verify( - hashedMessageBytes, - signature, - alice.publicKey - ); - console.log(`Is the signature valid? ${isValid}`); - - ////////////////////////////////////////////////////////////////////// - // Verify the signature using the precompile contract - - const publicKeyBytes = toHex(alice.publicKey); - console.log(`publicKeyBytes = ${publicKeyBytes}`); - - // Split signture into Commitment (R) and response (s) - let r = signature.slice(0, 32); // Commitment, a.k.a. "r" - first 32 bytes - let s = signature.slice(32, 64); // Response, a.k.a. "s" - second 32 bytes - let rBytes = toHex(r); - let sBytes = toHex(s); - - const isPrecompileValid = await ethClient.readContract({ - address: IED25519VERIFY_ADDRESS, - abi: IEd25519VerifyABI, - functionName: "verify", - args: [messageHash, - publicKeyBytes, - rBytes, - sBytes] - - }); - - console.log( - `Is the signature valid according to the smart contract? ${isPrecompileValid}` - ); - assert.equal(isPrecompileValid, true) - - ////////////////////////////////////////////////////////////////////// - // Verify the signature for bad data using the precompile contract - - let brokenHashedMessageBytes = hashedMessageBytes; - brokenHashedMessageBytes[0] = (brokenHashedMessageBytes[0] + 1) % 0xff; - const brokenMessageHash = toHex(brokenHashedMessageBytes); - console.log(`brokenMessageHash = ${brokenMessageHash}`); - - const isPrecompileValidBadData = await ethClient.readContract({ - address: IED25519VERIFY_ADDRESS, - abi: IEd25519VerifyABI, - functionName: "verify", - args: [brokenMessageHash, - publicKeyBytes, - rBytes, - sBytes] - - }); - - console.log( - `Is the signature valid according to the smart contract for broken data? ${isPrecompileValidBadData}` - ); - assert.equal(isPrecompileValidBadData, false) - - ////////////////////////////////////////////////////////////////////// - // Verify the bad signature for good data using the precompile contract - - let brokenR = r; - brokenR[0] = (brokenR[0] + 1) % 0xff; - rBytes = toHex(r); - const isPrecompileValidBadSignature = await ethClient.readContract({ - address: IED25519VERIFY_ADDRESS, - abi: IEd25519VerifyABI, - functionName: "verify", - args: [messageHash, - publicKeyBytes, - rBytes, - sBytes] - - }); - - console.log( - `Is the signature valid according to the smart contract for broken signature? ${isPrecompileValidBadSignature}` - ); - assert.equal(isPrecompileValidBadSignature, false) - - }); -}); \ No newline at end of file diff --git a/runtime/tests/precompiles.rs b/runtime/tests/precompiles.rs index 8f906a09dc..bccb37c0a6 100644 --- a/runtime/tests/precompiles.rs +++ b/runtime/tests/precompiles.rs @@ -9,12 +9,12 @@ use fp_evm::{Context, ExitError, PrecompileFailure, PrecompileResult}; use node_subtensor_runtime::{BuildStorage, Runtime, RuntimeGenesisConfig, System}; use pallet_evm::{AddressMapping, BalanceConverter, PrecompileSet}; use precompile_utils::testing::{MockHandle, PrecompileTesterExt}; -use sp_core::{H160, H256, U256}; +use sp_core::{H160, H256, Pair, U256, ed25519}; use sp_runtime::traits::Hash; use substrate_fixed::types::{I96F32, U96F32}; use subtensor_precompiles::{ - AddressMappingPrecompile, AlphaPrecompile, BalanceTransferPrecompile, PrecompileExt, - Precompiles, + AddressMappingPrecompile, AlphaPrecompile, BalanceTransferPrecompile, Ed25519Verify, + PrecompileExt, Precompiles, }; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; use subtensor_swap_interface::{Order, SwapHandler}; @@ -556,6 +556,70 @@ mod alpha { } } +mod ed25519_verify { + use super::*; + + fn ed25519_verify_call_data( + message: [u8; 32], + public_key: [u8; 32], + signature: [u8; 64], + ) -> Vec { + let mut input = Vec::with_capacity(4 + 32 * 4); + input.extend_from_slice(&selector("verify(bytes32,bytes32,bytes32,bytes32)")); + input.extend_from_slice(&message); + input.extend_from_slice(&public_key); + input.extend_from_slice(&signature[..32]); + input.extend_from_slice(&signature[32..]); + input + } + + #[test] + fn ed25519_precompile_verifies_valid_and_invalid_signatures() { + new_test_ext().execute_with(|| { + let pair = ed25519::Pair::from_string("//Alice", None) + .expect("Alice ed25519 key should be available"); + let message = sp_io::hashing::keccak_256(b"Sign this message"); + let signature = pair.sign(&message).0; + let public_key = pair.public().0; + + let mut broken_message = message; + broken_message[0] ^= 0x01; + + let mut broken_signature = signature; + broken_signature[0] ^= 0x01; + + let precompiles = Precompiles::::new(); + let caller = addr_from_index(1); + let precompile_addr = addr_from_index(Ed25519Verify::::INDEX); + + precompiles + .prepare_test( + caller, + precompile_addr, + ed25519_verify_call_data(message, public_key, signature), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::one())); + precompiles + .prepare_test( + caller, + precompile_addr, + ed25519_verify_call_data(broken_message, public_key, signature), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::zero())); + precompiles + .prepare_test( + caller, + precompile_addr, + ed25519_verify_call_data(message, public_key, broken_signature), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::zero())); + }); + } +} + mod balance_transfer { use super::*; From 7dee435efe12b10994edd5d3d5a71a44a32c0f72 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 20 Apr 2026 16:31:21 +0200 Subject: [PATCH 10/21] Port uid lookup precompile --- .../test/evm-uid.precompile.lookup.test.ts | 89 ------------------- runtime/tests/precompiles.rs | 70 ++++++++++++++- 2 files changed, 69 insertions(+), 90 deletions(-) delete mode 100644 contract-tests/test/evm-uid.precompile.lookup.test.ts diff --git a/contract-tests/test/evm-uid.precompile.lookup.test.ts b/contract-tests/test/evm-uid.precompile.lookup.test.ts deleted file mode 100644 index d8de138aaa..0000000000 --- a/contract-tests/test/evm-uid.precompile.lookup.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as assert from "assert"; - -import { getAliceSigner, getDevnetApi, waitForTransactionCompletion, getRandomSubstrateKeypair, getSignerFromKeypair } from "../src/substrate" -import { convertToFixedSizeBinary, generateRandomEthersWallet, getPublicClient } from "../src/utils"; -import { ETH_LOCAL_URL } from "../src/config"; -import { devnet } from "@polkadot-api/descriptors" -import { hexToU8a } from "@polkadot/util"; -import { u64 } from "scale-ts"; -import { PublicClient } from "viem"; -import { PolkadotSigner, TypedApi } from "polkadot-api"; -import { toViemAddress, convertPublicKeyToSs58 } from "../src/address-utils" -import { IUIDLookupABI, IUID_LOOKUP_ADDRESS } from "../src/contracts/uidLookup" -import { keccak256 } from 'ethers'; -import { addNewSubnetwork, forceSetBalanceToSs58Address, startCall } from "../src/subtensor"; - -describe("Test the UID Lookup precompile", () => { - const hotkey = getRandomSubstrateKeypair(); - const coldkey = getRandomSubstrateKeypair(); - const evmWallet = generateRandomEthersWallet(); - let publicClient: PublicClient; - - let api: TypedApi - - let alice: PolkadotSigner; - - let uid: number; - let blockNumber: number; - let netuid: number; - let blockNumberAssociated: bigint; - - before(async () => { - publicClient = await getPublicClient(ETH_LOCAL_URL) - api = await getDevnetApi() - alice = await getAliceSigner(); - - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(alice.publicKey)) - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) - - netuid = await addNewSubnetwork(api, hotkey, coldkey) - await startCall(api, netuid, coldkey) - - const maybeUid = await api.query.SubtensorModule.Uids.getValue(netuid, convertPublicKeyToSs58(hotkey.publicKey)) - - if (maybeUid === undefined) { - throw new Error("UID should be defined") - } - uid = maybeUid - - // Associate EVM key - blockNumber = await api.query.System.Number.getValue(); - const blockNumberBytes = u64.enc(BigInt(blockNumber)); - const blockNumberHash = hexToU8a(keccak256(blockNumberBytes)); - const concatenatedArray = new Uint8Array([...hotkey.publicKey, ...blockNumberHash]); - const signature = await evmWallet.signMessage(concatenatedArray); - const associateEvmKeyTx = api.tx.SubtensorModule.associate_evm_key({ - netuid: netuid, - evm_key: convertToFixedSizeBinary(evmWallet.address, 20), - block_number: BigInt(blockNumber), - signature: convertToFixedSizeBinary(signature, 65) - }); - const signer = getSignerFromKeypair(hotkey); - await waitForTransactionCompletion(api, associateEvmKeyTx, signer) - .then(() => { }) - .catch((error) => { console.log(`transaction error ${error}`) }); - - const storedEvmKey = await api.query.SubtensorModule.AssociatedEvmAddress.getValue(netuid, uid) - assert.notEqual(storedEvmKey, undefined, "storedEvmKey should be defined") - if (storedEvmKey !== undefined) { - assert.equal(storedEvmKey[0].asHex(), convertToFixedSizeBinary(evmWallet.address, 20).asHex()) - blockNumberAssociated = storedEvmKey[1] - } - }) - - it("UID lookup via precompile contract works correctly", async () => { - // Get UID for the EVM address - const uidArray = await publicClient.readContract({ - abi: IUIDLookupABI, - address: toViemAddress(IUID_LOOKUP_ADDRESS), - functionName: "uidLookup", - args: [netuid, evmWallet.address, 1024] - }) - - assert.notEqual(uidArray, undefined, "UID should be defined") - assert.ok(Array.isArray(uidArray), `UID should be an array, got ${typeof uidArray}`) - assert.ok(uidArray.length > 0, "UID array should not be empty") - assert.deepStrictEqual(uidArray[0], { uid: uid, block_associated: blockNumberAssociated }) - }) -}); diff --git a/runtime/tests/precompiles.rs b/runtime/tests/precompiles.rs index bccb37c0a6..df2b9259b9 100644 --- a/runtime/tests/precompiles.rs +++ b/runtime/tests/precompiles.rs @@ -14,7 +14,7 @@ use sp_runtime::traits::Hash; use substrate_fixed::types::{I96F32, U96F32}; use subtensor_precompiles::{ AddressMappingPrecompile, AlphaPrecompile, BalanceTransferPrecompile, Ed25519Verify, - PrecompileExt, Precompiles, + PrecompileExt, Precompiles, UidLookupPrecompile, }; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; use subtensor_swap_interface::{Order, SwapHandler}; @@ -82,6 +82,7 @@ fn call_data_no_args(signature: &str) -> Vec { /// Encodes a selector plus one uint16 ABI argument. fn call_data_u16(signature: &str, value: u16) -> Vec { + // 4-byte selector + 1 ABI word for the uint16 argument. let mut input = Vec::with_capacity(4 + 32); input.extend_from_slice(&selector(signature)); push_abi_word(&mut input, U256::from(value)); @@ -90,6 +91,7 @@ fn call_data_u16(signature: &str, value: u16) -> Vec { /// Encodes a selector plus `(uint16,uint64)` ABI arguments. fn call_data_u16_u64(signature: &str, first: u16, second: u64) -> Vec { + // 4-byte selector + 2 ABI words for `(uint16,uint64)`. let mut input = Vec::with_capacity(4 + 64); input.extend_from_slice(&selector(signature)); push_abi_word(&mut input, U256::from(first)); @@ -118,6 +120,7 @@ mod address_mapping { use super::*; fn address_mapping_call_data(target: H160) -> Vec { + // 4-byte selector + 1 ABI word for the address argument. let mut input = Vec::with_capacity(4 + 32); input.extend_from_slice(&selector("addressMapping(address)")); // Left-pad the 20-byte address argument to a 32-byte ABI word. @@ -564,6 +567,7 @@ mod ed25519_verify { public_key: [u8; 32], signature: [u8; 64], ) -> Vec { + // 4-byte selector + 4 ABI words: message, public key, signature R, signature S. let mut input = Vec::with_capacity(4 + 32 * 4); input.extend_from_slice(&selector("verify(bytes32,bytes32,bytes32,bytes32)")); input.extend_from_slice(&message); @@ -620,10 +624,74 @@ mod ed25519_verify { } } +mod uid_lookup { + use super::*; + + fn uid_lookup_call_data(netuid: u16, evm_address: H160, limit: u16) -> Vec { + // 4-byte selector + 3 ABI words: netuid, address, limit. + let mut input = Vec::with_capacity(4 + 32 * 3); + input.extend_from_slice(&selector("uidLookup(uint16,address,uint16)")); + push_abi_word(&mut input, U256::from(netuid)); + input.extend_from_slice(&[0u8; 12]); + input.extend_from_slice(evm_address.as_bytes()); + push_abi_word(&mut input, U256::from(limit)); + input + } + + fn abi_uid_lookup_output(entries: &[(u16, u64)]) -> Vec { + // ABI dynamic array encoding: + // head offset word + array length word + 2 words per tuple entry `(uid, block_associated)`. + let mut output = Vec::with_capacity(64 + entries.len() * 64); + push_abi_word(&mut output, U256::from(32u64)); + push_abi_word(&mut output, U256::from(entries.len())); + for (uid, block_associated) in entries { + push_abi_word(&mut output, U256::from(*uid)); + push_abi_word(&mut output, U256::from(*block_associated)); + } + output + } + + #[test] + fn uid_lookup_precompile_returns_associated_uid_and_block() { + new_test_ext().execute_with(|| { + let precompiles = Precompiles::::new(); + let caller = addr_from_index(1); + let precompile_addr = addr_from_index(UidLookupPrecompile::::INDEX); + + let netuid = NetUid::from(1); + let netuid_u16: u16 = netuid.into(); + let uid = 0u16; + let evm_address = H160::from_low_u64_be(0xdead_beef); + let block_associated = 42u64; + let limit = 1024u16; + + pallet_subtensor::AssociatedEvmAddress::::insert( + netuid, + uid, + (evm_address, block_associated), + ); + + let expected = + pallet_subtensor::Pallet::::uid_lookup(netuid, evm_address, limit); + assert_eq!(expected, vec![(uid, block_associated)]); + + precompiles + .prepare_test( + caller, + precompile_addr, + uid_lookup_call_data(netuid_u16, evm_address, limit), + ) + .with_static_call(true) + .execute_returns_raw(abi_uid_lookup_output(&expected)); + }); + } +} + mod balance_transfer { use super::*; fn balance_transfer_call_data(target: H256) -> Vec { + // 4-byte selector + 1 ABI word for the bytes32 destination. let mut input = Vec::with_capacity(4 + 32); input.extend_from_slice(&selector("transfer(bytes32)")); input.extend_from_slice(target.as_bytes()); From 7fc089150a28fd4682af51b38921645580419e34 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 20 Apr 2026 17:27:23 +0200 Subject: [PATCH 11/21] Port metagraph precompile --- .../test/metagraph.precompile.test.ts | 146 -------------- runtime/tests/precompiles.rs | 178 +++++++++++++++++- 2 files changed, 176 insertions(+), 148 deletions(-) delete mode 100644 contract-tests/test/metagraph.precompile.test.ts diff --git a/contract-tests/test/metagraph.precompile.test.ts b/contract-tests/test/metagraph.precompile.test.ts deleted file mode 100644 index 18ab4bd421..0000000000 --- a/contract-tests/test/metagraph.precompile.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import * as assert from "assert"; - -import { getAliceSigner, getDevnetApi, convertPublicKeyToMultiAddress, getRandomSubstrateKeypair, getSignerFromKeypair, waitForTransactionWithRetry } 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 { PolkadotSigner, TypedApi } from "polkadot-api"; -import { toViemAddress, convertPublicKeyToSs58 } from "../src/address-utils" -import { IMetagraphABI, IMETAGRAPH_ADDRESS } from "../src/contracts/metagraph" - -describe("Test the Metagraph precompile", () => { - // init substrate part - const hotkey = getRandomSubstrateKeypair(); - const coldkey = getRandomSubstrateKeypair(); - let publicClient: PublicClient; - - let api: TypedApi - - // sudo account alice as signer - let alice: PolkadotSigner; - - // init other variable - let subnetId = 0; - - before(async () => { - // init variables got from await and async - publicClient = await getPublicClient(ETH_LOCAL_URL) - api = await getDevnetApi() - alice = await getAliceSigner(); - - { - const multiAddress = convertPublicKeyToMultiAddress(hotkey.publicKey) - const internalCall = api.tx.Balances.force_set_balance({ who: multiAddress, new_free: BigInt(1e12) }) - const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) - - await waitForTransactionWithRetry(api, tx, alice) - .then(() => { }) - .catch((error) => { console.log(`transaction error ${error}`) }); - } - - { - const multiAddress = convertPublicKeyToMultiAddress(coldkey.publicKey) - const internalCall = api.tx.Balances.force_set_balance({ who: multiAddress, new_free: BigInt(1e12) }) - const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) - - await waitForTransactionWithRetry(api, tx, alice) - .then(() => { }) - .catch((error) => { console.log(`transaction error ${error}`) }); - } - - const signer = getSignerFromKeypair(coldkey) - const registerNetworkTx = api.tx.SubtensorModule.register_network({ hotkey: convertPublicKeyToSs58(hotkey.publicKey) }) - await waitForTransactionWithRetry(api, registerNetworkTx, signer) - .then(() => { }) - .catch((error) => { console.log(`transaction error ${error}`) }); - - let totalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue() - assert.ok(totalNetworks > 1) - subnetId = totalNetworks - 1 - - let uid_count = - await api.query.SubtensorModule.SubnetworkN.getValue(subnetId) - if (uid_count === 0) { - const tx = api.tx.SubtensorModule.burned_register({ hotkey: convertPublicKeyToSs58(hotkey.publicKey), netuid: subnetId }) - await waitForTransactionWithRetry(api, tx, signer) - .then(() => { }) - .catch((error) => { console.log(`transaction error ${error}`) }); - } - }) - - it("Metagraph data access via precompile contract is ok", async () => { - const uid = 0 - const uid_count = await publicClient.readContract({ - abi: IMetagraphABI, - address: toViemAddress(IMETAGRAPH_ADDRESS), - functionName: "getUidCount", - args: [subnetId] - }) - // back to original value for other tests. and we can run it repeatedly - assert.ok(uid_count != undefined); - - // const axon = api.query.SubtensorModule.Axons.getValue() - - const axon = await publicClient.readContract({ - abi: IMetagraphABI, - address: toViemAddress(IMETAGRAPH_ADDRESS), - functionName: "getAxon", - args: [subnetId, uid] - }) - - assert.ok(axon != undefined); - if (axon instanceof Object) { - assert.ok(axon != undefined); - if ("block" in axon) { - assert.ok(axon.block != undefined); - } else { - throw new Error("block not included in axon") - } - - if ("version" in axon) { - assert.ok(axon.version != undefined); - } else { - throw new Error("version not included in axon") - } - - if ("ip" in axon) { - assert.ok(axon.ip != undefined); - } else { - throw new Error("ip not included in axon") - } - - if ("port" in axon) { - assert.ok(axon.port != undefined); - } else { - throw new Error("port not included in axon") - } - - if ("ip_type" in axon) { - assert.ok(axon.ip_type != undefined); - } else { - throw new Error("ip_type not included in axon") - } - - if ("protocol" in axon) { - assert.ok(axon.protocol != undefined); - } else { - throw new Error("protocol not included in axon") - } - } - - const methodList = ["getEmission", "getVtrust", "getValidatorStatus", "getLastUpdate", "getIsActive", - "getHotkey", "getColdkey" - ] - for (const method of methodList) { - const value = await publicClient.readContract({ - abi: IMetagraphABI, - address: toViemAddress(IMETAGRAPH_ADDRESS), - functionName: method, - args: [subnetId, uid] - }) - - assert.ok(value != undefined); - } - }); -}); \ No newline at end of file diff --git a/runtime/tests/precompiles.rs b/runtime/tests/precompiles.rs index df2b9259b9..66f77ec7f5 100644 --- a/runtime/tests/precompiles.rs +++ b/runtime/tests/precompiles.rs @@ -14,9 +14,9 @@ use sp_runtime::traits::Hash; use substrate_fixed::types::{I96F32, U96F32}; use subtensor_precompiles::{ AddressMappingPrecompile, AlphaPrecompile, BalanceTransferPrecompile, Ed25519Verify, - PrecompileExt, Precompiles, UidLookupPrecompile, + MetagraphPrecompile, PrecompileExt, Precompiles, UidLookupPrecompile, }; -use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance, Token}; use subtensor_swap_interface::{Order, SwapHandler}; type AccountId = ::AccountId; @@ -99,6 +99,16 @@ fn call_data_u16_u64(signature: &str, first: u16, second: u64) -> Vec { input } +/// Encodes a selector plus `(uint16,uint16)` ABI arguments. +fn call_data_u16_u16(signature: &str, first: u16, second: u16) -> Vec { + // 4-byte selector + 2 ABI words for `(uint16,uint16)`. + 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 * U96F32::from_num(1_000_000_000)).to_num::(); @@ -687,6 +697,170 @@ mod uid_lookup { } } +mod metagraph { + use super::*; + + const NETUID_U16: u16 = 1; + const UID: u16 = 0; + const EMISSION: u64 = 111; + const VTRUST: u16 = 222; + const LAST_UPDATE: u64 = 333; + const AXON_BLOCK: u64 = 444; + const AXON_VERSION: u32 = 555; + const AXON_IP: u128 = 666; + const AXON_PORT: u16 = 777; + const AXON_IP_TYPE: u8 = 4; + const AXON_PROTOCOL: u8 = 1; + + fn abi_axon_output(axon: &pallet_subtensor::AxonInfo) -> Vec { + // 6 ABI words for the static AxonInfo fields returned by the precompile. + let mut output = Vec::with_capacity(32 * 6); + push_abi_word(&mut output, U256::from(axon.block)); + push_abi_word(&mut output, U256::from(axon.version)); + push_abi_word(&mut output, U256::from(axon.ip)); + push_abi_word(&mut output, U256::from(axon.port)); + push_abi_word(&mut output, U256::from(axon.ip_type)); + push_abi_word(&mut output, U256::from(axon.protocol)); + output + } + + fn seed_metagraph_test_state() -> (NetUid, AccountId, AccountId, pallet_subtensor::AxonInfo) { + let netuid = NetUid::from(NETUID_U16); + let hotkey = pallet_subtensor::Keys::::get(netuid, UID); + let coldkey = pallet_subtensor::Owner::::get(&hotkey); + + let axon = pallet_subtensor::AxonInfo { + block: AXON_BLOCK, + version: AXON_VERSION, + ip: AXON_IP, + port: AXON_PORT, + ip_type: AXON_IP_TYPE, + protocol: AXON_PROTOCOL, + placeholder1: 0, + placeholder2: 0, + }; + + pallet_subtensor::SubnetworkN::::insert(netuid, 1); + pallet_subtensor::Emission::::insert(netuid, vec![AlphaBalance::from(EMISSION)]); + pallet_subtensor::ValidatorTrust::::insert(netuid, vec![VTRUST]); + pallet_subtensor::ValidatorPermit::::insert(netuid, vec![true]); + pallet_subtensor::LastUpdate::::insert( + NetUidStorageIndex::from(netuid), + vec![LAST_UPDATE], + ); + pallet_subtensor::Active::::insert(netuid, vec![true]); + pallet_subtensor::Axons::::insert(netuid, &hotkey, axon.clone()); + + (netuid, hotkey, coldkey, axon) + } + + #[test] + fn metagraph_precompile_matches_runtime_values() { + new_test_ext().execute_with(|| { + let (netuid, hotkey, coldkey, axon) = seed_metagraph_test_state(); + + let precompiles = Precompiles::::new(); + let caller = addr_from_index(1); + let precompile_addr = addr_from_index(MetagraphPrecompile::::INDEX); + + let uid_count = pallet_subtensor::SubnetworkN::::get(netuid); + let emission = + pallet_subtensor::Pallet::::get_emission_for_uid(netuid, UID).to_u64(); + let vtrust = + pallet_subtensor::Pallet::::get_validator_trust_for_uid(netuid, UID); + let validator_status = + pallet_subtensor::Pallet::::get_validator_permit_for_uid(netuid, UID); + let last_update = pallet_subtensor::Pallet::::get_last_update_for_uid( + NetUidStorageIndex::from(netuid), + UID, + ); + let is_active = pallet_subtensor::Pallet::::get_active_for_uid(netuid, UID); + let runtime_axon = pallet_subtensor::Pallet::::get_axon_info(netuid, &hotkey); + + assert_eq!(uid_count, 1); + assert_eq!(emission, EMISSION); + assert_eq!(vtrust, VTRUST); + assert!(validator_status); + assert_eq!(last_update, LAST_UPDATE); + assert!(is_active); + assert_eq!(runtime_axon, axon); + + precompiles + .prepare_test( + caller, + precompile_addr, + call_data_u16("getUidCount(uint16)", NETUID_U16), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::from(uid_count))); + precompiles + .prepare_test( + caller, + precompile_addr, + call_data_u16_u16("getAxon(uint16,uint16)", NETUID_U16, UID), + ) + .with_static_call(true) + .execute_returns_raw(abi_axon_output(&runtime_axon)); + precompiles + .prepare_test( + caller, + precompile_addr, + call_data_u16_u16("getEmission(uint16,uint16)", NETUID_U16, UID), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::from(emission))); + precompiles + .prepare_test( + caller, + precompile_addr, + call_data_u16_u16("getVtrust(uint16,uint16)", NETUID_U16, UID), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::from(vtrust))); + precompiles + .prepare_test( + caller, + precompile_addr, + call_data_u16_u16("getValidatorStatus(uint16,uint16)", NETUID_U16, UID), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::from(validator_status as u8))); + precompiles + .prepare_test( + caller, + precompile_addr, + call_data_u16_u16("getLastUpdate(uint16,uint16)", NETUID_U16, UID), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::from(last_update))); + precompiles + .prepare_test( + caller, + precompile_addr, + call_data_u16_u16("getIsActive(uint16,uint16)", NETUID_U16, UID), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::from(is_active as u8))); + precompiles + .prepare_test( + caller, + precompile_addr, + call_data_u16_u16("getHotkey(uint16,uint16)", NETUID_U16, UID), + ) + .with_static_call(true) + .execute_returns_raw(H256::from_slice(hotkey.as_ref()).as_bytes().to_vec()); + precompiles + .prepare_test( + caller, + precompile_addr, + call_data_u16_u16("getColdkey(uint16,uint16)", NETUID_U16, UID), + ) + .with_static_call(true) + .execute_returns_raw(H256::from_slice(coldkey.as_ref()).as_bytes().to_vec()); + }); + } +} + mod balance_transfer { use super::*; From fbe080ad8066da8ce8bb2748bedbe25d90fa3ecc Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 20 Apr 2026 18:16:56 +0200 Subject: [PATCH 12/21] Port neuron emission check precompile --- .../neuron.precompile.emission-check.test.ts | 73 --------------- runtime/tests/precompiles.rs | 89 ++++++++++++++++--- 2 files changed, 76 insertions(+), 86 deletions(-) delete mode 100644 contract-tests/test/neuron.precompile.emission-check.test.ts diff --git a/contract-tests/test/neuron.precompile.emission-check.test.ts b/contract-tests/test/neuron.precompile.emission-check.test.ts deleted file mode 100644 index 1a2b053ed0..0000000000 --- a/contract-tests/test/neuron.precompile.emission-check.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as assert from "assert"; - -import { getAliceSigner, 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 { PolkadotSigner, TypedApi } from "polkadot-api"; -import { convertPublicKeyToSs58, } from "../src/address-utils" -import { ethers } from "ethers" -import { INEURON_ADDRESS, INeuronABI } from "../src/contracts/neuron" -import { generateRandomEthersWallet } from "../src/utils" -import { forceSetBalanceToSs58Address, forceSetBalanceToEthAddress, addNewSubnetwork, startCall, setSubtokenEnable } from "../src/subtensor" - -describe("Test the Neuron precompile with emission", () => { - // init eth part - const wallet = generateRandomEthersWallet(); - - // init substrate part - const hotkey = getRandomSubstrateKeypair(); - const hotkey2 = getRandomSubstrateKeypair(); - const coldkey = getRandomSubstrateKeypair(); - let publicClient: PublicClient; - - let api: TypedApi - - // sudo account alice as signer - let alice: PolkadotSigner; - - before(async () => { - // init variables got from await and async - publicClient = await getPublicClient(ETH_LOCAL_URL) - api = await getDevnetApi() - alice = await getAliceSigner(); - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey2.publicKey)) - - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) - await forceSetBalanceToEthAddress(api, wallet.address) - - const netuid = await addNewSubnetwork(api, hotkey2, coldkey) - await startCall(api, netuid, coldkey) - console.log("test on subnet ", netuid) - }) - - it("Burned register and check emission", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 - - const uid = await api.query.SubtensorModule.SubnetworkN.getValue(netuid) - const contract = new ethers.Contract(INEURON_ADDRESS, INeuronABI, wallet); - - const tx = await contract.burnedRegister( - netuid, - hotkey.publicKey - ); - await tx.wait(); - - const uidAfterNew = await api.query.SubtensorModule.SubnetworkN.getValue(netuid) - assert.equal(uid + 1, uidAfterNew) - - const key = await api.query.SubtensorModule.Keys.getValue(netuid, uid) - assert.equal(key, convertPublicKeyToSs58(hotkey.publicKey)) - - let i = 0; - while (i < 10) { - const emission = await api.query.SubtensorModule.Emission.getValue(netuid) - - console.log("emission is ", emission); - await new Promise((resolve) => setTimeout(resolve, 2000)); - i += 1; - } - }) -}); \ No newline at end of file diff --git a/runtime/tests/precompiles.rs b/runtime/tests/precompiles.rs index 66f77ec7f5..9f515181c3 100644 --- a/runtime/tests/precompiles.rs +++ b/runtime/tests/precompiles.rs @@ -14,13 +14,15 @@ use sp_runtime::traits::Hash; use substrate_fixed::types::{I96F32, U96F32}; use subtensor_precompiles::{ AddressMappingPrecompile, AlphaPrecompile, BalanceTransferPrecompile, Ed25519Verify, - MetagraphPrecompile, PrecompileExt, Precompiles, UidLookupPrecompile, + MetagraphPrecompile, NeuronPrecompile, PrecompileExt, Precompiles, UidLookupPrecompile, }; use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance, Token}; use subtensor_swap_interface::{Order, SwapHandler}; type AccountId = ::AccountId; +const TEST_NETUID_U16: u16 = 1; + fn new_test_ext() -> sp_io::TestExternalities { let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig::default() .build_storage() @@ -668,7 +670,7 @@ mod uid_lookup { let caller = addr_from_index(1); let precompile_addr = addr_from_index(UidLookupPrecompile::::INDEX); - let netuid = NetUid::from(1); + let netuid = NetUid::from(TEST_NETUID_U16); let netuid_u16: u16 = netuid.into(); let uid = 0u16; let evm_address = H160::from_low_u64_be(0xdead_beef); @@ -700,7 +702,6 @@ mod uid_lookup { mod metagraph { use super::*; - const NETUID_U16: u16 = 1; const UID: u16 = 0; const EMISSION: u64 = 111; const VTRUST: u16 = 222; @@ -725,7 +726,7 @@ mod metagraph { } fn seed_metagraph_test_state() -> (NetUid, AccountId, AccountId, pallet_subtensor::AxonInfo) { - let netuid = NetUid::from(NETUID_U16); + let netuid = NetUid::from(TEST_NETUID_U16); let hotkey = pallet_subtensor::Keys::::get(netuid, UID); let coldkey = pallet_subtensor::Owner::::get(&hotkey); @@ -789,7 +790,7 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16("getUidCount(uint16)", NETUID_U16), + call_data_u16("getUidCount(uint16)", TEST_NETUID_U16), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::from(uid_count))); @@ -797,7 +798,7 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getAxon(uint16,uint16)", NETUID_U16, UID), + call_data_u16_u16("getAxon(uint16,uint16)", TEST_NETUID_U16, UID), ) .with_static_call(true) .execute_returns_raw(abi_axon_output(&runtime_axon)); @@ -805,7 +806,7 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getEmission(uint16,uint16)", NETUID_U16, UID), + call_data_u16_u16("getEmission(uint16,uint16)", TEST_NETUID_U16, UID), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::from(emission))); @@ -813,7 +814,7 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getVtrust(uint16,uint16)", NETUID_U16, UID), + call_data_u16_u16("getVtrust(uint16,uint16)", TEST_NETUID_U16, UID), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::from(vtrust))); @@ -821,7 +822,7 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getValidatorStatus(uint16,uint16)", NETUID_U16, UID), + call_data_u16_u16("getValidatorStatus(uint16,uint16)", TEST_NETUID_U16, UID), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::from(validator_status as u8))); @@ -829,7 +830,7 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getLastUpdate(uint16,uint16)", NETUID_U16, UID), + call_data_u16_u16("getLastUpdate(uint16,uint16)", TEST_NETUID_U16, UID), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::from(last_update))); @@ -837,7 +838,7 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getIsActive(uint16,uint16)", NETUID_U16, UID), + call_data_u16_u16("getIsActive(uint16,uint16)", TEST_NETUID_U16, UID), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::from(is_active as u8))); @@ -845,7 +846,7 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getHotkey(uint16,uint16)", NETUID_U16, UID), + call_data_u16_u16("getHotkey(uint16,uint16)", TEST_NETUID_U16, UID), ) .with_static_call(true) .execute_returns_raw(H256::from_slice(hotkey.as_ref()).as_bytes().to_vec()); @@ -853,7 +854,7 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getColdkey(uint16,uint16)", NETUID_U16, UID), + call_data_u16_u16("getColdkey(uint16,uint16)", TEST_NETUID_U16, UID), ) .with_static_call(true) .execute_returns_raw(H256::from_slice(coldkey.as_ref()).as_bytes().to_vec()); @@ -861,6 +862,68 @@ mod metagraph { } } +mod neuron { + use super::*; + + const REGISTRATION_BURN: u64 = 1_000; + const RESERVE: u64 = 1_000_000_000; + const COLDKEY_BALANCE: u64 = 50_000; + + fn burned_register_call_data(netuid: u16, hotkey: H256) -> Vec { + // 4-byte selector + 2 ABI words for `(uint16,bytes32)`. + let mut input = Vec::with_capacity(4 + 64); + input.extend_from_slice(&selector("burnedRegister(uint16,bytes32)")); + push_abi_word(&mut input, U256::from(netuid)); + input.extend_from_slice(hotkey.as_bytes()); + input + } + + #[test] + fn neuron_precompile_burned_register_adds_a_new_uid_and_key() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(TEST_NETUID_U16); + let caller = addr_from_index(0x1234); + let caller_account = + ::AddressMapping::into_account_id(caller); + let hotkey_account = AccountId::from([0x42; 32]); + let hotkey = H256::from_slice(hotkey_account.as_ref()); + + pallet_subtensor::Pallet::::set_network_registration_allowed(netuid, true); + pallet_subtensor::Pallet::::set_burn(netuid, REGISTRATION_BURN.into()); + pallet_subtensor::Pallet::::set_max_allowed_uids(netuid, 4096); + pallet_subtensor::SubnetTAO::::insert(netuid, TaoBalance::from(RESERVE)); + pallet_subtensor::SubnetAlphaIn::::insert(netuid, AlphaBalance::from(RESERVE)); + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &caller_account, + COLDKEY_BALANCE.into(), + ); + + let uid_before = pallet_subtensor::SubnetworkN::::get(netuid); + let balance_before = + pallet_subtensor::Pallet::::get_coldkey_balance(&caller_account).to_u64(); + + Precompiles::::new() + .prepare_test( + caller, + addr_from_index(NeuronPrecompile::::INDEX), + burned_register_call_data(TEST_NETUID_U16, hotkey), + ) + .execute_returns(()); + + let uid_after = pallet_subtensor::SubnetworkN::::get(netuid); + let registered_hotkey = pallet_subtensor::Keys::::get(netuid, uid_before); + let owner = pallet_subtensor::Owner::::get(&hotkey_account); + let balance_after = + pallet_subtensor::Pallet::::get_coldkey_balance(&caller_account).to_u64(); + + assert_eq!(uid_after, uid_before + 1); + assert_eq!(registered_hotkey, hotkey_account); + assert_eq!(owner, caller_account); + assert!(balance_after < balance_before); + }); + } +} + mod balance_transfer { use super::*; From ec3e78516c68bcfe53d7a17c72c8157aad79f68a Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Mon, 20 Apr 2026 19:53:39 +0200 Subject: [PATCH 13/21] Port commite reveal precompile methods tests --- .../neuron.precompile.reveal-weights.test.ts | 238 -------- runtime/tests/precompiles.rs | 513 ++++++++++++------ 2 files changed, 347 insertions(+), 404 deletions(-) delete mode 100644 contract-tests/test/neuron.precompile.reveal-weights.test.ts diff --git a/contract-tests/test/neuron.precompile.reveal-weights.test.ts b/contract-tests/test/neuron.precompile.reveal-weights.test.ts deleted file mode 100644 index 5d80183ec7..0000000000 --- a/contract-tests/test/neuron.precompile.reveal-weights.test.ts +++ /dev/null @@ -1,238 +0,0 @@ -import * as assert from "assert"; -import { getAliceSigner, getDevnetApi, getRandomSubstrateKeypair, waitForTransactionWithRetry } from "../src/substrate" -import { devnet } from "@polkadot-api/descriptors" -import { PolkadotSigner, TypedApi } from "polkadot-api"; -import { convertPublicKeyToSs58, convertH160ToSS58 } from "../src/address-utils" -import { Vec, Tuple, VecFixed, u16, u8, u64 } from "@polkadot/types-codec"; -import { TypeRegistry } from "@polkadot/types"; -import { ethers } from "ethers" -import { INEURON_ADDRESS, INeuronABI } from "../src/contracts/neuron" -import { generateRandomEthersWallet } from "../src/utils" -import { convertH160ToPublicKey } from "../src/address-utils" -import { blake2AsU8a } from "@polkadot/util-crypto" -import { - forceSetBalanceToEthAddress, forceSetBalanceToSs58Address, addNewSubnetwork, setWeightsSetRateLimit, burnedRegister, - setTempo, setCommitRevealWeightsInterval, - startCall, - disableAdminFreezeWindowAndOwnerHyperparamRateLimit, -} from "../src/subtensor" - -// hardcode some values for reveal hash -const uids = [1]; -const values = [5]; -const salt = [9]; -const version_key = 0; - -async function setStakeThreshold( - api: TypedApi, - alice: PolkadotSigner, - minStake: bigint, -) { - const internalCall = api.tx.AdminUtils.sudo_set_stake_threshold({ min_stake: minStake }) - const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }) - await waitForTransactionWithRetry(api, tx, alice) -} - -function getCommitHash(netuid: number, address: string) { - const registry = new TypeRegistry(); - let publicKey = convertH160ToPublicKey(address); - - const tupleData = new Tuple( - registry, - [ - VecFixed.with(u8, 32), - u16, - Vec.with(u16), - Vec.with(u16), - Vec.with(u16), - u64, - ], - [publicKey, netuid, uids, values, salt, version_key] - ); - - const hash = blake2AsU8a(tupleData.toU8a()); - return hash; -} - -describe("Test neuron precompile reveal weights", () => { - // init eth part - const wallet = generateRandomEthersWallet(); - - // init substrate part - const hotkey = getRandomSubstrateKeypair(); - const coldkey = getRandomSubstrateKeypair(); - - let api: TypedApi - let commitEpoch: number | undefined; - - // sudo account alice as signer - let alice: PolkadotSigner; - before(async () => { - // init variables got from await and async - api = await getDevnetApi() - alice = await getAliceSigner(); - - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(alice.publicKey)) - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) - await forceSetBalanceToEthAddress(api, wallet.address) - let netuid = await addNewSubnetwork(api, hotkey, coldkey) - // await disableCommitRevealWeights(api, netuid) - await startCall(api, netuid, coldkey) - - console.log("test the case on subnet ", netuid) - await disableAdminFreezeWindowAndOwnerHyperparamRateLimit(api) - - await setWeightsSetRateLimit(api, netuid, BigInt(0)) - - const ss58Address = convertH160ToSS58(wallet.address) - await burnedRegister(api, netuid, ss58Address, coldkey) - - const uid = await api.query.SubtensorModule.Uids.getValue( - netuid, - ss58Address - ) - // eth wallet account should be the first neuron in the subnet - assert.equal(uid, uids[0]) - }) - - async function ensureCommitEpoch(netuid: number, contract: ethers.Contract) { - if (commitEpoch !== undefined) { - return - } - - const ss58Address = convertH160ToSS58(wallet.address) - const existingCommits = await api.query.SubtensorModule.WeightCommits.getValue( - netuid, - ss58Address - ) - if (Array.isArray(existingCommits) && existingCommits.length > 0) { - const entry = existingCommits[0] - const commitBlockRaw = - Array.isArray(entry) && entry.length > 1 ? entry[1] : undefined - const commitBlock = - typeof commitBlockRaw === "bigint" - ? Number(commitBlockRaw) - : Number(commitBlockRaw ?? NaN) - if (Number.isFinite(commitBlock)) { - commitEpoch = Math.trunc(commitBlock / (100 + 1)) - return - } - } - - await setStakeThreshold(api, alice, BigInt(0)) - const commitHash = getCommitHash(netuid, wallet.address) - const tx = await contract.commitWeights(netuid, commitHash) - await tx.wait() - - const commitBlock = await api.query.System.Number.getValue() - commitEpoch = Math.trunc(commitBlock / (100 + 1)) - } - - it("EVM neuron commit weights via call precompile", async () => { - let totalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue() - const subnetId = totalNetworks - 1 - const commitHash = getCommitHash(subnetId, wallet.address) - const contract = new ethers.Contract(INEURON_ADDRESS, INeuronABI, wallet); - - await setStakeThreshold(api, alice, BigInt(1)) - await assert.rejects(async () => { - const tx = await contract.commitWeights(subnetId, commitHash) - await tx.wait() - }) - await setStakeThreshold(api, alice, BigInt(0)) - - try { - const tx = await contract.commitWeights(subnetId, commitHash) - await tx.wait() - } catch (e) { - console.log("commitWeights failed", e) - } - - const commitBlock = await api.query.System.Number.getValue() - commitEpoch = Math.trunc(commitBlock / (100 + 1)) - - const ss58Address = convertH160ToSS58(wallet.address) - - const weightsCommit = await api.query.SubtensorModule.WeightCommits.getValue(subnetId, ss58Address) - if (weightsCommit === undefined) { - throw new Error("submit weights failed") - } - else { assert.ok(weightsCommit.length > 0) } - }) - - // Temporarily disable it, there is a type error in CI. - it("EVM neuron reveal weights via call precompile", async () => { - let totalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue() - const netuid = totalNetworks - 1 - const contract = new ethers.Contract(INEURON_ADDRESS, INeuronABI, wallet); - // set tempo or epoch large, then enough time to reveal weight - await setTempo(api, netuid, 100) - // set interval epoch as 1, it is the minimum value now - await setCommitRevealWeightsInterval(api, netuid, BigInt(1)) - - await ensureCommitEpoch(netuid, contract) - if (commitEpoch === undefined) { - throw new Error("commitEpoch should be set before revealing weights") - } - - while (true) { - const currentBlock = await api.query.System.Number.getValue() - const currentEpoch = Math.trunc(currentBlock / (100 + 1)) - // wait for one second for fast blocks - if (currentEpoch > commitEpoch) { - break - } - await new Promise(resolve => setTimeout(resolve, 1000)) - } - - await setStakeThreshold(api, alice, BigInt(1)) - await assert.rejects(async () => { - const tx = await contract.revealWeights( - netuid, - uids, - values, - salt, - version_key - ); - await tx.wait() - }) - await setStakeThreshold(api, alice, BigInt(0)) - - const tx = await contract.revealWeights( - netuid, - uids, - values, - salt, - version_key - ); - await tx.wait() - - const ss58Address = convertH160ToSS58(wallet.address) - - // check the weight commit is removed after reveal successfully - const weightsCommit = await api.query.SubtensorModule.WeightCommits.getValue(netuid, ss58Address) - assert.equal(weightsCommit, undefined) - - // check the weight is set after reveal with correct uid - const neuron_uid = await api.query.SubtensorModule.Uids.getValue( - netuid, - ss58Address - ) - - if (neuron_uid === undefined) { - throw new Error("neuron_uid not available onchain or invalid type") - } - - const weights = await api.query.SubtensorModule.Weights.getValue(netuid, neuron_uid) - - if (weights === undefined || !Array.isArray(weights)) { - throw new Error("weights not available onchain or invalid type") - } - - for (const weight of weights) { - assert.equal(weight[0], neuron_uid) - assert.ok(weight[1] !== undefined) - } - }) -}); diff --git a/runtime/tests/precompiles.rs b/runtime/tests/precompiles.rs index 9f515181c3..eb1c92faef 100644 --- a/runtime/tests/precompiles.rs +++ b/runtime/tests/precompiles.rs @@ -8,6 +8,7 @@ use std::collections::BTreeSet; use fp_evm::{Context, ExitError, PrecompileFailure, PrecompileResult}; use node_subtensor_runtime::{BuildStorage, Runtime, RuntimeGenesisConfig, System}; use pallet_evm::{AddressMapping, BalanceConverter, PrecompileSet}; +use precompile_utils::solidity::{codec::Address, encode_return_value, encode_with_selector}; use precompile_utils::testing::{MockHandle, PrecompileTesterExt}; use sp_core::{H160, H256, Pair, U256, ed25519}; use sp_runtime::traits::Hash; @@ -61,54 +62,15 @@ 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] { +fn selector_u32(signature: &str) -> u32 { 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 { - // 4-byte selector + 1 ABI word for the uint16 argument. - 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 { - // 4-byte selector + 2 ABI words for `(uint16,uint64)`. - 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 -} - -/// Encodes a selector plus `(uint16,uint16)` ABI arguments. -fn call_data_u16_u16(signature: &str, first: u16, second: u16) -> Vec { - // 4-byte selector + 2 ABI words for `(uint16,uint16)`. - 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 + u32::from_be_bytes([hash[0], hash[1], hash[2], hash[3]]) } /// Matches the alpha precompile conversion from fixed-point price to EVM `uint256`. @@ -131,17 +93,6 @@ fn precompile_registry_addresses_are_unique() { mod address_mapping { use super::*; - fn address_mapping_call_data(target: H160) -> Vec { - // 4-byte selector + 1 ABI word for the address argument. - let mut input = Vec::with_capacity(4 + 32); - 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). - input.extend_from_slice(target.as_bytes()); - input - } - #[test] fn address_mapping_precompile_returns_runtime_address_mapping() { new_test_ext().execute_with(|| { @@ -149,7 +100,10 @@ mod address_mapping { let caller = addr_from_index(1); let target_address = addr_from_index(0x1234); - let input = address_mapping_call_data(target_address); + let input = encode_with_selector( + selector_u32("addressMapping(address)"), + (Address(target_address),), + ); let mapped_account = ::AddressMapping::into_account_id(target_address); @@ -175,7 +129,10 @@ mod address_mapping { &Precompiles::::new(), precompile_addr, caller, - address_mapping_call_data(first_address), + encode_with_selector( + selector_u32("addressMapping(address)"), + (Address(first_address),), + ), U256::zero(), ) .expect("expected precompile mapping call to be routed to a precompile") @@ -185,7 +142,10 @@ mod address_mapping { &Precompiles::::new(), precompile_addr, caller, - address_mapping_call_data(second_address), + encode_with_selector( + selector_u32("addressMapping(address)"), + (Address(second_address),), + ), U256::zero(), ) .expect("expected precompile mapping call to be routed to a precompile") @@ -202,7 +162,10 @@ mod address_mapping { 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 input = encode_with_selector( + selector_u32("addressMapping(address)"), + (Address(target_address),), + ); let first_output = execute_precompile( &Precompiles::::new(), @@ -343,28 +306,34 @@ mod alpha { &precompiles, caller, precompile_addr, - call_data_u16("getAlphaPrice(uint16)", DYNAMIC_NETUID_U16), + encode_with_selector(selector_u32("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), + encode_with_selector( + selector_u32("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), + encode_with_selector(selector_u32("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), + encode_with_selector( + selector_u32("getAlphaInPool(uint16)"), + (DYNAMIC_NETUID_U16,), + ), U256::from(u64::from(pallet_subtensor::SubnetAlphaIn::::get( dynamic_netuid, ))), @@ -373,7 +342,10 @@ mod alpha { &precompiles, caller, precompile_addr, - call_data_u16("getAlphaOutPool(uint16)", DYNAMIC_NETUID_U16), + encode_with_selector( + selector_u32("getAlphaOutPool(uint16)"), + (DYNAMIC_NETUID_U16,), + ), U256::from(u64::from(pallet_subtensor::SubnetAlphaOut::::get( dynamic_netuid, ))), @@ -382,7 +354,10 @@ mod alpha { &precompiles, caller, precompile_addr, - call_data_u16("getAlphaIssuance(uint16)", DYNAMIC_NETUID_U16), + encode_with_selector( + selector_u32("getAlphaIssuance(uint16)"), + (DYNAMIC_NETUID_U16,), + ), U256::from(u64::from( pallet_subtensor::Pallet::::get_alpha_issuance(dynamic_netuid), )), @@ -391,7 +366,10 @@ mod alpha { &precompiles, caller, precompile_addr, - call_data_u16("getSubnetMechanism(uint16)", DYNAMIC_NETUID_U16), + encode_with_selector( + selector_u32("getSubnetMechanism(uint16)"), + (DYNAMIC_NETUID_U16,), + ), U256::from(pallet_subtensor::SubnetMechanism::::get( dynamic_netuid, )), @@ -400,7 +378,10 @@ mod alpha { &precompiles, caller, precompile_addr, - call_data_u16("getEMAPriceHalvingBlocks(uint16)", DYNAMIC_NETUID_U16), + encode_with_selector( + selector_u32("getEMAPriceHalvingBlocks(uint16)"), + (DYNAMIC_NETUID_U16,), + ), U256::from(pallet_subtensor::EMAPriceHalvingBlocks::::get( dynamic_netuid, )), @@ -409,7 +390,10 @@ mod alpha { &precompiles, caller, precompile_addr, - call_data_u16("getSubnetVolume(uint16)", DYNAMIC_NETUID_U16), + encode_with_selector( + selector_u32("getSubnetVolume(uint16)"), + (DYNAMIC_NETUID_U16,), + ), U256::from(pallet_subtensor::SubnetVolume::::get( dynamic_netuid, )), @@ -418,7 +402,10 @@ mod alpha { &precompiles, caller, precompile_addr, - call_data_u16("getTaoInEmission(uint16)", DYNAMIC_NETUID_U16), + encode_with_selector( + selector_u32("getTaoInEmission(uint16)"), + (DYNAMIC_NETUID_U16,), + ), U256::from( pallet_subtensor::SubnetTaoInEmission::::get(dynamic_netuid).to_u64(), ), @@ -427,7 +414,10 @@ mod alpha { &precompiles, caller, precompile_addr, - call_data_u16("getAlphaInEmission(uint16)", DYNAMIC_NETUID_U16), + encode_with_selector( + selector_u32("getAlphaInEmission(uint16)"), + (DYNAMIC_NETUID_U16,), + ), U256::from( pallet_subtensor::SubnetAlphaInEmission::::get(dynamic_netuid) .to_u64(), @@ -437,7 +427,10 @@ mod alpha { &precompiles, caller, precompile_addr, - call_data_u16("getAlphaOutEmission(uint16)", DYNAMIC_NETUID_U16), + encode_with_selector( + selector_u32("getAlphaOutEmission(uint16)"), + (DYNAMIC_NETUID_U16,), + ), U256::from( pallet_subtensor::SubnetAlphaOutEmission::::get(dynamic_netuid) .to_u64(), @@ -475,28 +468,28 @@ mod alpha { &precompiles, caller, precompile_addr, - call_data_no_args("getCKBurn()"), + selector_u32("getCKBurn()").to_be_bytes().to_vec(), U256::from(pallet_subtensor::CKBurn::::get()), ); assert_static_call( &precompiles, caller, precompile_addr, - call_data_no_args("getTaoWeight()"), + selector_u32("getTaoWeight()").to_be_bytes().to_vec(), U256::from(pallet_subtensor::TaoWeight::::get()), ); assert_static_call( &precompiles, caller, precompile_addr, - call_data_no_args("getRootNetuid()"), + selector_u32("getRootNetuid()").to_be_bytes().to_vec(), U256::from(u16::from(NetUid::ROOT)), ); assert_static_call( &precompiles, caller, precompile_addr, - call_data_no_args("getSumAlphaPrice()"), + selector_u32("getSumAlphaPrice()").to_be_bytes().to_vec(), alpha_price_to_evm(sum_alpha_price), ); }); @@ -535,10 +528,9 @@ mod alpha { &precompiles, caller, precompile_addr, - call_data_u16_u64( - "simSwapTaoForAlpha(uint16,uint64)", - DYNAMIC_NETUID_U16, - tao_amount, + encode_with_selector( + selector_u32("simSwapTaoForAlpha(uint16,uint64)"), + (DYNAMIC_NETUID_U16, tao_amount), ), U256::from(expected_alpha), ); @@ -546,10 +538,9 @@ mod alpha { &precompiles, caller, precompile_addr, - call_data_u16_u64( - "simSwapAlphaForTao(uint16,uint64)", - DYNAMIC_NETUID_U16, - alpha_amount, + encode_with_selector( + selector_u32("simSwapAlphaForTao(uint16,uint64)"), + (DYNAMIC_NETUID_U16, alpha_amount), ), U256::from(expected_tao), ); @@ -557,14 +548,20 @@ mod alpha { &precompiles, caller, precompile_addr, - call_data_u16_u64("simSwapTaoForAlpha(uint16,uint64)", DYNAMIC_NETUID_U16, 0), + encode_with_selector( + selector_u32("simSwapTaoForAlpha(uint16,uint64)"), + (DYNAMIC_NETUID_U16, 0_u64), + ), U256::zero(), ); assert_static_call( &precompiles, caller, precompile_addr, - call_data_u16_u64("simSwapAlphaForTao(uint16,uint64)", DYNAMIC_NETUID_U16, 0), + encode_with_selector( + selector_u32("simSwapAlphaForTao(uint16,uint64)"), + (DYNAMIC_NETUID_U16, 0_u64), + ), U256::zero(), ); }); @@ -574,21 +571,6 @@ mod alpha { mod ed25519_verify { use super::*; - fn ed25519_verify_call_data( - message: [u8; 32], - public_key: [u8; 32], - signature: [u8; 64], - ) -> Vec { - // 4-byte selector + 4 ABI words: message, public key, signature R, signature S. - let mut input = Vec::with_capacity(4 + 32 * 4); - input.extend_from_slice(&selector("verify(bytes32,bytes32,bytes32,bytes32)")); - input.extend_from_slice(&message); - input.extend_from_slice(&public_key); - input.extend_from_slice(&signature[..32]); - input.extend_from_slice(&signature[32..]); - input - } - #[test] fn ed25519_precompile_verifies_valid_and_invalid_signatures() { new_test_ext().execute_with(|| { @@ -607,12 +589,22 @@ mod ed25519_verify { let precompiles = Precompiles::::new(); let caller = addr_from_index(1); let precompile_addr = addr_from_index(Ed25519Verify::::INDEX); + let message = H256::from(message); + let broken_message = H256::from(broken_message); + let public_key = H256::from(public_key); + let signature_r = H256::from_slice(&signature[..32]); + let signature_s = H256::from_slice(&signature[32..]); + let broken_signature_r = H256::from_slice(&broken_signature[..32]); + let broken_signature_s = H256::from_slice(&broken_signature[32..]); precompiles .prepare_test( caller, precompile_addr, - ed25519_verify_call_data(message, public_key, signature), + encode_with_selector( + selector_u32("verify(bytes32,bytes32,bytes32,bytes32)"), + (message, public_key, signature_r, signature_s), + ), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::one())); @@ -620,7 +612,10 @@ mod ed25519_verify { .prepare_test( caller, precompile_addr, - ed25519_verify_call_data(broken_message, public_key, signature), + encode_with_selector( + selector_u32("verify(bytes32,bytes32,bytes32,bytes32)"), + (broken_message, public_key, signature_r, signature_s), + ), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::zero())); @@ -628,7 +623,10 @@ mod ed25519_verify { .prepare_test( caller, precompile_addr, - ed25519_verify_call_data(message, public_key, broken_signature), + encode_with_selector( + selector_u32("verify(bytes32,bytes32,bytes32,bytes32)"), + (message, public_key, broken_signature_r, broken_signature_s), + ), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::zero())); @@ -639,30 +637,6 @@ mod ed25519_verify { mod uid_lookup { use super::*; - fn uid_lookup_call_data(netuid: u16, evm_address: H160, limit: u16) -> Vec { - // 4-byte selector + 3 ABI words: netuid, address, limit. - let mut input = Vec::with_capacity(4 + 32 * 3); - input.extend_from_slice(&selector("uidLookup(uint16,address,uint16)")); - push_abi_word(&mut input, U256::from(netuid)); - input.extend_from_slice(&[0u8; 12]); - input.extend_from_slice(evm_address.as_bytes()); - push_abi_word(&mut input, U256::from(limit)); - input - } - - fn abi_uid_lookup_output(entries: &[(u16, u64)]) -> Vec { - // ABI dynamic array encoding: - // head offset word + array length word + 2 words per tuple entry `(uid, block_associated)`. - let mut output = Vec::with_capacity(64 + entries.len() * 64); - push_abi_word(&mut output, U256::from(32u64)); - push_abi_word(&mut output, U256::from(entries.len())); - for (uid, block_associated) in entries { - push_abi_word(&mut output, U256::from(*uid)); - push_abi_word(&mut output, U256::from(*block_associated)); - } - output - } - #[test] fn uid_lookup_precompile_returns_associated_uid_and_block() { new_test_ext().execute_with(|| { @@ -691,10 +665,13 @@ mod uid_lookup { .prepare_test( caller, precompile_addr, - uid_lookup_call_data(netuid_u16, evm_address, limit), + encode_with_selector( + selector_u32("uidLookup(uint16,address,uint16)"), + (netuid_u16, Address(evm_address), limit), + ), ) .with_static_call(true) - .execute_returns_raw(abi_uid_lookup_output(&expected)); + .execute_returns_raw(encode_return_value(expected)); }); } } @@ -713,18 +690,6 @@ mod metagraph { const AXON_IP_TYPE: u8 = 4; const AXON_PROTOCOL: u8 = 1; - fn abi_axon_output(axon: &pallet_subtensor::AxonInfo) -> Vec { - // 6 ABI words for the static AxonInfo fields returned by the precompile. - let mut output = Vec::with_capacity(32 * 6); - push_abi_word(&mut output, U256::from(axon.block)); - push_abi_word(&mut output, U256::from(axon.version)); - push_abi_word(&mut output, U256::from(axon.ip)); - push_abi_word(&mut output, U256::from(axon.port)); - push_abi_word(&mut output, U256::from(axon.ip_type)); - push_abi_word(&mut output, U256::from(axon.protocol)); - output - } - fn seed_metagraph_test_state() -> (NetUid, AccountId, AccountId, pallet_subtensor::AxonInfo) { let netuid = NetUid::from(TEST_NETUID_U16); let hotkey = pallet_subtensor::Keys::::get(netuid, UID); @@ -790,7 +755,7 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16("getUidCount(uint16)", TEST_NETUID_U16), + encode_with_selector(selector_u32("getUidCount(uint16)"), (TEST_NETUID_U16,)), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::from(uid_count))); @@ -798,15 +763,28 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getAxon(uint16,uint16)", TEST_NETUID_U16, UID), + encode_with_selector( + selector_u32("getAxon(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), ) .with_static_call(true) - .execute_returns_raw(abi_axon_output(&runtime_axon)); + .execute_returns_raw(encode_return_value(( + runtime_axon.block, + runtime_axon.version, + runtime_axon.ip, + runtime_axon.port, + runtime_axon.ip_type, + runtime_axon.protocol, + ))); precompiles .prepare_test( caller, precompile_addr, - call_data_u16_u16("getEmission(uint16,uint16)", TEST_NETUID_U16, UID), + encode_with_selector( + selector_u32("getEmission(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::from(emission))); @@ -814,7 +792,10 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getVtrust(uint16,uint16)", TEST_NETUID_U16, UID), + encode_with_selector( + selector_u32("getVtrust(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::from(vtrust))); @@ -822,7 +803,10 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getValidatorStatus(uint16,uint16)", TEST_NETUID_U16, UID), + encode_with_selector( + selector_u32("getValidatorStatus(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::from(validator_status as u8))); @@ -830,7 +814,10 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getLastUpdate(uint16,uint16)", TEST_NETUID_U16, UID), + encode_with_selector( + selector_u32("getLastUpdate(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::from(last_update))); @@ -838,7 +825,10 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getIsActive(uint16,uint16)", TEST_NETUID_U16, UID), + encode_with_selector( + selector_u32("getIsActive(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), ) .with_static_call(true) .execute_returns_raw(abi_word(U256::from(is_active as u8))); @@ -846,7 +836,10 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getHotkey(uint16,uint16)", TEST_NETUID_U16, UID), + encode_with_selector( + selector_u32("getHotkey(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), ) .with_static_call(true) .execute_returns_raw(H256::from_slice(hotkey.as_ref()).as_bytes().to_vec()); @@ -854,7 +847,10 @@ mod metagraph { .prepare_test( caller, precompile_addr, - call_data_u16_u16("getColdkey(uint16,uint16)", TEST_NETUID_U16, UID), + encode_with_selector( + selector_u32("getColdkey(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), ) .with_static_call(true) .execute_returns_raw(H256::from_slice(coldkey.as_ref()).as_bytes().to_vec()); @@ -868,14 +864,65 @@ mod neuron { const REGISTRATION_BURN: u64 = 1_000; const RESERVE: u64 = 1_000_000_000; const COLDKEY_BALANCE: u64 = 50_000; + const TEMPO: u16 = 100; + const REVEAL_PERIOD: u64 = 1; + const VERSION_KEY: u64 = 0; + const REGISTERED_UID: u16 = 1; + const REVEAL_UIDS: [u16; 1] = [REGISTERED_UID]; + const REVEAL_VALUES: [u16; 1] = [5]; + const REVEAL_SALT: [u16; 1] = [9]; + + fn setup_registered_caller(caller: H160) -> (NetUid, AccountId) { + let netuid = NetUid::from(TEST_NETUID_U16); + let caller_account = + ::AddressMapping::into_account_id(caller); + let caller_hotkey = H256::from_slice(caller_account.as_ref()); + + pallet_subtensor::Pallet::::set_network_registration_allowed(netuid, true); + pallet_subtensor::Pallet::::set_burn(netuid, REGISTRATION_BURN.into()); + pallet_subtensor::Pallet::::set_max_allowed_uids(netuid, 4096); + pallet_subtensor::Pallet::::set_weights_set_rate_limit(netuid, 0); + pallet_subtensor::Pallet::::set_tempo(netuid, TEMPO); + pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid, true); + pallet_subtensor::Pallet::::set_reveal_period(netuid, REVEAL_PERIOD) + .expect("reveal period setup should succeed"); + pallet_subtensor::SubnetTAO::::insert(netuid, TaoBalance::from(RESERVE)); + pallet_subtensor::SubnetAlphaIn::::insert(netuid, AlphaBalance::from(RESERVE)); + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &caller_account, + COLDKEY_BALANCE.into(), + ); + + Precompiles::::new() + .prepare_test( + caller, + addr_from_index(NeuronPrecompile::::INDEX), + encode_with_selector( + selector_u32("burnedRegister(uint16,bytes32)"), + (TEST_NETUID_U16, caller_hotkey), + ), + ) + .execute_returns(()); + + let registered_uid = pallet_subtensor::Pallet::::get_uid_for_net_and_hotkey( + netuid, + &caller_account, + ) + .expect("caller should be registered on subnet"); + assert_eq!(registered_uid, REGISTERED_UID); - fn burned_register_call_data(netuid: u16, hotkey: H256) -> Vec { - // 4-byte selector + 2 ABI words for `(uint16,bytes32)`. - let mut input = Vec::with_capacity(4 + 64); - input.extend_from_slice(&selector("burnedRegister(uint16,bytes32)")); - push_abi_word(&mut input, U256::from(netuid)); - input.extend_from_slice(hotkey.as_bytes()); - input + (netuid, caller_account) + } + + fn reveal_commit_hash(caller_account: &AccountId, netuid: NetUid) -> H256 { + ::Hashing::hash_of(&( + caller_account.clone(), + NetUidStorageIndex::from(netuid), + REVEAL_UIDS.as_slice(), + REVEAL_VALUES.as_slice(), + REVEAL_SALT.as_slice(), + VERSION_KEY, + )) } #[test] @@ -906,7 +953,10 @@ mod neuron { .prepare_test( caller, addr_from_index(NeuronPrecompile::::INDEX), - burned_register_call_data(TEST_NETUID_U16, hotkey), + encode_with_selector( + selector_u32("burnedRegister(uint16,bytes32)"), + (TEST_NETUID_U16, hotkey), + ), ) .execute_returns(()); @@ -922,19 +972,150 @@ mod neuron { assert!(balance_after < balance_before); }); } + + #[test] + fn neuron_precompile_commit_weights_respects_stake_threshold_and_stores_commit() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(0x2234); + let (netuid, caller_account) = setup_registered_caller(caller); + let commit_hash = reveal_commit_hash(&caller_account, netuid); + let precompile_addr = addr_from_index(NeuronPrecompile::::INDEX); + + pallet_subtensor::Pallet::::set_stake_threshold(1); + let rejected = execute_precompile( + &Precompiles::::new(), + precompile_addr, + caller, + encode_with_selector( + selector_u32("commitWeights(uint16,bytes32)"), + (TEST_NETUID_U16, commit_hash), + ), + U256::zero(), + ) + .expect("commit weights should route to neuron precompile"); + assert!(rejected.is_err()); + + pallet_subtensor::Pallet::::set_stake_threshold(0); + Precompiles::::new() + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("commitWeights(uint16,bytes32)"), + (TEST_NETUID_U16, commit_hash), + ), + ) + .execute_returns(()); + + let commits = pallet_subtensor::WeightCommits::::get( + NetUidStorageIndex::from(netuid), + &caller_account, + ) + .expect("weight commits should be stored after successful commit"); + assert_eq!(commits.len(), 1); + }); + } + + #[test] + fn neuron_precompile_reveal_weights_respects_stake_threshold_and_sets_weights() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(0x3234); + let (netuid, caller_account) = setup_registered_caller(caller); + let commit_hash = reveal_commit_hash(&caller_account, netuid); + let precompile_addr = addr_from_index(NeuronPrecompile::::INDEX); + + Precompiles::::new() + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("commitWeights(uint16,bytes32)"), + (TEST_NETUID_U16, commit_hash), + ), + ) + .execute_returns(()); + + let commits = pallet_subtensor::WeightCommits::::get( + NetUidStorageIndex::from(netuid), + &caller_account, + ) + .expect("weight commit should exist before reveal"); + let (_, _, first_reveal_block, _) = commits + .front() + .copied() + .expect("weight commit queue should contain the committed hash"); + + System::set_block_number( + u32::try_from(first_reveal_block) + .expect("first reveal block should fit in runtime block number"), + ); + + pallet_subtensor::Pallet::::set_stake_threshold(1); + let rejected = execute_precompile( + &Precompiles::::new(), + precompile_addr, + caller, + encode_with_selector( + selector_u32("revealWeights(uint16,uint16[],uint16[],uint16[],uint64)"), + ( + TEST_NETUID_U16, + REVEAL_UIDS.to_vec(), + REVEAL_VALUES.to_vec(), + REVEAL_SALT.to_vec(), + VERSION_KEY, + ), + ), + U256::zero(), + ) + .expect("reveal weights should route to neuron precompile"); + assert!(rejected.is_err()); + + pallet_subtensor::Pallet::::set_stake_threshold(0); + Precompiles::::new() + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("revealWeights(uint16,uint16[],uint16[],uint16[],uint64)"), + ( + TEST_NETUID_U16, + REVEAL_UIDS.to_vec(), + REVEAL_VALUES.to_vec(), + REVEAL_SALT.to_vec(), + VERSION_KEY, + ), + ), + ) + .execute_returns(()); + + assert!( + pallet_subtensor::WeightCommits::::get( + NetUidStorageIndex::from(netuid), + &caller_account, + ) + .is_none() + ); + + let neuron_uid = pallet_subtensor::Pallet::::get_uid_for_net_and_hotkey( + netuid, + &caller_account, + ) + .expect("caller should remain registered after reveal"); + let weights = pallet_subtensor::Weights::::get( + NetUidStorageIndex::from(netuid), + neuron_uid, + ); + + assert_eq!(weights.len(), 1); + assert_eq!(weights[0].0, neuron_uid); + assert!(weights[0].1 > 0); + }); + } } mod balance_transfer { use super::*; - fn balance_transfer_call_data(target: H256) -> Vec { - // 4-byte selector + 1 ABI word for the bytes32 destination. - let mut input = Vec::with_capacity(4 + 32); - input.extend_from_slice(&selector("transfer(bytes32)")); - input.extend_from_slice(target.as_bytes()); - input - } - #[test] fn balance_transfer_precompile_transfers_balance() { new_test_ext().execute_with(|| { @@ -959,7 +1140,7 @@ mod balance_transfer { &precompiles, precompile_addr, addr_from_index(1), - balance_transfer_call_data(destination_raw), + encode_with_selector(selector_u32("transfer(bytes32)"), (destination_raw,)), evm_apparent_value_from_substrate(amount), ); let precompile_result = @@ -1012,7 +1193,7 @@ mod balance_transfer { &precompiles, precompile_addr, addr_from_index(1), - balance_transfer_call_data(destination_raw), + encode_with_selector(selector_u32("transfer(bytes32)"), (destination_raw,)), evm_apparent_value_from_substrate(amount), ); let precompile_result = From 0b9679352b31280e1aa30bb8ade421b506c913e1 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 21 Apr 2026 13:36:37 +0200 Subject: [PATCH 14/21] Move precompile tests to the unit level --- Cargo.lock | 5 + precompiles/Cargo.toml | 21 +- precompiles/src/address_mapping.rs | 114 +++ precompiles/src/alpha.rs | 362 ++++++++ precompiles/src/ed25519.rs | 79 ++ precompiles/src/lib.rs | 45 + precompiles/src/metagraph.rs | 201 +++++ precompiles/src/mock.rs | 597 +++++++++++++ precompiles/src/neuron.rs | 271 ++++++ precompiles/src/uid_lookup.rs | 50 ++ runtime/tests/precompiles.rs | 1245 ++-------------------------- 11 files changed, 1834 insertions(+), 1156 deletions(-) create mode 100644 precompiles/src/mock.rs diff --git a/Cargo.lock b/Cargo.lock index e7cf36d22e..a9b72f109e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18234,16 +18234,21 @@ dependencies = [ "pallet-admin-utils", "pallet-balances", "pallet-crowdloan", + "pallet-drand", "pallet-evm", "pallet-evm-precompile-bn128", "pallet-evm-precompile-dispatch", "pallet-evm-precompile-modexp", "pallet-evm-precompile-sha3fips", "pallet-evm-precompile-simple", + "pallet-preimage", + "pallet-scheduler", "pallet-shield", "pallet-subtensor", "pallet-subtensor-proxy", "pallet-subtensor-swap", + "pallet-timestamp", + "parity-scale-codec", "precompile-utils", "scale-info", "sp-core", diff --git a/precompiles/Cargo.toml b/precompiles/Cargo.toml index be1824cf91..8fe19835af 100644 --- a/precompiles/Cargo.toml +++ b/precompiles/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/opentensor/subtensor/" targets = ["x86_64-unknown-linux-gnu"] [dependencies] -# codec.workspace = true +codec.workspace = true ed25519-dalek = { workspace = true, features = ["alloc"] } fp-evm.workspace = true frame-support.workspace = true @@ -46,7 +46,7 @@ workspace = true [features] default = ["std"] std = [ - # "codec/std", + "codec/std", "ed25519-dalek/std", "fp-evm/std", "frame-support/std", @@ -75,3 +75,20 @@ std = [ "subtensor-runtime-common/std", "subtensor-swap-interface/std", ] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-admin-utils/runtime-benchmarks", + "pallet-crowdloan/runtime-benchmarks", + "pallet-subtensor-swap/runtime-benchmarks", + "pallet-subtensor/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "subtensor-runtime-common/runtime-benchmarks", +] + +[dev-dependencies] +pallet-drand = { workspace = true, features = ["std"] } +pallet-preimage = { workspace = true, features = ["std"] } +pallet-scheduler = { workspace = true, features = ["std"] } +pallet-timestamp = { workspace = true, features = ["std"] } +precompile-utils = { workspace = true, features = ["std", "testing"] } diff --git a/precompiles/src/address_mapping.rs b/precompiles/src/address_mapping.rs index fa34692657..c8f3815c49 100644 --- a/precompiles/src/address_mapping.rs +++ b/precompiles/src/address_mapping.rs @@ -75,3 +75,117 @@ where Ok(target_address.into()) } } + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + + use super::*; + use crate::mock::{ + Runtime, addr_from_index, execute_precompile, new_test_ext, precompiles, selector_u32, + }; + use pallet_evm::AddressMapping; + use precompile_utils::solidity::{codec::Address, encode_with_selector}; + use precompile_utils::testing::PrecompileTesterExt; + use sp_core::U256; + + #[test] + fn address_mapping_precompile_returns_runtime_address_mapping() { + new_test_ext().execute_with(|| { + let precompiles = precompiles::>(); + let caller = addr_from_index(1); + let target_address = addr_from_index(0x1234); + let input = encode_with_selector( + selector_u32("addressMapping(address)"), + (Address(target_address),), + ); + let mapped_account = + ::AddressMapping::into_account_id(target_address); + let expected_output: [u8; 32] = mapped_account.into(); + + precompiles + .prepare_test( + caller, + addr_from_index(AddressMappingPrecompile::::INDEX), + input, + ) + .with_static_call(true) + .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::>(), + precompile_addr, + caller, + encode_with_selector( + selector_u32("addressMapping(address)"), + (Address(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::>(), + precompile_addr, + caller, + encode_with_selector( + selector_u32("addressMapping(address)"), + (Address(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 = encode_with_selector( + selector_u32("addressMapping(address)"), + (Address(target_address),), + ); + + let first_output = execute_precompile( + &precompiles::>(), + 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::>(), + 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); + }); + } +} diff --git a/precompiles/src/alpha.rs b/precompiles/src/alpha.rs index c90851c543..b183c5ec23 100644 --- a/precompiles/src/alpha.rs +++ b/precompiles/src/alpha.rs @@ -214,3 +214,365 @@ where Ok(price_eth) } } + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + + use super::*; + use crate::PrecompileExt; + use crate::mock::{ + Runtime, addr_from_index, alpha_price_to_evm, assert_static_call, new_test_ext, + precompiles, selector_u32, + }; + use precompile_utils::solidity::encode_with_selector; + use substrate_fixed::types::I96F32; + use subtensor_runtime_common::{AlphaBalance, TaoBalance}; + + 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 seed_alpha_test_state() { + let dynamic_netuid = NetUid::from(DYNAMIC_NETUID_U16); + let sum_price_netuid = NetUid::from(SUM_PRICE_NETUID_U16); + + 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), + ); + } + + #[test] + fn alpha_precompile_matches_runtime_values_for_dynamic_subnet() { + new_test_ext().execute_with(|| { + seed_alpha_test_state(); + + let precompiles = precompiles::>(); + let caller = addr_from_index(1); + let precompile_addr = addr_from_index(AlphaPrecompile::::INDEX); + + let dynamic_netuid = NetUid::from(DYNAMIC_NETUID_U16); + 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, + encode_with_selector(selector_u32("getAlphaPrice(uint16)"), (DYNAMIC_NETUID_U16,)), + alpha_price_to_evm(alpha_price), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getMovingAlphaPrice(uint16)"), + (DYNAMIC_NETUID_U16,), + ), + alpha_price_to_evm(moving_alpha_price), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector(selector_u32("getTaoInPool(uint16)"), (DYNAMIC_NETUID_U16,)), + pallet_subtensor::SubnetTAO::::get(dynamic_netuid) + .to_u64() + .into(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getAlphaInPool(uint16)"), + (DYNAMIC_NETUID_U16,), + ), + u64::from(pallet_subtensor::SubnetAlphaIn::::get( + dynamic_netuid, + )) + .into(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getAlphaOutPool(uint16)"), + (DYNAMIC_NETUID_U16,), + ), + u64::from(pallet_subtensor::SubnetAlphaOut::::get( + dynamic_netuid, + )) + .into(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getAlphaIssuance(uint16)"), + (DYNAMIC_NETUID_U16,), + ), + u64::from(pallet_subtensor::Pallet::::get_alpha_issuance( + dynamic_netuid, + )) + .into(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getSubnetMechanism(uint16)"), + (DYNAMIC_NETUID_U16,), + ), + pallet_subtensor::SubnetMechanism::::get(dynamic_netuid).into(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getEMAPriceHalvingBlocks(uint16)"), + (DYNAMIC_NETUID_U16,), + ), + pallet_subtensor::EMAPriceHalvingBlocks::::get(dynamic_netuid).into(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getSubnetVolume(uint16)"), + (DYNAMIC_NETUID_U16,), + ), + pallet_subtensor::SubnetVolume::::get(dynamic_netuid).into(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getTaoInEmission(uint16)"), + (DYNAMIC_NETUID_U16,), + ), + pallet_subtensor::SubnetTaoInEmission::::get(dynamic_netuid) + .to_u64() + .into(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getAlphaInEmission(uint16)"), + (DYNAMIC_NETUID_U16,), + ), + pallet_subtensor::SubnetAlphaInEmission::::get(dynamic_netuid) + .to_u64() + .into(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getAlphaOutEmission(uint16)"), + (DYNAMIC_NETUID_U16,), + ), + pallet_subtensor::SubnetAlphaOutEmission::::get(dynamic_netuid) + .to_u64() + .into(), + ); + }); + } + + #[test] + fn alpha_precompile_matches_runtime_global_values() { + new_test_ext().execute_with(|| { + seed_alpha_test_state(); + + let precompiles = precompiles::>(); + 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 += price; + } + } + + assert!(sum_alpha_price > U96F32::from_num(0)); + + assert_static_call( + &precompiles, + caller, + precompile_addr, + selector_u32("getCKBurn()").to_be_bytes().to_vec(), + pallet_subtensor::CKBurn::::get().into(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + selector_u32("getTaoWeight()").to_be_bytes().to_vec(), + pallet_subtensor::TaoWeight::::get().into(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + selector_u32("getRootNetuid()").to_be_bytes().to_vec(), + u16::from(NetUid::ROOT).into(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + selector_u32("getSumAlphaPrice()").to_be_bytes().to_vec(), + 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::>(); + 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( + NetUid::from(DYNAMIC_NETUID_U16), + 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( + NetUid::from(DYNAMIC_NETUID_U16), + 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, + encode_with_selector( + selector_u32("simSwapTaoForAlpha(uint16,uint64)"), + (DYNAMIC_NETUID_U16, tao_amount), + ), + expected_alpha.into(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("simSwapAlphaForTao(uint16,uint64)"), + (DYNAMIC_NETUID_U16, alpha_amount), + ), + expected_tao.into(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("simSwapTaoForAlpha(uint16,uint64)"), + (DYNAMIC_NETUID_U16, 0_u64), + ), + U256::zero(), + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("simSwapAlphaForTao(uint16,uint64)"), + (DYNAMIC_NETUID_U16, 0_u64), + ), + U256::zero(), + ); + }); + } +} diff --git a/precompiles/src/ed25519.rs b/precompiles/src/ed25519.rs index dbfe032cdf..38204c4304 100644 --- a/precompiles/src/ed25519.rs +++ b/precompiles/src/ed25519.rs @@ -56,3 +56,82 @@ where Ok((ExitSucceed::Returned, buf.to_vec())) } } + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + + use super::*; + use crate::mock::{ + AccountId, abi_word, addr_from_index, new_test_ext, precompiles, selector_u32, + }; + use precompile_utils::solidity::encode_with_selector; + use precompile_utils::testing::PrecompileTesterExt; + use sp_core::{H256, Pair, U256, ed25519}; + + #[test] + fn ed25519_precompile_verifies_valid_and_invalid_signatures() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(1); + let precompile_addr = addr_from_index(Ed25519Verify::::INDEX); + + let pair = ed25519::Pair::from_seed(&[1u8; 32]); + let message = [7u8; 32]; + let signature = pair.sign(&message); + let public_key = pair.public(); + let broken_message = [8u8; 32]; + let mut broken_signature = signature.0; + broken_signature[0] ^= 1; + let broken_signature = ed25519::Signature::from_raw(broken_signature); + + precompiles::>() + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("verify(bytes32,bytes32,bytes32,bytes32)"), + ( + H256::from(message), + H256::from(public_key.0), + H256::from_slice(&signature.0[..32]), + H256::from_slice(&signature.0[32..]), + ), + ), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::one())); + precompiles::>() + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("verify(bytes32,bytes32,bytes32,bytes32)"), + ( + H256::from(broken_message), + H256::from(public_key.0), + H256::from_slice(&signature.0[..32]), + H256::from_slice(&signature.0[32..]), + ), + ), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::zero())); + precompiles::>() + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("verify(bytes32,bytes32,bytes32,bytes32)"), + ( + H256::from(message), + H256::from(public_key.0), + H256::from_slice(&broken_signature.0[..32]), + H256::from_slice(&broken_signature.0[32..]), + ), + ), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(U256::zero())); + }); + } +} diff --git a/precompiles/src/lib.rs b/precompiles/src/lib.rs index a824ac39d4..6b8b34b89c 100644 --- a/precompiles/src/lib.rs +++ b/precompiles/src/lib.rs @@ -61,6 +61,9 @@ mod subnet; mod uid_lookup; mod voting_power; +#[cfg(test)] +mod mock; + pub struct Precompiles(PhantomData); impl Default for Precompiles @@ -310,3 +313,45 @@ fn parse_slice(data: &[u8], from: usize, to: usize) -> Result<&[u8], PrecompileF }) } } + +#[cfg(test)] +mod test { + use super::*; + use core::iter::IntoIterator; + use std::collections::BTreeSet; + + #[test] + fn precompile_registry_addresses_are_unique() { + let addresses = [ + hash(1), + hash(2), + hash(3), + hash(4), + hash(5), + hash(6), + hash(7), + hash(8), + hash(9), + hash(1024), + hash(1025), + hash(1026), + hash(1027), + hash(2048), + hash(2053), + hash(2051), + hash(2050), + hash(2052), + hash(2049), + hash(2055), + hash(2054), + hash(2056), + hash(2057), + hash(2058), + hash(2061), + hash(2059), + hash(2060), + ]; + let unique: BTreeSet<_> = IntoIterator::into_iter(addresses).collect(); + assert_eq!(unique.len(), addresses.len()); + } +} diff --git a/precompiles/src/metagraph.rs b/precompiles/src/metagraph.rs index c5b0b931f2..1cc43e71f4 100644 --- a/precompiles/src/metagraph.rs +++ b/precompiles/src/metagraph.rs @@ -187,3 +187,204 @@ impl From for AxonInfo { } } } + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + + use super::*; + use crate::PrecompileExt; + use crate::mock::{ + Runtime, TEST_NETUID_U16, abi_word, addr_from_index, new_test_ext, precompiles, + selector_u32, + }; + use precompile_utils::solidity::{encode_return_value, encode_with_selector}; + use precompile_utils::testing::PrecompileTesterExt; + use sp_core::H256; + use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex}; + + const UID: u16 = 0; + const EMISSION: u64 = 111; + const VTRUST: u16 = 222; + const LAST_UPDATE: u64 = 333; + const AXON_BLOCK: u64 = 444; + const AXON_VERSION: u32 = 555; + const AXON_IP: u128 = 666; + const AXON_PORT: u16 = 777; + const AXON_IP_TYPE: u8 = 4; + const AXON_PROTOCOL: u8 = 1; + + fn seed_metagraph_test_state() -> ( + NetUid, + ::AccountId, + ::AccountId, + pallet_subtensor::AxonInfo, + ) { + let netuid = NetUid::from(TEST_NETUID_U16); + let hotkey = ::AccountId::from([0x11; 32]); + let coldkey = ::AccountId::from([0x22; 32]); + + let axon = pallet_subtensor::AxonInfo { + block: AXON_BLOCK, + version: AXON_VERSION, + ip: AXON_IP, + port: AXON_PORT, + ip_type: AXON_IP_TYPE, + protocol: AXON_PROTOCOL, + placeholder1: 0, + placeholder2: 0, + }; + + pallet_subtensor::SubnetworkN::::insert(netuid, 1); + pallet_subtensor::Keys::::insert(netuid, UID, hotkey.clone()); + pallet_subtensor::Uids::::insert(netuid, &hotkey, UID); + pallet_subtensor::Owner::::insert(&hotkey, coldkey.clone()); + pallet_subtensor::Emission::::insert(netuid, vec![AlphaBalance::from(EMISSION)]); + pallet_subtensor::ValidatorTrust::::insert(netuid, vec![VTRUST]); + pallet_subtensor::ValidatorPermit::::insert(netuid, vec![true]); + pallet_subtensor::LastUpdate::::insert( + NetUidStorageIndex::from(netuid), + vec![LAST_UPDATE], + ); + pallet_subtensor::Active::::insert(netuid, vec![true]); + pallet_subtensor::Axons::::insert(netuid, &hotkey, axon.clone()); + + (netuid, hotkey, coldkey, axon) + } + + #[test] + fn metagraph_precompile_matches_runtime_values() { + new_test_ext().execute_with(|| { + let (netuid, hotkey, coldkey, axon) = seed_metagraph_test_state(); + let precompiles = precompiles::>(); + let caller = addr_from_index(1); + let precompile_addr = addr_from_index(MetagraphPrecompile::::INDEX); + + let uid_count = pallet_subtensor::SubnetworkN::::get(netuid); + let emission = + pallet_subtensor::Pallet::::get_emission_for_uid(netuid, UID).to_u64(); + let vtrust = + pallet_subtensor::Pallet::::get_validator_trust_for_uid(netuid, UID); + let validator_status = + pallet_subtensor::Pallet::::get_validator_permit_for_uid(netuid, UID); + let last_update = pallet_subtensor::Pallet::::get_last_update_for_uid( + NetUidStorageIndex::from(netuid), + UID, + ); + let is_active = pallet_subtensor::Pallet::::get_active_for_uid(netuid, UID); + let runtime_axon = pallet_subtensor::Pallet::::get_axon_info(netuid, &hotkey); + + assert_eq!(uid_count, 1); + assert_eq!(emission, EMISSION); + assert_eq!(vtrust, VTRUST); + assert!(validator_status); + assert_eq!(last_update, LAST_UPDATE); + assert!(is_active); + assert_eq!(runtime_axon, axon); + + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector(selector_u32("getUidCount(uint16)"), (TEST_NETUID_U16,)), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(uid_count.into())); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("getAxon(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), + ) + .with_static_call(true) + .execute_returns_raw(encode_return_value(( + runtime_axon.block, + runtime_axon.version, + runtime_axon.ip, + runtime_axon.port, + runtime_axon.ip_type, + runtime_axon.protocol, + ))); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("getEmission(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(emission.into())); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("getVtrust(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(vtrust.into())); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("getValidatorStatus(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), + ) + .with_static_call(true) + .execute_returns_raw(abi_word((validator_status as u8).into())); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("getLastUpdate(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), + ) + .with_static_call(true) + .execute_returns_raw(abi_word(last_update.into())); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("getIsActive(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), + ) + .with_static_call(true) + .execute_returns_raw(abi_word((is_active as u8).into())); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("getHotkey(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), + ) + .with_static_call(true) + .execute_returns_raw(H256::from_slice(hotkey.as_ref()).as_bytes().to_vec()); + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("getColdkey(uint16,uint16)"), + (TEST_NETUID_U16, UID), + ), + ) + .with_static_call(true) + .execute_returns_raw(H256::from_slice(coldkey.as_ref()).as_bytes().to_vec()); + }); + } +} diff --git a/precompiles/src/mock.rs b/precompiles/src/mock.rs new file mode 100644 index 0000000000..18dd67a7f9 --- /dev/null +++ b/precompiles/src/mock.rs @@ -0,0 +1,597 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] +#![allow(clippy::arithmetic_side_effects)] + +use core::{marker::PhantomData, num::NonZeroU64}; + +use fp_evm::{Context, PrecompileResult}; +use frame_support::{ + PalletId, derive_impl, parameter_types, + traits::{Everything, InherentBuilder, PrivilegeCmp}, + weights::Weight, +}; +use frame_system::{EnsureRoot, limits, offchain::CreateTransactionBase}; +use pallet_evm::{ + BalanceConverter, EnsureAddressNever, EnsureAddressRoot, EvmBalance, PrecompileHandle, + PrecompileSet, SubstrateBalance, +}; +use precompile_utils::testing::MockHandle; +use sp_core::{ConstU64, H160, H256, U256, crypto::AccountId32}; +use sp_runtime::{ + BuildStorage, KeyTypeId, Perbill, Percent, + testing::TestXt, + traits::{BlakeTwo256, ConstU32, IdentityLookup}, +}; +use substrate_fixed::types::U96F32; +use subtensor_runtime_common::{AuthorshipInfo, NetUid, ProxyType, TaoBalance}; + +use crate::PrecompileExt; + +pub(crate) type AccountId = AccountId32; +pub(crate) type Block = frame_system::mocking::MockBlock; +pub(crate) type UncheckedExtrinsic = TestXt; + +frame_support::construct_runtime!( + pub enum Runtime { + System: frame_system = 1, + Balances: pallet_balances = 2, + Timestamp: pallet_timestamp = 3, + Shield: pallet_shield = 4, + SubtensorModule: pallet_subtensor::{Pallet, Call, Storage, Event} = 5, + Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event} = 6, + Preimage: pallet_preimage::{Pallet, Call, Storage, Event} = 7, + Drand: pallet_drand::{Pallet, Call, Storage, Event} = 8, + Swap: pallet_subtensor_swap::{Pallet, Call, Storage, Event} = 9, + Crowdloan: pallet_crowdloan::{Pallet, Call, Storage, Event} = 10, + Proxy: pallet_subtensor_proxy = 11, + Evm: pallet_evm = 12, + } +); + +pub(crate) const TEST_NETUID_U16: u16 = 1; +const EVM_DECIMALS_FACTOR: u64 = 1_000_000_000; + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; + pub BlockWeights: limits::BlockWeights = limits::BlockWeights::with_sensible_defaults( + Weight::from_parts(2_000_000_000_000, u64::MAX), + Perbill::from_percent(75), + ); + pub const ExistentialDeposit: TaoBalance = TaoBalance::new(1); + pub const MinimumPeriod: u64 = 5; + pub const PreimageMaxSize: u32 = 4096 * 1024; + pub const PreimageBaseDeposit: TaoBalance = TaoBalance::new(1); + pub const PreimageByteDeposit: TaoBalance = TaoBalance::new(1); + pub const CrowdloanPalletId: PalletId = PalletId(*b"bt/cloan"); + pub const MinimumDeposit: TaoBalance = TaoBalance::new(50); + pub const AbsoluteMinimumContribution: TaoBalance = TaoBalance::new(10); + pub const MinimumBlockDuration: u64 = 20; + pub const MaximumBlockDuration: u64 = 100; + pub const RefundContributorsLimit: u32 = 5; + pub const MaxContributors: u32 = 10; + pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); + pub const SwapMaxFeeRate: u16 = 10000; + pub const SwapMaxPositions: u32 = 100; + pub const SwapMinimumLiquidity: u64 = 1_000; + pub const SwapMinimumReserve: NonZeroU64 = NonZeroU64::new(1_000_000).unwrap(); + pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * + BlockWeights::get().max_block; + pub const MaxScheduledPerBlock: u32 = 50; + pub const MaxAuthorities: u32 = 32; + pub static BlockGasLimit: U256 = U256::max_value(); + pub WeightPerGas: Weight = Weight::from_parts(20_000, 0); + pub const ProxyDepositBase: TaoBalance = TaoBalance::new(1); + pub const ProxyDepositFactor: TaoBalance = TaoBalance::new(1); + pub const MaxProxies: u32 = 20; + pub const MaxPending: u32 = 15; + pub const AnnouncementDepositBase: TaoBalance = TaoBalance::new(1); + pub const AnnouncementDepositFactor: TaoBalance = TaoBalance::new(1); + pub const InitialMinAllowedWeights: u16 = 0; + pub const InitialEmissionValue: u16 = 0; + pub const InitialRho: u16 = 30; + pub const InitialAlphaSigmoidSteepness: i16 = 1000; + pub const InitialKappa: u16 = 32_767; + pub const InitialTempo: u16 = 360; + pub const InitialImmunityPeriod: u16 = 2; + pub const InitialMinAllowedUids: u16 = 2; + pub const InitialMaxAllowedUids: u16 = 256; + pub const InitialBondsMovingAverage: u64 = 900_000; + pub const InitialBondsPenalty: u16 = u16::MAX; + pub const InitialBondsResetOn: bool = false; + pub const InitialDefaultDelegateTake: u16 = 11_796; + pub const InitialMinDelegateTake: u16 = 5_898; + pub const InitialDefaultChildKeyTake: u16 = 0; + pub const InitialMinChildKeyTake: u16 = 0; + pub const InitialMaxChildKeyTake: u16 = 11_796; + pub const InitialWeightsVersionKey: u64 = 0; + pub const InitialServingRateLimit: u64 = 0; + pub const InitialTxRateLimit: u64 = 0; + pub const InitialTxDelegateTakeRateLimit: u64 = 0; + pub const InitialTxChildKeyTakeRateLimit: u64 = 0; + pub const InitialBurn: TaoBalance = TaoBalance::new(0); + pub const InitialMinBurn: TaoBalance = TaoBalance::new(500_000); + pub const InitialMaxBurn: TaoBalance = TaoBalance::new(1_000_000_000); + pub const MinBurnUpperBound: TaoBalance = TaoBalance::new(1_000_000_000); + pub const MaxBurnLowerBound: TaoBalance = TaoBalance::new(100_000_000); + pub const InitialValidatorPruneLen: u64 = 0; + pub const InitialScalingLawPower: u16 = 50; + pub const InitialMaxAllowedValidators: u16 = 100; + pub const InitialIssuance: TaoBalance = TaoBalance::new(0); + pub const InitialDifficulty: u64 = 10_000; + pub const InitialActivityCutoff: u16 = 5_000; + pub const InitialAdjustmentInterval: u16 = 100; + pub const InitialAdjustmentAlpha: u64 = 0; + pub const InitialMaxRegistrationsPerBlock: u16 = 3; + pub const InitialTargetRegistrationsPerInterval: u16 = 2; + pub const InitialPruningScore: u16 = u16::MAX; + pub const InitialMinDifficulty: u64 = 1; + pub const InitialMaxDifficulty: u64 = u64::MAX; + pub const InitialRAORecycledForRegistration: TaoBalance = TaoBalance::new(0); + pub const InitialNetworkImmunityPeriod: u64 = 1_296_000; + pub const InitialNetworkMinLockCost: TaoBalance = TaoBalance::new(100_000_000_000); + pub const InitialSubnetOwnerCut: u16 = 0; + pub const InitialNetworkLockReductionInterval: u64 = 2; + pub const InitialNetworkRateLimit: u64 = 0; + pub const InitialKeySwapCost: TaoBalance = TaoBalance::new(1_000_000_000); + pub const InitialAlphaHigh: u16 = 58_982; + pub const InitialAlphaLow: u16 = 45_875; + pub const InitialLiquidAlphaOn: bool = false; + pub const InitialYuma3On: bool = false; + pub const InitialColdkeySwapAnnouncementDelay: u64 = 50; + pub const InitialColdkeySwapReannouncementDelay: u64 = 10; + pub const InitialDissolveNetworkScheduleDuration: u64 = 36_000; + pub const InitialTaoWeight: u64 = u64::MAX / 10; + pub const InitialEmaPriceHalvingPeriod: u64 = 201_600; + pub const InitialStartCallDelay: u64 = 0; + pub const InitialKeySwapOnSubnetCost: TaoBalance = TaoBalance::new(10_000_000); + pub const HotkeySwapOnSubnetInterval: u64 = 50_400; + pub const LeaseDividendsDistributionInterval: u32 = 100; + pub const MaxImmuneUidsPercentage: Percent = Percent::from_percent(80); + pub const EvmKeyAssociateRateLimit: u64 = 0; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Runtime { + type BaseCallFilter = Everything; + type BlockWeights = BlockWeights; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type SS58Prefix = SS58Prefix; + type MaxConsumers = ConstU32<16>; + type Block = Block; + type Nonce = u64; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Runtime { + type Balance = TaoBalance; + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = (); + type RuntimeHoldReason = (); +} + +#[derive_impl(pallet_timestamp::config_preludes::TestDefaultConfig)] +impl pallet_timestamp::Config for Runtime { + type MinimumPeriod = MinimumPeriod; +} + +impl pallet_shield::Config for Runtime { + type AuthorityId = sp_core::sr25519::Public; + type FindAuthors = (); + type RuntimeCall = RuntimeCall; + type ExtrinsicDecryptor = (); + type WeightInfo = (); +} + +impl pallet_preimage::Config for Runtime { + type WeightInfo = pallet_preimage::weights::SubstrateWeight; + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type ManagerOrigin = EnsureRoot; + type Consideration = (); +} + +pub struct FixedGasPrice; +impl pallet_evm::FeeCalculator for FixedGasPrice { + fn min_gas_price() -> (U256, Weight) { + (1_000_000_000u128.into(), Weight::from_parts(7, 0)) + } +} + +pub struct SubtensorEvmBalanceConverter; +impl BalanceConverter for SubtensorEvmBalanceConverter { + fn into_evm_balance(value: SubstrateBalance) -> Option { + value + .into_u256() + .checked_mul(U256::from(EVM_DECIMALS_FACTOR)) + .and_then(|evm_value| (evm_value <= U256::MAX).then(|| EvmBalance::new(evm_value))) + } + + fn into_substrate_balance(value: EvmBalance) -> Option { + value + .into_u256() + .checked_div(U256::from(EVM_DECIMALS_FACTOR)) + .and_then(|substrate_value| { + (substrate_value <= U256::from(u64::MAX)) + .then(|| SubstrateBalance::new(substrate_value)) + }) + } +} + +impl pallet_evm::Config for Runtime { + type BalanceConverter = SubtensorEvmBalanceConverter; + type AccountProvider = pallet_evm::FrameSystemAccountProvider; + type FeeCalculator = FixedGasPrice; + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = pallet_evm::HashedAddressMapping; + type Currency = Balances; + type PrecompilesType = (); + type PrecompilesValue = (); + type ChainId = (); + type BlockGasLimit = BlockGasLimit; + type Runner = pallet_evm::runner::stack::Runner; + type OnChargeTransaction = (); + type OnCreate = (); + type FindAuthor = (); + type GasLimitPovSizeRatio = (); + type GasLimitStorageGrowthRatio = (); + type Timestamp = Timestamp; + type CreateInnerOriginFilter = (); + type CreateOriginFilter = (); + type WeightInfo = pallet_evm::weights::SubstrateWeight; +} + +impl pallet_crowdloan::Config for Runtime { + type PalletId = CrowdloanPalletId; + type Currency = Balances; + type RuntimeCall = RuntimeCall; + type WeightInfo = pallet_crowdloan::weights::SubstrateWeight; + type Preimages = Preimage; + type MinimumDeposit = MinimumDeposit; + type AbsoluteMinimumContribution = AbsoluteMinimumContribution; + type MinimumBlockDuration = MinimumBlockDuration; + type MaximumBlockDuration = MaximumBlockDuration; + type RefundContributorsLimit = RefundContributorsLimit; + type MaxContributors = MaxContributors; +} + +impl pallet_subtensor_swap::Config for Runtime { + type SubnetInfo = SubtensorModule; + type BalanceOps = SubtensorModule; + type ProtocolId = SwapProtocolId; + type TaoReserve = pallet_subtensor::TaoBalanceReserve; + type AlphaReserve = pallet_subtensor::AlphaBalanceReserve; + type MaxFeeRate = SwapMaxFeeRate; + type MaxPositions = SwapMaxPositions; + type MinimumLiquidity = SwapMinimumLiquidity; + type MinimumReserve = SwapMinimumReserve; + type WeightInfo = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +pub struct OriginPrivilegeCmp; +impl PrivilegeCmp for OriginPrivilegeCmp { + fn cmp_privilege(_left: &OriginCaller, _right: &OriginCaller) -> Option { + None + } +} + +impl pallet_scheduler::Config for Runtime { + type RuntimeOrigin = RuntimeOrigin; + type RuntimeEvent = RuntimeEvent; + type PalletsOrigin = OriginCaller; + type RuntimeCall = RuntimeCall; + type MaximumWeight = MaximumSchedulerWeight; + type ScheduleOrigin = EnsureRoot; + type MaxScheduledPerBlock = MaxScheduledPerBlock; + type WeightInfo = pallet_scheduler::weights::SubstrateWeight; + type OriginPrivilegeCmp = OriginPrivilegeCmp; + type Preimages = Preimage; + type BlockNumberProvider = System; +} + +pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"test"); + +mod test_crypto { + use super::{AccountId, KEY_TYPE}; + use sp_core::sr25519::{Public as Sr25519Public, Signature as Sr25519Signature}; + use sp_runtime::{ + app_crypto::{app_crypto, sr25519}, + traits::IdentifyAccount, + }; + + app_crypto!(sr25519, KEY_TYPE); + + pub struct TestAuthId; + + impl frame_system::offchain::AppCrypto for TestAuthId { + type RuntimeAppPublic = Public; + type GenericSignature = Sr25519Signature; + type GenericPublic = Sr25519Public; + } + + impl IdentifyAccount for Public { + type AccountId = AccountId; + + fn into_account(self) -> AccountId { + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(self.as_ref()); + AccountId::new(bytes) + } + } +} + +impl pallet_drand::Config for Runtime { + type AuthorityId = test_crypto::TestAuthId; + type Verifier = pallet_drand::verifier::QuicknetVerifier; + type UnsignedPriority = ConstU64<{ 1 << 20 }>; + type HttpFetchTimeout = ConstU64<1_000>; + type WeightInfo = (); +} + +impl frame_system::offchain::SigningTypes for Runtime { + type Public = test_crypto::Public; + type Signature = test_crypto::Signature; +} + +impl CreateTransactionBase for Runtime +where + RuntimeCall: From, +{ + type Extrinsic = UncheckedExtrinsic; + type RuntimeCall = RuntimeCall; +} + +impl frame_system::offchain::CreateInherent for Runtime +where + RuntimeCall: From, +{ + fn create_bare(call: Self::RuntimeCall) -> Self::Extrinsic { + UncheckedExtrinsic::new_inherent(call) + } +} + +impl frame_system::offchain::CreateSignedTransaction for Runtime +where + RuntimeCall: From, +{ + fn create_signed_transaction< + C: frame_system::offchain::AppCrypto, + >( + call: >::RuntimeCall, + _public: Self::Public, + _account: Self::AccountId, + nonce: Self::Nonce, + ) -> Option { + Some(UncheckedExtrinsic::new_signed(call, nonce, (), ())) + } +} + +pub struct MockAuthorshipProvider; +impl AuthorshipInfo for MockAuthorshipProvider { + fn author() -> Option { + Some(AccountId::new([1; 32])) + } +} + +pub struct CommitmentsI; +impl pallet_subtensor::CommitmentsInterface for CommitmentsI { + fn purge_netuid(_netuid: NetUid) {} +} + +impl pallet_subtensor::Config for Runtime { + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type InitialIssuance = InitialIssuance; + type SudoRuntimeCall = frame_system::Call; + type Scheduler = Scheduler; + type InitialMinAllowedWeights = InitialMinAllowedWeights; + type InitialEmissionValue = InitialEmissionValue; + type InitialTempo = InitialTempo; + type InitialDifficulty = InitialDifficulty; + type InitialAdjustmentInterval = InitialAdjustmentInterval; + type InitialAdjustmentAlpha = InitialAdjustmentAlpha; + type InitialTargetRegistrationsPerInterval = InitialTargetRegistrationsPerInterval; + type InitialRho = InitialRho; + type InitialAlphaSigmoidSteepness = InitialAlphaSigmoidSteepness; + type InitialKappa = InitialKappa; + type InitialMinAllowedUids = InitialMinAllowedUids; + type InitialMaxAllowedUids = InitialMaxAllowedUids; + type InitialValidatorPruneLen = InitialValidatorPruneLen; + type InitialScalingLawPower = InitialScalingLawPower; + type InitialImmunityPeriod = InitialImmunityPeriod; + type InitialActivityCutoff = InitialActivityCutoff; + type InitialMaxRegistrationsPerBlock = InitialMaxRegistrationsPerBlock; + type InitialPruningScore = InitialPruningScore; + type InitialBondsMovingAverage = InitialBondsMovingAverage; + type InitialBondsPenalty = InitialBondsPenalty; + type InitialBondsResetOn = InitialBondsResetOn; + type InitialMaxAllowedValidators = InitialMaxAllowedValidators; + type InitialDefaultDelegateTake = InitialDefaultDelegateTake; + type InitialMinDelegateTake = InitialMinDelegateTake; + type InitialDefaultChildKeyTake = InitialDefaultChildKeyTake; + type InitialMinChildKeyTake = InitialMinChildKeyTake; + type InitialMaxChildKeyTake = InitialMaxChildKeyTake; + type InitialWeightsVersionKey = InitialWeightsVersionKey; + type InitialMaxDifficulty = InitialMaxDifficulty; + type InitialMinDifficulty = InitialMinDifficulty; + type InitialServingRateLimit = InitialServingRateLimit; + type InitialTxRateLimit = InitialTxRateLimit; + type InitialTxDelegateTakeRateLimit = InitialTxDelegateTakeRateLimit; + type InitialTxChildKeyTakeRateLimit = InitialTxChildKeyTakeRateLimit; + type InitialBurn = InitialBurn; + type InitialMaxBurn = InitialMaxBurn; + type InitialMinBurn = InitialMinBurn; + type MinBurnUpperBound = MinBurnUpperBound; + type MaxBurnLowerBound = MaxBurnLowerBound; + type InitialRAORecycledForRegistration = InitialRAORecycledForRegistration; + type InitialNetworkImmunityPeriod = InitialNetworkImmunityPeriod; + type InitialNetworkMinLockCost = InitialNetworkMinLockCost; + type InitialSubnetOwnerCut = InitialSubnetOwnerCut; + type InitialNetworkLockReductionInterval = InitialNetworkLockReductionInterval; + type InitialNetworkRateLimit = InitialNetworkRateLimit; + type KeySwapCost = InitialKeySwapCost; + type AlphaHigh = InitialAlphaHigh; + type AlphaLow = InitialAlphaLow; + type LiquidAlphaOn = InitialLiquidAlphaOn; + type Yuma3On = InitialYuma3On; + type Preimages = Preimage; + type InitialColdkeySwapAnnouncementDelay = InitialColdkeySwapAnnouncementDelay; + type InitialColdkeySwapReannouncementDelay = InitialColdkeySwapReannouncementDelay; + type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; + type InitialTaoWeight = InitialTaoWeight; + type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; + type InitialStartCallDelay = InitialStartCallDelay; + type SwapInterface = Swap; + type KeySwapOnSubnetCost = InitialKeySwapOnSubnetCost; + type HotkeySwapOnSubnetInterval = HotkeySwapOnSubnetInterval; + type ProxyInterface = (); + type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; + type GetCommitments = (); + type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; + type CommitmentsInterface = CommitmentsI; + type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; + type AuthorshipProvider = MockAuthorshipProvider; + type WeightInfo = (); +} + +impl frame_support::traits::InstanceFilter for ProxyType { + fn filter(&self, _c: &RuntimeCall) -> bool { + true + } + + fn is_superset(&self, o: &Self) -> bool { + match (self, o) { + (x, y) if x == y => true, + (ProxyType::Any, _) => true, + _ => false, + } + } +} + +impl pallet_subtensor_proxy::Config for Runtime { + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type ProxyType = ProxyType; + type ProxyDepositBase = ProxyDepositBase; + type ProxyDepositFactor = ProxyDepositFactor; + type MaxProxies = MaxProxies; + type WeightInfo = pallet_subtensor_proxy::weights::SubstrateWeight; + type MaxPending = MaxPending; + type CallHasher = BlakeTwo256; + type AnnouncementDepositBase = AnnouncementDepositBase; + type AnnouncementDepositFactor = AnnouncementDepositFactor; + type BlockNumberProvider = System; +} + +pub(crate) struct SinglePrecompileSet

(PhantomData

); + +impl

Default for SinglePrecompileSet

{ + fn default() -> Self { + Self(PhantomData) + } +} + +impl

PrecompileSet for SinglePrecompileSet

+where + P: pallet_evm::Precompile + PrecompileExt, +{ + fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { + (handle.code_address() == H160::from_low_u64_be(P::INDEX)).then(|| P::execute(handle)) + } + + fn is_precompile(&self, address: H160, _gas: u64) -> pallet_evm::IsPrecompileResult { + pallet_evm::IsPrecompileResult::Answer { + is_precompile: address == H160::from_low_u64_be(P::INDEX), + extra_cost: 0, + } + } +} + +pub(crate) fn precompiles

() -> SinglePrecompileSet

+where + P: pallet_evm::Precompile + PrecompileExt, +{ + SinglePrecompileSet::default() +} + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig::default() + .build_storage() + .unwrap() + .into(); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +pub(crate) fn execute_precompile( + precompiles: &PSet, + precompile_address: H160, + caller: H160, + input: Vec, + apparent_value: U256, +) -> Option { + let mut handle = MockHandle::new( + precompile_address, + Context { + address: precompile_address, + caller, + apparent_value, + }, + ); + handle.input = input; + precompiles.execute(&mut handle) +} + +pub(crate) fn addr_from_index(index: u64) -> H160 { + H160::from_low_u64_be(index) +} + +pub(crate) fn abi_word(value: U256) -> Vec { + value.to_big_endian().to_vec() +} + +pub(crate) fn assert_static_call( + precompiles: &PSet, + caller: H160, + precompile_addr: H160, + input: Vec, + expected: U256, +) { + use precompile_utils::testing::PrecompileTesterExt; + + precompiles + .prepare_test(caller, precompile_addr, input) + .with_static_call(true) + .execute_returns_raw(abi_word(expected)); +} + +pub(crate) fn selector_u32(signature: &str) -> u32 { + let hash = sp_io::hashing::keccak_256(signature.as_bytes()); + u32::from_be_bytes([hash[0], hash[1], hash[2], hash[3]]) +} + +pub(crate) fn alpha_price_to_evm(price: U96F32) -> U256 { + let scaled_price = (price * U96F32::from_num(EVM_DECIMALS_FACTOR)).to_num::(); + ::BalanceConverter::into_evm_balance(scaled_price.into()) + .expect("runtime balance conversion should work for alpha price") + .into_u256() +} diff --git a/precompiles/src/neuron.rs b/precompiles/src/neuron.rs index 6c0b7f744f..f605905006 100644 --- a/precompiles/src/neuron.rs +++ b/precompiles/src/neuron.rs @@ -252,3 +252,274 @@ where ) } } + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used, clippy::indexing_slicing)] + + use super::*; + use crate::PrecompileExt; + use crate::mock::{ + AccountId, Runtime, System, TEST_NETUID_U16, addr_from_index, execute_precompile, + new_test_ext, precompiles, selector_u32, + }; + use pallet_evm::AddressMapping; + use precompile_utils::solidity::encode_with_selector; + use precompile_utils::testing::PrecompileTesterExt; + use sp_core::{H160, H256, U256}; + use sp_runtime::traits::Hash; + use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance, Token}; + + const REGISTRATION_BURN: u64 = 1_000; + const RESERVE: u64 = 1_000_000_000; + const COLDKEY_BALANCE: u64 = 50_000; + const TEMPO: u16 = 100; + const REVEAL_PERIOD: u64 = 1; + const VERSION_KEY: u64 = 0; + const REGISTERED_UID: u16 = 0; + const REVEAL_UIDS: [u16; 1] = [REGISTERED_UID]; + const REVEAL_VALUES: [u16; 1] = [5]; + const REVEAL_SALT: [u16; 1] = [9]; + + fn setup_registered_caller(caller: H160) -> (NetUid, AccountId) { + let netuid = NetUid::from(TEST_NETUID_U16); + let caller_account = + ::AddressMapping::into_account_id(caller); + let caller_hotkey = H256::from_slice(caller_account.as_ref()); + + pallet_subtensor::Pallet::::init_new_network(netuid, TEMPO); + pallet_subtensor::Pallet::::set_network_registration_allowed(netuid, true); + pallet_subtensor::Pallet::::set_burn(netuid, REGISTRATION_BURN.into()); + pallet_subtensor::Pallet::::set_max_allowed_uids(netuid, 4096); + pallet_subtensor::Pallet::::set_weights_set_rate_limit(netuid, 0); + pallet_subtensor::Pallet::::set_tempo(netuid, TEMPO); + pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid, true); + pallet_subtensor::Pallet::::set_reveal_period(netuid, REVEAL_PERIOD) + .expect("reveal period setup should succeed"); + pallet_subtensor::SubnetTAO::::insert(netuid, TaoBalance::from(RESERVE)); + pallet_subtensor::SubnetAlphaIn::::insert(netuid, AlphaBalance::from(RESERVE)); + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &caller_account, + COLDKEY_BALANCE.into(), + ); + + precompiles::>() + .prepare_test( + caller, + addr_from_index(NeuronPrecompile::::INDEX), + encode_with_selector( + selector_u32("burnedRegister(uint16,bytes32)"), + (TEST_NETUID_U16, caller_hotkey), + ), + ) + .execute_returns(()); + + let registered_uid = pallet_subtensor::Pallet::::get_uid_for_net_and_hotkey( + netuid, + &caller_account, + ) + .expect("caller should be registered on subnet"); + assert_eq!(registered_uid, REGISTERED_UID); + + (netuid, caller_account) + } + + fn reveal_commit_hash(caller_account: &AccountId, netuid: NetUid) -> H256 { + ::Hashing::hash_of(&( + caller_account.clone(), + NetUidStorageIndex::from(netuid), + REVEAL_UIDS.as_slice(), + REVEAL_VALUES.as_slice(), + REVEAL_SALT.as_slice(), + VERSION_KEY, + )) + } + + #[test] + fn neuron_precompile_burned_register_adds_a_new_uid_and_key() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(TEST_NETUID_U16); + let caller = addr_from_index(0x1234); + let caller_account = + ::AddressMapping::into_account_id(caller); + let hotkey_account = AccountId::from([0x42; 32]); + let hotkey = H256::from_slice(hotkey_account.as_ref()); + + pallet_subtensor::Pallet::::init_new_network(netuid, TEMPO); + pallet_subtensor::Pallet::::set_network_registration_allowed(netuid, true); + pallet_subtensor::Pallet::::set_burn(netuid, REGISTRATION_BURN.into()); + pallet_subtensor::Pallet::::set_max_allowed_uids(netuid, 4096); + pallet_subtensor::SubnetTAO::::insert(netuid, TaoBalance::from(RESERVE)); + pallet_subtensor::SubnetAlphaIn::::insert(netuid, AlphaBalance::from(RESERVE)); + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &caller_account, + COLDKEY_BALANCE.into(), + ); + + let uid_before = pallet_subtensor::SubnetworkN::::get(netuid); + let balance_before = + pallet_subtensor::Pallet::::get_coldkey_balance(&caller_account).to_u64(); + + precompiles::>() + .prepare_test( + caller, + addr_from_index(NeuronPrecompile::::INDEX), + encode_with_selector( + selector_u32("burnedRegister(uint16,bytes32)"), + (TEST_NETUID_U16, hotkey), + ), + ) + .execute_returns(()); + + let uid_after = pallet_subtensor::SubnetworkN::::get(netuid); + let registered_hotkey = pallet_subtensor::Keys::::get(netuid, uid_before); + let owner = pallet_subtensor::Owner::::get(&hotkey_account); + let balance_after = + pallet_subtensor::Pallet::::get_coldkey_balance(&caller_account).to_u64(); + + assert_eq!(uid_after, uid_before + 1); + assert_eq!(registered_hotkey, hotkey_account); + assert_eq!(owner, caller_account); + assert!(balance_after < balance_before); + }); + } + + #[test] + fn neuron_precompile_commit_weights_respects_stake_threshold_and_stores_commit() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(0x2234); + let (netuid, caller_account) = setup_registered_caller(caller); + let commit_hash = reveal_commit_hash(&caller_account, netuid); + let precompile_addr = addr_from_index(NeuronPrecompile::::INDEX); + + pallet_subtensor::Pallet::::set_stake_threshold(1); + let rejected = execute_precompile( + &precompiles::>(), + precompile_addr, + caller, + encode_with_selector( + selector_u32("commitWeights(uint16,bytes32)"), + (TEST_NETUID_U16, commit_hash), + ), + U256::zero(), + ) + .expect("commit weights should route to neuron precompile"); + assert!(rejected.is_err()); + + pallet_subtensor::Pallet::::set_stake_threshold(0); + precompiles::>() + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("commitWeights(uint16,bytes32)"), + (TEST_NETUID_U16, commit_hash), + ), + ) + .execute_returns(()); + + let commits = pallet_subtensor::WeightCommits::::get( + NetUidStorageIndex::from(netuid), + &caller_account, + ) + .expect("weight commits should be stored after successful commit"); + assert_eq!(commits.len(), 1); + }); + } + + #[test] + fn neuron_precompile_reveal_weights_respects_stake_threshold_and_sets_weights() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(0x3234); + let (netuid, caller_account) = setup_registered_caller(caller); + let commit_hash = reveal_commit_hash(&caller_account, netuid); + let precompile_addr = addr_from_index(NeuronPrecompile::::INDEX); + + precompiles::>() + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("commitWeights(uint16,bytes32)"), + (TEST_NETUID_U16, commit_hash), + ), + ) + .execute_returns(()); + + let commits = pallet_subtensor::WeightCommits::::get( + NetUidStorageIndex::from(netuid), + &caller_account, + ) + .expect("weight commit should exist before reveal"); + let (_, _, first_reveal_block, _) = commits + .front() + .copied() + .expect("weight commit queue should contain the committed hash"); + + System::set_block_number(u64::from( + u32::try_from(first_reveal_block) + .expect("first reveal block should fit in runtime block number"), + )); + + pallet_subtensor::Pallet::::set_stake_threshold(1); + let rejected = execute_precompile( + &precompiles::>(), + precompile_addr, + caller, + encode_with_selector( + selector_u32("revealWeights(uint16,uint16[],uint16[],uint16[],uint64)"), + ( + TEST_NETUID_U16, + REVEAL_UIDS.to_vec(), + REVEAL_VALUES.to_vec(), + REVEAL_SALT.to_vec(), + VERSION_KEY, + ), + ), + U256::zero(), + ) + .expect("reveal weights should route to neuron precompile"); + assert!(rejected.is_err()); + + pallet_subtensor::Pallet::::set_stake_threshold(0); + precompiles::>() + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("revealWeights(uint16,uint16[],uint16[],uint16[],uint64)"), + ( + TEST_NETUID_U16, + REVEAL_UIDS.to_vec(), + REVEAL_VALUES.to_vec(), + REVEAL_SALT.to_vec(), + VERSION_KEY, + ), + ), + ) + .execute_returns(()); + + assert!( + pallet_subtensor::WeightCommits::::get( + NetUidStorageIndex::from(netuid), + &caller_account + ) + .is_none() + ); + + let neuron_uid = pallet_subtensor::Pallet::::get_uid_for_net_and_hotkey( + netuid, + &caller_account, + ) + .expect("caller should remain registered after reveal"); + let weights = pallet_subtensor::Weights::::get( + NetUidStorageIndex::from(netuid), + neuron_uid, + ); + + assert_eq!(weights.len(), 1); + assert_eq!(weights[0].0, neuron_uid); + assert!(weights[0].1 > 0); + }); + } +} diff --git a/precompiles/src/uid_lookup.rs b/precompiles/src/uid_lookup.rs index b791b96786..3089122e0f 100644 --- a/precompiles/src/uid_lookup.rs +++ b/precompiles/src/uid_lookup.rs @@ -51,3 +51,53 @@ where )) } } + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + + use super::*; + use crate::mock::{ + Runtime, TEST_NETUID_U16, addr_from_index, new_test_ext, precompiles, selector_u32, + }; + use precompile_utils::solidity::{codec::Address, encode_return_value, encode_with_selector}; + use precompile_utils::testing::PrecompileTesterExt; + use subtensor_runtime_common::NetUid; + + #[test] + fn uid_lookup_precompile_returns_associated_uid_and_block() { + new_test_ext().execute_with(|| { + let precompiles = precompiles::>(); + let caller = addr_from_index(1); + let precompile_addr = addr_from_index(UidLookupPrecompile::::INDEX); + + let netuid = NetUid::from(TEST_NETUID_U16); + let uid = 0u16; + let evm_address = addr_from_index(0xdead_beef); + let block_associated = 42u64; + let limit = 1024u16; + + pallet_subtensor::AssociatedEvmAddress::::insert( + netuid, + uid, + (evm_address, block_associated), + ); + + let expected = + pallet_subtensor::Pallet::::uid_lookup(netuid, evm_address, limit); + assert_eq!(expected, vec![(uid, block_associated)]); + + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("uidLookup(uint16,address,uint16)"), + (TEST_NETUID_U16, Address(evm_address), limit), + ), + ) + .with_static_call(true) + .execute_returns_raw(encode_return_value(expected)); + }); + } +} diff --git a/runtime/tests/precompiles.rs b/runtime/tests/precompiles.rs index eb1c92faef..cbb8a06a22 100644 --- a/runtime/tests/precompiles.rs +++ b/runtime/tests/precompiles.rs @@ -1,29 +1,17 @@ #![allow(clippy::unwrap_used)] #![allow(clippy::expect_used)] -#![allow(clippy::arithmetic_side_effects)] - -use core::iter::IntoIterator; -use std::collections::BTreeSet; use fp_evm::{Context, ExitError, PrecompileFailure, PrecompileResult}; use node_subtensor_runtime::{BuildStorage, Runtime, RuntimeGenesisConfig, System}; -use pallet_evm::{AddressMapping, BalanceConverter, PrecompileSet}; -use precompile_utils::solidity::{codec::Address, encode_return_value, encode_with_selector}; -use precompile_utils::testing::{MockHandle, PrecompileTesterExt}; -use sp_core::{H160, H256, Pair, U256, ed25519}; +use pallet_evm::{BalanceConverter, PrecompileSet}; +use precompile_utils::solidity::encode_with_selector; +use precompile_utils::testing::MockHandle; +use sp_core::{H160, H256, U256}; use sp_runtime::traits::Hash; -use substrate_fixed::types::{I96F32, U96F32}; -use subtensor_precompiles::{ - AddressMappingPrecompile, AlphaPrecompile, BalanceTransferPrecompile, Ed25519Verify, - MetagraphPrecompile, NeuronPrecompile, PrecompileExt, Precompiles, UidLookupPrecompile, -}; -use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance, Token}; -use subtensor_swap_interface::{Order, SwapHandler}; +use subtensor_precompiles::{BalanceTransferPrecompile, PrecompileExt, Precompiles}; type AccountId = ::AccountId; -const TEST_NETUID_U16: u16 = 1; - fn new_test_ext() -> sp_io::TestExternalities { let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig::default() .build_storage() @@ -62,1161 +50,110 @@ fn addr_from_index(index: u64) -> H160 { H160::from_low_u64_be(index) } -/// 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_u32(signature: &str) -> u32 { let hash = sp_io::hashing::keccak_256(signature.as_bytes()); u32::from_be_bytes([hash[0], hash[1], hash[2], hash[3]]) } -/// Matches the alpha precompile conversion from fixed-point price to EVM `uint256`. -fn alpha_price_to_evm(price: U96F32) -> U256 { - 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() -} - #[test] -fn precompile_registry_addresses_are_unique() { +fn balance_transfer_precompile_transfers_balance() { new_test_ext().execute_with(|| { - let addresses = Precompiles::::used_addresses(); - let unique: BTreeSet<_> = IntoIterator::into_iter(addresses).collect(); - assert_eq!(unique.len(), addresses.len()); - }); -} - -mod address_mapping { - use super::*; - - #[test] - fn address_mapping_precompile_returns_runtime_address_mapping() { - new_test_ext().execute_with(|| { - let precompiles = Precompiles::::new(); - - let caller = addr_from_index(1); - let target_address = addr_from_index(0x1234); - let input = encode_with_selector( - selector_u32("addressMapping(address)"), - (Address(target_address),), - ); - - let mapped_account = - ::AddressMapping::into_account_id(target_address); - let expected_output: [u8; 32] = mapped_account.into(); - - let precompile_addr = addr_from_index(AddressMappingPrecompile::::INDEX); - precompiles - .prepare_test(caller, precompile_addr, input) - .with_static_call(true) - .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, - encode_with_selector( - selector_u32("addressMapping(address)"), - (Address(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, - encode_with_selector( - selector_u32("addressMapping(address)"), - (Address(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); - }); - } + let precompiles = Precompiles::::new(); + let precompile_addr = addr_from_index(BalanceTransferPrecompile::::INDEX); + let dispatch_account: AccountId = BalanceTransferPrecompile::::account_id(); + let destination_raw = H256::repeat_byte(7); + let destination_account: AccountId = destination_raw.0.into(); - #[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 = encode_with_selector( - selector_u32("addressMapping(address)"), - (Address(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 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), + let amount = 123_456; + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &dispatch_account, + (amount * 2).into(), ); - 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), + let source_balance_before = + pallet_balances::Pallet::::free_balance(&dispatch_account); + let destination_balance_before = + pallet_balances::Pallet::::free_balance(&destination_account); + + let result = execute_precompile( + &precompiles, + precompile_addr, + addr_from_index(1), + encode_with_selector(selector_u32("transfer(bytes32)"), (destination_raw,)), + evm_apparent_value_from_substrate(amount), ); - } - - 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, - encode_with_selector(selector_u32("getAlphaPrice(uint16)"), (DYNAMIC_NETUID_U16,)), - alpha_price_to_evm(alpha_price), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector( - selector_u32("getMovingAlphaPrice(uint16)"), - (DYNAMIC_NETUID_U16,), - ), - alpha_price_to_evm(moving_alpha_price), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector(selector_u32("getTaoInPool(uint16)"), (DYNAMIC_NETUID_U16,)), - U256::from(pallet_subtensor::SubnetTAO::::get(dynamic_netuid).to_u64()), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector( - selector_u32("getAlphaInPool(uint16)"), - (DYNAMIC_NETUID_U16,), - ), - U256::from(u64::from(pallet_subtensor::SubnetAlphaIn::::get( - dynamic_netuid, - ))), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector( - selector_u32("getAlphaOutPool(uint16)"), - (DYNAMIC_NETUID_U16,), - ), - U256::from(u64::from(pallet_subtensor::SubnetAlphaOut::::get( - dynamic_netuid, - ))), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector( - selector_u32("getAlphaIssuance(uint16)"), - (DYNAMIC_NETUID_U16,), - ), - U256::from(u64::from( - pallet_subtensor::Pallet::::get_alpha_issuance(dynamic_netuid), - )), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector( - selector_u32("getSubnetMechanism(uint16)"), - (DYNAMIC_NETUID_U16,), - ), - U256::from(pallet_subtensor::SubnetMechanism::::get( - dynamic_netuid, - )), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector( - selector_u32("getEMAPriceHalvingBlocks(uint16)"), - (DYNAMIC_NETUID_U16,), - ), - U256::from(pallet_subtensor::EMAPriceHalvingBlocks::::get( - dynamic_netuid, - )), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector( - selector_u32("getSubnetVolume(uint16)"), - (DYNAMIC_NETUID_U16,), - ), - U256::from(pallet_subtensor::SubnetVolume::::get( - dynamic_netuid, - )), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector( - selector_u32("getTaoInEmission(uint16)"), - (DYNAMIC_NETUID_U16,), - ), - U256::from( - pallet_subtensor::SubnetTaoInEmission::::get(dynamic_netuid).to_u64(), - ), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector( - selector_u32("getAlphaInEmission(uint16)"), - (DYNAMIC_NETUID_U16,), - ), - U256::from( - pallet_subtensor::SubnetAlphaInEmission::::get(dynamic_netuid) - .to_u64(), - ), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector( - selector_u32("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 += price; - } - } - - assert!(sum_alpha_price > U96F32::from_num(0)); - - assert_static_call( - &precompiles, - caller, - precompile_addr, - selector_u32("getCKBurn()").to_be_bytes().to_vec(), - U256::from(pallet_subtensor::CKBurn::::get()), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - selector_u32("getTaoWeight()").to_be_bytes().to_vec(), - U256::from(pallet_subtensor::TaoWeight::::get()), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - selector_u32("getRootNetuid()").to_be_bytes().to_vec(), - U256::from(u16::from(NetUid::ROOT)), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - selector_u32("getSumAlphaPrice()").to_be_bytes().to_vec(), - 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, - encode_with_selector( - selector_u32("simSwapTaoForAlpha(uint16,uint64)"), - (DYNAMIC_NETUID_U16, tao_amount), - ), - U256::from(expected_alpha), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector( - selector_u32("simSwapAlphaForTao(uint16,uint64)"), - (DYNAMIC_NETUID_U16, alpha_amount), - ), - U256::from(expected_tao), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector( - selector_u32("simSwapTaoForAlpha(uint16,uint64)"), - (DYNAMIC_NETUID_U16, 0_u64), - ), - U256::zero(), - ); - assert_static_call( - &precompiles, - caller, - precompile_addr, - encode_with_selector( - selector_u32("simSwapAlphaForTao(uint16,uint64)"), - (DYNAMIC_NETUID_U16, 0_u64), - ), - U256::zero(), - ); - }); - } -} - -mod ed25519_verify { - use super::*; - - #[test] - fn ed25519_precompile_verifies_valid_and_invalid_signatures() { - new_test_ext().execute_with(|| { - let pair = ed25519::Pair::from_string("//Alice", None) - .expect("Alice ed25519 key should be available"); - let message = sp_io::hashing::keccak_256(b"Sign this message"); - let signature = pair.sign(&message).0; - let public_key = pair.public().0; - - let mut broken_message = message; - broken_message[0] ^= 0x01; - - let mut broken_signature = signature; - broken_signature[0] ^= 0x01; - - let precompiles = Precompiles::::new(); - let caller = addr_from_index(1); - let precompile_addr = addr_from_index(Ed25519Verify::::INDEX); - let message = H256::from(message); - let broken_message = H256::from(broken_message); - let public_key = H256::from(public_key); - let signature_r = H256::from_slice(&signature[..32]); - let signature_s = H256::from_slice(&signature[32..]); - let broken_signature_r = H256::from_slice(&broken_signature[..32]); - let broken_signature_s = H256::from_slice(&broken_signature[32..]); - - precompiles - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("verify(bytes32,bytes32,bytes32,bytes32)"), - (message, public_key, signature_r, signature_s), - ), - ) - .with_static_call(true) - .execute_returns_raw(abi_word(U256::one())); - precompiles - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("verify(bytes32,bytes32,bytes32,bytes32)"), - (broken_message, public_key, signature_r, signature_s), - ), - ) - .with_static_call(true) - .execute_returns_raw(abi_word(U256::zero())); - precompiles - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("verify(bytes32,bytes32,bytes32,bytes32)"), - (message, public_key, broken_signature_r, broken_signature_s), - ), - ) - .with_static_call(true) - .execute_returns_raw(abi_word(U256::zero())); - }); - } -} - -mod uid_lookup { - use super::*; - - #[test] - fn uid_lookup_precompile_returns_associated_uid_and_block() { - new_test_ext().execute_with(|| { - let precompiles = Precompiles::::new(); - let caller = addr_from_index(1); - let precompile_addr = addr_from_index(UidLookupPrecompile::::INDEX); - - let netuid = NetUid::from(TEST_NETUID_U16); - let netuid_u16: u16 = netuid.into(); - let uid = 0u16; - let evm_address = H160::from_low_u64_be(0xdead_beef); - let block_associated = 42u64; - let limit = 1024u16; - - pallet_subtensor::AssociatedEvmAddress::::insert( - netuid, - uid, - (evm_address, block_associated), - ); - - let expected = - pallet_subtensor::Pallet::::uid_lookup(netuid, evm_address, limit); - assert_eq!(expected, vec![(uid, block_associated)]); - - precompiles - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("uidLookup(uint16,address,uint16)"), - (netuid_u16, Address(evm_address), limit), - ), - ) - .with_static_call(true) - .execute_returns_raw(encode_return_value(expected)); - }); - } -} - -mod metagraph { - use super::*; - - const UID: u16 = 0; - const EMISSION: u64 = 111; - const VTRUST: u16 = 222; - const LAST_UPDATE: u64 = 333; - const AXON_BLOCK: u64 = 444; - const AXON_VERSION: u32 = 555; - const AXON_IP: u128 = 666; - const AXON_PORT: u16 = 777; - const AXON_IP_TYPE: u8 = 4; - const AXON_PROTOCOL: u8 = 1; - - fn seed_metagraph_test_state() -> (NetUid, AccountId, AccountId, pallet_subtensor::AxonInfo) { - let netuid = NetUid::from(TEST_NETUID_U16); - let hotkey = pallet_subtensor::Keys::::get(netuid, UID); - let coldkey = pallet_subtensor::Owner::::get(&hotkey); - - let axon = pallet_subtensor::AxonInfo { - block: AXON_BLOCK, - version: AXON_VERSION, - ip: AXON_IP, - port: AXON_PORT, - ip_type: AXON_IP_TYPE, - protocol: AXON_PROTOCOL, - placeholder1: 0, - placeholder2: 0, - }; - - pallet_subtensor::SubnetworkN::::insert(netuid, 1); - pallet_subtensor::Emission::::insert(netuid, vec![AlphaBalance::from(EMISSION)]); - pallet_subtensor::ValidatorTrust::::insert(netuid, vec![VTRUST]); - pallet_subtensor::ValidatorPermit::::insert(netuid, vec![true]); - pallet_subtensor::LastUpdate::::insert( - NetUidStorageIndex::from(netuid), - vec![LAST_UPDATE], + let precompile_result = + result.expect("expected precompile transfer call to be routed to a precompile"); + precompile_result.expect("expected successful precompile transfer dispatch"); + + let source_balance_after = + pallet_balances::Pallet::::free_balance(&dispatch_account); + let destination_balance_after = + pallet_balances::Pallet::::free_balance(&destination_account); + + assert_eq!(source_balance_after, source_balance_before - amount.into()); + assert_eq!( + destination_balance_after, + destination_balance_before + amount.into() ); - pallet_subtensor::Active::::insert(netuid, vec![true]); - pallet_subtensor::Axons::::insert(netuid, &hotkey, axon.clone()); - - (netuid, hotkey, coldkey, axon) - } - - #[test] - fn metagraph_precompile_matches_runtime_values() { - new_test_ext().execute_with(|| { - let (netuid, hotkey, coldkey, axon) = seed_metagraph_test_state(); - - let precompiles = Precompiles::::new(); - let caller = addr_from_index(1); - let precompile_addr = addr_from_index(MetagraphPrecompile::::INDEX); - - let uid_count = pallet_subtensor::SubnetworkN::::get(netuid); - let emission = - pallet_subtensor::Pallet::::get_emission_for_uid(netuid, UID).to_u64(); - let vtrust = - pallet_subtensor::Pallet::::get_validator_trust_for_uid(netuid, UID); - let validator_status = - pallet_subtensor::Pallet::::get_validator_permit_for_uid(netuid, UID); - let last_update = pallet_subtensor::Pallet::::get_last_update_for_uid( - NetUidStorageIndex::from(netuid), - UID, - ); - let is_active = pallet_subtensor::Pallet::::get_active_for_uid(netuid, UID); - let runtime_axon = pallet_subtensor::Pallet::::get_axon_info(netuid, &hotkey); - - assert_eq!(uid_count, 1); - assert_eq!(emission, EMISSION); - assert_eq!(vtrust, VTRUST); - assert!(validator_status); - assert_eq!(last_update, LAST_UPDATE); - assert!(is_active); - assert_eq!(runtime_axon, axon); - - precompiles - .prepare_test( - caller, - precompile_addr, - encode_with_selector(selector_u32("getUidCount(uint16)"), (TEST_NETUID_U16,)), - ) - .with_static_call(true) - .execute_returns_raw(abi_word(U256::from(uid_count))); - precompiles - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("getAxon(uint16,uint16)"), - (TEST_NETUID_U16, UID), - ), - ) - .with_static_call(true) - .execute_returns_raw(encode_return_value(( - runtime_axon.block, - runtime_axon.version, - runtime_axon.ip, - runtime_axon.port, - runtime_axon.ip_type, - runtime_axon.protocol, - ))); - precompiles - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("getEmission(uint16,uint16)"), - (TEST_NETUID_U16, UID), - ), - ) - .with_static_call(true) - .execute_returns_raw(abi_word(U256::from(emission))); - precompiles - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("getVtrust(uint16,uint16)"), - (TEST_NETUID_U16, UID), - ), - ) - .with_static_call(true) - .execute_returns_raw(abi_word(U256::from(vtrust))); - precompiles - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("getValidatorStatus(uint16,uint16)"), - (TEST_NETUID_U16, UID), - ), - ) - .with_static_call(true) - .execute_returns_raw(abi_word(U256::from(validator_status as u8))); - precompiles - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("getLastUpdate(uint16,uint16)"), - (TEST_NETUID_U16, UID), - ), - ) - .with_static_call(true) - .execute_returns_raw(abi_word(U256::from(last_update))); - precompiles - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("getIsActive(uint16,uint16)"), - (TEST_NETUID_U16, UID), - ), - ) - .with_static_call(true) - .execute_returns_raw(abi_word(U256::from(is_active as u8))); - precompiles - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("getHotkey(uint16,uint16)"), - (TEST_NETUID_U16, UID), - ), - ) - .with_static_call(true) - .execute_returns_raw(H256::from_slice(hotkey.as_ref()).as_bytes().to_vec()); - precompiles - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("getColdkey(uint16,uint16)"), - (TEST_NETUID_U16, UID), - ), - ) - .with_static_call(true) - .execute_returns_raw(H256::from_slice(coldkey.as_ref()).as_bytes().to_vec()); - }); - } + }); } -mod neuron { - use super::*; - - const REGISTRATION_BURN: u64 = 1_000; - const RESERVE: u64 = 1_000_000_000; - const COLDKEY_BALANCE: u64 = 50_000; - const TEMPO: u16 = 100; - const REVEAL_PERIOD: u64 = 1; - const VERSION_KEY: u64 = 0; - const REGISTERED_UID: u16 = 1; - const REVEAL_UIDS: [u16; 1] = [REGISTERED_UID]; - const REVEAL_VALUES: [u16; 1] = [5]; - const REVEAL_SALT: [u16; 1] = [9]; - - fn setup_registered_caller(caller: H160) -> (NetUid, AccountId) { - let netuid = NetUid::from(TEST_NETUID_U16); - let caller_account = - ::AddressMapping::into_account_id(caller); - let caller_hotkey = H256::from_slice(caller_account.as_ref()); +#[test] +fn balance_transfer_precompile_respects_dispatch_guard_policy() { + new_test_ext().execute_with(|| { + let precompiles = Precompiles::::new(); + let precompile_addr = addr_from_index(BalanceTransferPrecompile::::INDEX); + let dispatch_account: AccountId = BalanceTransferPrecompile::::account_id(); + let destination_raw = H256::repeat_byte(8); + let destination_account: AccountId = destination_raw.0.into(); - pallet_subtensor::Pallet::::set_network_registration_allowed(netuid, true); - pallet_subtensor::Pallet::::set_burn(netuid, REGISTRATION_BURN.into()); - pallet_subtensor::Pallet::::set_max_allowed_uids(netuid, 4096); - pallet_subtensor::Pallet::::set_weights_set_rate_limit(netuid, 0); - pallet_subtensor::Pallet::::set_tempo(netuid, TEMPO); - pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid, true); - pallet_subtensor::Pallet::::set_reveal_period(netuid, REVEAL_PERIOD) - .expect("reveal period setup should succeed"); - pallet_subtensor::SubnetTAO::::insert(netuid, TaoBalance::from(RESERVE)); - pallet_subtensor::SubnetAlphaIn::::insert(netuid, AlphaBalance::from(RESERVE)); + let amount = 100; pallet_subtensor::Pallet::::add_balance_to_coldkey_account( - &caller_account, - COLDKEY_BALANCE.into(), + &dispatch_account, + 1_000_000_u64.into(), ); - Precompiles::::new() - .prepare_test( - caller, - addr_from_index(NeuronPrecompile::::INDEX), - encode_with_selector( - selector_u32("burnedRegister(uint16,bytes32)"), - (TEST_NETUID_U16, caller_hotkey), - ), - ) - .execute_returns(()); - - let registered_uid = pallet_subtensor::Pallet::::get_uid_for_net_and_hotkey( - netuid, - &caller_account, - ) - .expect("caller should be registered on subnet"); - assert_eq!(registered_uid, REGISTERED_UID); - - (netuid, caller_account) - } - - fn reveal_commit_hash(caller_account: &AccountId, netuid: NetUid) -> H256 { - ::Hashing::hash_of(&( - caller_account.clone(), - NetUidStorageIndex::from(netuid), - REVEAL_UIDS.as_slice(), - REVEAL_VALUES.as_slice(), - REVEAL_SALT.as_slice(), - VERSION_KEY, - )) - } - - #[test] - fn neuron_precompile_burned_register_adds_a_new_uid_and_key() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(TEST_NETUID_U16); - let caller = addr_from_index(0x1234); - let caller_account = - ::AddressMapping::into_account_id(caller); - let hotkey_account = AccountId::from([0x42; 32]); - let hotkey = H256::from_slice(hotkey_account.as_ref()); - - pallet_subtensor::Pallet::::set_network_registration_allowed(netuid, true); - pallet_subtensor::Pallet::::set_burn(netuid, REGISTRATION_BURN.into()); - pallet_subtensor::Pallet::::set_max_allowed_uids(netuid, 4096); - pallet_subtensor::SubnetTAO::::insert(netuid, TaoBalance::from(RESERVE)); - pallet_subtensor::SubnetAlphaIn::::insert(netuid, AlphaBalance::from(RESERVE)); - pallet_subtensor::Pallet::::add_balance_to_coldkey_account( - &caller_account, - COLDKEY_BALANCE.into(), - ); - - let uid_before = pallet_subtensor::SubnetworkN::::get(netuid); - let balance_before = - pallet_subtensor::Pallet::::get_coldkey_balance(&caller_account).to_u64(); - - Precompiles::::new() - .prepare_test( - caller, - addr_from_index(NeuronPrecompile::::INDEX), - encode_with_selector( - selector_u32("burnedRegister(uint16,bytes32)"), - (TEST_NETUID_U16, hotkey), - ), - ) - .execute_returns(()); - - let uid_after = pallet_subtensor::SubnetworkN::::get(netuid); - let registered_hotkey = pallet_subtensor::Keys::::get(netuid, uid_before); - let owner = pallet_subtensor::Owner::::get(&hotkey_account); - let balance_after = - pallet_subtensor::Pallet::::get_coldkey_balance(&caller_account).to_u64(); - - assert_eq!(uid_after, uid_before + 1); - assert_eq!(registered_hotkey, hotkey_account); - assert_eq!(owner, caller_account); - assert!(balance_after < balance_before); - }); - } - - #[test] - fn neuron_precompile_commit_weights_respects_stake_threshold_and_stores_commit() { - new_test_ext().execute_with(|| { - let caller = addr_from_index(0x2234); - let (netuid, caller_account) = setup_registered_caller(caller); - let commit_hash = reveal_commit_hash(&caller_account, netuid); - let precompile_addr = addr_from_index(NeuronPrecompile::::INDEX); - - pallet_subtensor::Pallet::::set_stake_threshold(1); - let rejected = execute_precompile( - &Precompiles::::new(), - precompile_addr, - caller, - encode_with_selector( - selector_u32("commitWeights(uint16,bytes32)"), - (TEST_NETUID_U16, commit_hash), - ), - U256::zero(), - ) - .expect("commit weights should route to neuron precompile"); - assert!(rejected.is_err()); - - pallet_subtensor::Pallet::::set_stake_threshold(0); - Precompiles::::new() - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("commitWeights(uint16,bytes32)"), - (TEST_NETUID_U16, commit_hash), - ), - ) - .execute_returns(()); - - let commits = pallet_subtensor::WeightCommits::::get( - NetUidStorageIndex::from(netuid), - &caller_account, - ) - .expect("weight commits should be stored after successful commit"); - assert_eq!(commits.len(), 1); - }); - } - - #[test] - fn neuron_precompile_reveal_weights_respects_stake_threshold_and_sets_weights() { - new_test_ext().execute_with(|| { - let caller = addr_from_index(0x3234); - let (netuid, caller_account) = setup_registered_caller(caller); - let commit_hash = reveal_commit_hash(&caller_account, netuid); - let precompile_addr = addr_from_index(NeuronPrecompile::::INDEX); - - Precompiles::::new() - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("commitWeights(uint16,bytes32)"), - (TEST_NETUID_U16, commit_hash), - ), - ) - .execute_returns(()); - - let commits = pallet_subtensor::WeightCommits::::get( - NetUidStorageIndex::from(netuid), - &caller_account, - ) - .expect("weight commit should exist before reveal"); - let (_, _, first_reveal_block, _) = commits - .front() - .copied() - .expect("weight commit queue should contain the committed hash"); - - System::set_block_number( - u32::try_from(first_reveal_block) - .expect("first reveal block should fit in runtime block number"), - ); - - pallet_subtensor::Pallet::::set_stake_threshold(1); - let rejected = execute_precompile( - &Precompiles::::new(), - precompile_addr, - caller, - encode_with_selector( - selector_u32("revealWeights(uint16,uint16[],uint16[],uint16[],uint64)"), - ( - TEST_NETUID_U16, - REVEAL_UIDS.to_vec(), - REVEAL_VALUES.to_vec(), - REVEAL_SALT.to_vec(), - VERSION_KEY, - ), - ), - U256::zero(), - ) - .expect("reveal weights should route to neuron precompile"); - assert!(rejected.is_err()); - - pallet_subtensor::Pallet::::set_stake_threshold(0); - Precompiles::::new() - .prepare_test( - caller, - precompile_addr, - encode_with_selector( - selector_u32("revealWeights(uint16,uint16[],uint16[],uint16[],uint64)"), - ( - TEST_NETUID_U16, - REVEAL_UIDS.to_vec(), - REVEAL_VALUES.to_vec(), - REVEAL_SALT.to_vec(), - VERSION_KEY, - ), - ), - ) - .execute_returns(()); - - assert!( - pallet_subtensor::WeightCommits::::get( - NetUidStorageIndex::from(netuid), - &caller_account, - ) - .is_none() - ); - - let neuron_uid = pallet_subtensor::Pallet::::get_uid_for_net_and_hotkey( - netuid, - &caller_account, - ) - .expect("caller should remain registered after reveal"); - let weights = pallet_subtensor::Weights::::get( - NetUidStorageIndex::from(netuid), - neuron_uid, - ); - - assert_eq!(weights.len(), 1); - assert_eq!(weights[0].0, neuron_uid); - assert!(weights[0].1 > 0); - }); - } -} - -mod balance_transfer { - use super::*; - - #[test] - fn balance_transfer_precompile_transfers_balance() { - new_test_ext().execute_with(|| { - let precompiles = Precompiles::::new(); - let precompile_addr = addr_from_index(BalanceTransferPrecompile::::INDEX); - let dispatch_account: AccountId = BalanceTransferPrecompile::::account_id(); - let destination_raw = H256::repeat_byte(7); - let destination_account: AccountId = destination_raw.0.into(); - - let amount = 123_456; - pallet_subtensor::Pallet::::add_balance_to_coldkey_account( - &dispatch_account, - (amount * 2).into(), - ); - - let source_balance_before = - pallet_balances::Pallet::::free_balance(&dispatch_account); - let destination_balance_before = - pallet_balances::Pallet::::free_balance(&destination_account); - - let result = execute_precompile( - &precompiles, - precompile_addr, - addr_from_index(1), - encode_with_selector(selector_u32("transfer(bytes32)"), (destination_raw,)), - evm_apparent_value_from_substrate(amount), - ); - let precompile_result = - result.expect("expected precompile transfer call to be routed to a precompile"); - precompile_result.expect("expected successful precompile transfer dispatch"); - - let source_balance_after = - pallet_balances::Pallet::::free_balance(&dispatch_account); - let destination_balance_after = - pallet_balances::Pallet::::free_balance(&destination_account); - - assert_eq!(source_balance_after, source_balance_before - amount.into()); - assert_eq!( - destination_balance_after, - destination_balance_before + amount.into() - ); - }); - } - - #[test] - fn balance_transfer_precompile_respects_dispatch_guard_policy() { - new_test_ext().execute_with(|| { - let precompiles = Precompiles::::new(); - let precompile_addr = addr_from_index(BalanceTransferPrecompile::::INDEX); - let dispatch_account: AccountId = BalanceTransferPrecompile::::account_id(); - let destination_raw = H256::repeat_byte(8); - let destination_account: AccountId = destination_raw.0.into(); - - let amount = 100; - pallet_subtensor::Pallet::::add_balance_to_coldkey_account( - &dispatch_account, - 1_000_000_u64.into(), - ); - - // Activate coldkey-swap guard for precompile dispatch account. - let replacement_coldkey = AccountId::from([9u8; 32]); - let replacement_hash = - ::Hashing::hash_of(&replacement_coldkey); - pallet_subtensor::ColdkeySwapAnnouncements::::insert( - &dispatch_account, - (System::block_number(), replacement_hash), - ); - - let source_balance_before = - pallet_balances::Pallet::::free_balance(&dispatch_account); - let destination_balance_before = - pallet_balances::Pallet::::free_balance(&destination_account); + let replacement_coldkey = AccountId::from([9u8; 32]); + let replacement_hash = + ::Hashing::hash_of(&replacement_coldkey); + pallet_subtensor::ColdkeySwapAnnouncements::::insert( + &dispatch_account, + (System::block_number(), replacement_hash), + ); - let result = execute_precompile( - &precompiles, - precompile_addr, - addr_from_index(1), - encode_with_selector(selector_u32("transfer(bytes32)"), (destination_raw,)), - evm_apparent_value_from_substrate(amount), - ); - let precompile_result = - result.expect("expected precompile transfer call to be routed to a precompile"); - let failure = precompile_result - .expect_err("expected transaction extension rejection on precompile dispatch"); - let message = match failure { - PrecompileFailure::Error { - exit_status: ExitError::Other(message), - } => message, - other => panic!("unexpected precompile failure: {other:?}"), - }; - assert!( - message.contains("dispatch execution failed: ColdkeySwapAnnounced"), - "unexpected precompile failure: {message}" - ); + let source_balance_before = + pallet_balances::Pallet::::free_balance(&dispatch_account); + let destination_balance_before = + pallet_balances::Pallet::::free_balance(&destination_account); + + let result = execute_precompile( + &precompiles, + precompile_addr, + addr_from_index(1), + encode_with_selector(selector_u32("transfer(bytes32)"), (destination_raw,)), + evm_apparent_value_from_substrate(amount), + ); + let precompile_result = + result.expect("expected precompile transfer call to be routed to a precompile"); + let failure = precompile_result + .expect_err("expected transaction extension rejection on precompile dispatch"); + let message = match failure { + PrecompileFailure::Error { + exit_status: ExitError::Other(message), + } => message, + other => panic!("unexpected precompile failure: {other:?}"), + }; + assert!( + message.contains("dispatch execution failed: ColdkeySwapAnnounced"), + "unexpected precompile failure: {message}" + ); - let source_balance_after = - pallet_balances::Pallet::::free_balance(&dispatch_account); - let destination_balance_after = - pallet_balances::Pallet::::free_balance(&destination_account); - assert_eq!(source_balance_after, source_balance_before); - assert_eq!(destination_balance_after, destination_balance_before); - }); - } + let source_balance_after = + pallet_balances::Pallet::::free_balance(&dispatch_account); + let destination_balance_after = + pallet_balances::Pallet::::free_balance(&destination_account); + assert_eq!(source_balance_after, source_balance_before); + assert_eq!(destination_balance_after, destination_balance_before); + }); } From 5e75f9746f844af5e3fd67ce15ebb6bb324b298e Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 21 Apr 2026 13:53:46 +0200 Subject: [PATCH 15/21] Port neuron precompile set weights tests to rust --- .../neuron.precompile.set-weights.test.ts | 73 ------------------- precompiles/src/neuron.rs | 41 +++++++++++ 2 files changed, 41 insertions(+), 73 deletions(-) delete mode 100644 contract-tests/test/neuron.precompile.set-weights.test.ts diff --git a/contract-tests/test/neuron.precompile.set-weights.test.ts b/contract-tests/test/neuron.precompile.set-weights.test.ts deleted file mode 100644 index 8ff9258664..0000000000 --- a/contract-tests/test/neuron.precompile.set-weights.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as assert from "assert"; - -import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" -import { devnet } from "@polkadot-api/descriptors" -import { TypedApi } from "polkadot-api"; -import { convertH160ToSS58, convertPublicKeyToSs58, } from "../src/address-utils" -import { ethers } from "ethers" -import { INEURON_ADDRESS, INeuronABI } from "../src/contracts/neuron" -import { generateRandomEthersWallet } from "../src/utils" -import { - forceSetBalanceToSs58Address, forceSetBalanceToEthAddress, addNewSubnetwork, burnedRegister, setCommitRevealWeightsEnabled, - setWeightsSetRateLimit, - startCall, - disableAdminFreezeWindowAndOwnerHyperparamRateLimit -} from "../src/subtensor" - -describe("Test neuron precompile contract, set weights function", () => { - // init eth part - const wallet = generateRandomEthersWallet(); - - // init substrate part - const hotkey = getRandomSubstrateKeypair(); - const coldkey = getRandomSubstrateKeypair(); - - let api: TypedApi - - before(async () => { - api = await getDevnetApi() - - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) - - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) - await forceSetBalanceToEthAddress(api, wallet.address) - - const netuid = await addNewSubnetwork(api, hotkey, coldkey) - await startCall(api, netuid, coldkey) - console.log("test on subnet ", netuid) - - await burnedRegister(api, netuid, convertH160ToSS58(wallet.address), coldkey) - const uid = await api.query.SubtensorModule.Uids.getValue(netuid, convertH160ToSS58(wallet.address)) - assert.notEqual(uid, undefined) - await disableAdminFreezeWindowAndOwnerHyperparamRateLimit(api) - // disable reveal and enable direct set weights - await setCommitRevealWeightsEnabled(api, netuid, false) - await setWeightsSetRateLimit(api, netuid, BigInt(0)) - }) - - it("Set weights is ok", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 - const uid = await api.query.SubtensorModule.Uids.getValue(netuid, convertH160ToSS58(wallet.address)) - - const contract = new ethers.Contract(INEURON_ADDRESS, INeuronABI, wallet); - const dests = [1]; - const weights = [2]; - const version_key = 0; - - const tx = await contract.setWeights(netuid, dests, weights, version_key); - - await tx.wait(); - if (uid === undefined) { - throw new Error("uid not get on chain") - } else { - const weightsOnChain = await api.query.SubtensorModule.Weights.getValue(netuid, uid) - - weightsOnChain.forEach((weight, _) => { - const uidInWeight = weight[0]; - const value = weight[1]; - assert.equal(uidInWeight, uid) - assert.ok(value > 0) - }); - } - }) -}); diff --git a/precompiles/src/neuron.rs b/precompiles/src/neuron.rs index f605905006..b32a86fdd5 100644 --- a/precompiles/src/neuron.rs +++ b/precompiles/src/neuron.rs @@ -522,4 +522,45 @@ mod tests { assert!(weights[0].1 > 0); }); } + + #[test] + fn neuron_precompile_set_weights_sets_weights_when_commit_reveal_is_disabled() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(0x4234); + let (netuid, caller_account) = setup_registered_caller(caller); + let precompile_addr = addr_from_index(NeuronPrecompile::::INDEX); + + pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid, false); + + precompiles::>() + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("setWeights(uint16,uint16[],uint16[],uint64)"), + ( + TEST_NETUID_U16, + vec![REGISTERED_UID], + vec![2_u16], + VERSION_KEY, + ), + ), + ) + .execute_returns(()); + + let neuron_uid = pallet_subtensor::Pallet::::get_uid_for_net_and_hotkey( + netuid, + &caller_account, + ) + .expect("caller should remain registered after setting weights"); + let weights = pallet_subtensor::Weights::::get( + NetUidStorageIndex::from(netuid), + neuron_uid, + ); + + assert_eq!(weights.len(), 1); + assert_eq!(weights[0].0, neuron_uid); + assert!(weights[0].1 > 0); + }); + } } From 5914b29edc3d82ba730d0dd09892a2ad30b6afc5 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 21 Apr 2026 14:13:45 +0200 Subject: [PATCH 16/21] Port neuron serving tests to rust --- ...n.precompile.serve.axon-prometheus.test.ts | 161 ------------------ precompiles/src/metagraph.rs | 4 +- precompiles/src/mock.rs | 1 - precompiles/src/neuron.rs | 137 ++++++++++++++- precompiles/src/uid_lookup.rs | 6 +- 5 files changed, 140 insertions(+), 169 deletions(-) delete mode 100644 contract-tests/test/neuron.precompile.serve.axon-prometheus.test.ts diff --git a/contract-tests/test/neuron.precompile.serve.axon-prometheus.test.ts b/contract-tests/test/neuron.precompile.serve.axon-prometheus.test.ts deleted file mode 100644 index a80d79d486..0000000000 --- a/contract-tests/test/neuron.precompile.serve.axon-prometheus.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import * as assert from "assert"; -import { getAliceSigner, getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate" -import { devnet } from "@polkadot-api/descriptors" -import { PolkadotSigner, TypedApi } from "polkadot-api"; -import { convertPublicKeyToSs58, convertH160ToSS58 } from "../src/address-utils" -import { ethers } from "ethers" -import { INEURON_ADDRESS, INeuronABI } from "../src/contracts/neuron" -import { generateRandomEthersWallet } from "../src/utils" -import { forceSetBalanceToEthAddress, forceSetBalanceToSs58Address, addNewSubnetwork, burnedRegister, startCall } from "../src/subtensor" - -describe("Test neuron precompile Serve Axon Prometheus", () => { - // init eth part - const wallet1 = generateRandomEthersWallet(); - const wallet2 = generateRandomEthersWallet(); - const wallet3 = generateRandomEthersWallet(); - - // init substrate part - const hotkey = getRandomSubstrateKeypair(); - const coldkey = getRandomSubstrateKeypair(); - - let api: TypedApi - - // sudo account alice as signer - let alice: PolkadotSigner; - before(async () => { - // init variables got from await and async - api = await getDevnetApi() - alice = await getAliceSigner(); - - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(alice.publicKey)) - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey)) - await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey)) - await forceSetBalanceToEthAddress(api, wallet1.address) - await forceSetBalanceToEthAddress(api, wallet2.address) - await forceSetBalanceToEthAddress(api, wallet3.address) - let netuid = await addNewSubnetwork(api, hotkey, coldkey) - await startCall(api, netuid, coldkey) - - console.log("test the case on subnet ", netuid) - - await burnedRegister(api, netuid, convertH160ToSS58(wallet1.address), coldkey) - await burnedRegister(api, netuid, convertH160ToSS58(wallet2.address), coldkey) - await burnedRegister(api, netuid, convertH160ToSS58(wallet3.address), coldkey) - }) - - it("Serve Axon", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 - const version = 0; - const ip = 1; - const port = 2; - const ipType = 4; - const protocol = 0; - const placeholder1 = 8; - const placeholder2 = 9; - - const contract = new ethers.Contract(INEURON_ADDRESS, INeuronABI, wallet1); - - const tx = await contract.serveAxon( - netuid, - version, - ip, - port, - ipType, - protocol, - placeholder1, - placeholder2 - ); - await tx.wait(); - - const axon = await api.query.SubtensorModule.Axons.getValue( - netuid, - convertH160ToSS58(wallet1.address) - ) - assert.notEqual(axon?.block, undefined) - assert.equal(axon?.version, version) - assert.equal(axon?.ip, ip) - assert.equal(axon?.port, port) - assert.equal(axon?.ip_type, ipType) - assert.equal(axon?.protocol, protocol) - assert.equal(axon?.placeholder1, placeholder1) - assert.equal(axon?.placeholder2, placeholder2) - }); - - it("Serve Axon TLS", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 - const version = 0; - const ip = 1; - const port = 2; - const ipType = 4; - const protocol = 0; - const placeholder1 = 8; - const placeholder2 = 9; - // certificate length is 65 - const certificate = new Uint8Array([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, - 57, 58, 59, 60, 61, 62, 63, 64, 65, - ]); - - const contract = new ethers.Contract(INEURON_ADDRESS, INeuronABI, wallet2); - - const tx = await contract.serveAxonTls( - netuid, - version, - ip, - port, - ipType, - protocol, - placeholder1, - placeholder2, - certificate - ); - await tx.wait(); - - const axon = await api.query.SubtensorModule.Axons.getValue( - netuid, - convertH160ToSS58(wallet2.address)) - - assert.notEqual(axon?.block, undefined) - assert.equal(axon?.version, version) - assert.equal(axon?.ip, ip) - assert.equal(axon?.port, port) - assert.equal(axon?.ip_type, ipType) - assert.equal(axon?.protocol, protocol) - assert.equal(axon?.placeholder1, placeholder1) - assert.equal(axon?.placeholder2, placeholder2) - }); - - it("Serve Prometheus", async () => { - let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1 - const version = 0; - const ip = 1; - const port = 2; - const ipType = 4; - - const contract = new ethers.Contract(INEURON_ADDRESS, INeuronABI, wallet3); - - const tx = await contract.servePrometheus( - netuid, - version, - ip, - port, - ipType - ); - await tx.wait(); - - const prometheus = ( - await api.query.SubtensorModule.Prometheus.getValue( - netuid, - convertH160ToSS58(wallet3.address) - ) - ) - - assert.notEqual(prometheus?.block, undefined) - assert.equal(prometheus?.version, version) - assert.equal(prometheus?.ip, ip) - assert.equal(prometheus?.port, port) - assert.equal(prometheus?.ip_type, ipType) - }); -}); \ No newline at end of file diff --git a/precompiles/src/metagraph.rs b/precompiles/src/metagraph.rs index 1cc43e71f4..4cffb76a4f 100644 --- a/precompiles/src/metagraph.rs +++ b/precompiles/src/metagraph.rs @@ -195,14 +195,14 @@ mod tests { use super::*; use crate::PrecompileExt; use crate::mock::{ - Runtime, TEST_NETUID_U16, abi_word, addr_from_index, new_test_ext, precompiles, - selector_u32, + Runtime, abi_word, addr_from_index, new_test_ext, precompiles, selector_u32, }; use precompile_utils::solidity::{encode_return_value, encode_with_selector}; use precompile_utils::testing::PrecompileTesterExt; use sp_core::H256; use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex}; + const TEST_NETUID_U16: u16 = 1; const UID: u16 = 0; const EMISSION: u64 = 111; const VTRUST: u16 = 222; diff --git a/precompiles/src/mock.rs b/precompiles/src/mock.rs index 18dd67a7f9..452d8cf6a7 100644 --- a/precompiles/src/mock.rs +++ b/precompiles/src/mock.rs @@ -48,7 +48,6 @@ frame_support::construct_runtime!( } ); -pub(crate) const TEST_NETUID_U16: u16 = 1; const EVM_DECIMALS_FACTOR: u64 = 1_000_000_000; parameter_types! { diff --git a/precompiles/src/neuron.rs b/precompiles/src/neuron.rs index b32a86fdd5..1b66ea902c 100644 --- a/precompiles/src/neuron.rs +++ b/precompiles/src/neuron.rs @@ -260,8 +260,8 @@ mod tests { use super::*; use crate::PrecompileExt; use crate::mock::{ - AccountId, Runtime, System, TEST_NETUID_U16, addr_from_index, execute_precompile, - new_test_ext, precompiles, selector_u32, + AccountId, Runtime, System, addr_from_index, execute_precompile, new_test_ext, precompiles, + selector_u32, }; use pallet_evm::AddressMapping; use precompile_utils::solidity::encode_with_selector; @@ -270,6 +270,7 @@ mod tests { use sp_runtime::traits::Hash; use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance, Token}; + const TEST_NETUID_U16: u16 = 1; const REGISTRATION_BURN: u64 = 1_000; const RESERVE: u64 = 1_000_000_000; const COLDKEY_BALANCE: u64 = 50_000; @@ -280,6 +281,13 @@ mod tests { const REVEAL_UIDS: [u16; 1] = [REGISTERED_UID]; const REVEAL_VALUES: [u16; 1] = [5]; const REVEAL_SALT: [u16; 1] = [9]; + const SERVE_VERSION: u32 = 0; + const SERVE_IP: u128 = 1; + const SERVE_PORT: u16 = 2; + const SERVE_IP_TYPE: u8 = 4; + const SERVE_PROTOCOL: u8 = 0; + const SERVE_PLACEHOLDER1: u8 = 8; + const SERVE_PLACEHOLDER2: u8 = 9; fn setup_registered_caller(caller: H160) -> (NetUid, AccountId) { let netuid = NetUid::from(TEST_NETUID_U16); @@ -563,4 +571,129 @@ mod tests { assert!(weights[0].1 > 0); }); } + + #[test] + fn neuron_precompile_serve_axon_sets_axon_info() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(0x5234); + let (netuid, caller_account) = setup_registered_caller(caller); + + precompiles::>() + .prepare_test( + caller, + addr_from_index(NeuronPrecompile::::INDEX), + encode_with_selector( + selector_u32( + "serveAxon(uint16,uint32,uint128,uint16,uint8,uint8,uint8,uint8)", + ), + ( + TEST_NETUID_U16, + SERVE_VERSION, + SERVE_IP, + SERVE_PORT, + SERVE_IP_TYPE, + SERVE_PROTOCOL, + SERVE_PLACEHOLDER1, + SERVE_PLACEHOLDER2, + ), + ), + ) + .execute_returns(()); + + let axon = pallet_subtensor::Axons::::get(netuid, &caller_account) + .expect("axon info should be stored"); + assert!(axon.block > 0); + assert_eq!(axon.version, SERVE_VERSION); + assert_eq!(axon.ip, SERVE_IP); + assert_eq!(axon.port, SERVE_PORT); + assert_eq!(axon.ip_type, SERVE_IP_TYPE); + assert_eq!(axon.protocol, SERVE_PROTOCOL); + assert_eq!(axon.placeholder1, SERVE_PLACEHOLDER1); + assert_eq!(axon.placeholder2, SERVE_PLACEHOLDER2); + }); + } + + #[test] + fn neuron_precompile_serve_axon_tls_sets_axon_info_and_certificate() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(0x6234); + let (netuid, caller_account) = setup_registered_caller(caller); + let certificate: Vec = (1u8..=65).collect(); + + precompiles::>() + .prepare_test( + caller, + addr_from_index(NeuronPrecompile::::INDEX), + encode_with_selector( + selector_u32( + "serveAxonTls(uint16,uint32,uint128,uint16,uint8,uint8,uint8,uint8,bytes)", + ), + ( + TEST_NETUID_U16, + SERVE_VERSION, + SERVE_IP, + SERVE_PORT, + SERVE_IP_TYPE, + SERVE_PROTOCOL, + SERVE_PLACEHOLDER1, + SERVE_PLACEHOLDER2, + UnboundedBytes::from(certificate.clone()), + ), + ), + ) + .execute_returns(()); + + let axon = pallet_subtensor::Axons::::get(netuid, &caller_account) + .expect("axon info should be stored"); + assert!(axon.block > 0); + assert_eq!(axon.version, SERVE_VERSION); + assert_eq!(axon.ip, SERVE_IP); + assert_eq!(axon.port, SERVE_PORT); + assert_eq!(axon.ip_type, SERVE_IP_TYPE); + assert_eq!(axon.protocol, SERVE_PROTOCOL); + assert_eq!(axon.placeholder1, SERVE_PLACEHOLDER1); + assert_eq!(axon.placeholder2, SERVE_PLACEHOLDER2); + + let stored_certificate = + pallet_subtensor::NeuronCertificates::::get(netuid, caller_account) + .expect("certificate should be stored"); + assert_eq!( + stored_certificate.public_key.into_inner(), + certificate[1..].to_vec() + ); + }); + } + + #[test] + fn neuron_precompile_serve_prometheus_sets_prometheus_info() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(0x7234); + let (netuid, caller_account) = setup_registered_caller(caller); + + precompiles::>() + .prepare_test( + caller, + addr_from_index(NeuronPrecompile::::INDEX), + encode_with_selector( + selector_u32("servePrometheus(uint16,uint32,uint128,uint16,uint8)"), + ( + TEST_NETUID_U16, + SERVE_VERSION, + SERVE_IP, + SERVE_PORT, + SERVE_IP_TYPE, + ), + ), + ) + .execute_returns(()); + + let prometheus = pallet_subtensor::Prometheus::::get(netuid, caller_account) + .expect("prometheus info should be stored"); + assert!(prometheus.block > 0); + assert_eq!(prometheus.version, SERVE_VERSION); + assert_eq!(prometheus.ip, SERVE_IP); + assert_eq!(prometheus.port, SERVE_PORT); + assert_eq!(prometheus.ip_type, SERVE_IP_TYPE); + }); + } } diff --git a/precompiles/src/uid_lookup.rs b/precompiles/src/uid_lookup.rs index 3089122e0f..5d87973368 100644 --- a/precompiles/src/uid_lookup.rs +++ b/precompiles/src/uid_lookup.rs @@ -57,13 +57,13 @@ mod tests { #![allow(clippy::expect_used)] use super::*; - use crate::mock::{ - Runtime, TEST_NETUID_U16, addr_from_index, new_test_ext, precompiles, selector_u32, - }; + use crate::mock::{Runtime, addr_from_index, new_test_ext, precompiles, selector_u32}; use precompile_utils::solidity::{codec::Address, encode_return_value, encode_with_selector}; use precompile_utils::testing::PrecompileTesterExt; use subtensor_runtime_common::NetUid; + const TEST_NETUID_U16: u16 = 1; + #[test] fn uid_lookup_precompile_returns_associated_uid_and_block() { new_test_ext().execute_with(|| { From 71c34f54babd4ab47c855328dab3897d576ee4cd Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 21 Apr 2026 14:36:01 +0200 Subject: [PATCH 17/21] Fix zepter --- precompiles/Cargo.toml | 12 ++++++++++++ runtime/Cargo.toml | 1 + 2 files changed, 13 insertions(+) diff --git a/precompiles/Cargo.toml b/precompiles/Cargo.toml index 8fe19835af..68f617176d 100644 --- a/precompiles/Cargo.toml +++ b/precompiles/Cargo.toml @@ -55,16 +55,20 @@ std = [ "pallet-admin-utils/std", "pallet-balances/std", "pallet-crowdloan/std", + "pallet-drand/std", "pallet-evm-precompile-bn128/std", "pallet-evm-precompile-dispatch/std", "pallet-evm-precompile-modexp/std", "pallet-evm-precompile-sha3fips/std", "pallet-evm-precompile-simple/std", "pallet-evm/std", + "pallet-preimage/std", + "pallet-scheduler/std", "pallet-subtensor-proxy/std", "pallet-subtensor-swap/std", "pallet-subtensor/std", "pallet-shield/std", + "pallet-timestamp/std", "precompile-utils/std", "scale-info/std", "sp-core/std", @@ -79,9 +83,17 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "pallet-admin-utils/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", "pallet-crowdloan/runtime-benchmarks", + "pallet-drand/runtime-benchmarks", + "pallet-evm/runtime-benchmarks", + "pallet-preimage/runtime-benchmarks", + "pallet-scheduler/runtime-benchmarks", + "pallet-shield/runtime-benchmarks", "pallet-subtensor-swap/runtime-benchmarks", + "pallet-subtensor-proxy/runtime-benchmarks", "pallet-subtensor/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "subtensor-runtime-common/runtime-benchmarks", ] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 0c6a21acb6..e163661a8d 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -321,6 +321,7 @@ runtime-benchmarks = [ "pallet-drand/runtime-benchmarks", "pallet-transaction-payment/runtime-benchmarks", "pallet-subtensor-swap/runtime-benchmarks", + "subtensor-precompiles/runtime-benchmarks", # Smart Tx fees pallet "subtensor-transaction-fee/runtime-benchmarks", From d7709e79b48628c6bb3c55778e8f5f252a7662b3 Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 21 Apr 2026 14:36:40 +0200 Subject: [PATCH 18/21] Bump spec version --- runtime/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index ed6d4d6176..d2e2bd10ed 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -272,9 +272,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: 397, - impl_version: 1, - apis: RUNTIME_API_VERSIONS, + spec_version: 398, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, system_version: 1, }; From 43d976a46b7fe0bf8db2e9fcd7b9eb364dad552d Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 21 Apr 2026 15:33:05 +0200 Subject: [PATCH 19/21] Reformat --- runtime/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index d2e2bd10ed..a2911cbe5e 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -272,7 +272,9 @@ 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: 398, impl_version: 1, apis: RUNTIME_API_VERSIONS, + spec_version: 398, + impl_version: 1, + apis: RUNTIME_API_VERSIONS, transaction_version: 1, system_version: 1, }; From e3b037788536f6c1a25b3a22078e6c16d164449d Mon Sep 17 00:00:00 2001 From: Aliaksandr Tsurko Date: Tue, 21 Apr 2026 18:41:21 +0200 Subject: [PATCH 20/21] Move precompile addresses tests back to integration --- precompiles/src/lib.rs | 42 ------------------------------------ runtime/tests/precompiles.rs | 8 +++++++ 2 files changed, 8 insertions(+), 42 deletions(-) diff --git a/precompiles/src/lib.rs b/precompiles/src/lib.rs index 6b8b34b89c..39815a6946 100644 --- a/precompiles/src/lib.rs +++ b/precompiles/src/lib.rs @@ -313,45 +313,3 @@ fn parse_slice(data: &[u8], from: usize, to: usize) -> Result<&[u8], PrecompileF }) } } - -#[cfg(test)] -mod test { - use super::*; - use core::iter::IntoIterator; - use std::collections::BTreeSet; - - #[test] - fn precompile_registry_addresses_are_unique() { - let addresses = [ - hash(1), - hash(2), - hash(3), - hash(4), - hash(5), - hash(6), - hash(7), - hash(8), - hash(9), - hash(1024), - hash(1025), - hash(1026), - hash(1027), - hash(2048), - hash(2053), - hash(2051), - hash(2050), - hash(2052), - hash(2049), - hash(2055), - hash(2054), - hash(2056), - hash(2057), - hash(2058), - hash(2061), - hash(2059), - hash(2060), - ]; - let unique: BTreeSet<_> = IntoIterator::into_iter(addresses).collect(); - assert_eq!(unique.len(), addresses.len()); - } -} diff --git a/runtime/tests/precompiles.rs b/runtime/tests/precompiles.rs index cbb8a06a22..519e434533 100644 --- a/runtime/tests/precompiles.rs +++ b/runtime/tests/precompiles.rs @@ -8,6 +8,7 @@ use precompile_utils::solidity::encode_with_selector; use precompile_utils::testing::MockHandle; use sp_core::{H160, H256, U256}; use sp_runtime::traits::Hash; +use std::collections::BTreeSet; use subtensor_precompiles::{BalanceTransferPrecompile, PrecompileExt, Precompiles}; type AccountId = ::AccountId; @@ -55,6 +56,13 @@ fn selector_u32(signature: &str) -> u32 { u32::from_be_bytes([hash[0], hash[1], hash[2], hash[3]]) } +#[test] +fn precompile_registry_addresses_are_unique() { + let addresses = Precompiles::::used_addresses(); + let unique: BTreeSet<_> = addresses.into_iter().collect(); + assert_eq!(unique.len(), addresses.len()); +} + #[test] fn balance_transfer_precompile_transfers_balance() { new_test_ext().execute_with(|| { From 03a46164acd799c2d49e41a52c00c82616f0e92d Mon Sep 17 00:00:00 2001 From: boskodev790 Date: Wed, 22 Apr 2026 20:48:45 +0200 Subject: [PATCH 21/21] fix(leasing): floor per-contributor dividend share so beneficiary gets the leftover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit distribute_leased_network_dividends in pallets/subtensor/src/subnets/leasing.rs rounded each contributor's share UP with `.ceil()` before accumulating into `alpha_distributed`. Summed across contributors, the ceilings can accumulate to MORE than `total_contributors_cut_alpha`, at which point `total_contributors_cut_alpha.saturating_sub(alpha_distributed)` on line ~327 clamps to zero — the beneficiary receives nothing, even when they were legitimately entitled to the leftover dust between the contributors' cut and the beneficiary's implicit share (`raised - deposit`). Concrete example: deposit = 1 TAO, raised = 1_000 TAO (999 TAO from many small contributors). Beneficiary's intended share per dividend event is ~`deposit / raised = 0.1 %` of `total_contributors_cut_alpha`. With enough contributors each rounding UP by ~1 rao, the cumulative rounding can exceed the beneficiary's intended slice, zeroing their payout. In the degenerate case where `alpha_distributed` exceeds the lease coldkey's actual stake on the subnet (the sum of previously-accumulated dividends plus the current owner cut), `transfer_stake_within_subnet` will fail mid-loop and the whole `with_storage_layer` block rolls back — those dividends then re-accumulate silently and the pattern repeats on the next distribution, breaking the invariant that dividends are actually distributed at each `LeaseDividendsDistributionInterval` boundary. Switch the per-contributor rounding to `.floor()` so the sum is always `<= total_contributors_cut_alpha`, letting the beneficiary's `saturating_sub` on line ~327 always produce the true leftover. The top-level `lease.emissions_share.mul_ceil(owner_cut_alpha.to_u64())` higher up (line ~273) still rounds UP in favour of the contributors, which is the deliberate intent called out by the comment there. Update the matching `.ceil()` calls in `tests/leasing.rs::test_distribute_lease_network_dividends_multiple_contributors_works` so the expected per-contributor alpha is computed with the same rounding as production; the `distributed_alpha == bene + c1 + c2` assertion now holds unconditionally rather than only for fractional shares that happen to round cleanly in U64F64. --- pallets/subtensor/src/subnets/leasing.rs | 12 ++++++++++-- pallets/subtensor/src/tests/leasing.rs | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pallets/subtensor/src/subnets/leasing.rs b/pallets/subtensor/src/subnets/leasing.rs index cc1094d227..4ff7888f8f 100644 --- a/pallets/subtensor/src/subnets/leasing.rs +++ b/pallets/subtensor/src/subnets/leasing.rs @@ -298,11 +298,19 @@ impl Pallet { let mut alpha_distributed = AlphaBalance::ZERO; // Distribute the contributors cut to the contributors and accumulate the alpha - // distributed so far to obtain how much alpha is left to distribute to the beneficiary + // distributed so far to obtain how much alpha is left to distribute to the beneficiary. + // + // Use `floor` per contributor so that the sum of all per-contributor shares is + // guaranteed to be less than or equal to `total_contributors_cut_alpha`. Rounding + // up per contributor (as this code previously did) would accumulate over multiple + // contributors to more than the total, causing either an over-transfer from the + // lease coldkey stake or the beneficiary's leftover calculation on line ~327 to + // saturate to zero (so the beneficiary would receive nothing even when there was + // legitimately some leftover dust to send to them). for (contributor, share) in SubnetLeaseShares::::iter_prefix(lease_id) { let alpha_for_contributor = share .saturating_mul(U64F64::from(total_contributors_cut_alpha.to_u64())) - .ceil() + .floor() .saturating_to_num::(); Self::transfer_stake_within_subnet( diff --git a/pallets/subtensor/src/tests/leasing.rs b/pallets/subtensor/src/tests/leasing.rs index 0c6ae629c3..c5336e96df 100644 --- a/pallets/subtensor/src/tests/leasing.rs +++ b/pallets/subtensor/src/tests/leasing.rs @@ -571,7 +571,7 @@ fn test_distribute_lease_network_dividends_multiple_contributors_works() { let expected_contributor1_alpha = SubnetLeaseShares::::get(lease_id, contributions[0].0) .saturating_mul(U64F64::from(distributed_alpha.to_u64())) - .ceil() + .floor() .to_num::(); assert_eq!(contributor1_alpha_delta, expected_contributor1_alpha.into()); assert_eq!( @@ -586,7 +586,7 @@ fn test_distribute_lease_network_dividends_multiple_contributors_works() { let expected_contributor2_alpha = SubnetLeaseShares::::get(lease_id, contributions[1].0) .saturating_mul(U64F64::from(distributed_alpha.to_u64())) - .ceil() + .floor() .to_num::(); assert_eq!(contributor2_alpha_delta, expected_contributor2_alpha.into()); assert_eq!(