diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e75d83f4c..419a02a292 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 9.11.0 /2025-09-25 + +## What's Changed +* Fix broken links in CONTRIBUTING.md by @Gimzou in https://github.com/opentensor/bittensor/pull/3041 +* Fix after `Rate limit hyperparams setting` by @basfroman in https://github.com/opentensor/bittensor/pull/3052 +* Fix SubtensorApi unit tests by @basfroman in https://github.com/opentensor/bittensor/pull/3054 +* add commitments data into metagraph by @basfroman in https://github.com/opentensor/bittensor/pull/3055 +* add getting OwnerHyperparamRateLimit in e2e test by @basfroman in https://github.com/opentensor/bittensor/pull/3059 +* update make file by @basfroman in https://github.com/opentensor/bittensor/pull/3058 +* `Subnet Mechanism` logic by @basfroman in https://github.com/opentensor/bittensor/pull/3056 +* let do less calculations on `get_subnet_price` by @basfroman in https://github.com/opentensor/bittensor/pull/3063 +* deps: update munch to prevent deprecation warning by @Arthurdw in https://github.com/opentensor/bittensor/pull/3061 + +## New Contributors +* @Gimzou made their first contribution in https://github.com/opentensor/bittensor/pull/3041 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.10.1...v9.11.0 + ## 9.10.1 /2025-09-05 ## What's Changed diff --git a/Makefile b/Makefile index d68152d42d..c4a49e8d1d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ SHELL := /bin/bash -.PHONY: init-venv clean-venv clean install install-dev reinstall reinstall-dev +.PHONY: init-venv clean-venv clean install install-dev reinstall reinstall-dev ruff check init-venv: python3 -m venv venv && source ./venv/bin/activate @@ -11,7 +11,8 @@ clean-venv: rm make_venv_to_uninstall.txt clean: - rm -rf dist/ build/ bittensor.egg-info/ .pytest_cache/ lib/ + @rm -rf dist/ build/ bittensor.egg-info/ .pytest_cache/ lib/ .mypy_cache .ruff_cache \ + $(shell find . -type d \( -name ".hypothesis" -o -name "__pycache__" \)) install: init-venv source ./venv/bin/activate && \ @@ -24,3 +25,14 @@ install-dev: init-venv reinstall: clean clean-venv install reinstall-dev: clean clean-venv install-dev + +ruff: + @python -m ruff format bittensor + +check: ruff + @mypy --ignore-missing-imports bittensor/ --python-version=3.9 + @mypy --ignore-missing-imports bittensor/ --python-version=3.10 + @mypy --ignore-missing-imports bittensor/ --python-version=3.11 + @mypy --ignore-missing-imports bittensor/ --python-version=3.12 + @mypy --ignore-missing-imports bittensor/ --python-version=3.13 + @flake8 bittensor/ --count diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index ea7ec359bb..ea7e43eb4a 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -6,14 +6,12 @@ from typing import cast, Optional, Any, Union, Iterable, TYPE_CHECKING import asyncstdlib as a -import numpy as np import scalecodec from async_substrate_interface import AsyncSubstrateInterface from async_substrate_interface.substrate_addons import RetryAsyncSubstrate from async_substrate_interface.utils.storage import StorageKey from bittensor_drand import get_encrypted_commitment from bittensor_wallet.utils import SS58_FORMAT -from numpy.typing import NDArray from scalecodec import GenericCall from bittensor.core.chain_data import ( @@ -45,7 +43,12 @@ root_set_pending_childkey_cooldown_extrinsic, set_children_extrinsic, ) -from bittensor.core.extrinsics.asyncex.commit_reveal import commit_reveal_v3_extrinsic +from bittensor.core.extrinsics.asyncex.liquidity import ( + add_liquidity_extrinsic, + modify_liquidity_extrinsic, + remove_liquidity_extrinsic, + toggle_user_liquidity_extrinsic, +) from bittensor.core.extrinsics.asyncex.move_stake import ( transfer_stake_extrinsic, swap_stake_extrinsic, @@ -72,6 +75,12 @@ add_stake_multiple_extrinsic, ) from bittensor.core.extrinsics.asyncex.start_call import start_call_extrinsic +from bittensor.core.extrinsics.asyncex.mechanism import ( + commit_mechanism_weights_extrinsic, + commit_timelocked_mechanism_weights_extrinsic, + reveal_mechanism_weights_extrinsic, + set_mechanism_weights_extrinsic, +) from bittensor.core.extrinsics.asyncex.take import ( decrease_take_extrinsic, increase_take_extrinsic, @@ -82,36 +91,31 @@ unstake_extrinsic, unstake_multiple_extrinsic, ) -from bittensor.core.extrinsics.asyncex.weights import ( - commit_weights_extrinsic, - set_weights_extrinsic, - reveal_weights_extrinsic, -) from bittensor.core.metagraph import AsyncMetagraph from bittensor.core.settings import version_as_int, TYPE_REGISTRY -from bittensor.core.types import ParamWithTypes, SubtensorMixin +from bittensor.core.types import ( + ParamWithTypes, + Salt, + SubtensorMixin, + UIDs, + Weights, +) from bittensor.utils import ( Certificate, decode_hex_identity_dict, format_error_message, is_valid_ss58_address, - torch, u16_normalized_float, u64_normalized_float, get_transfer_fn_params, + get_mechid_storage_index, ) -from bittensor.core.extrinsics.asyncex.liquidity import ( - add_liquidity_extrinsic, - modify_liquidity_extrinsic, - remove_liquidity_extrinsic, - toggle_user_liquidity_extrinsic, -) +from bittensor.utils import deprecated_message from bittensor.utils.balance import ( Balance, fixed_to_float, check_and_convert_to_balance, ) -from bittensor.utils import deprecated_message from bittensor.utils.btlogging import logging from bittensor.utils.liquidity import ( calculate_fees, @@ -121,7 +125,6 @@ LiquidityPosition, ) from bittensor.utils.weight_utils import ( - generate_weight_hash, convert_uids_and_weights, U16_MAX, ) @@ -897,6 +900,7 @@ async def bonds( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, + mechid: int = 0, ) -> list[tuple[int, list[tuple[int, int]]]]: """Retrieves the bond distribution set by subnet validators within a specific subnet. @@ -904,11 +908,12 @@ async def bonds( bonding mechanism is integral to the Yuma Consensus' design intent of incentivizing high-quality performance by subnet miners, and honest evaluation by subnet validators. - Arguments: - netuid: The unique identifier of the subnet. + Parameters: + netuid: Subnet identifier. block: The block number for this query. Do not specify if using block_hash or reuse_block. block_hash: The hash of the block for the query. Do not specify if using reuse_block or block. reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + mechid: Subnet mechanism identifier. Returns: List of tuples mapping each neuron's UID to its bonds with other neurons. @@ -921,11 +926,12 @@ async def bonds( - See - See """ + storage_index = get_mechid_storage_index(netuid, mechid) block_hash = await self.determine_block_hash(block, block_hash, reuse_block) b_map_encoded = await self.substrate.query_map( module="SubtensorModule", storage_function="Bonds", - params=[netuid], + params=[storage_index], block_hash=block_hash, reuse_block_hash=reuse_block, ) @@ -1104,6 +1110,33 @@ async def does_hotkey_exist( ) return return_val + async def get_admin_freeze_window( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> int: + """ + Returns the number of blocks when dependent transactions will be frozen for execution. + + Arguments: + block: The block number at which to retrieve the hyperparameter. Do not specify if using block_hash or + reuse_block. + block_hash: The hash of the blockchain block for the query. Do not specify if using block or reuse_block. + reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + + Returns: + AdminFreezeWindow as integer. The number of blocks are frozen. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + return ( + await self.substrate.query( + module="SubtensorModule", + storage_function="AdminFreezeWindow", + block_hash=block_hash, + ) + ).value + async def get_all_subnets_info( self, block: Optional[int] = None, @@ -1511,7 +1544,8 @@ async def get_commitment( ) try: return decode_metadata(metadata) - except TypeError: + except Exception as error: + logging.error(error) return "" async def get_last_commitment_bonds_reset_block( @@ -1582,7 +1616,12 @@ async def get_all_commitments( ) result = {} async for id_, value in query: - result[decode_account_id(id_[0])] = decode_metadata(value.value) + try: + result[decode_account_id(id_[0])] = decode_metadata(value) + except Exception as error: + logging.error( + f"Error decoding [red]{id_}[/red] and [red]{value}[/red]: {error}" + ) return result async def get_revealed_commitment_by_hotkey( @@ -1694,6 +1733,7 @@ async def get_all_revealed_commitments( result[hotkey_ss58_address] = commitment_message return result + # TODO: deprecated in SDKv10 async def get_current_weight_commit_info( self, netuid: int, @@ -1734,6 +1774,7 @@ async def get_current_weight_commit_info( commits = result.records[0][1] if result.records else [] return [WeightCommitInfo.from_vec_u8(commit) for commit in commits] + # TODO: deprecated in SDKv10 async def get_current_weight_commit_info_v2( self, netuid: int, @@ -2032,6 +2073,7 @@ async def get_minimum_required_stake(self): return Balance.from_rao(getattr(result, "value", 0)) + # TODO: update parameters order in SDKv10, rename `field_indices` to `selected_indices` async def get_metagraph_info( self, netuid: int, @@ -2039,6 +2081,7 @@ async def get_metagraph_info( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, + mechid: int = 0, ) -> Optional[MetagraphInfo]: """ Retrieves full or partial metagraph information for the specified subnet (netuid). @@ -2049,26 +2092,36 @@ async def get_metagraph_info( Arguments: netuid: The unique identifier of the subnet to query. - field_indices: An optional list of SelectiveMetagraphIndex or int values specifying which fields to - retrieve. If not provided, all available fields will be returned. - block: the block number at which to retrieve the hyperparameter. Do not specify if using block_hash or - reuse_block - block_hash: The hash of blockchain block number for the query. Do not specify if using - block or reuse_block - reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + field_indices: Optional list of SelectiveMetagraphIndex or int values specifying which fields to retrieve. + If not provided, all available fields will be returned. + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number at which to perform the query. + reuse_block: Whether to reuse the last-used block hash when retrieving info. + mechid: Subnet mechanism unique identifier. Returns: - Optional[MetagraphInfo]: A MetagraphInfo object containing the requested subnet data, or None if the subnet - with the given netuid does not exist. + MetagraphInfo object with the requested subnet mechanism data, None if the subnet mechanism does not exist. Example: - meta_info = await subtensor.get_metagraph_info(netuid=2) + # Retrieve all fields from the metagraph from subnet 2 mechanism 0 + meta_info = subtensor.get_metagraph_info(netuid=2) + + # Retrieve all fields from the metagraph from subnet 2 mechanism 1 + meta_info = subtensor.get_metagraph_info(netuid=2, mechid=1) - partial_meta_info = await subtensor.get_metagraph_info( + # Retrieve selective data from the metagraph from subnet 2 mechanism 0 + partial_meta_info = subtensor.get_metagraph_info( netuid=2, field_indices=[SelectiveMetagraphIndex.Name, SelectiveMetagraphIndex.OwnerHotkeys] ) + # Retrieve selective data from the metagraph from subnet 2 mechanism 1 + partial_meta_info = subtensor.get_metagraph_info( + netuid=2, + mechid=1, + field_indices=[SelectiveMetagraphIndex.Name, SelectiveMetagraphIndex.OwnerHotkeys] + ) + Notes: See also: - @@ -2078,57 +2131,48 @@ async def get_metagraph_info( if not block_hash and reuse_block: block_hash = self.substrate.last_block_hash - if field_indices: - if isinstance(field_indices, list) and all( - isinstance(f, (SelectiveMetagraphIndex, int)) for f in field_indices - ): - indexes = [ - f.value if isinstance(f, SelectiveMetagraphIndex) else f - for f in field_indices - ] - else: - raise ValueError( - "`field_indices` must be a list of SelectiveMetagraphIndex enums or ints." - ) + indexes = ( + [ + f.value if isinstance(f, SelectiveMetagraphIndex) else f + for f in field_indices + ] + if field_indices is not None + else [f for f in range(len(SelectiveMetagraphIndex))] + ) - query = await self.substrate.runtime_call( - "SubnetInfoRuntimeApi", - "get_selective_metagraph", - params=[netuid, indexes if 0 in indexes else [0] + indexes], - block_hash=block_hash, - ) - else: - query = await self.substrate.runtime_call( - "SubnetInfoRuntimeApi", - "get_metagraph", - params=[netuid], - block_hash=block_hash, + query = await self.substrate.runtime_call( + api="SubnetInfoRuntimeApi", + method="get_selective_mechagraph", + params=[netuid, mechid, indexes if 0 in indexes else [0] + indexes], + block_hash=block_hash, + ) + if getattr(query, "value", None) is None: + logging.error( + f"Subnet mechanism {netuid}.{mechid if mechid else 0} does not exist." ) - - if query.value is None: - logging.error(f"Subnet {netuid} does not exist.") return None return MetagraphInfo.from_dict(query.value) + # TODO: update parameters order in SDKv10 async def get_all_metagraphs_info( self, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list[MetagraphInfo]: + all_mechanisms: bool = False, + ) -> Optional[list[MetagraphInfo]]: """ Retrieves a list of MetagraphInfo objects for all subnets - Arguments: - block: the block number at which to retrieve the hyperparameter. Do not specify if using block_hash or - reuse_block - block_hash: The hash of blockchain block number for the query. Do not specify if using - block or reuse_block - reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + Parameters: + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number at which to perform the query. + reuse_block: Whether to reuse the last-used block hash when retrieving info. + all_mechanisms: If True then returns all mechanisms, otherwise only those with index 0 for all subnets. Returns: - MetagraphInfo dataclass + List of MetagraphInfo objects for all existing subnets. Notes: See also: See @@ -2136,12 +2180,16 @@ async def get_all_metagraphs_info( block_hash = await self.determine_block_hash(block, block_hash, reuse_block) if not block_hash and reuse_block: block_hash = self.substrate.last_block_hash + method = "get_all_mechagraphs" if all_mechanisms else "get_all_metagraphs" query = await self.substrate.runtime_call( - "SubnetInfoRuntimeApi", - "get_all_metagraphs", + api="SubnetInfoRuntimeApi", + method=method, block_hash=block_hash, ) - return MetagraphInfo.list_from_dicts(query.decode()) + if query is None or not hasattr(query, "value"): + return None + + return MetagraphInfo.list_from_dicts(query.value) async def get_netuids_for_hotkey( self, @@ -2637,6 +2685,64 @@ async def get_stake_add_fee( amount=amount, netuid=netuid, block=block ) + async def get_mechanism_emission_split( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[list[int]]: + """Returns the emission percentages allocated to each subnet mechanism. + + Parameters: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + block_hash: The hash of the block to retrieve the stake from. Do not specify if using block or reuse_block. + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + A list of integers representing the percentage of emission allocated to each subnet mechanism (rounded to + whole numbers). Returns None if emission is evenly split or if the data is unavailable. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query( + module="SubtensorModule", + storage_function="MechanismEmissionSplit", + params=[netuid], + block_hash=block_hash, + ) + if result is None or not hasattr(result, "value"): + return None + + return [round(i / sum(result.value) * 100) for i in result.value] + + async def get_mechanism_count( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> int: + """Retrieves the number of mechanisms for the given subnet. + + Parameters: + netuid: Subnet identifier. + block: The blockchain block number for the query. + block_hash: The hash of the block to retrieve the stake from. Do not specify if using block or reuse_block. + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + The number of mechanisms for the given subnet. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + query = await self.substrate.query( + module="SubtensorModule", + storage_function="MechanismCountCurrent", + params=[netuid], + block_hash=block_hash, + ) + return getattr(query, "value", 1) + async def get_subnet_info( self, netuid: int, @@ -2682,11 +2788,10 @@ async def get_subnet_price( ) -> Balance: """Gets the current Alpha price in TAO for all subnets. - Arguments: + Parameters: netuid: The unique identifier of the subnet. block: The blockchain block number for the query. - block_hash: The hash of the block to retrieve the stake from. Do not specify if using block - or reuse_block + block_hash: The hash of the block to retrieve the stake from. Do not specify if using block or reuse_block. reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. Returns: @@ -2696,17 +2801,15 @@ async def get_subnet_price( if netuid == 0: return Balance.from_tao(1) - block_hash = await self.determine_block_hash(block=block) - current_sqrt_price = await self.substrate.query( - module="Swap", - storage_function="AlphaSqrtPrice", + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.substrate.runtime_call( + api="SwapRuntimeApi", + method="current_alpha_price", params=[netuid], block_hash=block_hash, ) - - current_sqrt_price = fixed_to_float(current_sqrt_price) - current_price = current_sqrt_price * current_sqrt_price - return Balance.from_rao(int(current_price * 1e9)) + price_rao = call.value + return Balance.from_rao(price_rao) async def get_subnet_prices( self, @@ -2749,22 +2852,24 @@ async def get_subnet_prices( prices.update({0: Balance.from_tao(1)}) return prices + # TODO: update order in SDKv10 async def get_timelocked_weight_commits( self, netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, + mechid: int = 0, ) -> list[tuple[str, int, str, int]]: """ Retrieves CRv4 weight commit information for a specific subnet. - Arguments: - netuid (int): The unique identifier of the subnet. + Parameters: + netuid: Subnet identifier. block (Optional[int]): The blockchain block number for the query. Default is ``None``. - block_hash: The hash of the block to retrieve the stake from. Do not specify if using block - or reuse_block + block_hash: The hash of the block to retrieve the stake from. Do not specify if using block or reuse_block. reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + mechid: Subnet mechanism identifier. Returns: A list of commit details, where each item contains: @@ -2775,13 +2880,14 @@ async def get_timelocked_weight_commits( The list may be empty if there are no commits found. """ + storage_index = get_mechid_storage_index(netuid, mechid) block_hash = await self.determine_block_hash( block=block, block_hash=block_hash, reuse_block=reuse_block ) result = await self.substrate.query_map( module="SubtensorModule", storage_function="TimelockedWeightCommits", - params=[netuid], + params=[storage_index], block_hash=block_hash, ) @@ -2850,7 +2956,7 @@ async def get_stake_for_coldkey_and_hotkey( self, coldkey_ss58: str, hotkey_ss58: str, - netuids: Optional[list[int]] = None, + netuids: Optional[UIDs] = None, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, @@ -2926,7 +3032,7 @@ async def get_stake_for_coldkey( if result is None: return [] - stakes = StakeInfo.list_from_dicts(result) # type: ignore + stakes: list[StakeInfo] = StakeInfo.list_from_dicts(result) return [stake for stake in stakes if stake.stake > 0] get_stake_info_for_coldkey = get_stake_for_coldkey @@ -3382,6 +3488,48 @@ async def immunity_period( ) return None if call is None else int(call) + async def is_in_admin_freeze_window( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """ + Returns True if the current block is within the terminal freeze window of the tempo + for the given subnet. During this window, admin ops are prohibited to avoid interference + with validator weight submissions. + + Parameters: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + block_hash: The blockchain block_hash representation of the block id. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + bool: True if in freeze window, else False. + """ + # SN0 doesn't have admin_freeze_window + if netuid == 0: + return False + + next_epoch_start_block, window = await asyncio.gather( + self.get_next_epoch_start_block( + netuid=netuid, + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ), + self.get_admin_freeze_window( + block=block, block_hash=block_hash, reuse_block=reuse_block + ), + ) + + if next_epoch_start_block is not None: + remaining = next_epoch_start_block - await self.block + return remaining < window + return False + async def is_fast_blocks(self): """Returns True if the node is running with fast blocks. False if not.""" return ( @@ -3573,22 +3721,26 @@ async def max_weight_limit( ) return None if call is None else u16_normalized_float(int(call)) + # TODO: update parameters order in SDKv10 async def metagraph( - self, netuid: int, lite: bool = True, block: Optional[int] = None + self, + netuid: int, + lite: bool = True, + block: Optional[int] = None, + mechid: int = 0, ) -> "AsyncMetagraph": """ - Returns a synced metagraph for a specified subnet within the Bittensor network. The metagraph represents the - network's structure, including neuron connections and interactions. + Returns a synced metagraph for a specified subnet within the Bittensor network. + The metagraph represents the network's structure, including neuron connections and interactions. - Arguments: + Parameters: netuid: The network UID of the subnet to query. - lite: If true, returns a metagraph using a lightweight sync (no weights, no bonds). Default is - ``True``. + lite: If true, returns a metagraph using a lightweight sync (no weights, no bonds). block: Block number for synchronization, or `None` for the latest block. + mechid: Subnet mechanism identifier. Returns: - bittensor.core.metagraph.Metagraph: The metagraph representing the subnet's structure and neuron - relationships. + The metagraph representing the subnet's structure and neuron relationships. The metagraph is an essential tool for understanding the topology and dynamics of the Bittensor network's decentralized architecture, particularly in relation to neuron interconnectivity and consensus processes. @@ -3599,6 +3751,7 @@ async def metagraph( lite=lite, sync=False, subtensor=self, + mechid=mechid, ) await metagraph.sync(block=block, lite=lite, subtensor=self) @@ -4085,12 +4238,14 @@ async def handler(block_data: dict): ) return True + # TODO: update order in SDKv10 async def weights( self, netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, + mechid: int = 0, ) -> list[tuple[int, list[tuple[int, int]]]]: """ Retrieves the weight distribution set by neurons within a specific subnet of the Bittensor network. @@ -4102,6 +4257,7 @@ async def weights( block: Block number for synchronization, or `None` for the latest block. block_hash: The hash of the blockchain block for the query. reuse_block: reuse the last-used blockchain block hash. + mechid: Subnet mechanism identifier. Returns: A list of tuples mapping each neuron's UID to its assigned weights. @@ -4109,12 +4265,13 @@ async def weights( The weight distribution is a key factor in the network's consensus algorithm and the ranking of neurons, influencing their influence and reward allocation within the subnet. """ + storage_index = get_mechid_storage_index(netuid, mechid) block_hash = await self.determine_block_hash(block, block_hash, reuse_block) # TODO look into seeing if we can speed this up with storage query w_map_encoded = await self.substrate.query_map( module="SubtensorModule", storage_function="Weights", - params=[netuid], + params=[storage_index], block_hash=block_hash, reuse_block_hash=reuse_block, ) @@ -4420,7 +4577,7 @@ async def add_stake_multiple( self, wallet: "Wallet", hotkey_ss58s: list[str], - netuids: list[int], + netuids: UIDs, amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -4501,14 +4658,15 @@ async def commit_weights( self, wallet: "Wallet", netuid: int, - salt: list[int], - uids: Union[NDArray[np.int64], list], - weights: Union[NDArray[np.int64], list], + salt: Salt, + uids: UIDs, + weights: Weights, version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, max_retries: int = 5, period: Optional[int] = 16, + mechid: int = 0, ) -> tuple[bool, str]: """ Commits a hash of the subnet validator's weight vector to the Bittensor blockchain using the provided wallet. @@ -4528,6 +4686,7 @@ async def commit_weights( period: The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. + mechid: The subnet mechanism unique identifier. Returns: tuple[bool, str]: @@ -4550,23 +4709,16 @@ async def commit_weights( f"version_key=[blue]{version_key}[/blue]" ) - # Generate the hash of the weights - commit_hash = generate_weight_hash( - address=wallet.hotkey.ss58_address, - netuid=netuid, - uids=list(uids), - values=list(weights), - salt=salt, - version_key=version_key, - ) - while retries < max_retries and success is False: try: - success, message = await commit_weights_extrinsic( + success, message = await commit_mechanism_weights_extrinsic( subtensor=self, wallet=wallet, netuid=netuid, - commit_hash=commit_hash, + mechid=mechid, + uids=uids, + weights=weights, + salt=salt, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, period=period, @@ -4575,7 +4727,7 @@ async def commit_weights( break except Exception as e: logging.error(f"Error committing weights: {e}") - retries += 1 + retries += 1 return success, message @@ -4842,41 +4994,42 @@ async def reveal_weights( self, wallet: "Wallet", netuid: int, - uids: Union[NDArray[np.int64], list], - weights: Union[NDArray[np.int64], list], - salt: Union[NDArray[np.int64], list], + uids: UIDs, + weights: Weights, + salt: Salt, version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, max_retries: int = 5, period: Optional[int] = None, + mechid: int = 0, ) -> tuple[bool, str]: """ - Reveals the weight vector for a specific subnet on the Bittensor blockchain using the provided wallet. - This action serves as a revelation of the subnet validator's previously committed weight distribution as part - of the commit-reveal mechanism. + Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. + This action serves as a revelation of the neuron's previously committed weight distribution. - Arguments: - wallet: The wallet associated with the subnet validator revealing the weights. - netuid: unique identifier of the subnet. - uids: NumPy array of subnet miner neuron UIDs for which weights are being revealed. + Parameters: + wallet: Bittensor Wallet instance. + netuid: The unique identifier of the subnet. + uids: NumPy array of neuron UIDs for which weights are being revealed. weights: NumPy array of weight values corresponding to each UID. - salt: NumPy array of salt values - version_key: Version key for compatibility with the network. Default is `int representation of - the Bittensor version`. - wait_for_inclusion: Waits for the transaction to be included in a block. Default is `False`. - wait_for_finalization: Waits for the transaction to be finalized on the blockchain. Default is - `False`. - max_retries: The number of maximum attempts to reveal weights. Default is `5`. - period: The number of blocks during which the transaction will remain valid after it's - submitted. If the transaction is not included in a block within that number of blocks, it will expire - and be rejected. You can think of it as an expiration date for the transaction. + salt: NumPy array of salt values corresponding to the hash function. + version_key: Version key for compatibility with the network. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + max_retries: The number of maximum attempts to reveal weights. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + mechid: The subnet mechanism unique identifier. Returns: - tuple[bool, str]: `True` if the weight revelation is successful, False otherwise. And `msg`, a string - value describing the success or potential error. + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. - This function allows subnet validators to reveal their previously committed weight vector. + This function allows neurons to reveal their previously committed weight distribution, ensuring transparency and + accountability within the Bittensor network. See also: , """ @@ -4886,13 +5039,14 @@ async def reveal_weights( while retries < max_retries and success is False: try: - success, message = await reveal_weights_extrinsic( + success, message = await reveal_mechanism_weights_extrinsic( subtensor=self, wallet=wallet, netuid=netuid, - uids=list(uids), - weights=list(weights), - salt=list(salt), + mechid=mechid, + uids=uids, + weights=weights, + salt=salt, version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, @@ -4902,7 +5056,7 @@ async def reveal_weights( break except Exception as e: logging.error(f"Error revealing weights: {e}") - retries += 1 + retries += 1 return success, message @@ -4978,8 +5132,8 @@ async def root_register( async def root_set_weights( self, wallet: "Wallet", - netuids: list[int], - weights: list[float], + netuids: UIDs, + weights: Weights, version_key: int = 0, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, @@ -5053,7 +5207,7 @@ async def set_children( NotEnoughStakeToSetChildkeys: Parent key doesn't have minimum own stake. ProportionOverflow: The sum of the proportions does exceed uint64. RegistrationNotPermittedOnRootSubnet: Attempting to register a child on the root network. - SubNetworkDoesNotExist: Attempting to register to a non-existent network. + SubnetNotExists: Attempting to register to a non-existent network. TooManyChildren: Too many children in request. TxRateLimitExceeded: Hotkey hit the rate limit. bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. @@ -5205,14 +5359,16 @@ async def set_weights( self, wallet: "Wallet", netuid: int, - uids: Union[NDArray[np.int64], "torch.LongTensor", list], - weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + uids: UIDs, + weights: Weights, version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, max_retries: int = 5, block_time: float = 12.0, period: Optional[int] = 8, + mechid: int = 0, + commit_reveal_version: int = 4, ): """ Sets the weight vector for a neuron acting as a validator, specifying the weights assigned to subnet miners @@ -5222,26 +5378,26 @@ async def set_weights( work. These weight vectors are used by the Yuma Consensus algorithm to compute emissions for both validators and miners. - Arguments: - wallet: The wallet associated with the subnet validator setting the weights. + Parameters: + wallet: The wallet associated with the neuron setting the weights. netuid: The unique identifier of the subnet. - uids: The list of subnet miner neuron UIDs that the weights are being set for. - weights: The corresponding weights to be set for each UID, representing the validator's evaluation of each - miner's performance. - version_key: Version key for compatibility with the network. Default is int representation of - the Bittensor version. - wait_for_inclusion: Waits for the transaction to be included in a block. Default is `False`. - wait_for_finalization: Waits for the transaction to be finalized on the blockchain. Default is - `False`. - max_retries: The number of maximum attempts to set weights. Default is `5`. - block_time: The number of seconds for block duration. Default is 12.0 seconds. - period: The number of blocks during which the transaction will remain valid after it's - submitted. If the transaction is not included in a block within that number of blocks, it will expire - and be rejected. You can think of it as an expiration date for the transaction. Default is 8. + uids: The list of neuron UIDs that the weights are being set for. + weights: The corresponding weights to be set for each UID. + version_key: Version key for compatibility with the network. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + max_retries: The number of maximum attempts to set weights. + block_time: The number of seconds for block duration. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + mechid: The subnet mechanism unique identifier. + commit_reveal_version: The version of the commit-reveal in the chain. Returns: - tuple[bool, str]: `True` if the setting of weights is successful, False otherwise. And `msg`, a string - value describing the success or potential error. + tuple: + `True` if the setting of weights is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. This function is crucial in the Yuma Consensus mechanism, where each validator's weight vector contributes to the overall weight matrix used to calculate emissions and maintain network consensus. @@ -5271,7 +5427,7 @@ async def _blocks_weight_limit() -> bool: ) if await self.commit_reveal_enabled(netuid=netuid): - # go with `commit reveal v3` extrinsic + # go with `commit_timelocked_mechanism_weights_extrinsic` extrinsic while ( retries < max_retries @@ -5281,10 +5437,11 @@ async def _blocks_weight_limit() -> bool: logging.info( f"Committing weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." ) - success, message = await commit_reveal_v3_extrinsic( + success, message = await commit_timelocked_mechanism_weights_extrinsic( subtensor=self, wallet=wallet, netuid=netuid, + mechid=mechid, uids=uids, weights=weights, version_key=version_key, @@ -5292,11 +5449,12 @@ async def _blocks_weight_limit() -> bool: wait_for_finalization=wait_for_finalization, block_time=block_time, period=period, + commit_reveal_version=commit_reveal_version, ) retries += 1 return success, message else: - # go with classic `set weights extrinsic` + # go with `set_mechanism_weights_extrinsic` while ( retries < max_retries @@ -5308,10 +5466,11 @@ async def _blocks_weight_limit() -> bool: f"Setting weights for subnet #[blue]{netuid}[/blue]. " f"Attempt [blue]{retries + 1}[/blue] of [green]{max_retries}[/green]." ) - success, message = await set_weights_extrinsic( + success, message = await set_mechanism_weights_extrinsic( subtensor=self, wallet=wallet, netuid=netuid, + mechid=mechid, uids=uids, weights=weights, version_key=version_key, @@ -5736,7 +5895,7 @@ async def unstake_multiple( self, wallet: "Wallet", hotkey_ss58s: list[str], - netuids: list[int], + netuids: UIDs, amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, diff --git a/bittensor/core/chain_data/metagraph_info.py b/bittensor/core/chain_data/metagraph_info.py index cc329a84ad..69b170c1c1 100644 --- a/bittensor/core/chain_data/metagraph_info.py +++ b/bittensor/core/chain_data/metagraph_info.py @@ -1,18 +1,40 @@ -from enum import Enum - from dataclasses import dataclass +from enum import Enum from typing import Optional, Union - from bittensor.core import settings from bittensor.core.chain_data.axon_info import AxonInfo from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.info_base import InfoBase from bittensor.core.chain_data.subnet_identity import SubnetIdentity from bittensor.core.chain_data.utils import decode_account_id -from bittensor.utils import u64_normalized_float as u64tf, u16_normalized_float as u16tf +from bittensor.utils import ( + get_netuid_and_mechid_by_storage_index, + u64_normalized_float as u64tf, + u16_normalized_float as u16tf, +) from bittensor.utils.balance import Balance, fixed_to_float +SELECTIVE_METAGRAPH_COMMITMENTS_OFFSET = 14 + + +def get_selective_metagraph_commitments( + decoded: dict, +) -> Optional[tuple[tuple[str, str]]]: + """Returns a tuple of hotkeys and commitments from decoded chain data if provided, else None.""" + if commitments := decoded.get("commitments"): + result = [] + for commitment in commitments: + account_id_bytes, commitment_bytes = commitment + hotkey = decode_account_id(account_id_bytes) + commitment = bytes( + commitment_bytes[SELECTIVE_METAGRAPH_COMMITMENTS_OFFSET:] + ).decode("utf-8", errors="ignore") + result.append((hotkey, commitment)) + return tuple(result) + return None + + # to balance with unit (shortcut) def _tbwu(val: Optional[int], netuid: Optional[int] = 0) -> Optional[Balance]: """Returns a Balance object from a value and unit.""" @@ -147,13 +169,17 @@ class MetagraphInfo(InfoBase): ] # List of dividend payout in alpha via subnet. # List of validators - validators: list[str] + validators: Optional[list[str]] + + commitments: Optional[tuple[tuple[str, str]]] + + mechid: int = 0 @classmethod def _from_dict(cls, decoded: dict) -> "MetagraphInfo": """Returns a MetagraphInfo object from decoded chain data.""" # Subnet index - _netuid = decoded["netuid"] + _netuid, _mechid = get_netuid_and_mechid_by_storage_index(decoded["netuid"]) # Name and symbol if name := decoded.get("name"): @@ -177,6 +203,7 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": return cls( # Subnet index netuid=_netuid, + mechid=_mechid, # Name and symbol name=decoded["name"], symbol=decoded["symbol"], @@ -373,8 +400,9 @@ def _from_dict(cls, decoded: dict) -> "MetagraphInfo": else None ), validators=[v for v in decoded["validators"]] - if decoded.get("validators") is not None + if decoded.get("validators") else None, + commitments=get_selective_metagraph_commitments(decoded), ) @@ -508,6 +536,7 @@ class SelectiveMetagraphIndex(Enum): TaoDividendsPerHotkey = 70 AlphaDividendsPerHotkey = 71 Validators = 72 + Commitments = 73 @staticmethod def all_indices() -> list[int]: diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index b1a81ce517..2916581b1b 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -137,9 +137,9 @@ def process_stake_data(stake_data: list) -> dict: def decode_metadata(metadata: dict) -> str: commitment = metadata["info"]["fields"][0][0] - bytes_tuple_ = commitment[next(iter(commitment.keys()))] - bytes_tuple = bytes_tuple_[0] if len(bytes_tuple_) > 0 else bytes_tuple_ - return bytes(bytes_tuple).decode() + raw_bytes = next(iter(commitment.values())) + byte_tuple = raw_bytes[0] if raw_bytes else raw_bytes + return bytes(byte_tuple).decode("utf-8", errors="ignore") def decode_block(data: bytes) -> int: diff --git a/bittensor/core/errors.py b/bittensor/core/errors.py index d4b438bd2f..e52e994fcd 100644 --- a/bittensor/core/errors.py +++ b/bittensor/core/errors.py @@ -159,7 +159,7 @@ class NotDelegateError(StakeError): """ -class SubNetworkDoesNotExist(ChainTransactionError): +class SubnetNotExists(ChainTransactionError): """ The subnet does not exist. """ diff --git a/bittensor/core/extrinsics/asyncex/children.py b/bittensor/core/extrinsics/asyncex/children.py index 46853642fe..8cfca27f78 100644 --- a/bittensor/core/extrinsics/asyncex/children.py +++ b/bittensor/core/extrinsics/asyncex/children.py @@ -44,7 +44,7 @@ async def set_children_extrinsic( NotEnoughStakeToSetChildkeys: Parent key doesn't have minimum own stake. ProportionOverflow: The sum of the proportions does exceed uint64. RegistrationNotPermittedOnRootSubnet: Attempting to register a child on the root network. - SubNetworkDoesNotExist: Attempting to register to a non-existent network. + SubnetNotExists: Attempting to register to a non-existent network. TooManyChildren: Too many children in request. TxRateLimitExceeded: Hotkey hit the rate limit. bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. diff --git a/bittensor/core/extrinsics/asyncex/mechanism.py b/bittensor/core/extrinsics/asyncex/mechanism.py new file mode 100644 index 0000000000..41702fe5f2 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/mechanism.py @@ -0,0 +1,381 @@ +from typing import TYPE_CHECKING, Optional, Union + +from bittensor_drand import get_encrypted_commit + +from bittensor.core.settings import version_as_int +from bittensor.core.types import Salt, UIDs, Weights +from bittensor.utils import unlock_key, get_mechid_storage_index +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import ( + convert_and_normalize_weights_and_uids, + generate_weight_hash, +) + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def commit_mechanism_weights_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + mechid: int, + uids: UIDs, + weights: Weights, + salt: Salt, + version_key: int = version_as_int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Commits the weights for a specific sub subnet on the Bittensor blockchain using the provided wallet. + + Parameters: + subtensor: AsyncSubtensor instance. + wallet: Bittensor Wallet instance. + netuid: The subnet unique identifier. + mechid: The subnet mechanism unique identifier. + uids: NumPy array of neuron UIDs for which weights are being committed. + weights: NumPy array of weight values corresponding to each UID. + salt: list of randomly generated integers as salt to generated weighted hash. + version_key: Version key for compatibility with the network. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + """ + try: + unlock = unlock_key(wallet, raise_error=raise_error) + if not unlock.success: + logging.error(unlock.message) + return False, unlock.message + + storage_index = get_mechid_storage_index(netuid=netuid, mechid=mechid) + # Generate the hash of the weights + commit_hash = generate_weight_hash( + address=wallet.hotkey.ss58_address, + netuid=storage_index, + uids=list(uids), + values=list(weights), + salt=salt, + version_key=version_key, + ) + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "commit_hash": commit_hash, + }, + ) + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + sign_with="hotkey", + nonce_key="hotkey", + raise_error=raise_error, + ) + + if success: + logging.debug(message) + return True, message + + logging.error(message) + return False, message + + except Exception as error: + if raise_error: + raise error + logging.error(str(error)) + + return False, str(error) + + +async def commit_timelocked_mechanism_weights_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + mechid: int, + uids: UIDs, + weights: Weights, + block_time: Union[int, float], + commit_reveal_version: int = 4, + version_key: int = version_as_int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Commits the weights for a specific sub subnet on the Bittensor blockchain using the provided wallet. + + Parameters: + subtensor: AsyncSubtensor instance. + wallet: Bittensor Wallet instance. + netuid: The unique identifier of the subnet. + mechid: The subnet mechanism unique identifier. + uids: The list of neuron UIDs that the weights are being set for. + weights: The corresponding weights to be set for each UID. + block_time: The number of seconds for block duration. + commit_reveal_version: The version of the commit-reveal in the chain. + version_key: Version key for compatibility with the network. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + """ + try: + unlock = unlock_key(wallet, raise_error=raise_error) + if not unlock.success: + logging.error(unlock.message) + return False, unlock.message + + uids, weights = convert_and_normalize_weights_and_uids(uids, weights) + + current_block = await subtensor.block + subnet_hyperparameters = await subtensor.get_subnet_hyperparameters( + netuid, block=current_block + ) + tempo = subnet_hyperparameters.tempo + subnet_reveal_period_epochs = subnet_hyperparameters.commit_reveal_period + + storage_index = get_mechid_storage_index(netuid=netuid, mechid=mechid) + + # Encrypt `commit_hash` with t-lock and `get reveal_round` + commit_for_reveal, reveal_round = get_encrypted_commit( + uids=uids, + weights=weights, + version_key=version_key, + tempo=tempo, + current_block=current_block, + netuid=storage_index, + subnet_reveal_period_epochs=subnet_reveal_period_epochs, + block_time=block_time, + hotkey=wallet.hotkey.public_key, + ) + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_timelocked_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "commit": commit_for_reveal, + "reveal_round": reveal_round, + "commit_reveal_version": commit_reveal_version, + }, + ) + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + sign_with="hotkey", + nonce_key="hotkey", + raise_error=raise_error, + ) + + if success: + logging.debug(message) + return True, f"reveal_round:{reveal_round}" + + logging.error(message) + return False, message + + except Exception as error: + if raise_error: + raise error + logging.error(str(error)) + + return False, str(error) + + +async def reveal_mechanism_weights_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + mechid: int, + uids: UIDs, + weights: Weights, + salt: Salt, + version_key: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Reveals the weights for a specific sub subnet on the Bittensor blockchain using the provided wallet. + + Parameters: + subtensor: AsyncSubtensor instance. + wallet: Bittensor Wallet instance. + netuid: The unique identifier of the subnet. + mechid: The subnet mechanism unique identifier. + uids: List of neuron UIDs for which weights are being revealed. + weights: List of weight values corresponding to each UID. + salt: List of salt values corresponding to the hash function. + version_key: Version key for compatibility with the network. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + """ + try: + unlock = unlock_key(wallet, raise_error=raise_error) + if not unlock.success: + logging.error(unlock.message) + return False, unlock.message + + uids, weights = convert_and_normalize_weights_and_uids(uids, weights) + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="reveal_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "uids": uids, + "values": weights, + "salt": salt, + "version_key": version_key, + }, + ) + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + sign_with="hotkey", + nonce_key="hotkey", + raise_error=raise_error, + ) + + if success: + logging.debug(message) + return True, message + + logging.error(message) + return False, message + + except Exception as error: + if raise_error: + raise error + logging.error(str(error)) + + return False, str(error) + + +async def set_mechanism_weights_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + mechid: int, + uids: UIDs, + weights: Weights, + version_key: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Sets the passed weights in the chain for hotkeys in the sub-subnet of the passed subnet. + + Parameters: + subtensor: AsyncSubtensor instance. + wallet: Bittensor Wallet instance. + netuid: The unique identifier of the subnet. + mechid: The subnet mechanism unique identifier. + uids: List of neuron UIDs for which weights are being revealed. + weights: List of weight values corresponding to each UID. + version_key: Version key for compatibility with the network. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + """ + try: + unlock = unlock_key(wallet, raise_error=raise_error) + if not unlock.success: + logging.error(unlock.message) + return False, unlock.message + + # Convert, reformat and normalize. + uids, weights = convert_and_normalize_weights_and_uids(uids, weights) + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "dests": uids, + "weights": weights, + "version_key": version_key, + }, + ) + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + use_nonce=True, + nonce_key="hotkey", + sign_with="hotkey", + raise_error=raise_error, + ) + + if success: + logging.debug("Successfully set weights and Finalized.") + return True, message + + logging.error(message) + return False, message + + except Exception as error: + if raise_error: + raise error + logging.error(str(error)) + + return False, str(error) diff --git a/bittensor/core/extrinsics/asyncex/staking.py b/bittensor/core/extrinsics/asyncex/staking.py index 76ffe73285..dcd227868d 100644 --- a/bittensor/core/extrinsics/asyncex/staking.py +++ b/bittensor/core/extrinsics/asyncex/staking.py @@ -4,6 +4,7 @@ from async_substrate_interface.errors import SubstrateRequestException from bittensor.core.extrinsics.utils import get_old_stakes +from bittensor.core.types import UIDs from bittensor.utils import unlock_key, format_error_message from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -219,7 +220,7 @@ async def add_stake_multiple_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", hotkey_ss58s: list[str], - netuids: list[int], + netuids: UIDs, old_balance: Optional[Balance] = None, amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, diff --git a/bittensor/core/extrinsics/asyncex/sudo.py b/bittensor/core/extrinsics/asyncex/sudo.py new file mode 100644 index 0000000000..b61cb10bfc --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/sudo.py @@ -0,0 +1,148 @@ +from typing import Optional, TYPE_CHECKING + +from bittensor.core.extrinsics.asyncex.utils import sudo_call_extrinsic +from bittensor.core.types import Weights as MaybeSplit +from bittensor.utils.weight_utils import convert_maybe_split_to_u16 + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def sudo_set_admin_freeze_window_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + window: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Sets the admin freeze window length (in blocks) at the end of a tempo. + + Parameters: + subtensor: AsyncSubtensor instance. + wallet: Bittensor Wallet instance. + window: The amount of blocks to freeze in the end of a tempo. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + """ + call_function = "sudo_set_admin_freeze_window" + call_params = {"window": window} + return await sudo_call_extrinsic( + subtensor=subtensor, + wallet=wallet, + call_function=call_function, + call_params=call_params, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + +async def sudo_set_mechanism_count_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + mech_count: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Sets the number of subnet mechanisms. + + Parameters: + subtensor: Subtensor instance. + wallet: Bittensor Wallet instance. + netuid: The subnet unique identifier. + mech_count: The amount of subnet mechanism to be set. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + """ + call_function = "sudo_set_mechanism_count" + call_params = {"netuid": netuid, "mechanism_count": mech_count} + return await sudo_call_extrinsic( + subtensor=subtensor, + wallet=wallet, + call_function=call_function, + call_params=call_params, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + +async def sudo_set_mechanism_emission_split_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + maybe_split: MaybeSplit, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Sets the emission split between mechanisms in a provided subnet. + + Parameters: + subtensor: AsyncSubtensor instance. + wallet: Bittensor Wallet instance. + netuid: The subnet unique identifier. + maybe_split: List of emission weights (positive integers) for each subnet mechanism. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + + Note: + The `maybe_split` list defines the relative emission share for each subnet mechanism. + Its length must match the number of active mechanisms in the subnet or be shorter, but not equal to zero. For + example, [3, 1, 1] distributes emissions in a 3:1:1 ratio across subnet mechanisms 0, 1, and 2. Each mechanism's + emission share is calculated as: share[i] = maybe_split[i] / sum(maybe_split) + """ + call_function = "sudo_set_mechanism_emission_split" + call_params = { + "netuid": netuid, + "maybe_split": convert_maybe_split_to_u16(maybe_split), + } + return await sudo_call_extrinsic( + subtensor=subtensor, + wallet=wallet, + call_function=call_function, + call_params=call_params, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) diff --git a/bittensor/core/extrinsics/asyncex/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index fcc0416dd5..3bbc40a146 100644 --- a/bittensor/core/extrinsics/asyncex/unstaking.py +++ b/bittensor/core/extrinsics/asyncex/unstaking.py @@ -2,8 +2,10 @@ from typing import Optional, TYPE_CHECKING from async_substrate_interface.errors import SubstrateRequestException + from bittensor.core.extrinsics.asyncex.utils import get_extrinsic_fee from bittensor.core.extrinsics.utils import get_old_stakes +from bittensor.core.types import UIDs from bittensor.utils import unlock_key, format_error_message from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -275,7 +277,7 @@ async def unstake_multiple_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", hotkey_ss58s: list[str], - netuids: list[int], + netuids: UIDs, amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, diff --git a/bittensor/core/extrinsics/asyncex/utils.py b/bittensor/core/extrinsics/asyncex/utils.py index 7c756e2499..d103b38f0b 100644 --- a/bittensor/core/extrinsics/asyncex/utils.py +++ b/bittensor/core/extrinsics/asyncex/utils.py @@ -1,11 +1,14 @@ from typing import TYPE_CHECKING, Optional +from bittensor.utils import unlock_key from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging if TYPE_CHECKING: from scalecodec import GenericCall from bittensor_wallet import Keypair from bittensor.core.async_subtensor import AsyncSubtensor + from bittensor_wallet import Wallet async def get_extrinsic_fee( @@ -32,3 +35,75 @@ async def get_extrinsic_fee( return Balance.from_rao(amount=payment_info["partial_fee"]).set_unit( netuid=netuid or 0 ) + + +async def sudo_call_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + call_function: str, + call_params: dict, + call_module: str = "AdminUtils", + sign_with: str = "coldkey", + use_nonce: bool = False, + nonce_key: str = "hotkey", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Execute a sudo call extrinsic. + + Parameters: + subtensor: AsyncSubtensor instance. + wallet: The wallet instance. + call_function: The call function to execute. + call_params: The call parameters. + call_module: The call module. + sign_with: The keypair to sign the extrinsic with. + use_nonce: Whether to use a nonce. + nonce_key: The key to use for the nonce. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + """ + try: + unlock = unlock_key(wallet, raise_error=raise_error) + if not unlock.success: + logging.error(unlock.message) + return False, unlock.message + sudo_call = await subtensor.substrate.compose_call( + call_module="Sudo", + call_function="sudo", + call_params={ + "call": await subtensor.substrate.compose_call( + call_module=call_module, + call_function=call_function, + call_params=call_params, + ) + }, + ) + return await subtensor.sign_and_send_extrinsic( + call=sudo_call, + wallet=wallet, + sign_with=sign_with, + use_nonce=use_nonce, + nonce_key=nonce_key, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + except Exception as error: + if raise_error: + raise error + + return False, str(error) diff --git a/bittensor/core/extrinsics/children.py b/bittensor/core/extrinsics/children.py index dd91fbe97b..993016e6ca 100644 --- a/bittensor/core/extrinsics/children.py +++ b/bittensor/core/extrinsics/children.py @@ -44,7 +44,7 @@ def set_children_extrinsic( NotEnoughStakeToSetChildkeys: Parent key doesn't have minimum own stake. ProportionOverflow: The sum of the proportions does exceed uint64. RegistrationNotPermittedOnRootSubnet: Attempting to register a child on the root network. - SubNetworkDoesNotExist: Attempting to register to a non-existent network. + SubnetNotExists: Attempting to register to a non-existent network. TooManyChildren: Too many children in request. TxRateLimitExceeded: Hotkey hit the rate limit. bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. diff --git a/bittensor/core/extrinsics/commit_reveal.py b/bittensor/core/extrinsics/commit_reveal.py index 7709619d7e..44c6f16675 100644 --- a/bittensor/core/extrinsics/commit_reveal.py +++ b/bittensor/core/extrinsics/commit_reveal.py @@ -73,7 +73,7 @@ def _do_commit_reveal_v3( ) -# TODO: rename this extrinsic to `commit_reveal_extrinsic` in SDK.v10 +# TODO: deprecate in SDKv10 def commit_reveal_v3_extrinsic( subtensor: "Subtensor", wallet: "Wallet", diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index 511d1364ef..932a0c95d6 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -61,6 +61,7 @@ def _do_commit_weights( ) +# TODO: deprecate in SDKv10 def commit_weights_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -175,6 +176,7 @@ def _do_reveal_weights( ) +# TODO: deprecate in SDKv10 def reveal_weights_extrinsic( subtensor: "Subtensor", wallet: "Wallet", diff --git a/bittensor/core/extrinsics/mechanism.py b/bittensor/core/extrinsics/mechanism.py new file mode 100644 index 0000000000..d8e83b6183 --- /dev/null +++ b/bittensor/core/extrinsics/mechanism.py @@ -0,0 +1,381 @@ +from typing import TYPE_CHECKING, Optional, Union + +from bittensor_drand import get_encrypted_commit + +from bittensor.core.settings import version_as_int +from bittensor.core.types import Salt, UIDs, Weights +from bittensor.utils import unlock_key, get_mechid_storage_index +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import ( + convert_and_normalize_weights_and_uids, + generate_weight_hash, +) + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +def commit_mechanism_weights_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + mechid: int, + uids: UIDs, + weights: Weights, + salt: Salt, + version_key: int = version_as_int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Commits the weights for a specific sub subnet on the Bittensor blockchain using the provided wallet. + + Parameters: + subtensor: Subtensor instance. + wallet: Bittensor Wallet instance. + netuid: The subnet unique identifier. + mechid: The subnet mechanism unique identifier. + uids: NumPy array of neuron UIDs for which weights are being committed. + weights: NumPy array of weight values corresponding to each UID. + salt: list of randomly generated integers as salt to generated weighted hash. + version_key: Version key for compatibility with the network. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + """ + try: + unlock = unlock_key(wallet, raise_error=raise_error) + if not unlock.success: + logging.error(unlock.message) + return False, unlock.message + + storage_index = get_mechid_storage_index(netuid=netuid, mechid=mechid) + # Generate the hash of the weights + commit_hash = generate_weight_hash( + address=wallet.hotkey.ss58_address, + netuid=storage_index, + uids=list(uids), + values=list(weights), + salt=salt, + version_key=version_key, + ) + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "commit_hash": commit_hash, + }, + ) + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + sign_with="hotkey", + nonce_key="hotkey", + raise_error=raise_error, + ) + + if success: + logging.debug(message) + return True, message + + logging.error(message) + return False, message + + except Exception as error: + if raise_error: + raise error + logging.error(str(error)) + + return False, str(error) + + +def commit_timelocked_mechanism_weights_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + mechid: int, + uids: UIDs, + weights: Weights, + block_time: Union[int, float], + commit_reveal_version: int = 4, + version_key: int = version_as_int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Commits the weights for a specific sub subnet on the Bittensor blockchain using the provided wallet. + + Parameters: + subtensor: Subtensor instance. + wallet: Bittensor Wallet instance. + netuid: The unique identifier of the subnet. + mechid: The sub-subnet unique identifier. + uids: The list of neuron UIDs that the weights are being set for. + weights: The corresponding weights to be set for each UID. + block_time: The number of seconds for block duration. + commit_reveal_version: The version of the commit-reveal in the chain. + version_key: Version key for compatibility with the network. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + """ + try: + unlock = unlock_key(wallet, raise_error=raise_error) + if not unlock.success: + logging.error(unlock.message) + return False, unlock.message + + uids, weights = convert_and_normalize_weights_and_uids(uids, weights) + + current_block = subtensor.get_current_block() + subnet_hyperparameters = subtensor.get_subnet_hyperparameters( + netuid, block=current_block + ) + tempo = subnet_hyperparameters.tempo + subnet_reveal_period_epochs = subnet_hyperparameters.commit_reveal_period + + storage_index = get_mechid_storage_index(netuid=netuid, mechid=mechid) + + # Encrypt `commit_hash` with t-lock and `get reveal_round` + commit_for_reveal, reveal_round = get_encrypted_commit( + uids=uids, + weights=weights, + version_key=version_key, + tempo=tempo, + current_block=current_block, + netuid=storage_index, + subnet_reveal_period_epochs=subnet_reveal_period_epochs, + block_time=block_time, + hotkey=wallet.hotkey.public_key, + ) + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_timelocked_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "commit": commit_for_reveal, + "reveal_round": reveal_round, + "commit_reveal_version": commit_reveal_version, + }, + ) + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + sign_with="hotkey", + nonce_key="hotkey", + raise_error=raise_error, + ) + + if success: + logging.debug(message) + return True, f"reveal_round:{reveal_round}" + + logging.error(message) + return False, message + + except Exception as error: + if raise_error: + raise error + logging.error(str(error)) + + return False, str(error) + + +def reveal_mechanism_weights_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + mechid: int, + uids: UIDs, + weights: Weights, + salt: Salt, + version_key: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Reveals the weights for a specific sub subnet on the Bittensor blockchain using the provided wallet. + + Parameters: + subtensor: Subtensor instance. + wallet: Bittensor Wallet instance. + netuid: The unique identifier of the subnet. + mechid: The subnet mechanism unique identifier. + uids: List of neuron UIDs for which weights are being revealed. + weights: List of weight values corresponding to each UID. + salt: List of salt values corresponding to the hash function. + version_key: Version key for compatibility with the network. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + """ + try: + unlock = unlock_key(wallet, raise_error=raise_error) + if not unlock.success: + logging.error(unlock.message) + return False, unlock.message + + uids, weights = convert_and_normalize_weights_and_uids(uids, weights) + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="reveal_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "uids": uids, + "values": weights, + "salt": salt, + "version_key": version_key, + }, + ) + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + sign_with="hotkey", + nonce_key="hotkey", + raise_error=raise_error, + ) + + if success: + logging.debug(message) + return True, message + + logging.error(message) + return False, message + + except Exception as error: + if raise_error: + raise error + logging.error(str(error)) + + return False, str(error) + + +def set_mechanism_weights_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + mechid: int, + uids: UIDs, + weights: Weights, + version_key: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Sets the passed weights in the chain for hotkeys in the sub-subnet of the passed subnet. + + Parameters: + subtensor: Subtensor instance. + wallet: Bittensor Wallet instance. + netuid: The unique identifier of the subnet. + mechid: The subnet mechanism unique identifier. + uids: List of neuron UIDs for which weights are being revealed. + weights: List of weight values corresponding to each UID. + version_key: Version key for compatibility with the network. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + """ + try: + unlock = unlock_key(wallet, raise_error=raise_error) + if not unlock.success: + logging.error(unlock.message) + return False, unlock.message + + # Convert, reformat and normalize. + uids, weights = convert_and_normalize_weights_and_uids(uids, weights) + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "dests": uids, + "weights": weights, + "version_key": version_key, + }, + ) + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + use_nonce=True, + nonce_key="hotkey", + sign_with="hotkey", + raise_error=raise_error, + ) + + if success: + logging.debug("Successfully set weights and Finalized.") + return True, message + + logging.error(message) + return False, message + + except Exception as error: + if raise_error: + raise error + logging.error(str(error)) + + return False, str(error) diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 039dbdf837..b860620886 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -77,6 +77,7 @@ def _do_set_weights( return success, message +# TODO: deprecate in SDKv10 def set_weights_extrinsic( subtensor: "Subtensor", wallet: "Wallet", diff --git a/bittensor/core/extrinsics/staking.py b/bittensor/core/extrinsics/staking.py index fc8b69c48b..8397cafe92 100644 --- a/bittensor/core/extrinsics/staking.py +++ b/bittensor/core/extrinsics/staking.py @@ -3,6 +3,7 @@ from async_substrate_interface.errors import SubstrateRequestException from bittensor.core.extrinsics.utils import get_old_stakes +from bittensor.core.types import UIDs from bittensor.utils import unlock_key, format_error_message from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -209,7 +210,7 @@ def add_stake_multiple_extrinsic( subtensor: "Subtensor", wallet: "Wallet", hotkey_ss58s: list[str], - netuids: list[int], + netuids: UIDs, amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, diff --git a/bittensor/core/extrinsics/sudo.py b/bittensor/core/extrinsics/sudo.py new file mode 100644 index 0000000000..9606446858 --- /dev/null +++ b/bittensor/core/extrinsics/sudo.py @@ -0,0 +1,148 @@ +from typing import Optional, TYPE_CHECKING + +from bittensor.core.extrinsics.utils import sudo_call_extrinsic +from bittensor.core.types import Weights as MaybeSplit +from bittensor.utils.weight_utils import convert_maybe_split_to_u16 + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +def sudo_set_admin_freeze_window_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + window: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Sets the admin freeze window length (in blocks) at the end of a tempo. + + Parameters: + subtensor: Subtensor instance. + wallet: Bittensor Wallet instance. + window: The amount of blocks to freeze in the end of a tempo. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + """ + call_function = "sudo_set_admin_freeze_window" + call_params = {"window": window} + return sudo_call_extrinsic( + subtensor=subtensor, + wallet=wallet, + call_function=call_function, + call_params=call_params, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + +def sudo_set_mechanism_count_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + mech_count: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Sets the number of subnet mechanisms. + + Parameters: + subtensor: Subtensor instance. + wallet: Bittensor Wallet instance. + netuid: The subnet unique identifier. + mech_count: The amount of subnet mechanism to be set. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + """ + call_function = "sudo_set_mechanism_count" + call_params = {"netuid": netuid, "mechanism_count": mech_count} + return sudo_call_extrinsic( + subtensor=subtensor, + wallet=wallet, + call_function=call_function, + call_params=call_params, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + +def sudo_set_mechanism_emission_split_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + maybe_split: MaybeSplit, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Sets the emission split between mechanisms in a provided subnet. + + Parameters: + subtensor: Subtensor instance. + wallet: Bittensor Wallet instance. + netuid: The subnet unique identifier. + maybe_split: List of emission weights (positive integers) for each subnet mechanism. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + + Note: + The `maybe_split` list defines the relative emission share for each subnet mechanism. + Its length must match the number of active mechanisms in the subnet or be shorter, but not equal to zero. For + example, [3, 1, 1] distributes emissions in a 3:1:1 ratio across subnet mechanisms 0, 1, and 2. Each mechanism's + emission share is calculated as: share[i] = maybe_split[i] / sum(maybe_split) + """ + call_function = "sudo_set_mechanism_emission_split" + call_params = { + "netuid": netuid, + "maybe_split": convert_maybe_split_to_u16(maybe_split), + } + return sudo_call_extrinsic( + subtensor=subtensor, + wallet=wallet, + call_function=call_function, + call_params=call_params, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index 8f0ce3329d..bc546e36d8 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -1,9 +1,10 @@ from typing import Optional, TYPE_CHECKING from async_substrate_interface.errors import SubstrateRequestException -from bittensor.core.extrinsics.utils import get_extrinsic_fee +from bittensor.core.extrinsics.utils import get_extrinsic_fee from bittensor.core.extrinsics.utils import get_old_stakes +from bittensor.core.types import UIDs from bittensor.utils import unlock_key, format_error_message from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -274,7 +275,7 @@ def unstake_multiple_extrinsic( subtensor: "Subtensor", wallet: "Wallet", hotkey_ss58s: list[str], - netuids: list[int], + netuids: UIDs, amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, diff --git a/bittensor/core/extrinsics/utils.py b/bittensor/core/extrinsics/utils.py index 5092791f66..397a57a3fb 100644 --- a/bittensor/core/extrinsics/utils.py +++ b/bittensor/core/extrinsics/utils.py @@ -2,7 +2,9 @@ from typing import TYPE_CHECKING, Optional +from bittensor.utils import unlock_key from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging if TYPE_CHECKING: from scalecodec import GenericCall @@ -68,3 +70,75 @@ def get_extrinsic_fee( return Balance.from_rao(amount=payment_info["partial_fee"]).set_unit( netuid=netuid or 0 ) + + +def sudo_call_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + call_function: str, + call_params: dict, + call_module: str = "AdminUtils", + sign_with: str = "coldkey", + use_nonce: bool = False, + nonce_key: str = "hotkey", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Execute a sudo call extrinsic. + + Parameters: + subtensor: The Subtensor instance. + wallet: The wallet instance. + call_function: The call function to execute. + call_params: The call parameters. + call_module: The call module. + sign_with: The keypair to sign the extrinsic with. + use_nonce: Whether to use a nonce. + nonce_key: The key to use for the nonce. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. + wait_for_finalization: Whether to wait for the finalization of the transaction. + + Returns: + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + """ + try: + unlock = unlock_key(wallet, raise_error=raise_error) + if not unlock.success: + logging.error(unlock.message) + return False, unlock.message + sudo_call = subtensor.substrate.compose_call( + call_module="Sudo", + call_function="sudo", + call_params={ + "call": subtensor.substrate.compose_call( + call_module=call_module, + call_function=call_function, + call_params=call_params, + ) + }, + ) + return subtensor.sign_and_send_extrinsic( + call=sudo_call, + wallet=wallet, + sign_with=sign_with, + use_nonce=use_nonce, + nonce_key=nonce_key, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + except Exception as error: + if raise_error: + raise error + + return False, str(error) diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index af758939ea..2bc0cef868 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -283,6 +283,11 @@ class MetagraphMixin(ABC): pool: MetagraphInfoPool emissions: MetagraphInfoEmissions + # Mechanisms related fields + mechid: int + mechanisms_emissions_split: list[int] + mechanism_count: int + @property def TS(self) -> Tensor: """ @@ -525,6 +530,7 @@ def __init__( lite: bool = True, sync: bool = True, subtensor: Optional[Union["AsyncSubtensor", "Subtensor"]] = None, + mechid: int = 0, ): """ Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the @@ -532,17 +538,17 @@ def __init__( in representing the state of the Bittensor network. Args: - netuid (int): The unique identifier for the network, distinguishing this instance of the metagraph within + netuid: The unique identifier for the network, distinguishing this instance of the metagraph within potentially multiple network configurations. - network (str): The name of the network, which can indicate specific configurations or versions of the - Bittensor network. - lite (bool): A flag indicating whether to use a lite version of the metagraph. The lite version may contain - less detailed information but can be quicker to initialize and sync. - sync (bool): A flag indicating whether to synchronize the metagraph with the network upon initialization. + network: The name of the network, which can indicate specific configurations or versions of the Bittensor + network. + lite: A flag indicating whether to use a lite version of the metagraph. The lite version may contain less + detailed information but can be quicker to initialize and sync. + sync: A flag indicating whether to synchronize the metagraph with the network upon initialization. Synchronization involves updating the metagraph's parameters to reflect the current state of the network. Example: - Initializing a metagraph object for the Bittensor network with a specific network UID:: + Initializing a metagraph object for the Bittensor network with a specific network UID: metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) """ @@ -550,6 +556,7 @@ def __init__( self.subtensor = subtensor self.should_sync = sync self.netuid = netuid + self.mechid = mechid self.network, self.chain_endpoint = determine_chain_endpoint_and_network( network ) @@ -1033,31 +1040,32 @@ def __init__( lite: bool = True, sync: bool = True, subtensor: Optional[Union["AsyncSubtensor", "Subtensor"]] = None, + mechid: int = 0, ): """ Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the provided arguments. This class requires Torch to be installed. This method is the entry point for creating a metagraph object, which is a central component in representing the state of the Bittensor network. - Args: - netuid (int): The unique identifier for the network, distinguishing this instance of the metagraph within - potentially multiple network configurations. - network (str): The name of the network, which can indicate specific configurations or versions of the - Bittensor network. - lite (bool): A flag indicating whether to use a lite version of the metagraph. The lite version may contain - less detailed information but can be quicker to initialize and sync. - sync (bool): A flag indicating whether to synchronize the metagraph with the network upon initialization. + Parameters: + netuid: Subnet unique identifier. + network: The name of the network, which can indicate specific configurations or versions of the Bittensor + network. + lite: A flag indicating whether to use a lite version of the metagraph. The lite version may contain less + detailed information but can be quicker to initialize and sync. + sync: A flag indicating whether to synchronize the metagraph with the network upon initialization. Synchronization involves updating the metagraph's parameters to reflect the current state of the network. + mechid: Subnet mechanism unique identifier. Example: - Initializing a metagraph object for the Bittensor network with a specific network UID:: + Initializing a metagraph object for the Bittensor network with a specific network UID: from bittensor.core.metagraph import Metagraph metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) """ BaseClass.__init__(self) - MetagraphMixin.__init__(self, netuid, network, lite, sync, subtensor) + MetagraphMixin.__init__(self, netuid, network, lite, sync, subtensor, mechid) self._dtype_registry = { "int64": torch.int64, "float32": torch.float32, @@ -1198,21 +1206,22 @@ def __init__( lite: bool = True, sync: bool = True, subtensor: Optional[Union["AsyncSubtensor", "Subtensor"]] = None, + mechid: int = 0, ): """ Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the provided arguments. This class doesn't require installed Torch. This method is the entry point for creating a metagraph object, which is a central component in representing the state of the Bittensor network. - Args: - netuid (int): The unique identifier for the network, distinguishing this instance of the metagraph within - potentially multiple network configurations. - network (str): The name of the network, which can indicate specific configurations or versions of the - Bittensor network. - lite (bool): A flag indicating whether to use a lite version of the metagraph. The lite version may contain - less detailed information but can be quicker to initialize and sync. - sync (bool): A flag indicating whether to synchronize the metagraph with the network upon initialization. + Parameters: + netuid: Subnet unique identifier. + network: The name of the network, which can indicate specific configurations or versions of the Bittensor + network. + lite: A flag indicating whether to use a lite version of the metagraph. The lite version may contain less + detailed information but can be quicker to initialize and sync. + sync: A flag indicating whether to synchronize the metagraph with the network upon initialization. Synchronization involves updating the metagraph's parameters to reflect the current state of the network. + mechid: Subnet mechanism unique identifier. Example: Initializing a metagraph object for the Bittensor network with a specific network UID:: @@ -1221,7 +1230,7 @@ def __init__( metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) """ - MetagraphMixin.__init__(self, netuid, network, lite, sync, subtensor) + MetagraphMixin.__init__(self, netuid, network, lite, sync, subtensor, mechid) self.netuid = netuid self.network, self.chain_endpoint = determine_chain_endpoint_and_network( @@ -1335,8 +1344,9 @@ def __init__( lite: bool = True, sync: bool = True, subtensor: Optional["AsyncSubtensor"] = None, + mechid: int = 0, ): - super().__init__(netuid, network, lite, sync, subtensor) + super().__init__(netuid, network, lite, sync, subtensor, mechid) async def __aenter__(self): if self.should_sync: @@ -1429,7 +1439,7 @@ async def sync( await self._get_all_stakes_from_chain(block=block) # apply MetagraphInfo data to instance - await self._apply_metagraph_info(block=block) + await self._apply_extra_info(block=block) async def _initialize_subtensor( self, subtensor: "AsyncSubtensor" @@ -1639,13 +1649,19 @@ async def _get_all_stakes_from_chain(self, block: int): except (SubstrateRequestException, AttributeError) as e: logging.debug(e) - async def _apply_metagraph_info(self, block: int): + async def _apply_extra_info(self, block: int): """Retrieves metagraph information for a specific subnet and applies it using a mixin.""" metagraph_info = await self.subtensor.get_metagraph_info( - self.netuid, block=block + netuid=self.netuid, mechid=self.mechid, block=block ) if metagraph_info: self._apply_metagraph_info_mixin(metagraph_info=metagraph_info) + self.mechanism_count = await self.subtensor.get_mechanism_count( + netuid=self.netuid, block=block + ) + self.emissions_split = await self.subtensor.get_mechanism_emission_split( + netuid=self.netuid, block=block + ) class Metagraph(NumpyOrTorch): @@ -1656,8 +1672,9 @@ def __init__( lite: bool = True, sync: bool = True, subtensor: Optional["Subtensor"] = None, + mechid: int = 0, ): - super().__init__(netuid, network, lite, sync, subtensor) + super().__init__(netuid, network, lite, sync, subtensor, mechid) if self.should_sync: self.sync() @@ -1746,7 +1763,7 @@ def sync( self._get_all_stakes_from_chain(block=block) # apply MetagraphInfo data to instance - self._apply_metagraph_info(block=block) + self._apply_extra_info(block=block) def _initialize_subtensor(self, subtensor: "Subtensor") -> "Subtensor": """ @@ -1947,11 +1964,19 @@ def _get_all_stakes_from_chain(self, block: int): except (SubstrateRequestException, AttributeError) as e: logging.debug(e) - def _apply_metagraph_info(self, block: int): + def _apply_extra_info(self, block: int): """Retrieves metagraph information for a specific subnet and applies it using a mixin.""" - metagraph_info = self.subtensor.get_metagraph_info(self.netuid, block=block) + metagraph_info = self.subtensor.get_metagraph_info( + netuid=self.netuid, mechid=self.mechid, block=block + ) if metagraph_info: self._apply_metagraph_info_mixin(metagraph_info=metagraph_info) + self.mechanism_count = self.subtensor.get_mechanism_count( + netuid=self.netuid, block=block + ) + self.emissions_split = self.subtensor.get_mechanism_emission_split( + netuid=self.netuid, block=block + ) async def async_metagraph( diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 49729d6a8b..e511b34bfe 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -3,7 +3,6 @@ from functools import lru_cache from typing import TYPE_CHECKING, Any, Iterable, Optional, Union, cast -import numpy as np import scalecodec from async_substrate_interface.errors import SubstrateRequestException from async_substrate_interface.substrate_addons import RetrySyncSubstrate @@ -11,7 +10,6 @@ from async_substrate_interface.types import ScaleObj from async_substrate_interface.utils.storage import StorageKey from bittensor_drand import get_encrypted_commitment -from numpy.typing import NDArray from bittensor.core.async_subtensor import ProposalVoteData from bittensor.core.axon import Axon @@ -43,17 +41,18 @@ set_children_extrinsic, root_set_pending_childkey_cooldown_extrinsic, ) -from bittensor.core.extrinsics.commit_reveal import commit_reveal_v3_extrinsic -from bittensor.core.extrinsics.commit_weights import ( - commit_weights_extrinsic, - reveal_weights_extrinsic, -) from bittensor.core.extrinsics.liquidity import ( add_liquidity_extrinsic, modify_liquidity_extrinsic, remove_liquidity_extrinsic, toggle_user_liquidity_extrinsic, ) +from bittensor.core.extrinsics.mechanism import ( + commit_mechanism_weights_extrinsic, + commit_timelocked_mechanism_weights_extrinsic, + reveal_mechanism_weights_extrinsic, + set_mechanism_weights_extrinsic, +) from bittensor.core.extrinsics.move_stake import ( transfer_stake_extrinsic, swap_stake_extrinsic, @@ -75,7 +74,6 @@ get_metadata, serve_axon_extrinsic, ) -from bittensor.core.extrinsics.set_weights import set_weights_extrinsic from bittensor.core.extrinsics.staking import ( add_stake_extrinsic, add_stake_multiple_extrinsic, @@ -97,17 +95,23 @@ SS58_FORMAT, TYPE_REGISTRY, ) -from bittensor.core.types import ParamWithTypes, SubtensorMixin +from bittensor.core.types import ( + ParamWithTypes, + Salt, + SubtensorMixin, + UIDs, + Weights, +) from bittensor.utils import ( Certificate, decode_hex_identity_dict, format_error_message, is_valid_ss58_address, - torch, u16_normalized_float, u64_normalized_float, deprecated_message, get_transfer_fn_params, + get_mechid_storage_index, ) from bittensor.utils.balance import ( Balance, @@ -124,7 +128,6 @@ LiquidityPosition, ) from bittensor.utils.weight_utils import ( - generate_weight_hash, convert_uids_and_weights, U16_MAX, ) @@ -502,7 +505,10 @@ def blocks_since_last_update(self, netuid: int, uid: int) -> Optional[int]: return None if not call else (self.get_current_block() - int(call[uid])) def bonds( - self, netuid: int, block: Optional[int] = None + self, + netuid: int, + block: Optional[int] = None, + mechid: int = 0, ) -> list[tuple[int, list[tuple[int, int]]]]: """ Retrieves the bond distribution set by neurons within a specific subnet of the Bittensor network. @@ -510,9 +516,10 @@ def bonds( and perceived value. This bonding mechanism is integral to the network's market-based approach to measuring and rewarding machine intelligence. - Args: - netuid: The network UID of the subnet to query. + Parameters: + netuid: Subnet identifier. block: the block number for this query. + mechid: Subnet mechanism identifier. Returns: List of tuples mapping each neuron's UID to its bonds with other neurons. @@ -521,10 +528,11 @@ def bonds( subnet. It reflects how neurons recognize and invest in each other's intelligence and contributions, supporting diverse and niche systems within the Bittensor ecosystem. """ + storage_index = get_mechid_storage_index(netuid, mechid) b_map_encoded = self.substrate.query_map( module="SubtensorModule", storage_function="Bonds", - params=[netuid], + params=[storage_index], block_hash=self.determine_block_hash(block), ) b_map = [] @@ -630,6 +638,23 @@ def does_hotkey_exist(self, hotkey_ss58: str, block: Optional[int] = None) -> bo ) return return_val + def get_admin_freeze_window(self, block: Optional[int] = None) -> int: + """ + Returns the number of blocks when dependent transactions will be frozen for execution. + + Arguments: + block: The block number for which the children are to be retrieved. + + Returns: + AdminFreezeWindow as integer. The number of blocks are frozen. + """ + + return self.substrate.query( + module="SubtensorModule", + storage_function="AdminFreezeWindow", + block_hash=self.determine_block_hash(block), + ).value + def get_all_subnets_info(self, block: Optional[int] = None) -> list["SubnetInfo"]: """ Retrieves detailed information about all subnets within the Bittensor network. This function provides @@ -939,8 +964,8 @@ def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> metadata = cast(dict, get_metadata(self, netuid, hotkey, block)) try: return decode_metadata(metadata) - - except TypeError: + except Exception as error: + logging.error(error) return "" def get_last_commitment_bonds_reset_block( @@ -981,7 +1006,12 @@ def get_all_commitments( ) result = {} for id_, value in query: - result[decode_account_id(id_[0])] = decode_metadata(value) + try: + result[decode_account_id(id_[0])] = decode_metadata(value) + except Exception as error: + logging.error( + f"Error decoding [red]{id_}[/red] and [red]{value}[/red]: {error}" + ) return result def get_revealed_commitment_by_hotkey( @@ -1080,6 +1110,7 @@ def get_all_revealed_commitments( result[hotkey_ss58_address] = commitment_message return result + # TODO: deprecated in SDKv10 def get_current_weight_commit_info( self, netuid: int, block: Optional[int] = None ) -> list[tuple[str, str, int]]: @@ -1113,6 +1144,7 @@ def get_current_weight_commit_info( commits = result.records[0][1] if result.records else [] return [WeightCommitInfo.from_vec_u8(commit) for commit in commits] + # TODO: deprecated in SDKv10 def get_current_weight_commit_info_v2( self, netuid: int, block: Optional[int] = None ) -> list[tuple[str, int, str, int]]: @@ -1338,88 +1370,106 @@ def get_minimum_required_stake(self) -> Balance: return Balance.from_rao(getattr(result, "value", 0)) + # TODO: update parameters order in SDKv10, rename `field_indices` to `selected_indices` def get_metagraph_info( self, netuid: int, field_indices: Optional[Union[list[SelectiveMetagraphIndex], list[int]]] = None, block: Optional[int] = None, + mechid: int = 0, ) -> Optional[MetagraphInfo]: """ - Retrieves full or partial metagraph information for the specified subnet (netuid). + Retrieves full or partial metagraph information for the specified subnet mechanism (netuid, mechid). Arguments: - netuid: The NetUID of the subnet to query. - field_indices: An optional list of SelectiveMetagraphIndex or int values specifying which fields to retrieve. + netuid: Subnet unique identifier. + field_indices: Optional list of SelectiveMetagraphIndex or int values specifying which fields to retrieve. If not provided, all available fields will be returned. - block: The block number at which to query the data. If not specified, the current block or one determined - via reuse_block or block_hash will be used. + block: The block number at which to query the data. + mechid: Subnet mechanism unique identifier. Returns: - Optional[MetagraphInfo]: A MetagraphInfo object containing the requested subnet data, or None if the subnet - with the given netuid does not exist. + MetagraphInfo object with the requested subnet mechanism data, None if the subnet mechanism does not exist. Example: + # Retrieve all fields from the metagraph from subnet 2 mechanism 0 meta_info = subtensor.get_metagraph_info(netuid=2) + # Retrieve all fields from the metagraph from subnet 2 mechanism 1 + meta_info = subtensor.get_metagraph_info(netuid=2, mechid=1) + + # Retrieve selective data from the metagraph from subnet 2 mechanism 0 + partial_meta_info = subtensor.get_metagraph_info( + netuid=2, + field_indices=[SelectiveMetagraphIndex.Name, SelectiveMetagraphIndex.OwnerHotkeys] + ) + + # Retrieve selective data from the metagraph from subnet 2 mechanism 1 partial_meta_info = subtensor.get_metagraph_info( netuid=2, + mechid=1, field_indices=[SelectiveMetagraphIndex.Name, SelectiveMetagraphIndex.OwnerHotkeys] ) + + Notes: + See also: + - + - """ - block_hash = self.determine_block_hash(block) + block_hash = self.determine_block_hash(block=block) - if field_indices: - if isinstance(field_indices, list) and all( - isinstance(f, (SelectiveMetagraphIndex, int)) for f in field_indices - ): - indexes = [ - f.value if isinstance(f, SelectiveMetagraphIndex) else f - for f in field_indices - ] - else: - raise ValueError( - "`field_indices` must be a list of SelectiveMetagraphIndex enums or ints." - ) + indexes = ( + [ + f.value if isinstance(f, SelectiveMetagraphIndex) else f + for f in field_indices + ] + if field_indices is not None + else [f for f in range(len(SelectiveMetagraphIndex))] + ) - query = self.substrate.runtime_call( - "SubnetInfoRuntimeApi", - "get_selective_metagraph", - params=[netuid, indexes if 0 in indexes else [0] + indexes], - block_hash=block_hash, - ) - else: - query = self.substrate.runtime_call( - "SubnetInfoRuntimeApi", - "get_metagraph", - params=[netuid], - block_hash=block_hash, + query = self.substrate.runtime_call( + api="SubnetInfoRuntimeApi", + method="get_selective_mechagraph", + params=[netuid, mechid, indexes if 0 in indexes else [0] + indexes], + block_hash=block_hash, + ) + if query is None or not hasattr(query, "value") or query.value is None: + logging.error( + f"Subnet mechanism {netuid}.{mechid if mechid else 0} does not exist." ) - - if query.value is None: - logging.error(f"Subnet {netuid} does not exist.") return None return MetagraphInfo.from_dict(query.value) + # TODO: update parameters order in SDKv10 def get_all_metagraphs_info( - self, block: Optional[int] = None - ) -> list[MetagraphInfo]: + self, + block: Optional[int] = None, + all_mechanisms: bool = False, + ) -> Optional[list[MetagraphInfo]]: """ Retrieves a list of MetagraphInfo objects for all subnets - Arguments: - block: the block number at which to retrieve the hyperparameter. Do not specify if using block_hash or - reuse_block + Parameters: + block: The blockchain block number for the query. + all_mechanisms: If True then returns all mechanisms, otherwise only those with index 0 for all subnets. Returns: - MetagraphInfo dataclass + List of MetagraphInfo objects for all existing subnets. + + Notes: + See also: See """ block_hash = self.determine_block_hash(block) + method = "get_all_mechagraphs" if all_mechanisms else "get_all_metagraphs" query = self.substrate.runtime_call( - "SubnetInfoRuntimeApi", - "get_all_metagraphs", + api="SubnetInfoRuntimeApi", + method=method, block_hash=block_hash, ) + if query is None or not hasattr(query, "value"): + return None + return MetagraphInfo.list_from_dicts(query.value) def get_netuids_for_hotkey( @@ -1854,6 +1904,54 @@ def get_stake_add_fee( """ return self.get_stake_operations_fee(amount=amount, netuid=netuid, block=block) + def get_mechanism_emission_split( + self, netuid: int, block: Optional[int] = None + ) -> Optional[list[int]]: + """Returns the emission percentages allocated to each subnet mechanism. + + Parameters: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + A list of integers representing the percentage of emission allocated to each subnet mechanism (rounded to + whole numbers). Returns None if emission is evenly split or if the data is unavailable. + """ + block_hash = self.determine_block_hash(block) + result = self.substrate.query( + module="SubtensorModule", + storage_function="MechanismEmissionSplit", + params=[netuid], + block_hash=block_hash, + ) + if result is None or not hasattr(result, "value"): + return None + + return [round(i / sum(result.value) * 100) for i in result.value] + + def get_mechanism_count( + self, + netuid: int, + block: Optional[int] = None, + ) -> int: + """Retrieves the number of mechanisms for the given subnet. + + Parameters: + netuid: Subnet identifier. + block: The blockchain block number for the query. + + Returns: + The number of mechanisms for the given subnet. + """ + block_hash = self.determine_block_hash(block) + query = self.substrate.query( + module="SubtensorModule", + storage_function="MechanismCountCurrent", + params=[netuid], + block_hash=block_hash, + ) + return query.value if query is not None and hasattr(query, "value") else 1 + def get_subnet_info( self, netuid: int, block: Optional[int] = None ) -> Optional["SubnetInfo"]: @@ -1888,7 +1986,7 @@ def get_subnet_price( ) -> Balance: """Gets the current Alpha price in TAO for all subnets. - Arguments: + Parameters: netuid: The unique identifier of the subnet. block: The blockchain block number for the query. @@ -1900,16 +1998,13 @@ def get_subnet_price( return Balance.from_tao(1) block_hash = self.determine_block_hash(block=block) - current_sqrt_price = self.substrate.query( - module="Swap", - storage_function="AlphaSqrtPrice", + price_rao = self.substrate.runtime_call( + api="SwapRuntimeApi", + method="current_alpha_price", params=[netuid], block_hash=block_hash, - ) - - current_sqrt_price = fixed_to_float(current_sqrt_price) - current_price = current_sqrt_price * current_sqrt_price - return Balance.from_rao(int(current_price * 1e9)) + ).value + return Balance.from_rao(price_rao) def get_subnet_prices( self, @@ -1945,15 +2040,20 @@ def get_subnet_prices( prices.update({0: Balance.from_tao(1)}) return prices + # TODO: update order in SDKv10 def get_timelocked_weight_commits( - self, netuid: int, block: Optional[int] = None + self, + netuid: int, + block: Optional[int] = None, + mechid: int = 0, ) -> list[tuple[str, int, str, int]]: """ Retrieves CRv4 weight commit information for a specific subnet. - Arguments: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. Default is ``None``. + Parameters: + netuid: Subnet identifier. + block: The blockchain block number for the query. Default is ``None``. + mechid: Subnet mechanism identifier. Returns: A list of commit details, where each item contains: @@ -1964,10 +2064,11 @@ def get_timelocked_weight_commits( The list may be empty if there are no commits found. """ + storage_index = get_mechid_storage_index(netuid, mechid) result = self.substrate.query_map( module="SubtensorModule", storage_function="TimelockedWeightCommits", - params=[netuid], + params=[storage_index], block_hash=self.determine_block_hash(block=block), ) @@ -2034,7 +2135,7 @@ def get_stake_for_coldkey_and_hotkey( self, coldkey_ss58: str, hotkey_ss58: str, - netuids: Optional[list[int]] = None, + netuids: Optional[UIDs] = None, block: Optional[int] = None, ) -> dict[int, StakeInfo]: """ @@ -2083,13 +2184,13 @@ def get_stake_for_coldkey( result = self.query_runtime_api( runtime_api="StakeInfoRuntimeApi", method="get_stake_info_for_coldkey", - params=[coldkey_ss58], # type: ignore + params=[coldkey_ss58], block=block, ) if result is None: return [] - stakes = StakeInfo.list_from_dicts(result) # type: ignore + stakes: list[StakeInfo] = StakeInfo.list_from_dicts(result) return [stake for stake in stakes if stake.stake > 0] get_stake_info_for_coldkey = get_stake_for_coldkey @@ -2227,7 +2328,7 @@ def get_subnet_reveal_period_epochs( ), ) - def get_subnets(self, block: Optional[int] = None) -> list[int]: + def get_subnets(self, block: Optional[int] = None) -> UIDs: """ Retrieves the list of all subnet unique identifiers (netuids) currently present in the Bittensor network. @@ -2450,6 +2551,37 @@ def immunity_period( ) return None if call is None else int(call) + def is_in_admin_freeze_window( + self, + netuid: int, + block: Optional[int] = None, + ) -> bool: + """ + Returns True if the current block is within the terminal freeze window of the tempo + for the given subnet. During this window, admin ops are prohibited to avoid interference + with validator weight submissions. + + Parameters: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + bool: True if in freeze window, else False. + """ + # SN0 doesn't have admin_freeze_window + if netuid == 0: + return False + + next_epoch_start_block = self.get_next_epoch_start_block( + netuid=netuid, block=block + ) + + if next_epoch_start_block is not None: + remaining = next_epoch_start_block - self.block + window = self.get_admin_freeze_window(block=block) + return remaining < window + return False + def is_fast_blocks(self): """Returns True if the node is running with fast blocks. False if not.""" return self.query_constant("SubtensorModule", "DurationOfStartCall").value == 10 @@ -2582,15 +2714,37 @@ def max_weight_limit( ) return None if call is None else u16_normalized_float(int(call)) + # TODO: update parameters order in SDKv10 def metagraph( - self, netuid: int, lite: bool = True, block: Optional[int] = None + self, + netuid: int, + lite: bool = True, + block: Optional[int] = None, + mechid: int = 0, ) -> "Metagraph": + """ + Returns a synced metagraph for a specified subnet within the Bittensor network. + The metagraph represents the network's structure, including neuron connections and interactions. + + Parameters: + netuid: The network UID of the subnet to query. + lite: If true, returns a metagraph using a lightweight sync (no weights, no bonds). + block: Block number for synchronization, or `None` for the latest block. + mechid: Subnet mechanism identifier. + + Returns: + The metagraph representing the subnet's structure and neuron relationships. + + The metagraph is an essential tool for understanding the topology and dynamics of the Bittensor network's + decentralized architecture, particularly in relation to neuron interconnectivity and consensus processes. + """ metagraph = Metagraph( network=self.chain_endpoint, netuid=netuid, lite=lite, sync=False, subtensor=self, + mechid=mechid, ) metagraph.sync(block=block, lite=lite, subtensor=self) @@ -2952,8 +3106,12 @@ def handler(block_data: dict): ) return True + # TODO: update order in SDKv10 def weights( - self, netuid: int, block: Optional[int] = None + self, + netuid: int, + block: Optional[int] = None, + mechid: int = 0, ) -> list[tuple[int, list[tuple[int, int]]]]: """ Retrieves the weight distribution set by neurons within a specific subnet of the Bittensor network. @@ -2963,6 +3121,7 @@ def weights( Arguments: netuid (int): The network UID of the subnet to query. block (Optional[int]): Block number for synchronization, or ``None`` for the latest block. + mechid: Subnet mechanism identifier. Returns: A list of tuples mapping each neuron's UID to its assigned weights. @@ -2970,10 +3129,11 @@ def weights( The weight distribution is a key factor in the network's consensus algorithm and the ranking of neurons, influencing their influence and reward allocation within the subnet. """ + storage_index = get_mechid_storage_index(netuid, mechid) w_map_encoded = self.substrate.query_map( module="SubtensorModule", storage_function="Weights", - params=[netuid], + params=[storage_index], block_hash=self.determine_block_hash(block), ) w_map = [(uid, w.value or []) for uid, w in w_map_encoded] @@ -3251,7 +3411,7 @@ def add_stake_multiple( self, wallet: "Wallet", hotkey_ss58s: list[str], - netuids: list[int], + netuids: UIDs, amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -3338,34 +3498,34 @@ def commit_weights( self, wallet: "Wallet", netuid: int, - salt: list[int], - uids: Union[NDArray[np.int64], list], - weights: Union[NDArray[np.int64], list], + salt: Salt, + uids: UIDs, + weights: Weights, version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, max_retries: int = 5, period: Optional[int] = 16, + mechid: int = 0, ) -> tuple[bool, str]: """ Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. This action serves as a commitment or snapshot of the neuron's current weight distribution. - Arguments: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the weights. - netuid (int): The unique identifier of the subnet. - salt (list[int]): list of randomly generated integers as salt to generated weighted hash. - uids (np.ndarray): NumPy array of neuron UIDs for which weights are being committed. - weights (np.ndarray): NumPy array of weight values corresponding to each UID. - version_key (int): Version key for compatibility with the network. Default is ``int representation of - a Bittensor version.``. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is - ``False``. - max_retries (int): The number of maximum attempts to commit weights. Default is ``5``. - period (Optional[int]): The number of blocks during which the transaction will remain valid after it's - submitted. If the transaction is not included in a block within that number of blocks, it will expire - and be rejected. You can think of it as an expiration date for the transaction. + Parameters: + wallet: The wallet associated with the neuron committing the weights. + netuid: The unique identifier of the subnet. + salt: list of randomly generated integers as salt to generated weighted hash. + uids: Array/list of neuron UIDs for which weights are being committed. + weights: Array/list of weight values corresponding to each UID. + version_key: Version key for compatibility with the network. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + max_retries: The number of maximum attempts to commit weights. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + mechid: The subnet mechanism unique identifier. Returns: tuple[bool, str]: @@ -3385,23 +3545,16 @@ def commit_weights( f"version_key=[blue]{version_key}[/blue]" ) - # Generate the hash of the weights - commit_hash = generate_weight_hash( - address=wallet.hotkey.ss58_address, - netuid=netuid, - uids=list(uids), - values=list(weights), - salt=salt, - version_key=version_key, - ) - while retries < max_retries and success is False: try: - success, message = commit_weights_extrinsic( + success, message = commit_mechanism_weights_extrinsic( subtensor=self, wallet=wallet, netuid=netuid, - commit_hash=commit_hash, + mechid=mechid, + uids=uids, + weights=weights, + salt=salt, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, period=period, @@ -3410,7 +3563,7 @@ def commit_weights( break except Exception as e: logging.error(f"Error committing weights: {e}") - retries += 1 + retries += 1 return success, message @@ -3678,41 +3831,44 @@ def reveal_weights( self, wallet: "Wallet", netuid: int, - uids: Union[NDArray[np.int64], list], - weights: Union[NDArray[np.int64], list], - salt: Union[NDArray[np.int64], list], + uids: UIDs, + weights: Weights, + salt: Salt, version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, max_retries: int = 5, period: Optional[int] = 16, + mechid: int = 0, ) -> tuple[bool, str]: """ Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. This action serves as a revelation of the neuron's previously committed weight distribution. - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron revealing the weights. - netuid (int): The unique identifier of the subnet. - uids (np.ndarray): NumPy array of neuron UIDs for which weights are being revealed. - weights (np.ndarray): NumPy array of weight values corresponding to each UID. - salt (np.ndarray): NumPy array of salt values corresponding to the hash function. - version_key (int): Version key for compatibility with the network. Default is ``int representation of - the Bittensor version``. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is - ``False``. - max_retries (int): The number of maximum attempts to reveal weights. Default is ``5``. - period (Optional[int]): The number of blocks during which the transaction will remain valid after it's - submitted. If the transaction is not included in a block within that number of blocks, it will expire - and be rejected. You can think of it as an expiration date for the transaction. + Parameters: + wallet: Bittensor Wallet instance. + netuid: The unique identifier of the subnet. + uids: NumPy array of neuron UIDs for which weights are being revealed. + weights: NumPy array of weight values corresponding to each UID. + salt: NumPy array of salt values corresponding to the hash function. + version_key: Version key for compatibility with the network. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + max_retries: The number of maximum attempts to reveal weights. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + mechid: The subnet mechanism unique identifier. Returns: - tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string - value describing the success or potential error. + tuple[bool, str]: + `True` if the extrinsic executed successfully, `False` otherwise. + `message` is a string value describing the success or potential error. + + This function allows neurons to reveal their previously committed weight distribution, ensuring transparency and + accountability within the Bittensor network. - This function allows neurons to reveal their previously committed weight distribution, ensuring transparency - and accountability within the Bittensor network. + See also: , """ retries = 0 success = False @@ -3720,13 +3876,14 @@ def reveal_weights( while retries < max_retries and success is False: try: - success, message = reveal_weights_extrinsic( + success, message = reveal_mechanism_weights_extrinsic( subtensor=self, wallet=wallet, netuid=netuid, - uids=list(uids), - weights=list(weights), - salt=list(salt), + mechid=mechid, + uids=uids, + weights=weights, + salt=salt, version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, @@ -3736,7 +3893,7 @@ def reveal_weights( break except Exception as e: logging.error(f"Error revealing weights: {e}") - retries += 1 + retries += 1 return success, message @@ -3809,7 +3966,7 @@ def root_set_pending_childkey_cooldown( def root_set_weights( self, wallet: "Wallet", - netuids: list[int], + netuids: UIDs, weights: list[float], version_key: int = 0, wait_for_inclusion: bool = False, @@ -4024,42 +4181,48 @@ def set_weights( self, wallet: "Wallet", netuid: int, - uids: Union[NDArray[np.int64], "torch.LongTensor", list], - weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + uids: UIDs, + weights: Weights, version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, max_retries: int = 5, block_time: float = 12.0, period: Optional[int] = 8, + mechid: int = 0, + commit_reveal_version: int = 4, ) -> tuple[bool, str]: """ Sets the interneuronal weights for the specified neuron. This process involves specifying the influence or trust a neuron places on other neurons in the network, which is a fundamental aspect of Bittensor's decentralized learning architecture. - Arguments: + Parameters: wallet: The wallet associated with the neuron setting the weights. netuid: The unique identifier of the subnet. uids: The list of neuron UIDs that the weights are being set for. weights: The corresponding weights to be set for each UID. - version_key: Version key for compatibility with the network. Default is int representation of a Bittensor - version. - wait_for_inclusion: Waits for the transaction to be included in a block. Default is ``False``. - wait_for_finalization: Waits for the transaction to be finalized on the blockchain. Default is ``False``. - max_retries: The number of maximum attempts to set weights. Default is ``5``. - block_time: The number of seconds for block duration. Default is 12.0 seconds. - period (Optional[int]): The number of blocks during which the transaction will remain valid after it's - submitted. If the transaction is not included in a block within that number of blocks, it will expire - and be rejected. You can think of it as an expiration date for the transaction. Default is 8. + version_key: Version key for compatibility with the network. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. + max_retries: The number of maximum attempts to set weights. + block_time: The number of seconds for block duration. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. + mechid: The subnet mechanism unique identifier. + commit_reveal_version: The version of the commit-reveal in the chain. Returns: tuple: `True` if the setting of weights is successful, `False` otherwise. `msg` is a string value describing the success or potential error. - This function is crucial in shaping the network's collective intelligence, where each neuron's learning and - contribution are influenced by the weights it sets towards others. + This function is crucial in the Yuma Consensus mechanism, where each validator's weight vector contributes to + the overall weight matrix used to calculate emissions and maintain network consensus. + + Notes: + See """ def _blocks_weight_limit() -> bool: @@ -4079,17 +4242,18 @@ def _blocks_weight_limit() -> bool: ) if self.commit_reveal_enabled(netuid=netuid): - # go with `commit reveal v3` extrinsic + # go with `commit_timelocked_mechanism_weights_extrinsic` extrinsic while retries < max_retries and success is False and _blocks_weight_limit(): logging.info( f"Committing weights for subnet [blue]{netuid}[/blue]. " f"Attempt [blue]{retries + 1}[blue] of [green]{max_retries}[/green]." ) - success, message = commit_reveal_v3_extrinsic( + success, message = commit_timelocked_mechanism_weights_extrinsic( subtensor=self, wallet=wallet, netuid=netuid, + mechid=mechid, uids=uids, weights=weights, version_key=version_key, @@ -4097,11 +4261,12 @@ def _blocks_weight_limit() -> bool: wait_for_finalization=wait_for_finalization, block_time=block_time, period=period, + commit_reveal_version=commit_reveal_version, ) retries += 1 return success, message else: - # go with classic `set_weights_extrinsic` + # go with `set_mechanism_weights_extrinsic` while retries < max_retries and success is False and _blocks_weight_limit(): try: @@ -4109,10 +4274,11 @@ def _blocks_weight_limit() -> bool: f"Setting weights for subnet [blue]{netuid}[/blue]. " f"Attempt [blue]{retries + 1}[/blue] of [green]{max_retries}[/green]." ) - success, message = set_weights_extrinsic( + success, message = set_mechanism_weights_extrinsic( subtensor=self, wallet=wallet, netuid=netuid, + mechid=mechid, uids=uids, weights=weights, version_key=version_key, @@ -4543,7 +4709,7 @@ def unstake_multiple( self, wallet: "Wallet", hotkey_ss58s: list[str], - netuids: list[int], + netuids: UIDs, amounts: Optional[list[Balance]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, diff --git a/bittensor/core/subtensor_api/chain.py b/bittensor/core/subtensor_api/chain.py index cd2bfda02f..9f4c312f13 100644 --- a/bittensor/core/subtensor_api/chain.py +++ b/bittensor/core/subtensor_api/chain.py @@ -7,6 +7,7 @@ class Chain: """Class for managing chain state operations.""" def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.get_admin_freeze_window = subtensor.get_admin_freeze_window self.get_block_hash = subtensor.get_block_hash self.get_current_block = subtensor.get_current_block self.get_delegate_identities = subtensor.get_delegate_identities @@ -14,6 +15,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_minimum_required_stake = subtensor.get_minimum_required_stake self.get_vote_data = subtensor.get_vote_data self.get_timestamp = subtensor.get_timestamp + self.is_in_admin_freeze_window = subtensor.is_in_admin_freeze_window self.is_fast_blocks = subtensor.is_fast_blocks self.last_drand_round = subtensor.last_drand_round self.state_call = subtensor.state_call diff --git a/bittensor/core/subtensor_api/subnets.py b/bittensor/core/subtensor_api/subnets.py index cfdee525b8..d717017382 100644 --- a/bittensor/core/subtensor_api/subnets.py +++ b/bittensor/core/subtensor_api/subnets.py @@ -25,6 +25,8 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): subtensor.get_neuron_for_pubkey_and_subnet ) self.get_next_epoch_start_block = subtensor.get_next_epoch_start_block + self.get_mechanism_emission_split = subtensor.get_mechanism_emission_split + self.get_mechanism_count = subtensor.get_mechanism_count self.get_subnet_burn_cost = subtensor.get_subnet_burn_cost self.get_subnet_hyperparameters = subtensor.get_subnet_hyperparameters self.get_subnet_info = subtensor.get_subnet_info diff --git a/bittensor/core/subtensor_api/utils.py b/bittensor/core/subtensor_api/utils.py index 67399a37ed..f842802b82 100644 --- a/bittensor/core/subtensor_api/utils.py +++ b/bittensor/core/subtensor_api/utils.py @@ -25,6 +25,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.filter_netuids_by_registered_hotkeys = ( subtensor._subtensor.filter_netuids_by_registered_hotkeys ) + subtensor.get_admin_freeze_window = subtensor._subtensor.get_admin_freeze_window subtensor.get_all_commitments = subtensor._subtensor.get_all_commitments subtensor.get_all_metagraphs_info = subtensor._subtensor.get_all_metagraphs_info subtensor.get_all_neuron_certificates = ( @@ -91,6 +92,10 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.get_stake_movement_fee = subtensor._subtensor.get_stake_movement_fee subtensor.get_stake_operations_fee = subtensor._subtensor.get_stake_operations_fee subtensor.get_stake_weight = subtensor._subtensor.get_stake_weight + subtensor.get_mechanism_emission_split = ( + subtensor._subtensor.get_mechanism_emission_split + ) + subtensor.get_mechanism_count = subtensor._subtensor.get_mechanism_count subtensor.get_subnet_burn_cost = subtensor._subtensor.get_subnet_burn_cost subtensor.get_subnet_hyperparameters = ( subtensor._subtensor.get_subnet_hyperparameters @@ -125,6 +130,7 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor.is_hotkey_registered_on_subnet = ( subtensor._subtensor.is_hotkey_registered_on_subnet ) + subtensor.is_in_admin_freeze_window = subtensor._subtensor.is_in_admin_freeze_window subtensor.is_subnet_active = subtensor._subtensor.is_subnet_active subtensor.last_drand_round = subtensor._subtensor.last_drand_round subtensor.log_verbose = subtensor._subtensor.log_verbose diff --git a/bittensor/core/types.py b/bittensor/core/types.py index d0c8b79cc9..eb1e84478f 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -1,13 +1,21 @@ -from abc import ABC import argparse -from typing import TypedDict, Optional +from abc import ABC +from typing import TypedDict, Optional, Union + +import numpy as np +from numpy.typing import NDArray -from bittensor.utils import networking, Certificate -from bittensor.utils.btlogging import logging from bittensor.core import settings -from bittensor.core.config import Config from bittensor.core.chain_data import NeuronInfo, NeuronInfoLite +from bittensor.core.config import Config from bittensor.utils import determine_chain_endpoint_and_network +from bittensor.utils import networking, Certificate +from bittensor.utils.btlogging import logging + +# Type annotations for UIDs and weights. +UIDs = Union[NDArray[np.int64], list[Union[int]]] +Weights = Union[NDArray[np.float32], list[Union[int, float]]] +Salt = Union[NDArray[np.int64], list[int]] class SubtensorMixin(ABC): diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 57d528f76f..adc002ed4f 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -25,7 +25,12 @@ from bittensor.utils.balance import Balance BT_DOCS_LINK = "https://docs.bittensor.com" +RAOPERTAO = 1e9 +U16_MAX = 65535 +U64_MAX = 18446744073709551615 +GLOBAL_MAX_SUBNET_COUNT = 4096 +UnlockStatus = namedtuple("UnlockStatus", ["success", "message"]) # redundant aliases logging = logging @@ -38,11 +43,37 @@ hex_to_bytes = hex_to_bytes -RAOPERTAO = 1e9 -U16_MAX = 65535 -U64_MAX = 18446744073709551615 +def get_mechid_storage_index(netuid: int, mechid: int) -> int: + """Computes the storage index for a given netuid and mechid pair. -UnlockStatus = namedtuple("UnlockStatus", ["success", "message"]) + Parameters: + netuid: The netuid of the subnet. + mechid: The mechid of the subnet. + + Returns: + Storage index number for the subnet and mechanism id. + """ + return mechid * GLOBAL_MAX_SUBNET_COUNT + netuid + + +def get_netuid_and_mechid_by_storage_index(storage_index: int) -> tuple[int, int]: + """Returns the netuid and mechid from the storage index. + + Chain APIs (e.g., SubMetagraph response) returns netuid which is storage index that encodes both the netuid and + mechid. This function reverses the encoding to extract these components. + + Parameters: + storage_index: The storage index of the subnet. + + Returns: + tuple[int, int]: + - netuid - subnet identifier. + - mechid - mechanism identifier. + """ + return ( + storage_index % GLOBAL_MAX_SUBNET_COUNT, + storage_index // GLOBAL_MAX_SUBNET_COUNT, + ) class Certificate(str): diff --git a/bittensor/utils/easy_imports.py b/bittensor/utils/easy_imports.py index a2645b0668..f71ef909c6 100644 --- a/bittensor/utils/easy_imports.py +++ b/bittensor/utils/easy_imports.py @@ -25,7 +25,7 @@ decrypt_keyfile_data, Keyfile, ) -from bittensor_wallet.wallet import display_mnemonic_msg, Wallet +from bittensor_wallet.wallet import Wallet from bittensor.core import settings, timelock from bittensor.core.async_subtensor import AsyncSubtensor @@ -86,7 +86,7 @@ RegistrationNotPermittedOnRootSubnet, RunException, StakeError, - SubNetworkDoesNotExist, + SubnetNotExists, SynapseDendriteNoneException, SynapseParsingError, TooManyChildren, @@ -194,7 +194,6 @@ def info(on: bool = True): "get_coldkey_password_from_environment", "decrypt_keyfile_data", "Keyfile", - "display_mnemonic_msg", "Wallet", "settings", "timelock", @@ -253,7 +252,7 @@ def info(on: bool = True): "RegistrationNotPermittedOnRootSubnet", "RunException", "StakeError", - "SubNetworkDoesNotExist", + "SubnetNotExists", "SynapseDendriteNoneException", "SynapseParsingError", "TooManyChildren", diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index 2a7b7f3afd..92c798a12d 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -8,6 +8,7 @@ from bittensor_wallet import Keypair from numpy.typing import NDArray from scalecodec import U16, ScaleBytes, Vec +from bittensor.core.types import Weights as MaybeSplit from bittensor.utils.btlogging import logging from bittensor.utils.registration import legacy_torch_api_compat, torch, use_torch @@ -400,13 +401,13 @@ def generate_weight_hash( """ Generate a valid commit hash from the provided weights. - Args: - address (str): The account identifier. Wallet ss58_address. - netuid (int): The network unique identifier. - uids (list[int]): The list of UIDs. - salt (list[int]): The salt to add to hash. - values (list[int]): The list of weight values. - version_key (int): The version key. + Parameters: + address: The account identifier. Wallet ss58_address. + netuid: The subnet unique identifier. + uids: The list of UIDs. + salt: The salt to add to hash. + values: The list of weight values. + version_key: The version key. Returns: str: The generated commit hash. @@ -479,3 +480,42 @@ def convert_and_normalize_weights_and_uids( # Reformat and normalize and return return convert_weights_and_uids_for_emit(*convert_uids_and_weights(uids, weights)) + + +def convert_maybe_split_to_u16(maybe_split: MaybeSplit) -> list[int]: + # Convert np.ndarray to list + if isinstance(maybe_split, np.ndarray): + maybe_split = maybe_split.tolist() + + # Ensure we now work with list of numbers + if not isinstance(maybe_split, list) or not maybe_split: + raise ValueError("maybe_split must be a non-empty list or array of numbers.") + + # Ensure all elements are valid numbers + try: + values = [float(x) for x in maybe_split] + except Exception: + raise ValueError("maybe_split must contain numeric values (int or float).") + + if any(x < 0 for x in values): + raise ValueError("maybe_split cannot contain negative values.") + + total = sum(values) + if total <= 0: + raise ValueError("maybe_split must sum to a positive value.") + + # Normalize to sum = 1.0 + normalized = [x / total for x in values] + + # Scale to u16 and round + u16_vals = [round(val * U16_MAX) for val in normalized] + + # Fix rounding error + diff = sum(u16_vals) - U16_MAX + if diff != 0: + max_idx = u16_vals.index(max(u16_vals)) + u16_vals[max_idx] -= diff + + assert sum(u16_vals) == U16_MAX, "Final split must sum to U16_MAX (65535)." + + return u16_vals diff --git a/contrib/CONTRIBUTING.md b/contrib/CONTRIBUTING.md index c3d6ba850f..e0a3c287b4 100644 --- a/contrib/CONTRIBUTING.md +++ b/contrib/CONTRIBUTING.md @@ -28,14 +28,14 @@ The following is a set of guidelines for contributing to Bittensor, which are ho We have an official Discord server where the community chimes in with helpful advice if you have questions. This is the fastest way to get an answer and the core development team is active on Discord. -* [Official Bittensor Discord](https://discord.gg/7wvFuPJZgq) +* [Official Bittensor Discord](https://discord.gg/bittensor) ## What should I know before I get started? -Bittensor is still in the Alpha stages, and as such you will likely run into some problems in deploying your model or installing Bittensor itself. If you run into an issue or end up resolving an issue yourself, feel free to create a pull request with a fix or with a fix to the documentation. The documentation repository can be found [here](https://github.com/opentensor/docs). +Bittensor is still in the Alpha stages, and as such you will likely run into some problems in deploying your model or installing Bittensor itself. If you run into an issue or end up resolving an issue yourself, feel free to create a pull request with a fix or with a fix to the documentation. The documentation repository can be found [here](https://github.com/latent-to/developer-docs). Additionally, note that the core implementation of Bittensor consists of two separate repositories: [The core Bittensor code](https://github.com/opentensor/bittensor) and the Bittensor Blockchain [subtensor](https://github.com/opentensor/subtensor). -Supplemental repository for the Bittensor subnet template can be found [here](https://github.com/opentensor/bittensor-subnet-template). This is a great first place to look for getting your hands dirty and start learning and building on Bittensor. See the subnet links [page](https://github.com/opentensor/bittensor-subnet-template/blob/main/subnet_links.json) for a list of all the repositories for the active registered subnets. +Supplemental repository for the Bittensor subnet template can be found [here](https://github.com/opentensor/bittensor-subnet-template). This is a great first place to look for getting your hands dirty and start learning and building on Bittensor. See the [Tao.app](https://www.tao.app/explorer) explorer for a list of all the repositories for the active registered subnets. ## Getting Started New contributors are very welcome and needed. @@ -59,12 +59,9 @@ You can start by looking through these `beginner` and `help-wanted` issues: * [Help wanted issues](https://github.com/opentensor/bittensor/labels/help%20wanted) - issues which should be a bit more involved than `beginner` issues. ## Communication Channels -Most communication about Bittensor development happens on Discord channel. -Here's the link of Discord community. -[Bittensor Discord](https://discord.com/channels/799672011265015819/799672011814862902) +Most communication about Bittensor development happens on [Discord](https://discord.gg/bittensor). -And also here. -[Bittensor Community Discord](https://discord.com/channels/1120750674595024897/1120799375703162950) +You can engage with the community in the [general](https://discord.com/channels/799672011265015819/799672011814862902) channel and follow the release announcements posted [here](https://discord.com/channels/799672011265015819/1359587876563718144). ## How Can I Contribute? @@ -93,7 +90,7 @@ Here is a high-level summary: > Review the Bittensor [style guide](./STYLE.md) and [development workflow](./DEVELOPMENT_WORKFLOW.md) before contributing. -If you're looking to contribute to Bittensor but unsure where to start, please join our community [discord](https://discord.gg/bittensor), a developer-friendly Bittensor town square. Start with [#development](https://discord.com/channels/799672011265015819/799678806159392768) and [#bounties](https://discord.com/channels/799672011265015819/1095684873810890883) to see what issues are currently posted. For a greater understanding of Bittensor's usage and development, check the [Bittensor Documentation](https://docs.bittensor.com). +If you're looking to contribute to Bittensor but unsure where to start, please join our community [discord](https://discord.gg/bittensor), a developer-friendly Bittensor town square. You can also browse through the GitHub [issues](https://github.com/opentensor/bittensor/issues) to see where help might be needed. For a greater understanding of Bittensor's usage and development, check the [Bittensor Documentation](https://docs.learnbittensor.org). #### Pull Request Philosophy @@ -125,7 +122,7 @@ Please follow these steps to have your contribution considered by the maintainer *Before* creating the PR: 1. Read the [development workflow](./DEVELOPMENT_WORKFLOW.md) defined for this repository to understand our workflow. -2. Ensure your PR meets the criteria stated in the 'Pull Request Philosophy' section. +2. Ensure your PR meets the criteria stated in the [Pull Request Philosophy](#pull-request-philosophy) section. 3. Include relevant tests for any fixed bugs or new features as stated in the [testing guide](./TESTING.md). 4. Follow all instructions in [the template](../.github/pull_request_template.md) to create the PR. 5. Ensure your commit messages are clear and concise. Include the issue number if applicable. @@ -224,7 +221,7 @@ Reviewers should include the commit(s) they have reviewed in their comments. Thi A pull request that changes consensus-critical code is considerably more involved than a pull request that adds a feature to the wallet, for example. Such patches must be reviewed and thoroughly tested by several reviewers who are knowledgeable about the changed subsystems. Where new features are proposed, it is helpful for reviewers to try out the patch set on a test network and indicate that they have done so in their review. Project maintainers will take this into consideration when merging changes. -For a more detailed description of the review process, see the [Code Review Guidelines](CODE_REVIEW_DOCS.md). +For a more detailed description of the review process, see the [Code Review Guidelines](./CODE_REVIEW_DOCS.md). ### Reporting Bugs @@ -237,8 +234,8 @@ When you are creating a bug report, please [include as many details as possible] #### Before Submitting A Bug Report * **Check the [debugging guide](./DEBUGGING.md).** You might be able to find the cause of the problem and fix things yourself. Most importantly, check if you can reproduce the problem in the latest version of Bittensor by updating to the latest Master branch changes. -* **Check the [Discord Server](https://discord.gg/7wvFuPJZgq)** and ask in [#finney-issues](https://discord.com/channels/799672011265015819/1064247007688007800) or [#subnet-1-issues](https://discord.com/channels/799672011265015819/1096187495667998790). -* **Determine which repository the problem should be reported in**: if it has to do with your ML model, then it's likely [Bittensor](https://github.com/opentensor/bittensor). If you are having problems with your emissions or Blockchain, then it is in [subtensor](https://github.com/opentensor/subtensor) +* **Check the [Discord Server](https://discord.gg/bittensor)** and ask in [#general](https://discord.com/channels/799672011265015819/799672011814862902). +* **Determine which repository the problem should be reported in**: if it has to do with your ML model, then it's likely [Bittensor](https://github.com/opentensor/bittensor). If you are having problems with your emissions or Blockchain, then it is in [subtensor](https://github.com/opentensor/subtensor). #### How Do I Submit A (Good) Bug Report? @@ -248,11 +245,11 @@ Explain the problem and include additional details to help maintainers reproduce * **Use a clear and descriptive title** for the issue to identify the problem. * **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started Bittensor, e.g. which command exactly you used in the terminal, or how you started Bittensor otherwise. When listing steps, **don't just say what you did, but explain how you did it**. For example, if you ran Bittensor with a set of custom configs, explain if you used a config file or command line arguments. -* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). +* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks). * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. * **Explain which behavior you expected to see instead and why.** -* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. -* **If you're reporting that Bittensor crashed**, include a crash report with a stack trace from the operating system. On macOS, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". Include the crash report in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://docs.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist. +* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [Licecap](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [Silentcast](https://github.com/colinkeenan/silentcast) or [byzanz-record](https://manpages.ubuntu.com/manpages/questing/en/man1/byzanz-record.1.html) on Linux. +* **If you're reporting that Bittensor crashed**, include a crash report with a stack trace from the operating system. On macOS, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". Include the crash report in the issue in a [code block](https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks), a [file attachment](https://docs.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist. * **If the problem is related to performance or memory**, include a CPU profile capture with your report, if you're using a GPU then include a GPU profile capture as well. Look into the [PyTorch Profiler](https://pytorch.org/tutorials/recipes/recipes/profiler_recipe.html) to look at memory usage of your model. * **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below. @@ -264,12 +261,12 @@ Provide more context by answering these questions: Include details about your configuration and environment: -* **Which version of Bittensor are you using?** You can get the version by checking for `__version__` in [`bittensor/bittensor/__init.py`](https://github.com/opentensor/bittensor/blob/master/bittensor/__init__.py#L30). This is not sufficient. Also add the commit hash of the branch you are on. -* **What commit hash are you on?** You can get the exact commit hash by checking `git log` and pasting the full commit hash. +* **Which version of Bittensor are you using?** You can get the version of the Bittensor SDK by executing the `python3 -m bittensor` command. +* **What commit hash are you on?** You can get the exact commit hash by executing `git rev-parse HEAD` and pasting the full commit hash. * **What's the name and version of the OS you're using**? * **Are you running Bittensor in a virtual machine?** If so, which VM software are you using and which operating systems and versions are used for the host and the guest? * **Are you running Bittensor in a dockerized container?** If so, have you made sure that your docker container contains your latest changes and is up to date with Master branch? -* **Are you using [local configuration files](https://opentensor.github.io/getting-started/configuration.html)** `config.yaml` to customize your Bittensor experiment? If so, provide the contents of that config file, preferably in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines) or with a link to a [gist](https://gist.github.com/). +* **Are you using [local configuration files](https://docs.learnbittensor.org/getting-started/install-btcli#configuration)** `config.yml` to customize your Bittensor experiment? If so, provide the contents of that config file, preferably in a [code block](https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks) or with a link to a [gist](https://gist.github.com/). ### Suggesting Enhancements and Features @@ -284,16 +281,16 @@ When you are creating an enhancement suggestion, please [include as many details #### How To Submit A (Good) Feature Suggestion -Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined which repository ([Bittensor](https://github.com/opentensor/bittensor) or [subtensor](https://github.com/opentensor/subtensor)) your enhancement suggestion is related to, create an issue on that repository and provide the following information: +Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined which repository ([Bittensor](https://github.com/opentensor/bittensor) or [subtensor](https://github.com/opentensor/subtensor)) your enhancement suggestion is related to, create an issue on that repository and provide the following information: * **Use a clear and descriptive title** for the issue to identify the problem. * **Provide a step-by-step description of the suggested enhancement** in as many details as possible. -* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). +* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks). * **Describe the current behavior** and **explain which behavior you expected to see instead** and why. -* **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of Bittensor which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. +* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [Licecap](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [Silentcast](https://github.com/colinkeenan/silentcast) or [byzanz-record](https://manpages.ubuntu.com/manpages/questing/en/man1/byzanz-record.1.html) on Linux. * **Explain why this enhancement would be useful** to most Bittensor users. * **List some other text editors or applications where this enhancement exists.** -* **Specify which version of Bittensor are you using?** You can get the exact version by checking for `__version__` in [`bittensor/bittensor/__init.py`](https://github.com/opentensor/bittensor/blob/master/bittensor/__init__.py#L30). +* **Specify which version of Bittensor are you using?** You can get the version of the Bittensor SDK by executing the `python3 -m bittensor` command. * **Specify the name and version of the OS you're using.** Thank you for considering contributing to Bittensor! Any help is greatly appreciated along this journey to incentivize open and permissionless intelligence. diff --git a/pyproject.toml b/pyproject.toml index cfa7b2997e..19b5eebaaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor" -version = "9.10.1" +version = "9.11.0" description = "Bittensor" readme = "README.md" authors = [ @@ -13,14 +13,13 @@ authors = [ license = { file = "LICENSE" } requires-python = ">=3.9,<3.14" dependencies = [ - "wheel", "setuptools~=70.0.0", "aiohttp~=3.9", "asyncstdlib~=3.13.0", "colorama~=0.4.6", "fastapi~=0.110.1", - "munch~=2.5.0", + "munch>=4.0.0", "numpy>=2.0.1,<3.0.0", "msgpack-numpy-opentensor~=0.5.0", "nest_asyncio==1.6.0", @@ -36,7 +35,7 @@ dependencies = [ "uvicorn", "bittensor-drand>=1.0.0,<2.0.0", "bittensor-wallet>=4.0.0,<5.0", - "async-substrate-interface>=1.5.1" + "async-substrate-interface>=1.5.4" ] [project.optional-dependencies] @@ -66,7 +65,7 @@ torch = [ "torch>=1.13.1,<3.0" ] cli = [ - "bittensor-cli>=9.0.2" + "bittensor-cli>=9.11.2" ] @@ -95,4 +94,4 @@ classifiers = [ [tool.setuptools] package-dir = {"bittensor" = "bittensor"} -script-files = ["bittensor/utils/certifi.sh"] \ No newline at end of file +script-files = ["bittensor/utils/certifi.sh"] diff --git a/tests/e2e_tests/test_commit_reveal.py b/tests/e2e_tests/test_commit_reveal.py index 2531326583..61f6bdeeed 100644 --- a/tests/e2e_tests/test_commit_reveal.py +++ b/tests/e2e_tests/test_commit_reveal.py @@ -1,8 +1,18 @@ +import asyncio import re +import time import numpy as np import pytest +from bittensor.core.extrinsics.asyncex.sudo import ( + sudo_set_mechanism_count_extrinsic as async_sudo_set_mechanism_count_extrinsic, + sudo_set_admin_freeze_window_extrinsic as async_sudo_set_admin_freeze_window_extrinsic, +) +from bittensor.core.extrinsics.sudo import ( + sudo_set_mechanism_count_extrinsic, + sudo_set_admin_freeze_window_extrinsic, +) from bittensor.utils.btlogging import logging from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit from tests.e2e_tests.utils.chain_interactions import ( @@ -13,6 +23,8 @@ next_tempo, ) +TESTED_SUB_SUBNETS = 2 + # @pytest.mark.parametrize("local_chain", [True], indirect=True) @pytest.mark.asyncio @@ -33,6 +45,9 @@ async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_walle """ logging.console.info("Testing `test_commit_and_reveal_weights_cr4`") + # turn off admin freeze window limit for testing + assert sudo_set_admin_freeze_window_extrinsic(subtensor, alice_wallet, 0) + # 12 for non-fast-block, 0.25 for fast block BLOCK_TIME, TEMPO_TO_SET = ( (0.25, 100) if subtensor.chain.is_fast_blocks() else (12.0, 20) @@ -52,6 +67,10 @@ async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_walle f"SN #{alice_subnet_netuid} wasn't created successfully" ) + assert sudo_set_mechanism_count_extrinsic( + subtensor, alice_wallet, alice_subnet_netuid, TESTED_SUB_SUBNETS + ), "Cannot create sub-subnets." + logging.console.success(f"SN #{alice_subnet_netuid} is registered.") # Enable commit_reveal on the subnet @@ -145,88 +164,110 @@ async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_walle f"Post first wait_interval (to ensure window isn't too low): {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" ) - # commit_block is the block when weights were committed on the chain (transaction block) - expected_commit_block = subtensor.block + 1 - # Commit weights - success, message = subtensor.extrinsics.set_weights( - wallet=alice_wallet, - netuid=alice_subnet_netuid, - uids=weight_uids, - weights=weight_vals, - wait_for_inclusion=True, - wait_for_finalization=True, - block_time=BLOCK_TIME, - period=16, - ) + for mechid in range(TESTED_SUB_SUBNETS): + logging.console.info( + f"[magenta]Testing subnet mechanism: {alice_subnet_netuid}.{mechid}[/magenta]" + ) - # Assert committing was a success - assert success is True, message - assert bool(re.match(r"reveal_round:\d+", message)) + # commit_block is the block when weights were committed on the chain (transaction block) + expected_commit_block = subtensor.block + 1 + # Commit weights + success, message = subtensor.extrinsics.set_weights( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + mechid=mechid, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=True, + block_time=BLOCK_TIME, + period=16, + ) - # Parse expected reveal_round - expected_reveal_round = int(message.split(":")[1]) - logging.console.success( - f"Successfully set weights: uids {weight_uids}, weights {weight_vals}, reveal_round: {expected_reveal_round}" - ) + # Assert committing was a success + assert success is True, message + assert bool(re.match(r"reveal_round:\d+", message)) - # Fetch current commits pending on the chain - commits_on_chain = subtensor.commitments.get_timelocked_weight_commits( - netuid=alice_subnet_netuid - ) - address, commit_block, commit, reveal_round = commits_on_chain[0] + # Parse expected reveal_round + expected_reveal_round = int(message.split(":")[1]) + logging.console.success( + f"Successfully set weights: uids {weight_uids}, weights {weight_vals}, reveal_round: {expected_reveal_round}" + ) - # Assert correct values are committed on the chain - assert expected_reveal_round == reveal_round - assert address == alice_wallet.hotkey.ss58_address + # let chain to update + subtensor.wait_for_block(subtensor.block + 1) - # bc of the drand delay, the commit block can be either the previous block or the current block - assert expected_commit_block in [commit_block - 1, commit_block, commit_block + 1] + # Fetch current commits pending on the chain + commits_on_chain = subtensor.commitments.get_timelocked_weight_commits( + netuid=alice_subnet_netuid, mechid=mechid + ) + address, commit_block, commit, reveal_round = commits_on_chain[0] - # Ensure no weights are available as of now - assert subtensor.weights(netuid=alice_subnet_netuid) == [] - logging.console.success("No weights are available before next epoch.") + # Assert correct values are committed on the chain + assert expected_reveal_round == reveal_round + assert address == alice_wallet.hotkey.ss58_address - # 5 is safety drand offset - expected_reveal_block = ( - subtensor.subnets.get_next_epoch_start_block(alice_subnet_netuid) + 5 - ) + # bc of the drand delay, the commit block can be either the previous block or the current block + assert expected_commit_block in [ + commit_block - 1, + commit_block, + commit_block + 1, + ] - logging.console.info( - f"Waiting for the next epoch to ensure weights are revealed: block {expected_reveal_block}" - ) - subtensor.wait_for_block(expected_reveal_block) + # Ensure no weights are available as of now + assert subtensor.weights(netuid=alice_subnet_netuid, mechid=mechid) == [] + logging.console.success("No weights are available before next epoch.") - # Fetch the latest drand pulse - latest_drand_round = subtensor.chain.last_drand_round() - logging.console.info( - f"Latest drand round after waiting for tempo: {latest_drand_round}" - ) + # 5 is safety drand offset + expected_reveal_block = ( + subtensor.subnets.get_next_epoch_start_block(alice_subnet_netuid) + 5 + ) - # Fetch weights on the chain as they should be revealed now - subnet_weights = subtensor.subnets.weights(netuid=alice_subnet_netuid) - assert subnet_weights != [], "Weights are not available yet." + logging.console.info( + f"Waiting for the next epoch to ensure weights are revealed: block {expected_reveal_block}" + ) + subtensor.wait_for_block(expected_reveal_block) - logging.console.info(f"Revealed weights: {subnet_weights}") + # Fetch the latest drand pulse + latest_drand_round = 0 - revealed_weights = subnet_weights[0][1] - # Assert correct weights were revealed - assert weight_uids[0] == revealed_weights[0][0] - assert weight_vals[0] == revealed_weights[0][1] + while latest_drand_round <= expected_reveal_round: + latest_drand_round = subtensor.chain.last_drand_round() + logging.console.info( + f"Latest drand round: {latest_drand_round}, waiting for next round..." + ) + # drand round is 2 + time.sleep(3) - logging.console.success( - f"Successfully revealed weights: uids {weight_uids}, weights {weight_vals}" - ) + # Fetch weights on the chain as they should be revealed now + subnet_weights = subtensor.subnets.weights( + netuid=alice_subnet_netuid, mechid=mechid + ) + assert subnet_weights != [], "Weights are not available yet." - # Now that the commit has been revealed, there shouldn't be any pending commits - assert ( - subtensor.commitments.get_timelocked_weight_commits(netuid=alice_subnet_netuid) - == [] - ) + logging.console.info(f"Revealed weights: {subnet_weights}") - # Ensure the drand_round is always in the positive w.r.t expected when revealed - assert latest_drand_round - expected_reveal_round >= -3, ( - f"latest_drand_round ({latest_drand_round}) is less than expected_reveal_round ({expected_reveal_round})" - ) + revealed_weights = subnet_weights[0][1] + # Assert correct weights were revealed + assert weight_uids[0] == revealed_weights[0][0] + assert weight_vals[0] == revealed_weights[0][1] + + logging.console.success( + f"Successfully revealed weights: uids {weight_uids}, weights {weight_vals}" + ) + + # Now that the commit has been revealed, there shouldn't be any pending commits + assert ( + subtensor.commitments.get_timelocked_weight_commits( + netuid=alice_subnet_netuid, mechid=mechid + ) + == [] + ) + + # Ensure the drand_round is always in the positive w.r.t expected when revealed + assert latest_drand_round - expected_reveal_round >= -3, ( + f"latest_drand_round ({latest_drand_round}) is less than expected_reveal_round ({expected_reveal_round})" + ) logging.console.success("✅ Passed `test_commit_and_reveal_weights_cr4`") @@ -249,122 +290,134 @@ async def test_async_commit_and_reveal_weights_cr4( Raises: AssertionError: If any of the checks or verifications fail """ + logging.console.info("Testing `test_commit_and_reveal_weights_cr4`") - async with async_subtensor: - # 12 for non-fast-block, 0.25 for fast block - BLOCK_TIME, TEMPO_TO_SET = ( - (0.25, 100) if await async_subtensor.chain.is_fast_blocks() else (12.0, 20) - ) + # turn off admin freeze window limit for testing + assert await async_sudo_set_admin_freeze_window_extrinsic( + async_subtensor, alice_wallet, 0 + ) - logging.console.info(f"Using block time: {BLOCK_TIME}") + # 12 for non-fast-block, 0.25 for fast block + BLOCK_TIME, TEMPO_TO_SET = ( + (0.25, 100) if (await async_subtensor.chain.is_fast_blocks()) else (12.0, 20) + ) - alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + logging.console.info(f"Using block time: {BLOCK_TIME}") - # Register root as Alice - assert await async_subtensor.extrinsics.register_subnet(alice_wallet), ( - "Unable to register the subnet" - ) + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 - # Verify subnet 2 created successfully - assert await async_subtensor.subnet_exists(alice_subnet_netuid), ( - f"SN #{alice_subnet_netuid} wasn't created successfully" - ) + # Register root as Alice + assert await async_subtensor.extrinsics.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Verify subnet 2 created successfully + assert await async_subtensor.subnet_exists(alice_subnet_netuid), ( + f"SN #{alice_subnet_netuid} wasn't created successfully" + ) - logging.console.success(f"SN #{alice_subnet_netuid} is registered.") + assert await async_sudo_set_mechanism_count_extrinsic( + async_subtensor, alice_wallet, alice_subnet_netuid, TESTED_SUB_SUBNETS + ), "Cannot create sub-subnets." - # Enable commit_reveal on the subnet - assert sudo_set_hyperparameter_bool( - substrate=local_chain, - wallet=alice_wallet, - call_function="sudo_set_commit_reveal_weights_enabled", - value=True, - netuid=alice_subnet_netuid, - ), f"Unable to enable commit reveal on the SN #{alice_subnet_netuid}" + logging.console.success(f"SN #{alice_subnet_netuid} is registered.") - # Verify commit_reveal was enabled - assert await async_subtensor.subnets.commit_reveal_enabled( - alice_subnet_netuid - ), "Failed to enable commit/reveal" - logging.console.success("Commit reveal enabled") + # Enable commit_reveal on the subnet + assert sudo_set_hyperparameter_bool( + substrate=local_chain, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + value=True, + netuid=alice_subnet_netuid, + ), f"Unable to enable commit reveal on the SN #{alice_subnet_netuid}" - cr_version = await async_subtensor.substrate.query( - module="SubtensorModule", storage_function="CommitRevealWeightsVersion" - ) - assert cr_version == 4, f"Commit reveal version is not 3, got {cr_version}" + # Verify commit_reveal was enabled + assert await async_subtensor.subnets.commit_reveal_enabled(alice_subnet_netuid), ( + "Failed to enable commit/reveal" + ) + logging.console.success("Commit reveal enabled") - # Change the weights rate limit on the subnet - status, error = sudo_set_admin_utils( - substrate=local_chain, - wallet=alice_wallet, - call_function="sudo_set_weights_set_rate_limit", - call_params={"netuid": alice_subnet_netuid, "weights_set_rate_limit": "0"}, - ) + cr_version = await async_subtensor.substrate.query( + module="SubtensorModule", storage_function="CommitRevealWeightsVersion" + ) + assert cr_version == 4, f"Commit reveal version is not 3, got {cr_version}" - assert status is True - assert error is None + # Change the weights rate limit on the subnet + status, error = sudo_set_admin_utils( + substrate=local_chain, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": alice_subnet_netuid, "weights_set_rate_limit": "0"}, + ) - # Verify weights rate limit was changed - assert ( - await async_subtensor.subnets.get_subnet_hyperparameters( - netuid=alice_subnet_netuid - ) - ).weights_rate_limit == 0, "Failed to set weights_rate_limit" - assert await async_subtensor.weights_rate_limit(netuid=alice_subnet_netuid) == 0 - logging.console.success("sudo_set_weights_set_rate_limit executed: set to 0") + assert status is True + assert error is None - # Change the tempo of the subnet - assert ( - sudo_set_admin_utils( - local_chain, - alice_wallet, - call_function="sudo_set_tempo", - call_params={"netuid": alice_subnet_netuid, "tempo": TEMPO_TO_SET}, - )[0] - is True + # Verify weights rate limit was changed + assert ( + await async_subtensor.subnets.get_subnet_hyperparameters( + netuid=alice_subnet_netuid ) + ).weights_rate_limit == 0, "Failed to set weights_rate_limit" + assert await async_subtensor.weights_rate_limit(netuid=alice_subnet_netuid) == 0 + logging.console.success("sudo_set_weights_set_rate_limit executed: set to 0") - tempo = ( - await async_subtensor.subnets.get_subnet_hyperparameters( - netuid=alice_subnet_netuid - ) - ).tempo - assert tempo == TEMPO_TO_SET, "SN tempos has not been changed." - logging.console.success( - f"SN #{alice_subnet_netuid} tempo set to {TEMPO_TO_SET}" + # Change the tempo of the subnet + assert ( + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": alice_subnet_netuid, "tempo": TEMPO_TO_SET}, + )[0] + is True + ) + + tempo = ( + await async_subtensor.subnets.get_subnet_hyperparameters( + netuid=alice_subnet_netuid ) + ).tempo + assert tempo == TEMPO_TO_SET, "SN tempos has not been changed." + logging.console.success(f"SN #{alice_subnet_netuid} tempo set to {TEMPO_TO_SET}") - # Commit-reveal values - setting weights to self - uids = np.array([0], dtype=np.int64) - weights = np.array([0.1], dtype=np.float32) + # Commit-reveal values - setting weights to self + uids = np.array([0], dtype=np.int64) + weights = np.array([0.1], dtype=np.float32) - weight_uids, weight_vals = convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) - logging.console.info( - f"Committing weights: uids {weight_uids}, weights {weight_vals}" - ) + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) + logging.console.info( + f"Committing weights: uids {weight_uids}, weights {weight_vals}" + ) - # Fetch current block and calculate next tempo for the subnet - current_block = await async_subtensor.chain.get_current_block() - upcoming_tempo = next_tempo(current_block, tempo) - logging.console.info( - f"Checking if window is too low with Current block: {current_block}, next tempo: {upcoming_tempo}" + # Fetch current block and calculate next tempo for the subnet + current_block = await async_subtensor.chain.get_current_block() + upcoming_tempo = next_tempo(current_block, tempo) + logging.console.info( + f"Checking if window is too low with Current block: {current_block}, next tempo: {upcoming_tempo}" + ) + + # Lower than this might mean weights will get revealed before we can check them + if upcoming_tempo - current_block < 6: + await async_wait_interval( + tempo, + async_subtensor, + netuid=alice_subnet_netuid, + reporting_interval=1, ) + current_block = await async_subtensor.chain.get_current_block() + latest_drand_round = await async_subtensor.chain.last_drand_round() + upcoming_tempo = next_tempo(current_block, tempo) + logging.console.info( + f"Post first wait_interval (to ensure window isn't too low): {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" + ) - # Lower than this might mean weights will get revealed before we can check them - if upcoming_tempo - current_block < 6: - await async_wait_interval( - tempo, - async_subtensor, - netuid=alice_subnet_netuid, - reporting_interval=1, - ) - current_block = await async_subtensor.chain.get_current_block() - latest_drand_round = await async_subtensor.chain.last_drand_round() - upcoming_tempo = next_tempo(current_block, tempo) + for mechid in range(TESTED_SUB_SUBNETS): logging.console.info( - f"Post first wait_interval (to ensure window isn't too low): {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" + f"[magenta]Testing subnet mechanism: {alice_subnet_netuid}.{mechid}[/magenta]" ) # commit_block is the block when weights were committed on the chain (transaction block) @@ -373,6 +426,7 @@ async def test_async_commit_and_reveal_weights_cr4( success, message = await async_subtensor.extrinsics.set_weights( wallet=alice_wallet, netuid=alice_subnet_netuid, + mechid=mechid, uids=weight_uids, weights=weight_vals, wait_for_inclusion=True, @@ -392,9 +446,11 @@ async def test_async_commit_and_reveal_weights_cr4( ) # Fetch current commits pending on the chain + await async_subtensor.wait_for_block(await async_subtensor.block + 12) + commits_on_chain = ( await async_subtensor.commitments.get_timelocked_weight_commits( - netuid=alice_subnet_netuid + netuid=alice_subnet_netuid, mechid=mechid ) ) address, commit_block, commit, reveal_round = commits_on_chain[0] @@ -407,7 +463,10 @@ async def test_async_commit_and_reveal_weights_cr4( # assert expected_commit_block in [commit_block - 1, commit_block, commit_block + 1] # Ensure no weights are available as of now - assert await async_subtensor.weights(netuid=alice_subnet_netuid) == [] + assert ( + await async_subtensor.weights(netuid=alice_subnet_netuid, mechid=mechid) + == [] + ) logging.console.success("No weights are available before next epoch.") # 5 is safety drand offset @@ -424,14 +483,19 @@ async def test_async_commit_and_reveal_weights_cr4( await async_subtensor.wait_for_block(expected_reveal_block) # Fetch the latest drand pulse - latest_drand_round = await async_subtensor.chain.last_drand_round() - logging.console.info( - f"Latest drand round after waiting for tempo: {latest_drand_round}" - ) + latest_drand_round = 0 + + while latest_drand_round <= expected_reveal_round: + latest_drand_round = await async_subtensor.chain.last_drand_round() + logging.console.info( + f"Latest drand round: {latest_drand_round}, waiting for next round..." + ) + # drand round is 2 + await asyncio.sleep(3) # Fetch weights on the chain as they should be revealed now subnet_weights = await async_subtensor.subnets.weights( - netuid=alice_subnet_netuid + netuid=alice_subnet_netuid, mechid=mechid ) assert subnet_weights != [], "Weights are not available yet." @@ -449,7 +513,7 @@ async def test_async_commit_and_reveal_weights_cr4( # Now that the commit has been revealed, there shouldn't be any pending commits assert ( await async_subtensor.commitments.get_timelocked_weight_commits( - netuid=alice_subnet_netuid + netuid=alice_subnet_netuid, mechid=mechid ) == [] ) diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index e13964c34c..4de52ecf45 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -2,6 +2,11 @@ import pytest import retry +from bittensor.core.extrinsics.sudo import ( + sudo_set_mechanism_count_extrinsic, + sudo_set_admin_freeze_window_extrinsic, +) +from bittensor.utils import get_mechid_storage_index from bittensor.utils.btlogging import logging from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit from tests.e2e_tests.utils.chain_interactions import ( @@ -11,6 +16,8 @@ wait_epoch, ) +TESTED_SUB_SUBNETS = 2 + @pytest.mark.asyncio async def test_commit_and_reveal_weights_legacy(local_chain, subtensor, alice_wallet): @@ -26,8 +33,14 @@ async def test_commit_and_reveal_weights_legacy(local_chain, subtensor, alice_wa Raises: AssertionError: If any of the checks or verifications fail """ + + # turn off admin freeze window limit for testing + assert sudo_set_admin_freeze_window_extrinsic(subtensor, alice_wallet, 0), ( + "Failed to set admin freeze window to 0" + ) + netuid = subtensor.get_total_subnets() # 2 - set_tempo = 100 if subtensor.is_fast_blocks() else 10 + set_tempo = 50 if subtensor.is_fast_blocks() else 20 print("Testing test_commit_and_reveal_weights") # Register root as Alice @@ -36,6 +49,10 @@ async def test_commit_and_reveal_weights_legacy(local_chain, subtensor, alice_wa # Verify subnet 2 created successfully assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" + assert sudo_set_mechanism_count_extrinsic( + subtensor, alice_wallet, netuid, TESTED_SUB_SUBNETS + ), "Cannot create sub-subnets." + # Enable commit_reveal on the subnet assert sudo_set_hyperparameter_bool( local_chain, @@ -82,71 +99,76 @@ async def test_commit_and_reveal_weights_legacy(local_chain, subtensor, alice_wa }, ) - # Commit-reveal values - uids = np.array([0], dtype=np.int64) - weights = np.array([0.1], dtype=np.float32) - salt = [18, 179, 107, 0, 165, 211, 141, 197] - weight_uids, weight_vals = convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) + for mechid in range(TESTED_SUB_SUBNETS): + logging.console.info( + f"[magenta]Testing subnet mechanism {netuid}.{mechid}[/magenta]" + ) - # Commit weights - success, message = subtensor.commit_weights( - alice_wallet, - netuid, - salt=salt, - uids=weight_uids, - weights=weight_vals, - wait_for_inclusion=True, - wait_for_finalization=True, - ) + # Commit-reveal values + uids = np.array([0], dtype=np.int64) + weights = np.array([0.1], dtype=np.float32) + salt = [18, 179, 107, 0, 165, 211, 141, 197] + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) - assert success is True + # Commit weights + success, message = subtensor.commit_weights( + wallet=alice_wallet, + netuid=netuid, + mechid=mechid, + salt=salt, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=True, + ) - weight_commits = subtensor.query_module( - module="SubtensorModule", - name="WeightCommits", - params=[netuid, alice_wallet.hotkey.ss58_address], - ) - # Assert that the committed weights are set correctly - assert weight_commits is not None, "Weight commit not found in storage" - commit_hash, commit_block, reveal_block, expire_block = weight_commits[0] - assert commit_block > 0, f"Invalid block number: {commit_block}" + assert success is True, message - # Query the WeightCommitRevealInterval storage map - assert subtensor.get_subnet_reveal_period_epochs(netuid) > 0, ( - "Invalid RevealPeriodEpochs" - ) + storage_index = get_mechid_storage_index(netuid, mechid) + weight_commits = subtensor.query_module( + module="SubtensorModule", + name="WeightCommits", + params=[storage_index, alice_wallet.hotkey.ss58_address], + ) + # Assert that the committed weights are set correctly + assert weight_commits is not None, "Weight commit not found in storage" + commit_hash, commit_block, reveal_block, expire_block = weight_commits[0] + assert commit_block > 0, f"Invalid block number: {commit_block}" + + # Query the WeightCommitRevealInterval storage map + assert subtensor.get_subnet_reveal_period_epochs(netuid) > 0, ( + "Invalid RevealPeriodEpochs" + ) - # Wait until the reveal block range - await wait_epoch(subtensor, netuid) + # Wait until the reveal block range + await wait_epoch(subtensor, netuid) - # Reveal weights - success, message = subtensor.reveal_weights( - alice_wallet, - netuid, - uids=weight_uids, - weights=weight_vals, - salt=salt, - wait_for_inclusion=True, - wait_for_finalization=True, - ) + # Reveal weights + success, message = subtensor.reveal_weights( + wallet=alice_wallet, + netuid=netuid, + mechid=mechid, + uids=weight_uids, + weights=weight_vals, + salt=salt, + wait_for_inclusion=True, + wait_for_finalization=True, + ) - assert success is True + assert success is True, message - # Query the Weights storage map - revealed_weights = subtensor.query_module( - module="SubtensorModule", - name="Weights", - params=[netuid, 0], # netuid and uid - ) + revealed_weights = subtensor.weights(netuid, mechid=mechid) - # Assert that the revealed weights are set correctly - assert revealed_weights is not None, "Weight reveal not found in storage" + # Assert that the revealed weights are set correctly + assert revealed_weights is not None, "Weight reveal not found in storage" + + alice_weights = revealed_weights[0][1] + assert weight_vals[0] == alice_weights[0][1], ( + f"Incorrect revealed weights. Expected: {weights[0]}, Actual: {revealed_weights[0][1]}" + ) - assert weight_vals[0] == revealed_weights[0][1], ( - f"Incorrect revealed weights. Expected: {weights[0]}, Actual: {revealed_weights[0][1]}" - ) print("✅ Passed test_commit_and_reveal_weights") @@ -165,7 +187,13 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall Raises: AssertionError: If any of the checks or verifications fail """ - subnet_tempo = 50 if subtensor.is_fast_blocks() else 10 + + # turn off admin freeze window limit for testing + assert sudo_set_admin_freeze_window_extrinsic(subtensor, alice_wallet, 0), ( + "Failed to set admin freeze window to 0" + ) + + subnet_tempo = 50 if subtensor.is_fast_blocks() else 20 netuid = subtensor.get_total_subnets() # 2 # Wait for 2 tempos to pass as CR3 only reveals weights after 2 tempos @@ -180,8 +208,8 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall # weights sensitive to epoch changes assert sudo_set_admin_utils( - local_chain, - alice_wallet, + substrate=local_chain, + wallet=alice_wallet, call_function="sudo_set_tempo", call_params={ "netuid": netuid, @@ -191,11 +219,11 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall # Enable commit_reveal on the subnet assert sudo_set_hyperparameter_bool( - local_chain, - alice_wallet, - "sudo_set_commit_reveal_weights_enabled", - True, - netuid, + substrate=local_chain, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + value=True, + netuid=netuid, ), "Unable to enable commit reveal on the subnet" assert subtensor.commit_reveal_enabled(netuid), "Failed to enable commit/reveal" diff --git a/tests/e2e_tests/test_hotkeys.py b/tests/e2e_tests/test_hotkeys.py index 5831e4bf27..36d1bbfc4d 100644 --- a/tests/e2e_tests/test_hotkeys.py +++ b/tests/e2e_tests/test_hotkeys.py @@ -3,7 +3,7 @@ from bittensor.core.errors import ( NotEnoughStakeToSetChildkeys, RegistrationNotPermittedOnRootSubnet, - SubNetworkDoesNotExist, + SubnetNotExists, InvalidChild, TooManyChildren, ProportionOverflow, @@ -11,11 +11,13 @@ TxRateLimitExceeded, NonAssociatedColdKey, ) +from bittensor.core.extrinsics.sudo import ( + sudo_set_admin_freeze_window_extrinsic, +) from bittensor.utils.btlogging import logging from tests.e2e_tests.utils.chain_interactions import sudo_set_admin_utils from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call - SET_CHILDREN_RATE_LIMIT = 15 ROOT_COOLDOWN = 15 # blocks @@ -86,6 +88,9 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w - Clear children list """ + # turn off admin freeze window limit for testing + assert sudo_set_admin_freeze_window_extrinsic(subtensor, alice_wallet, 0) + dave_subnet_netuid = subtensor.get_total_subnets() # 2 set_tempo = 10 # affect to non-fast-blocks mode @@ -146,7 +151,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w raise_error=True, ) - with pytest.raises(SubNetworkDoesNotExist): + with pytest.raises(SubnetNotExists): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, @@ -241,8 +246,8 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w ) success, error = subtensor.set_children( - alice_wallet, - alice_wallet.hotkey.ss58_address, + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, netuid=dave_subnet_netuid, children=[ ( @@ -260,7 +265,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w # children not set yet (have to wait cool-down period) success, children, error = subtensor.get_children( - alice_wallet.hotkey.ss58_address, + hotkey=alice_wallet.hotkey.ss58_address, block=set_children_block, netuid=dave_subnet_netuid, ) @@ -271,7 +276,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w # children are in pending state pending, cooldown = subtensor.get_children_pending( - alice_wallet.hotkey.ss58_address, + hotkey=alice_wallet.hotkey.ss58_address, netuid=dave_subnet_netuid, ) @@ -281,7 +286,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w subtensor.wait_for_block(cooldown + SET_CHILDREN_RATE_LIMIT * 2) success, children, error = subtensor.get_children( - alice_wallet.hotkey.ss58_address, + hotkey=alice_wallet.hotkey.ss58_address, netuid=dave_subnet_netuid, ) @@ -295,7 +300,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w # pending queue is empty pending, cooldown = subtensor.get_children_pending( - alice_wallet.hotkey.ss58_address, + hotkey=alice_wallet.hotkey.ss58_address, netuid=dave_subnet_netuid, ) assert pending == [] @@ -303,25 +308,26 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w with pytest.raises(TxRateLimitExceeded): set_children_block = subtensor.get_current_block() subtensor.set_children( - alice_wallet, - alice_wallet.hotkey.ss58_address, + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, netuid=dave_subnet_netuid, children=[], raise_error=True, ) subtensor.set_children( - alice_wallet, - alice_wallet.hotkey.ss58_address, + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, netuid=dave_subnet_netuid, children=[], raise_error=True, ) - subtensor.wait_for_block(set_children_block + SET_CHILDREN_RATE_LIMIT) + # wait for rate limit to expire + 1 block to ensure that the rate limit is expired + subtensor.wait_for_block(set_children_block + SET_CHILDREN_RATE_LIMIT + 1) subtensor.set_children( - alice_wallet, - alice_wallet.hotkey.ss58_address, + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, netuid=dave_subnet_netuid, children=[], raise_error=True, @@ -329,16 +335,16 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_w set_children_block = subtensor.get_current_block() pending, cooldown = subtensor.get_children_pending( - alice_wallet.hotkey.ss58_address, + hotkey=alice_wallet.hotkey.ss58_address, netuid=dave_subnet_netuid, ) assert pending == [] - subtensor.wait_for_block(cooldown) + subtensor.wait_for_block(cooldown + 1) success, children, error = subtensor.get_children( - alice_wallet.hotkey.ss58_address, + hotkey=alice_wallet.hotkey.ss58_address, netuid=dave_subnet_netuid, ) diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index 8e834eb7f7..cf1289b977 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -5,15 +5,12 @@ from bittensor.utils.btlogging import logging from tests.e2e_tests.utils.chain_interactions import ( sudo_set_admin_utils, - wait_epoch, ) from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call -DURATION_OF_START_CALL = 10 - @pytest.mark.asyncio -async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wallet): +async def test_incentive(subtensor, templates, alice_wallet, bob_wallet): """ Test the incentive mechanism and interaction of miners/validators @@ -25,9 +22,19 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa Raises: AssertionError: If any of the checks or verifications fail """ - - print("Testing test_incentive") - alice_subnet_netuid = subtensor.get_total_subnets() # 2 + logging.console.info("Testing [blue]test_incentive[/blue]") + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + + # turn off admin freeze window limit for testing + assert ( + sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_admin_freeze_window", + call_params={"window": 0}, + )[0] + is True + ), "Failed to set admin freeze window to 0" # Register root as Alice - the subnet owner and validator assert subtensor.register_subnet(alice_wallet, True, True), "Subnet wasn't created" @@ -39,8 +46,8 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa # Disable commit_reveal on the subnet to check proper behavior status, error = sudo_set_admin_utils( - local_chain, - alice_wallet, + substrate=subtensor.substrate, + wallet=alice_wallet, call_function="sudo_set_commit_reveal_weights_enabled", call_params={ "netuid": alice_subnet_netuid, @@ -62,8 +69,9 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa ) # Wait for the first epoch to pass - await wait_epoch(subtensor, alice_subnet_netuid) - + subtensor.wait_for_block( + subtensor.subnets.get_next_epoch_start_block(alice_subnet_netuid) + 1 + ) # Get current miner/validator stats alice_neuron = subtensor.neurons(netuid=alice_subnet_netuid)[0] @@ -84,8 +92,8 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa # update weights_set_rate_limit for fast-blocks tempo = subtensor.tempo(alice_subnet_netuid) status, error = sudo_set_admin_utils( - local_chain, - alice_wallet, + substrate=subtensor.substrate, + wallet=alice_wallet, call_function="sudo_set_weights_set_rate_limit", call_params={ "netuid": alice_subnet_netuid, diff --git a/tests/e2e_tests/test_liquid_alpha.py b/tests/e2e_tests/test_liquid_alpha.py index d907f53598..f505b093ec 100644 --- a/tests/e2e_tests/test_liquid_alpha.py +++ b/tests/e2e_tests/test_liquid_alpha.py @@ -3,7 +3,9 @@ from tests.e2e_tests.utils.chain_interactions import ( sudo_set_hyperparameter_bool, sudo_set_hyperparameter_values, + sudo_set_admin_utils, ) +from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call def liquid_alpha_call_params(netuid: int, alpha_values: str): @@ -28,33 +30,48 @@ def test_liquid_alpha(local_chain, subtensor, alice_wallet): Raises: AssertionError: If any of the checks or verifications fail """ + # turn off admin freeze window limit for testing + assert ( + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_admin_freeze_window", + call_params={"window": 0}, + )[0] + is True + ), "Failed to set admin freeze window to 0" + u16_max = 65535 netuid = 2 logging.console.info("Testing test_liquid_alpha_enabled") # Register root as Alice - assert subtensor.register_subnet(alice_wallet), "Unable to register the subnet" + assert subtensor.register_subnet(alice_wallet), "Unable to register the subnet." # Verify subnet created successfully assert subtensor.subnet_exists(netuid) + assert wait_to_start_call(subtensor, alice_wallet, netuid), ( + "Subnet failed to start." + ) + # Register a neuron (Alice) to the subnet assert subtensor.burned_register(alice_wallet, netuid), ( "Unable to register Alice as a neuron" ) # Stake to become to top neuron after the first epoch - subtensor.add_stake( - alice_wallet, + assert subtensor.add_stake( + wallet=alice_wallet, netuid=netuid, amount=Balance.from_tao(10_000), - ) + ), "Failed to stake" # Assert liquid alpha is disabled assert ( subtensor.get_subnet_hyperparameters(netuid=netuid).liquid_alpha_enabled is False - ), "Liquid alpha is enabled by default" + ), "Liquid alpha is enabled by default." # Attempt to set alpha high/low while disabled (should fail) alpha_values = "6553, 53083" @@ -66,7 +83,7 @@ def test_liquid_alpha(local_chain, subtensor, alice_wallet): call_params=call_params, return_error_message=True, ) - assert result is False, "Alpha values set while being disabled" + assert result is False, "Alpha values set while being disabled." assert error_message["name"] == "LiquidAlphaDisabled" # Enabled liquid alpha on the subnet @@ -76,7 +93,7 @@ def test_liquid_alpha(local_chain, subtensor, alice_wallet): assert subtensor.get_subnet_hyperparameters( netuid, - ).liquid_alpha_enabled, "Failed to enable liquid alpha" + ).liquid_alpha_enabled, "Failed to enable liquid alpha." # Attempt to set alpha high & low after enabling the hyperparameter alpha_values = "26001, 54099" @@ -86,12 +103,12 @@ def test_liquid_alpha(local_chain, subtensor, alice_wallet): alice_wallet, call_function="sudo_set_alpha_values", call_params=call_params, - ), "Unable to set alpha_values" + ), "Unable to set alpha_values." assert subtensor.get_subnet_hyperparameters(netuid).alpha_high == 54099, ( - "Failed to set alpha high" + "Failed to set alpha high." ) assert subtensor.get_subnet_hyperparameters(netuid).alpha_low == 26001, ( - "Failed to set alpha low" + "Failed to set alpha low." ) # Testing alpha high upper and lower bounds @@ -99,6 +116,17 @@ def test_liquid_alpha(local_chain, subtensor, alice_wallet): # 1. Test setting Alpha_high too low alpha_high_too_low = 87 + # Test needs to wait for the amount of tempo in the chain equal to OwnerHyperparamRateLimit + owner_hyperparam_ratelimit = subtensor.substrate.query( + module="SubtensorModule", storage_function="OwnerHyperparamRateLimit" + ).value + logging.console.info( + f"OwnerHyperparamRateLimit is {owner_hyperparam_ratelimit} tempo(s)." + ) + subtensor.wait_for_block( + subtensor.block + subtensor.tempo(netuid) * owner_hyperparam_ratelimit + ) + call_params = liquid_alpha_call_params(netuid, f"6553, {alpha_high_too_low}") result, error_message = sudo_set_hyperparameter_values( local_chain, @@ -108,7 +136,7 @@ def test_liquid_alpha(local_chain, subtensor, alice_wallet): return_error_message=True, ) - assert result is False, "Able to set incorrect alpha_high value" + assert result is False, "Able to set incorrect alpha_high value." assert error_message["name"] == "AlphaHighTooLow" # 2. Test setting Alpha_high too high @@ -137,7 +165,7 @@ def test_liquid_alpha(local_chain, subtensor, alice_wallet): call_params=call_params, return_error_message=True, ) - assert result is False, "Able to set incorrect alpha_low value" + assert result is False, "Able to set incorrect alpha_low value." assert error_message["name"] == "AlphaLowOutOfRange" # 2. Test setting Alpha_low too high @@ -150,7 +178,7 @@ def test_liquid_alpha(local_chain, subtensor, alice_wallet): call_params=call_params, return_error_message=True, ) - assert result is False, "Able to set incorrect alpha_low value" + assert result is False, "Able to set incorrect alpha_low value." assert error_message["name"] == "AlphaLowOutOfRange" # Setting normal alpha values @@ -161,21 +189,21 @@ def test_liquid_alpha(local_chain, subtensor, alice_wallet): alice_wallet, call_function="sudo_set_alpha_values", call_params=call_params, - ), "Unable to set liquid alpha values" + ), "Unable to set liquid alpha values." assert subtensor.get_subnet_hyperparameters(netuid).alpha_high == 53083, ( - "Failed to set alpha high" + "Failed to set alpha high." ) assert subtensor.get_subnet_hyperparameters(netuid).alpha_low == 6553, ( - "Failed to set alpha low" + "Failed to set alpha low." ) # Disable Liquid Alpha assert sudo_set_hyperparameter_bool( local_chain, alice_wallet, "sudo_set_liquid_alpha_enabled", False, netuid - ), "Unable to disable liquid alpha" + ), "Unable to disable liquid alpha." assert subtensor.get_subnet_hyperparameters(netuid).liquid_alpha_enabled is False, ( - "Failed to disable liquid alpha" + "Failed to disable liquid alpha." ) logging.console.info("✅ Passed test_liquid_alpha") diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index 0e2fc4723c..b1b6d5ccc9 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -290,6 +290,7 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): ("5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", Balance(0).set_unit(1)) ], validators=None, + commitments=None, ) assert metagraph_info == expected_metagraph_info @@ -371,6 +372,7 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): tao_dividends_per_hotkey=[], alpha_dividends_per_hotkey=[], validators=None, + commitments=None, ), metagraph_info, ] @@ -552,6 +554,7 @@ def test_metagraph_info_with_indexes(subtensor, alice_wallet, bob_wallet): tao_dividends_per_hotkey=None, alpha_dividends_per_hotkey=None, validators=None, + commitments=None, ) assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) @@ -670,6 +673,7 @@ def test_metagraph_info_with_indexes(subtensor, alice_wallet, bob_wallet): tao_dividends_per_hotkey=None, alpha_dividends_per_hotkey=None, validators=None, + commitments=None, ) @@ -681,9 +685,11 @@ def test_blocks(subtensor): - Wait for block """ - block = subtensor.get_current_block() + get_current_block = subtensor.get_current_block() + block = subtensor.block - assert block == subtensor.block + # Several random tests fell during the block finalization period. Fast blocks of 0.25 seconds (very fast) + assert get_current_block in [block, block + 1] block_hash = subtensor.get_block_hash(block) @@ -691,4 +697,4 @@ def test_blocks(subtensor): subtensor.wait_for_block(block + 10) - assert subtensor.get_current_block() == block + 10 + assert subtensor.get_current_block() in [block + 10, block + 11] diff --git a/tests/e2e_tests/test_set_weights.py b/tests/e2e_tests/test_set_weights.py index ce8e628bb6..47b50f8d64 100644 --- a/tests/e2e_tests/test_set_weights.py +++ b/tests/e2e_tests/test_set_weights.py @@ -2,6 +2,10 @@ import pytest import retry +from bittensor.core.extrinsics.sudo import ( + sudo_set_mechanism_count_extrinsic, + sudo_set_admin_freeze_window_extrinsic, +) from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit @@ -27,24 +31,34 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) Raises: AssertionError: If any of the checks or verifications fail """ + # turn off admin freeze window limit for testing + assert sudo_set_admin_freeze_window_extrinsic( + subtensor=subtensor, + wallet=alice_wallet, + window=0, + ) netuids = [2, 3] - subnet_tempo = 50 - BLOCK_TIME = 0.25 # 12 for non-fast-block, 0.25 for fast block + TESTED_SUB_SUBNETS = 2 + + # 12 for non-fast-block, 0.25 for fast block + block_time, subnet_tempo = ( + (0.25, 50) if subtensor.chain.is_fast_blocks() else (12.0, 20) + ) print("Testing test_set_weights_uses_next_nonce") # Lower the network registration rate limit and cost sudo_set_admin_utils( - local_chain, - alice_wallet, + substrate=subtensor.substrate, + wallet=alice_wallet, call_function="sudo_set_network_rate_limit", call_params={"rate_limit": "0"}, # No limit ) # Set lock reduction interval sudo_set_admin_utils( - local_chain, - alice_wallet, + substrate=subtensor.substrate, + wallet=alice_wallet, call_function="sudo_set_lock_reduction_interval", call_params={"interval": "1"}, # 1 block # reduce lock every block ) @@ -52,7 +66,7 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) # Try to register the subnets for _ in netuids: assert subtensor.register_subnet( - alice_wallet, + wallet=alice_wallet, wait_for_inclusion=True, wait_for_finalization=True, ), "Unable to register the subnet" @@ -63,14 +77,20 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) # weights sensitive to epoch changes assert sudo_set_admin_utils( - local_chain, - alice_wallet, + substrate=subtensor.substrate, + wallet=alice_wallet, call_function="sudo_set_tempo", call_params={ "netuid": netuid, "tempo": subnet_tempo, }, ) + assert sudo_set_mechanism_count_extrinsic( + subtensor=subtensor, + wallet=alice_wallet, + netuid=netuid, + mech_count=2, + ) # make sure 2 epochs are passed subtensor.wait_for_block(subnet_tempo * 2 + 1) @@ -78,20 +98,20 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) # Stake to become to top neuron after the first epoch for netuid in netuids: subtensor.add_stake( - alice_wallet, - alice_wallet.hotkey.ss58_address, - netuid, - Balance.from_tao(10_000), + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=netuid, + amount=Balance.from_tao(10_000), ) # Set weight hyperparameters per subnet for netuid in netuids: assert sudo_set_hyperparameter_bool( - local_chain, - alice_wallet, - "sudo_set_commit_reveal_weights_enabled", - False, - netuid, + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + value=False, + netuid=netuid, ), "Unable to enable commit reveal on the subnet" assert not subtensor.commit_reveal_enabled( @@ -104,8 +124,8 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) # Lower set weights rate limit status, error = sudo_set_admin_utils( - local_chain, - alice_wallet, + substrate=subtensor.substrate, + wallet=alice_wallet, call_function="sudo_set_weights_set_rate_limit", call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, ) @@ -134,15 +154,17 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) # 3 time doing call if nonce wasn't updated, then raise error @retry.retry(exceptions=Exception, tries=3, delay=1) @execute_and_wait_for_next_nonce(subtensor=subtensor, wallet=alice_wallet) - def set_weights(netuid_): + def set_weights(netuid_, mechid_): success, message = subtensor.set_weights( wallet=alice_wallet, netuid=netuid_, + mechid=mechid_, uids=weight_uids, weights=weight_vals, wait_for_inclusion=True, wait_for_finalization=False, period=subnet_tempo, + block_time=block_time, ) assert success is True, message @@ -151,22 +173,25 @@ def set_weights(netuid_): f"{subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" ) - # Set weights for each subnet - for netuid in netuids: - set_weights(netuid) - - for netuid in netuids: - # Query the Weights storage map for all three subnets - query = subtensor.query_module( - module="SubtensorModule", - name="Weights", - params=[netuid, 0], # Alice should be the only UID - ) - - weights = query.value - logging.console.info(f"Weights for subnet {netuid}: {weights}") - - assert weights is not None, f"Weights not found for subnet {netuid}" - assert weights == list(zip(weight_uids, weight_vals)), ( - f"Weights do not match for subnet {netuid}" - ) + for mechid in range(TESTED_SUB_SUBNETS): + # Set weights for each subnet + for netuid in netuids: + set_weights(netuid, mechid) + + for netuid in netuids: + # Query the Weights storage map for all three subnets + weights = subtensor.subnets.weights( + netuid=netuid, + mechid=mechid, + ) + alice_weights = weights[0][1] + logging.console.info( + f"Weights for subnet mechanism {netuid}.{mechid}: {alice_weights}" + ) + + assert alice_weights is not None, ( + f"Weights not found for subnet mechanism {netuid}.{mechid}" + ) + assert alice_weights == list(zip(weight_uids, weight_vals)), ( + f"Weights do not match for subnet {netuid}" + ) diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index b0c21b6c22..1a273bbabc 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -342,6 +342,18 @@ def test_safe_staking_scenarios( 2. Succeeds with strict threshold (0.5%) and partial staking allowed 3. Succeeds with lenient threshold (10% and 30%) and no partial staking """ + + # turn off admin freeze window limit for testing + assert ( + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_admin_freeze_window", + call_params={"window": 0}, + )[0] + is True + ), "Failed to set admin freeze window to 0" + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 # Register root as Alice - the subnet owner and validator assert subtensor.extrinsics.register_subnet(alice_wallet, True, True) diff --git a/tests/e2e_tests/test_subtensor_functions.py b/tests/e2e_tests/test_subtensor_functions.py index 6dbd9805ef..286b16e076 100644 --- a/tests/e2e_tests/test_subtensor_functions.py +++ b/tests/e2e_tests/test_subtensor_functions.py @@ -1,11 +1,13 @@ import asyncio +import time import pytest - +from bittensor.core.extrinsics.utils import get_extrinsic_fee from bittensor.utils.balance import Balance from tests.e2e_tests.utils.chain_interactions import ( wait_epoch, ) +from bittensor.core.extrinsics.utils import get_extrinsic_fee from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call """ @@ -67,9 +69,31 @@ async def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wall "Unable to register the subnet" ) + # TODO: in SDKv10 replace this logic with using `ExtrinsicResponse.extrinsic_fee` + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="register_network", + call_params={ + "hotkey": alice_wallet.hotkey.ss58_address, + "mechid": 1, + }, + ) + register_fee = get_extrinsic_fee(call, alice_wallet.hotkey, subtensor) + # Subnet burn cost is increased immediately after a subnet is registered post_subnet_creation_cost = subtensor.get_subnet_burn_cost() + # TODO: in SDKv10 replace this logic with using `ExtrinsicResponse.extrinsic_fee` + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="register_network", + call_params={ + "hotkey": alice_wallet.hotkey.ss58_address, + "mechid": 1, + }, + ) + register_fee = get_extrinsic_fee(call, alice_wallet.hotkey, subtensor) + # Assert that the burn cost changed after registering a subnet assert Balance.from_tao(pre_subnet_creation_cost) < Balance.from_tao( post_subnet_creation_cost @@ -77,9 +101,10 @@ async def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wall # Assert amount is deducted once a subnetwork is registered by Alice alice_balance_post_sn = subtensor.get_balance(alice_wallet.coldkeypub.ss58_address) - assert alice_balance_post_sn + pre_subnet_creation_cost == initial_alice_balance, ( - "Balance is the same even after registering a subnet" - ) + assert ( + alice_balance_post_sn + pre_subnet_creation_cost + register_fee + == initial_alice_balance + ), "Balance is the same even after registering a subnet" # Subnet 2 is added after registration assert subtensor.get_subnets() == [0, 1, 2] diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 71692d316b..7fee568d4d 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -16,7 +16,12 @@ from bittensor import Wallet from bittensor.core.async_subtensor import AsyncSubtensor from bittensor.core.subtensor import Subtensor - from async_substrate_interface import SubstrateInterface, ExtrinsicReceipt + from async_substrate_interface import ( + AsyncSubstrateInterface, + AsyncExtrinsicReceipt, + SubstrateInterface, + ExtrinsicReceipt, + ) def get_dynamic_balance(rao: int, netuid: int = 0): @@ -274,6 +279,49 @@ def sudo_set_admin_utils( return response.is_success, response.error_message +async def async_sudo_set_admin_utils( + substrate: "AsyncSubstrateInterface", + wallet: "Wallet", + call_function: str, + call_params: dict, + call_module: str = "AdminUtils", +) -> tuple[bool, Optional[dict]]: + """ + Wraps the call in sudo to set hyperparameter values using AdminUtils. + + Parameters: + substrate: Substrate connection. + wallet: Wallet object with the keypair for signing. + call_function: The AdminUtils function to call. + call_params: Parameters for the AdminUtils function. + call_module: The AdminUtils module to call. Defaults to "AdminUtils". + + Returns: + tuple: (success status, error details). + """ + inner_call = await substrate.compose_call( + call_module=call_module, + call_function=call_function, + call_params=call_params, + ) + + sudo_call = await substrate.compose_call( + call_module="Sudo", + call_function="sudo", + call_params={"call": inner_call}, + ) + extrinsic = await substrate.create_signed_extrinsic( + call=sudo_call, keypair=wallet.coldkey + ) + response: "AsyncExtrinsicReceipt" = await substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + return await response.is_success, await response.error_message + + async def root_set_subtensor_hyperparameter_values( substrate: "SubstrateInterface", wallet: "Wallet", diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index d41c80982d..744663d08c 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -94,7 +94,7 @@ def __init__(self, dir, wallet, netuid): async def __aenter__(self): env = os.environ.copy() - env["BT_LOGGING_INFO"] = "1" + env["BT_LOGGING_DEBUG"] = "1" self.process = await asyncio.create_subprocess_exec( sys.executable, f"{self.dir}/miner.py", @@ -157,7 +157,7 @@ def __init__(self, dir, wallet, netuid): async def __aenter__(self): env = os.environ.copy() - env["BT_LOGGING_INFO"] = "1" + env["BT_LOGGING_DEBUG"] = "1" self.process = await asyncio.create_subprocess_exec( sys.executable, f"{self.dir}/validator.py", @@ -235,7 +235,7 @@ def wait_to_start_call( in_blocks: int = 10, ): """Waits for a certain number of blocks before making a start call.""" - if subtensor.is_fast_blocks() is False: + if subtensor.chain.is_fast_blocks() is False: in_blocks = 5 bittensor.logging.console.info( f"Waiting for [blue]{in_blocks}[/blue] blocks before [red]start call[/red]. " diff --git a/tests/unit_tests/extrinsics/asyncex/test_sub_subnet.py b/tests/unit_tests/extrinsics/asyncex/test_sub_subnet.py new file mode 100644 index 0000000000..144807ceb3 --- /dev/null +++ b/tests/unit_tests/extrinsics/asyncex/test_sub_subnet.py @@ -0,0 +1,288 @@ +import pytest +from bittensor.core.extrinsics.asyncex import mechanism + + +@pytest.mark.asyncio +async def test_commit_mechanism_weights_extrinsic(mocker, subtensor, fake_wallet): + """Test successful `commit_mechanism_weights_extrinsic` extrinsic.""" + # Preps + fake_wallet.hotkey.ss58_address = "hotkey" + + netuid = mocker.Mock() + mechid = mocker.Mock() + uids = [] + weights = [] + salt = [] + + mocked_get_mechanism_storage_index = mocker.patch.object( + mechanism, "get_mechid_storage_index" + ) + mocked_generate_weight_hash = mocker.patch.object(mechanism, "generate_weight_hash") + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic", return_value=(True, "") + ) + + # Call + result = await mechanism.commit_mechanism_weights_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + salt=salt, + ) + + # Asserts + mocked_get_mechanism_storage_index.assert_called_once_with( + netuid=netuid, mechid=mechid + ) + mocked_generate_weight_hash.assert_called_once_with( + address=fake_wallet.hotkey.ss58_address, + netuid=mocked_get_mechanism_storage_index.return_value, + uids=list(uids), + values=list(weights), + salt=salt, + version_key=mechanism.version_as_int, + ) + mocked_compose_call.assert_awaited_once_with( + call_module="SubtensorModule", + call_function="commit_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "commit_hash": mocked_generate_weight_hash.return_value, + }, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + wallet=fake_wallet, + call=mocked_compose_call.return_value, + nonce_key="hotkey", + sign_with="hotkey", + use_nonce=True, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_commit_timelocked_mechanism_weights_extrinsic( + mocker, subtensor, fake_wallet +): + """Test successful `commit_mechanism_weights_extrinsic` extrinsic.""" + # Preps + fake_wallet.hotkey.ss58_address = "hotkey" + + netuid = mocker.Mock() + mechid = mocker.Mock() + uids = [] + weights = [] + block_time = mocker.Mock() + + mocked_convert_and_normalize_weights_and_uids = mocker.patch.object( + mechanism, + "convert_and_normalize_weights_and_uids", + return_value=(uids, weights), + ) + mocked_get_current_block = mocker.patch.object(subtensor, "get_current_block") + mocked_get_subnet_hyperparameters = mocker.patch.object( + subtensor, "get_subnet_hyperparameters" + ) + mocked_get_mechanism_storage_index = mocker.patch.object( + mechanism, "get_mechid_storage_index" + ) + mocked_get_encrypted_commit = mocker.patch.object( + mechanism, + "get_encrypted_commit", + return_value=(mocker.Mock(), mocker.Mock()), + ) + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, + "sign_and_send_extrinsic", + return_value=( + True, + f"reveal_round:{mocked_get_encrypted_commit.return_value[1]}", + ), + ) + + # Call + result = await mechanism.commit_timelocked_mechanism_weights_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + block_time=block_time, + ) + + # Asserts + mocked_convert_and_normalize_weights_and_uids.assert_called_once_with(uids, weights) + mocked_get_mechanism_storage_index.assert_called_once_with( + netuid=netuid, mechid=mechid + ) + mocked_get_encrypted_commit.assert_called_once_with( + uids=uids, + weights=weights, + subnet_reveal_period_epochs=mocked_get_subnet_hyperparameters.return_value.commit_reveal_period, + version_key=mechanism.version_as_int, + tempo=mocked_get_subnet_hyperparameters.return_value.tempo, + netuid=mocked_get_mechanism_storage_index.return_value, + current_block=mocked_get_current_block.return_value, + block_time=block_time, + hotkey=fake_wallet.hotkey.public_key, + ) + mocked_compose_call.assert_awaited_once_with( + call_module="SubtensorModule", + call_function="commit_timelocked_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "commit": mocked_get_encrypted_commit.return_value[0], + "reveal_round": mocked_get_encrypted_commit.return_value[1], + "commit_reveal_version": 4, + }, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + wallet=fake_wallet, + call=mocked_compose_call.return_value, + nonce_key="hotkey", + sign_with="hotkey", + use_nonce=True, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_reveal_mechanism_weights_extrinsic(mocker, subtensor, fake_wallet): + """Test successful `reveal_mechanism_weights_extrinsic` extrinsic.""" + # Preps + fake_wallet.hotkey.ss58_address = "hotkey" + + netuid = mocker.Mock() + mechid = mocker.Mock() + uids = [] + weights = [] + salt = [] + + mocked_convert_and_normalize_weights_and_uids = mocker.patch.object( + mechanism, + "convert_and_normalize_weights_and_uids", + return_value=(uids, weights), + ) + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic", return_value=(True, "") + ) + + # Call + result = await mechanism.reveal_mechanism_weights_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + salt=salt, + version_key=mechanism.version_as_int, + ) + + # Asserts + mocked_convert_and_normalize_weights_and_uids.assert_called_once_with(uids, weights) + mocked_compose_call.assert_awaited_once_with( + call_module="SubtensorModule", + call_function="reveal_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "uids": mocked_convert_and_normalize_weights_and_uids.return_value[0], + "values": mocked_convert_and_normalize_weights_and_uids.return_value[0], + "salt": salt, + "version_key": mechanism.version_as_int, + }, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + wallet=fake_wallet, + call=mocked_compose_call.return_value, + nonce_key="hotkey", + sign_with="hotkey", + use_nonce=True, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result == mocked_sign_and_send_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_mechanism_sub_weights_extrinsic(mocker, subtensor, fake_wallet): + """Verify that the `set_mechanism_weights_extrinsic` function works as expected.""" + # Preps + fake_wallet.hotkey.ss58_address = "hotkey" + + netuid = mocker.Mock() + mechid = mocker.Mock() + uids = [] + weights = [] + + mocked_convert_and_normalize_weights_and_uids = mocker.patch.object( + mechanism, + "convert_and_normalize_weights_and_uids", + return_value=(uids, weights), + ) + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, + "sign_and_send_extrinsic", + return_value=( + True, + "", + ), + ) + + # Call + result = await mechanism.set_mechanism_weights_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + version_key=mechanism.version_as_int, + ) + + # Asserts + mocked_convert_and_normalize_weights_and_uids.assert_called_once_with(uids, weights) + mocked_compose_call.assert_awaited_once_with( + call_module="SubtensorModule", + call_function="set_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "dests": uids, + "weights": weights, + "version_key": mechanism.version_as_int, + }, + ) + mocked_sign_and_send_extrinsic.assert_awaited_once_with( + wallet=fake_wallet, + call=mocked_compose_call.return_value, + nonce_key="hotkey", + sign_with="hotkey", + use_nonce=True, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result == mocked_sign_and_send_extrinsic.return_value diff --git a/tests/unit_tests/extrinsics/test_sub_subnet.py b/tests/unit_tests/extrinsics/test_sub_subnet.py new file mode 100644 index 0000000000..624ffedbf3 --- /dev/null +++ b/tests/unit_tests/extrinsics/test_sub_subnet.py @@ -0,0 +1,282 @@ +import pytest +from bittensor.core.extrinsics import mechanism + + +def test_commit_mechanism_weights_extrinsic(mocker, subtensor, fake_wallet): + """Test successful `commit_mechanism_weights_extrinsic` extrinsic.""" + # Preps + fake_wallet.hotkey.ss58_address = "hotkey" + + netuid = mocker.Mock() + mechid = mocker.Mock() + uids = [] + weights = [] + salt = [] + + mocked_get_sub_subnet_storage_index = mocker.patch.object( + mechanism, "get_mechid_storage_index" + ) + mocked_generate_weight_hash = mocker.patch.object(mechanism, "generate_weight_hash") + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic", return_value=(True, "") + ) + + # Call + result = mechanism.commit_mechanism_weights_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + salt=salt, + ) + + # Asserts + mocked_get_sub_subnet_storage_index.assert_called_once_with( + netuid=netuid, mechid=mechid + ) + mocked_generate_weight_hash.assert_called_once_with( + address=fake_wallet.hotkey.ss58_address, + netuid=mocked_get_sub_subnet_storage_index.return_value, + uids=list(uids), + values=list(weights), + salt=salt, + version_key=mechanism.version_as_int, + ) + mocked_compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="commit_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "commit_hash": mocked_generate_weight_hash.return_value, + }, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + wallet=fake_wallet, + call=mocked_compose_call.return_value, + nonce_key="hotkey", + sign_with="hotkey", + use_nonce=True, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result == mocked_sign_and_send_extrinsic.return_value + + +def test_commit_timelocked_mechanism_weights_extrinsic(mocker, subtensor, fake_wallet): + """Test successful `commit_mechanism_weights_extrinsic` extrinsic.""" + # Preps + fake_wallet.hotkey.ss58_address = "hotkey" + + netuid = mocker.Mock() + mechid = mocker.Mock() + uids = [] + weights = [] + block_time = mocker.Mock() + + mocked_convert_and_normalize_weights_and_uids = mocker.patch.object( + mechanism, + "convert_and_normalize_weights_and_uids", + return_value=(uids, weights), + ) + mocked_get_current_block = mocker.patch.object(subtensor, "get_current_block") + mocked_get_subnet_hyperparameters = mocker.patch.object( + subtensor, "get_subnet_hyperparameters" + ) + mocked_get_sub_subnet_storage_index = mocker.patch.object( + mechanism, "get_mechid_storage_index" + ) + mocked_get_encrypted_commit = mocker.patch.object( + mechanism, + "get_encrypted_commit", + return_value=(mocker.Mock(), mocker.Mock()), + ) + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, + "sign_and_send_extrinsic", + return_value=( + True, + f"reveal_round:{mocked_get_encrypted_commit.return_value[1]}", + ), + ) + + # Call + result = mechanism.commit_timelocked_mechanism_weights_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + block_time=block_time, + ) + + # Asserts + mocked_convert_and_normalize_weights_and_uids.assert_called_once_with(uids, weights) + mocked_get_sub_subnet_storage_index.assert_called_once_with( + netuid=netuid, mechid=mechid + ) + mocked_get_encrypted_commit.assert_called_once_with( + uids=uids, + weights=weights, + subnet_reveal_period_epochs=mocked_get_subnet_hyperparameters.return_value.commit_reveal_period, + version_key=mechanism.version_as_int, + tempo=mocked_get_subnet_hyperparameters.return_value.tempo, + netuid=mocked_get_sub_subnet_storage_index.return_value, + current_block=mocked_get_current_block.return_value, + block_time=block_time, + hotkey=fake_wallet.hotkey.public_key, + ) + mocked_compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="commit_timelocked_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "commit": mocked_get_encrypted_commit.return_value[0], + "reveal_round": mocked_get_encrypted_commit.return_value[1], + "commit_reveal_version": 4, + }, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + wallet=fake_wallet, + call=mocked_compose_call.return_value, + nonce_key="hotkey", + sign_with="hotkey", + use_nonce=True, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result == mocked_sign_and_send_extrinsic.return_value + + +def test_reveal_mechanism_weights_extrinsic(mocker, subtensor, fake_wallet): + """Test successful `reveal_mechanism_weights_extrinsic` extrinsic.""" + # Preps + fake_wallet.hotkey.ss58_address = "hotkey" + + netuid = mocker.Mock() + mechid = mocker.Mock() + uids = [] + weights = [] + salt = [] + + mocked_convert_and_normalize_weights_and_uids = mocker.patch.object( + mechanism, + "convert_and_normalize_weights_and_uids", + return_value=(uids, weights), + ) + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, "sign_and_send_extrinsic", return_value=(True, "") + ) + + # Call + result = mechanism.reveal_mechanism_weights_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + salt=salt, + version_key=mechanism.version_as_int, + ) + + # Asserts + mocked_convert_and_normalize_weights_and_uids.assert_called_once_with(uids, weights) + mocked_compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="reveal_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "uids": mocked_convert_and_normalize_weights_and_uids.return_value[0], + "values": mocked_convert_and_normalize_weights_and_uids.return_value[0], + "salt": salt, + "version_key": mechanism.version_as_int, + }, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + wallet=fake_wallet, + call=mocked_compose_call.return_value, + nonce_key="hotkey", + sign_with="hotkey", + use_nonce=True, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result == mocked_sign_and_send_extrinsic.return_value + + +def test_mechanism_sub_weights_extrinsic(mocker, subtensor, fake_wallet): + """Verify that the `set_mechanism_weights_extrinsic` function works as expected.""" + # Preps + fake_wallet.hotkey.ss58_address = "hotkey" + + netuid = mocker.Mock() + mechid = mocker.Mock() + uids = [] + weights = [] + + mocked_convert_and_normalize_weights_and_uids = mocker.patch.object( + mechanism, + "convert_and_normalize_weights_and_uids", + return_value=(uids, weights), + ) + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_sign_and_send_extrinsic = mocker.patch.object( + subtensor, + "sign_and_send_extrinsic", + return_value=( + True, + "", + ), + ) + + # Call + result = mechanism.set_mechanism_weights_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + mechid=mechid, + uids=uids, + weights=weights, + version_key=mechanism.version_as_int, + ) + + # Asserts + mocked_convert_and_normalize_weights_and_uids.assert_called_once_with(uids, weights) + mocked_compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="set_mechanism_weights", + call_params={ + "netuid": netuid, + "mecid": mechid, + "dests": uids, + "weights": weights, + "version_key": mechanism.version_as_int, + }, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + wallet=fake_wallet, + call=mocked_compose_call.return_value, + nonce_key="hotkey", + sign_with="hotkey", + use_nonce=True, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result == mocked_sign_and_send_extrinsic.return_value diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index e1bf1420dd..366f22b9fb 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -1,6 +1,6 @@ import datetime import unittest.mock as mock - +import numpy as np import pytest from async_substrate_interface.types import ScaleObj from bittensor_wallet import Wallet @@ -2773,7 +2773,7 @@ async def test_set_weights_success(subtensor, fake_wallet, mocker): mocked_set_weights_extrinsic = mocker.AsyncMock(return_value=(True, "Success")) mocker.patch.object( - async_subtensor, "set_weights_extrinsic", mocked_set_weights_extrinsic + async_subtensor, "set_mechanism_weights_extrinsic", mocked_set_weights_extrinsic ) # Call @@ -2803,6 +2803,7 @@ async def test_set_weights_success(subtensor, fake_wallet, mocker): wait_for_inclusion=False, weights=fake_weights, period=8, + mechid=0, ) mocked_weights_rate_limit.assert_called_once_with(fake_netuid) assert result is True @@ -2832,7 +2833,7 @@ async def test_set_weights_with_exception(subtensor, fake_wallet, mocker): side_effect=Exception("Test exception") ) mocker.patch.object( - async_subtensor, "set_weights_extrinsic", mocked_set_weights_extrinsic + async_subtensor, "set_mechanism_weights_extrinsic", mocked_set_weights_extrinsic ) # Call @@ -2865,10 +2866,10 @@ async def test_root_set_weights_success(subtensor, fake_wallet, mocker): async_subtensor, "set_root_weights_extrinsic", mocked_set_root_weights_extrinsic ) - mocked_np_array_netuids = mocker.Mock(autospec=async_subtensor.np.ndarray) - mocked_np_array_weights = mocker.Mock(autospec=async_subtensor.np.ndarray) + mocked_np_array_netuids = mocker.Mock(autospec=np.ndarray) + mocked_np_array_weights = mocker.Mock(autospec=np.ndarray) mocker.patch.object( - async_subtensor.np, + np, "array", side_effect=[mocked_np_array_netuids, mocked_np_array_weights], ) @@ -2905,14 +2906,11 @@ async def test_commit_weights_success(subtensor, fake_wallet, mocker): fake_weights = [100, 200, 300] max_retries = 3 - mocked_generate_weight_hash = mocker.Mock(return_value="fake_commit_hash") - mocker.patch.object( - async_subtensor, "generate_weight_hash", mocked_generate_weight_hash - ) - mocked_commit_weights_extrinsic = mocker.AsyncMock(return_value=(True, "Success")) mocker.patch.object( - async_subtensor, "commit_weights_extrinsic", mocked_commit_weights_extrinsic + async_subtensor, + "commit_mechanism_weights_extrinsic", + mocked_commit_weights_extrinsic, ) # Call @@ -2926,22 +2924,17 @@ async def test_commit_weights_success(subtensor, fake_wallet, mocker): ) # Asserts - mocked_generate_weight_hash.assert_called_once_with( - address=fake_wallet.hotkey.ss58_address, - netuid=fake_netuid, - uids=fake_uids, - values=fake_weights, - salt=fake_salt, - version_key=async_subtensor.version_as_int, - ) mocked_commit_weights_extrinsic.assert_called_once_with( subtensor=subtensor, wallet=fake_wallet, netuid=fake_netuid, - commit_hash="fake_commit_hash", + salt=fake_salt, + uids=fake_uids, + weights=fake_weights, wait_for_inclusion=False, wait_for_finalization=False, period=16, + mechid=0, ) assert result is True assert message == "Success" @@ -2957,16 +2950,13 @@ async def test_commit_weights_with_exception(subtensor, fake_wallet, mocker): fake_weights = [100, 200, 300] max_retries = 1 - mocked_generate_weight_hash = mocker.Mock(return_value="fake_commit_hash") - mocker.patch.object( - async_subtensor, "generate_weight_hash", mocked_generate_weight_hash - ) - mocked_commit_weights_extrinsic = mocker.AsyncMock( side_effect=Exception("Test exception") ) mocker.patch.object( - async_subtensor, "commit_weights_extrinsic", mocked_commit_weights_extrinsic + async_subtensor, + "commit_mechanism_weights_extrinsic", + mocked_commit_weights_extrinsic, ) # Call @@ -3161,6 +3151,7 @@ async def test_get_metagraph_info_all_fields(subtensor, mocker): """Test get_metagraph_info with all fields (default behavior).""" # Preps netuid = 1 + default_mechid = 0 mock_value = {"mock": "data"} mock_runtime_call = mocker.patch.object( @@ -3174,15 +3165,15 @@ async def test_get_metagraph_info_all_fields(subtensor, mocker): # Call result = await subtensor.get_metagraph_info( - netuid=netuid, field_indices=[f for f in range(73)] + netuid=netuid, field_indices=[f for f in range(len(SelectiveMetagraphIndex))] ) # Asserts assert result == "parsed_metagraph" mock_runtime_call.assert_awaited_once_with( - "SubnetInfoRuntimeApi", - "get_selective_metagraph", - params=[netuid, SelectiveMetagraphIndex.all_indices()], + api="SubnetInfoRuntimeApi", + method="get_selective_mechagraph", + params=[netuid, default_mechid, SelectiveMetagraphIndex.all_indices()], block_hash=await subtensor.determine_block_hash(None), ) mock_from_dict.assert_called_once_with(mock_value) @@ -3193,6 +3184,7 @@ async def test_get_metagraph_info_specific_fields(subtensor, mocker): """Test get_metagraph_info with specific fields.""" # Preps netuid = 1 + default_mechid = 0 mock_value = {"mock": "data"} fields = [SelectiveMetagraphIndex.Name, 5] @@ -3211,10 +3203,11 @@ async def test_get_metagraph_info_specific_fields(subtensor, mocker): # Asserts assert result == "parsed_metagraph" mock_runtime_call.assert_awaited_once_with( - "SubnetInfoRuntimeApi", - "get_selective_metagraph", + api="SubnetInfoRuntimeApi", + method="get_selective_mechagraph", params=[ netuid, + default_mechid, [0] + [ f.value if isinstance(f, SelectiveMetagraphIndex) else f for f in fields @@ -3225,34 +3218,15 @@ async def test_get_metagraph_info_specific_fields(subtensor, mocker): mock_from_dict.assert_called_once_with(mock_value) -@pytest.mark.parametrize( - "wrong_fields", - [ - [ - "invalid", - ], - [SelectiveMetagraphIndex.Active, 1, "f"], - [1, 2, 3, "f"], - ], -) -@pytest.mark.asyncio -async def test_get_metagraph_info_invalid_field_indices(subtensor, wrong_fields): - """Test get_metagraph_info raises ValueError on invalid field_indices.""" - with pytest.raises( - ValueError, - match="`field_indices` must be a list of SelectiveMetagraphIndex enums or ints.", - ): - await subtensor.get_metagraph_info(netuid=1, field_indices=wrong_fields) - - @pytest.mark.asyncio async def test_get_metagraph_info_subnet_not_exist(subtensor, mocker): """Test get_metagraph_info returns None when subnet doesn't exist.""" netuid = 1 + default_mechid = 0 mocker.patch.object( subtensor.substrate, "runtime_call", - return_value=mocker.AsyncMock(value=None), + return_value=None, ) mocked_logger = mocker.Mock() @@ -3261,7 +3235,9 @@ async def test_get_metagraph_info_subnet_not_exist(subtensor, mocker): result = await subtensor.get_metagraph_info(netuid=netuid) assert result is None - mocked_logger.assert_called_once_with(f"Subnet {netuid} does not exist.") + mocked_logger.assert_called_once_with( + f"Subnet mechanism {netuid}.{default_mechid} does not exist." + ) @pytest.mark.asyncio @@ -3845,10 +3821,10 @@ async def test_get_subnet_price(subtensor, mocker): # preps netuid = 123 mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - fake_price = {"bits": 3155343338053956962} + fake_price = 29258617 expected_price = Balance.from_tao(0.029258617) mocked_query = mocker.patch.object( - subtensor.substrate, "query", return_value=fake_price + subtensor.substrate, "runtime_call", return_value=mocker.Mock(value=fake_price) ) # Call @@ -3857,10 +3833,10 @@ async def test_get_subnet_price(subtensor, mocker): ) # Asserts - mocked_determine_block_hash.assert_awaited_once_with(block=None) + mocked_determine_block_hash.assert_awaited_once() mocked_query.assert_awaited_once_with( - module="Swap", - storage_function="AlphaSqrtPrice", + api="SwapRuntimeApi", + method="current_alpha_price", params=[netuid], block_hash=mocked_determine_block_hash.return_value, ) @@ -4126,7 +4102,7 @@ async def test_get_stake_weight(subtensor, mocker): async def test_get_timelocked_weight_commits(subtensor, mocker): """Verify that `get_timelocked_weight_commits` method calls proper methods and returns the correct value.""" # Preps - netuid = mocker.Mock() + netuid = 14 mock_determine_block_hash = mocker.patch.object( subtensor, @@ -4151,3 +4127,128 @@ async def test_get_timelocked_weight_commits(subtensor, mocker): block_hash=mock_determine_block_hash.return_value, ) assert result == [] + + +@pytest.mark.parametrize( + "query_return, expected_result", + ( + ["value", [10, 90]], + [None, None], + ), +) +@pytest.mark.asyncio +async def test_get_mechanism_emission_split( + subtensor, mocker, query_return, expected_result +): + """Verify that get_mechanism_emission_split calls the correct methods.""" + # Preps + netuid = mocker.Mock() + query_return = ( + mocker.Mock(value=[6553, 58982]) if query_return == "value" else query_return + ) + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_query = mocker.patch.object( + subtensor.substrate, "query", return_value=query_return + ) + + # Call + + result = await subtensor.get_mechanism_emission_split(netuid) + + # Asserts + mocked_determine_block_hash.assert_awaited_once() + mocked_query.assert_awaited_once_with( + module="SubtensorModule", + storage_function="MechanismEmissionSplit", + params=[netuid], + block_hash=mocked_determine_block_hash.return_value, + ) + assert result == expected_result + + +@pytest.mark.asyncio +async def test_get_mechanism_count(subtensor, mocker): + """Verify that `get_mechanism_count` method processed the data correctly.""" + # Preps + netuid = 14 + + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_result = mocker.MagicMock() + mocker.patch.object(subtensor.substrate, "runtime_call", return_value=mocked_result) + mocked_query = mocker.patch.object(subtensor.substrate, "query") + + # Call + result = await subtensor.get_mechanism_count(netuid=netuid) + + # Asserts + mocked_determine_block_hash.assert_called_once() + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="MechanismCountCurrent", + params=[netuid], + block_hash=mocked_determine_block_hash.return_value, + ) + assert result is mocked_query.return_value.value + + +@pytest.mark.asyncio +async def test_is_in_admin_freeze_window_root_net(subtensor, mocker): + """Verify that root net has no admin freeze window.""" + # Preps + netuid = 0 + mocked_get_next_epoch_start_block = mocker.patch.object( + subtensor, "get_next_epoch_start_block" + ) + + # Call + result = await subtensor.is_in_admin_freeze_window(netuid=netuid) + + # Asserts + mocked_get_next_epoch_start_block.assert_not_called() + assert result is False + + +@pytest.mark.parametrize( + "block, next_esb, expected_result", + ( + [89, 100, False], + [90, 100, False], + [91, 100, True], + ), +) +@pytest.mark.asyncio +async def test_is_in_admin_freeze_window( + subtensor, mocker, block, next_esb, expected_result +): + """Verify that `is_in_admin_freeze_window` method processed the data correctly.""" + # Preps + netuid = 14 + mocker.patch.object(subtensor, "get_current_block", return_value=block) + mocker.patch.object(subtensor, "get_next_epoch_start_block", return_value=next_esb) + mocker.patch.object(subtensor, "get_admin_freeze_window", return_value=10) + + # Call + + result = await subtensor.is_in_admin_freeze_window(netuid=netuid) + + # Asserts + assert result is expected_result + + +@pytest.mark.asyncio +async def test_get_admin_freeze_window(subtensor, mocker): + """Verify that `get_admin_freeze_window` calls proper methods.""" + # Preps + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_query = mocker.patch.object(subtensor.substrate, "query") + + # Call + result = await subtensor.get_admin_freeze_window() + + # Asserts + mocked_query.assert_awaited_once_with( + module="SubtensorModule", + storage_function="AdminFreezeWindow", + block_hash=mocked_determine_block_hash.return_value, + ) + assert result == mocked_query.return_value.value diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 78d9ffeaa9..6bdeeb366a 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1018,6 +1018,7 @@ def test_metagraph(subtensor, mocker): """Tests subtensor.metagraph call.""" # Prep fake_netuid = 1 + default_mechid = 0 fake_lite = True mocked_metagraph = mocker.patch.object(subtensor_module, "Metagraph") @@ -1028,6 +1029,7 @@ def test_metagraph(subtensor, mocker): mocked_metagraph.assert_called_once_with( network=subtensor.chain_endpoint, netuid=fake_netuid, + mechid=default_mechid, lite=fake_lite, sync=False, subtensor=subtensor, @@ -1170,7 +1172,9 @@ def test_set_weights(subtensor, mocker, fake_wallet): subtensor.weights_rate_limit = mocked_weights_rate_limit mocked_set_weights_extrinsic = mocker.patch.object( - subtensor_module, "set_weights_extrinsic", return_value=expected_result + subtensor_module, + "set_mechanism_weights_extrinsic", + return_value=expected_result, ) # Call @@ -1203,6 +1207,7 @@ def test_set_weights(subtensor, mocker, fake_wallet): wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, period=8, + mechid=0, ) assert result == expected_result @@ -1952,11 +1957,10 @@ def test_commit_weights(subtensor, fake_wallet, mocker): max_retries = 5 expected_result = (True, None) - mocked_generate_weight_hash = mocker.patch.object( - subtensor_module, "generate_weight_hash", return_value=expected_result - ) mocked_commit_weights_extrinsic = mocker.patch.object( - subtensor_module, "commit_weights_extrinsic", return_value=expected_result + subtensor_module, + "commit_mechanism_weights_extrinsic", + return_value=expected_result, ) # Call @@ -1973,23 +1977,17 @@ def test_commit_weights(subtensor, fake_wallet, mocker): ) # Asserts - mocked_generate_weight_hash.assert_called_once_with( - address=fake_wallet.hotkey.ss58_address, - netuid=netuid, - uids=list(uids), - values=list(weights), - salt=list(salt), - version_key=settings.version_as_int, - ) - mocked_commit_weights_extrinsic.assert_called_once_with( subtensor=subtensor, wallet=fake_wallet, netuid=netuid, - commit_hash=mocked_generate_weight_hash.return_value, + salt=salt, + uids=uids, + weights=weights, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, period=16, + mechid=0, ) assert result == expected_result @@ -2003,7 +2001,9 @@ def test_reveal_weights(subtensor, fake_wallet, mocker): salt = [4, 2, 2, 1] expected_result = (True, None) mocked_extrinsic = mocker.patch.object( - subtensor_module, "reveal_weights_extrinsic", return_value=expected_result + subtensor_module, + "reveal_mechanism_weights_extrinsic", + return_value=expected_result, ) # Call @@ -2030,6 +2030,7 @@ def test_reveal_weights(subtensor, fake_wallet, mocker): wait_for_inclusion=False, wait_for_finalization=False, period=16, + mechid=0, ) @@ -2045,7 +2046,9 @@ def test_reveal_weights_false(subtensor, fake_wallet, mocker): False, "No attempt made. Perhaps it is too soon to reveal weights!", ) - mocked_extrinsic = mocker.patch.object(subtensor_module, "reveal_weights_extrinsic") + mocked_extrinsic = mocker.patch.object( + subtensor_module, "reveal_mechanism_weights_extrinsic" + ) # Call result = subtensor.reveal_weights( @@ -3150,10 +3153,10 @@ def test_set_weights_with_commit_reveal_enabled(subtensor, fake_wallet, mocker): mocked_commit_reveal_enabled = mocker.patch.object( subtensor, "commit_reveal_enabled", return_value=True ) - mocked_commit_reveal_v3_extrinsic = mocker.patch.object( - subtensor_module, "commit_reveal_v3_extrinsic" + mocked_commit_timelocked_mechanism_weights_extrinsic = mocker.patch.object( + subtensor_module, "commit_timelocked_mechanism_weights_extrinsic" ) - mocked_commit_reveal_v3_extrinsic.return_value = ( + mocked_commit_timelocked_mechanism_weights_extrinsic.return_value = ( True, "Weights committed successfully", ) @@ -3172,7 +3175,7 @@ def test_set_weights_with_commit_reveal_enabled(subtensor, fake_wallet, mocker): # Asserts mocked_commit_reveal_enabled.assert_called_once_with(netuid=fake_netuid) - mocked_commit_reveal_v3_extrinsic.assert_called_once_with( + mocked_commit_timelocked_mechanism_weights_extrinsic.assert_called_once_with( subtensor=subtensor, wallet=fake_wallet, netuid=fake_netuid, @@ -3183,8 +3186,10 @@ def test_set_weights_with_commit_reveal_enabled(subtensor, fake_wallet, mocker): wait_for_finalization=fake_wait_for_finalization, block_time=12.0, period=8, + commit_reveal_version=4, + mechid=0, ) - assert result == mocked_commit_reveal_v3_extrinsic.return_value + assert result == mocked_commit_timelocked_mechanism_weights_extrinsic.return_value def test_connection_limit(mocker): @@ -3350,6 +3355,7 @@ def test_get_metagraph_info_all_fields(subtensor, mocker): """Test get_metagraph_info with all fields (default behavior).""" # Preps netuid = 1 + default_mechid = 0 mock_value = {"mock": "data"} mock_runtime_call = mocker.patch.object( @@ -3363,15 +3369,15 @@ def test_get_metagraph_info_all_fields(subtensor, mocker): # Call result = subtensor.get_metagraph_info( - netuid=netuid, field_indices=[f for f in range(73)] + netuid=netuid, field_indices=[f for f in range(len(SelectiveMetagraphIndex))] ) # Asserts assert result == "parsed_metagraph" mock_runtime_call.assert_called_once_with( - "SubnetInfoRuntimeApi", - "get_selective_metagraph", - params=[netuid, SelectiveMetagraphIndex.all_indices()], + api="SubnetInfoRuntimeApi", + method="get_selective_mechagraph", + params=[netuid, default_mechid, SelectiveMetagraphIndex.all_indices()], block_hash=subtensor.determine_block_hash(None), ) mock_from_dict.assert_called_once_with(mock_value) @@ -3381,6 +3387,7 @@ def test_get_metagraph_info_specific_fields(subtensor, mocker): """Test get_metagraph_info with specific fields.""" # Preps netuid = 1 + default_mechid = 0 mock_value = {"mock": "data"} fields = [SelectiveMetagraphIndex.Name, 5] @@ -3399,10 +3406,11 @@ def test_get_metagraph_info_specific_fields(subtensor, mocker): # Asserts assert result == "parsed_metagraph" mock_runtime_call.assert_called_once_with( - "SubnetInfoRuntimeApi", - "get_selective_metagraph", + api="SubnetInfoRuntimeApi", + method="get_selective_mechagraph", params=[ netuid, + default_mechid, [0] + [ f.value if isinstance(f, SelectiveMetagraphIndex) else f for f in fields @@ -3413,32 +3421,14 @@ def test_get_metagraph_info_specific_fields(subtensor, mocker): mock_from_dict.assert_called_once_with(mock_value) -@pytest.mark.parametrize( - "wrong_fields", - [ - [ - "invalid", - ], - [SelectiveMetagraphIndex.Active, 1, "f"], - [1, 2, 3, "f"], - ], -) -def test_get_metagraph_info_invalid_field_indices(subtensor, wrong_fields): - """Test get_metagraph_info raises ValueError on invalid field_indices.""" - with pytest.raises( - ValueError, - match="`field_indices` must be a list of SelectiveMetagraphIndex enums or ints.", - ): - subtensor.get_metagraph_info(netuid=1, field_indices=wrong_fields) - - def test_get_metagraph_info_subnet_not_exist(subtensor, mocker): """Test get_metagraph_info returns None when subnet doesn't exist.""" netuid = 1 + default_mechid = 0 mocker.patch.object( subtensor.substrate, "runtime_call", - return_value=mocker.Mock(value=None), + return_value=None, ) mocked_logger = mocker.Mock() @@ -3447,7 +3437,9 @@ def test_get_metagraph_info_subnet_not_exist(subtensor, mocker): result = subtensor.get_metagraph_info(netuid=netuid) assert result is None - mocked_logger.assert_called_once_with(f"Subnet {netuid} does not exist.") + mocked_logger.assert_called_once_with( + f"Subnet mechanism {netuid}.{default_mechid} does not exist." + ) def test_blocks_since_last_step_with_value(subtensor, mocker): @@ -4056,10 +4048,10 @@ def test_get_subnet_price(subtensor, mocker): # preps netuid = 123 mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") - fake_price = {"bits": 3155343338053956962} + fake_price = 29258617 expected_price = Balance.from_tao(0.029258617) mocked_query = mocker.patch.object( - subtensor.substrate, "query", return_value=fake_price + subtensor.substrate, "runtime_call", return_value=mocker.Mock(value=fake_price) ) # Call @@ -4070,8 +4062,8 @@ def test_get_subnet_price(subtensor, mocker): # Asserts mocked_determine_block_hash.assert_called_once_with(block=None) mocked_query.assert_called_once_with( - module="Swap", - storage_function="AlphaSqrtPrice", + api="SwapRuntimeApi", + method="current_alpha_price", params=[netuid], block_hash=mocked_determine_block_hash.return_value, ) @@ -4310,7 +4302,7 @@ def test_get_stake_weight(subtensor, mocker): def test_get_timelocked_weight_commits(subtensor, mocker): """Verify that `get_timelocked_weight_commits` method calls proper methods and returns the correct value.""" # Preps - netuid = mocker.Mock() + netuid = 14 mock_determine_block_hash = mocker.patch.object( subtensor, @@ -4333,3 +4325,119 @@ def test_get_timelocked_weight_commits(subtensor, mocker): block_hash=mock_determine_block_hash.return_value, ) assert result == [] + + +@pytest.mark.parametrize( + "query_return, expected_result", + ( + ["value", [10, 90]], + [None, None], + ), +) +def test_get_mechanism_emission_split(subtensor, mocker, query_return, expected_result): + """Verify that get_mechanism_emission_split calls the correct methods.""" + # Preps + netuid = mocker.Mock() + query_return = ( + mocker.Mock(value=[6553, 58982]) if query_return == "value" else query_return + ) + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_query = mocker.patch.object( + subtensor.substrate, "query", return_value=query_return + ) + + # Call + + result = subtensor.get_mechanism_emission_split(netuid) + + # Asserts + mocked_determine_block_hash.assert_called_once() + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="MechanismEmissionSplit", + params=[netuid], + block_hash=mocked_determine_block_hash.return_value, + ) + assert result == expected_result + + +def test_get_mechanism_count(subtensor, mocker): + """Verify that `get_mechanism_count` method processed the data correctly.""" + # Preps + netuid = 14 + + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_result = mocker.MagicMock() + mocker.patch.object(subtensor.substrate, "runtime_call", return_value=mocked_result) + mocked_query = mocker.patch.object(subtensor.substrate, "query") + + # Call + result = subtensor.get_mechanism_count(netuid=netuid) + + # Asserts + mocked_determine_block_hash.assert_called_once() + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="MechanismCountCurrent", + params=[netuid], + block_hash=mocked_determine_block_hash.return_value, + ) + assert result is mocked_query.return_value.value + + +def test_is_in_admin_freeze_window_root_net(subtensor, mocker): + """Verify that root net has no admin freeze window.""" + # Preps + netuid = 0 + mocked_get_next_epoch_start_block = mocker.patch.object( + subtensor, "get_next_epoch_start_block" + ) + + # Call + result = subtensor.is_in_admin_freeze_window(netuid=netuid) + + # Asserts + mocked_get_next_epoch_start_block.assert_not_called() + assert result is False + + +@pytest.mark.parametrize( + "block, next_esb, expected_result", + ( + [89, 100, False], + [90, 100, False], + [91, 100, True], + ), +) +def test_is_in_admin_freeze_window(subtensor, mocker, block, next_esb, expected_result): + """Verify that `is_in_admin_freeze_window` method processed the data correctly.""" + # Preps + netuid = 14 + mocker.patch.object(subtensor, "get_current_block", return_value=block) + mocker.patch.object(subtensor, "get_next_epoch_start_block", return_value=next_esb) + mocker.patch.object(subtensor, "get_admin_freeze_window", return_value=10) + + # Call + + result = subtensor.is_in_admin_freeze_window(netuid=netuid) + + # Asserts + assert result is expected_result + + +def test_get_admin_freeze_window(subtensor, mocker): + """Verify that `get_admin_freeze_window` calls proper methods.""" + # Preps + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_query = mocker.patch.object(subtensor.substrate, "query") + + # Call + result = subtensor.get_admin_freeze_window() + + # Asserts + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="AdminFreezeWindow", + block_hash=mocked_determine_block_hash.return_value, + ) + assert result == mocked_query.return_value.value