diff --git a/bittensor/__init__.py b/bittensor/__init__.py index b6b2f08f4b..8d2049b355 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -18,19 +18,10 @@ import warnings from .core.settings import __version__, version_split, DEFAULTS, DEFAULT_NETWORK -from .core.async_subtensor import AsyncSubtensor from .utils.btlogging import logging from .utils.deprecated import * -async def async_subtensor(network: str = DEFAULT_NETWORK) -> AsyncSubtensor: - """ - Creates an initialised AsyncSubtensor object. - """ - async with AsyncSubtensor(network=network) as subtensor_: - return subtensor_ - - def __getattr__(name): if name == "version_split": warnings.warn( diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 56d1641ca1..aaaa58bde9 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1,76 +1,90 @@ +import argparse import asyncio +import copy from itertools import chain import ssl -from typing import Optional, Any, Union, TypedDict, Iterable, TYPE_CHECKING +from typing import Optional, Any, Union, Iterable, TYPE_CHECKING import aiohttp import asyncstdlib as a import numpy as np import scalecodec -from bittensor_wallet import Wallet from bittensor_wallet.utils import SS58_FORMAT from numpy.typing import NDArray -from scalecodec import GenericCall +from scalecodec import GenericCall, ScaleType from scalecodec.base import RuntimeConfiguration from scalecodec.type_registry import load_type_registry_preset from substrateinterface.exceptions import SubstrateRequestException +from bittensor.core import settings from bittensor.core.chain_data import ( DelegateInfo, - custom_rpc_type_registry, StakeInfo, NeuronInfoLite, NeuronInfo, + ProposalVoteData, SubnetHyperparameters, - decode_account_id, SubnetInfo, - PrometheusInfo, - ProposalVoteData, + WeightCommitInfo, + custom_rpc_type_registry, + decode_account_id, +) + +from bittensor.core.config import Config +from bittensor.core.extrinsics.asyncex.commit_reveal import commit_reveal_v3_extrinsic +from bittensor.core.extrinsics.asyncex.registration import ( + burned_register_extrinsic, + register_extrinsic, +) +from bittensor.core.extrinsics.asyncex.root import ( + set_root_weights_extrinsic, + root_register_extrinsic, +) +from bittensor.core.extrinsics.asyncex.serving import ( + publish_metadata, + get_metadata, +) +from bittensor.core.extrinsics.asyncex.serving import serve_axon_extrinsic +from bittensor.core.extrinsics.asyncex.staking import ( + add_stake_extrinsic, + add_stake_multiple_extrinsic, ) -from bittensor.core.extrinsics.asyncex.registration import register_extrinsic from bittensor.core.extrinsics.asyncex.transfer import transfer_extrinsic +from bittensor.core.extrinsics.asyncex.unstaking import ( + unstake_extrinsic, + unstake_multiple_extrinsic, +) from bittensor.core.extrinsics.asyncex.weights import ( commit_weights_extrinsic, set_weights_extrinsic, + reveal_weights_extrinsic, ) -from bittensor.core.extrinsics.asyncex.serving import serve_axon_extrinsic -from bittensor.core.extrinsics.asyncex.unstaking import unstake_extrinsic -from bittensor.core.extrinsics.asyncex.commit_reveal import commit_reveal_v3_extrinsic -from bittensor.core.settings import ( - TYPE_REGISTRY, - DEFAULTS, - NETWORK_MAP, - DELEGATES_DETAILS_URL, - DEFAULT_NETWORK, -) -from bittensor.core.settings import version_as_int +from bittensor.core.metagraph import AsyncMetagraph +from bittensor.core.types import ParamWithTypes +from bittensor.core.settings import version_as_int, TYPE_REGISTRY, DELEGATES_DETAILS_URL from bittensor.utils import ( - torch, - ss58_to_vec_u8, - format_error_message, decode_hex_identity_dict, - validate_chain_endpoint, + format_error_message, hex_to_bytes, - Certificate, + ss58_to_vec_u8, + torch, u16_normalized_float, - networking, + execute_coroutine, ) +from bittensor.utils import networking from bittensor.utils.substrate_interface import AsyncSubstrateInterface from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.delegates_details import DelegatesDetails from bittensor.utils.weight_utils import generate_weight_hash -from bittensor.core.metagraph import Metagraph + if TYPE_CHECKING: from scalecodec import ScaleType - from bittensor.utils.substrate_interface import QueryMapResult + from bittensor_wallet import Wallet from bittensor.core.axon import Axon - - -class ParamWithTypes(TypedDict): - name: str # Name of the parameter. - type: str # ScaleType string of the parameter. + from bittensor.utils import Certificate + from bittensor.utils.substrate_interface import QueryMapResult def _decode_hex_identity_dict(info_dictionary: dict[str, Any]) -> dict[str, Any]: @@ -102,47 +116,251 @@ def _decode_hex_identity_dict(info_dictionary: dict[str, Any]) -> dict[str, Any] class AsyncSubtensor: """Thin layer for interacting with Substrate Interface. Mostly a collection of frequently-used calls.""" - def __init__(self, network: str = DEFAULT_NETWORK) -> None: - if network in NETWORK_MAP: - self.chain_endpoint = NETWORK_MAP[network] - self.network = network - if network == "local": - logging.warning( - "Warning: Verify your local subtensor is running on port 9944." - ) - else: - is_valid, _ = validate_chain_endpoint(network) - if is_valid: - self.chain_endpoint = network - if network in NETWORK_MAP.values(): - self.network = next( - key for key, value in NETWORK_MAP.items() if value == network - ) - else: - self.network = "custom" - else: - logging.info( - f"Network not specified or not valid. Using default chain endpoint: " - f"[blue]{NETWORK_MAP[DEFAULTS.subtensor.network]}[/blue]." - ) - logging.info( - "You can set this for commands with the [blue]--network[/blue] flag, or by setting this in the " - "config." - ) - self.chain_endpoint = NETWORK_MAP[DEFAULTS.subtensor.network] - self.network = DEFAULTS.subtensor.network + def __init__( + self, + network: Optional[str] = None, + config: Optional["Config"] = None, + _mock: bool = False, + log_verbose: bool = False, + event_loop: asyncio.AbstractEventLoop = None, + ): + """ + Initializes an instance of the AsyncSubtensor class. + + Arguments: + network (str): The network name or type to connect to. + config (Optional[Config]): Configuration object for the AsyncSubtensor instance. + _mock: Whether this is a mock instance. Mainly just for use in testing. + log_verbose (bool): Enables or disables verbose logging. + event_loop (Optional[asyncio.AbstractEventLoop]): Custom asyncio event loop. + + Raises: + Any exceptions raised during the setup, configuration, or connection process. + """ + if config is None: + config = AsyncSubtensor.config() + self._config = copy.deepcopy(config) + self.chain_endpoint, self.network = AsyncSubtensor.setup_config( + network, self._config + ) + self._mock = _mock + + self.log_verbose = log_verbose + self._check_and_log_network_settings() + logging.debug( + f"Connecting to ..." + ) self.substrate = AsyncSubstrateInterface( url=self.chain_endpoint, ss58_format=SS58_FORMAT, type_registry=TYPE_REGISTRY, use_remote_preset=True, chain_name="Bittensor", + event_loop=event_loop, + _mock=_mock, ) + if self.log_verbose: + logging.info( + f"Connected to {self.network} network and {self.chain_endpoint}." + ) def __str__(self): return f"Network: {self.network}, Chain: {self.chain_endpoint}" + def __repr__(self): + return self.__str__() + + def __del__(self): + execute_coroutine(self.close()) + + def _check_and_log_network_settings(self): + if self.network == settings.NETWORKS[3]: # local + logging.warning( + ":warning: Verify your local subtensor is running on port [blue]9944[/blue]." + ) + + if ( + self.network == "finney" + or self.chain_endpoint == settings.FINNEY_ENTRYPOINT + ) and self.log_verbose: + logging.info( + f"You are connecting to {self.network} network with endpoint {self.chain_endpoint}." + ) + logging.debug( + "We strongly encourage running a local subtensor node whenever possible. " + "This increases decentralization and resilience of the network." + ) + # TODO: remove or apply this warning as updated default endpoint? + logging.debug( + "In a future release, local subtensor will become the default endpoint. " + "To get ahead of this change, please run a local subtensor node and point to it." + ) + + @staticmethod + def config() -> "Config": + """ + Creates and returns a Bittensor configuration object. + + Returns: + config (bittensor.core.config.Config): A Bittensor configuration object configured with arguments added by + the `subtensor.add_args` method. + """ + parser = argparse.ArgumentParser() + AsyncSubtensor.add_args(parser) + return Config(parser) + + @staticmethod + def setup_config(network: Optional[str], config: "Config"): + """ + Sets up and returns the configuration for the Subtensor network and endpoint. + + This method determines the appropriate network and chain endpoint based on the provided network string or + configuration object. It evaluates the network and endpoint in the following order of precedence: + 1. Provided network string. + 2. Configured chain endpoint in the `config` object. + 3. Configured network in the `config` object. + 4. Default chain endpoint. + 5. Default network. + + Arguments: + network (Optional[str]): The name of the Subtensor network. If None, the network and endpoint will be + determined from the `config` object. + config (bittensor.core.config.Config): The configuration object containing the network and chain endpoint + settings. + + Returns: + tuple: A tuple containing the formatted WebSocket endpoint URL and the evaluated network name. + """ + if network is None: + candidates = [ + ( + config.is_set("subtensor.chain_endpoint"), + config.subtensor.chain_endpoint, + ), + (config.is_set("subtensor.network"), config.subtensor.network), + ( + config.subtensor.get("chain_endpoint"), + config.subtensor.chain_endpoint, + ), + (config.subtensor.get("network"), config.subtensor.network), + ] + for check, config_network in candidates: + if check: + network = config_network + + evaluated_network, evaluated_endpoint = ( + AsyncSubtensor.determine_chain_endpoint_and_network(network) + ) + + return networking.get_formatted_ws_endpoint_url( + evaluated_endpoint + ), evaluated_network + + @classmethod + def help(cls): + """Print help to stdout.""" + parser = argparse.ArgumentParser() + cls.add_args(parser) + print(cls.__new__.__doc__) + parser.print_help() + + @classmethod + def add_args(cls, parser: "argparse.ArgumentParser", prefix: Optional[str] = None): + """ + Adds command-line arguments to the provided ArgumentParser for configuring the Subtensor settings. + + Arguments: + parser (argparse.ArgumentParser): The ArgumentParser object to which the Subtensor arguments will be added. + prefix (Optional[str]): An optional prefix for the argument names. If provided, the prefix is prepended to + each argument name. + + Arguments added: + --subtensor.network: The Subtensor network flag. Possible values are 'finney', 'test', 'archive', and + 'local'. Overrides the chain endpoint if set. + --subtensor.chain_endpoint: The Subtensor chain endpoint flag. If set, it overrides the network flag. + --subtensor._mock: If true, uses a mocked connection to the chain. + + Example: + parser = argparse.ArgumentParser() + Subtensor.add_args(parser) + """ + prefix_str = "" if prefix is None else f"{prefix}." + try: + default_network = settings.DEFAULT_NETWORK + default_chain_endpoint = settings.FINNEY_ENTRYPOINT + + parser.add_argument( + f"--{prefix_str}subtensor.network", + default=default_network, + type=str, + help="""The subtensor network flag. The likely choices are: + -- finney (main network) + -- test (test network) + -- archive (archive network +300 blocks) + -- local (local running network) + If this option is set it overloads subtensor.chain_endpoint with + an entry point node from that network. + """, + ) + parser.add_argument( + f"--{prefix_str}subtensor.chain_endpoint", + default=default_chain_endpoint, + type=str, + help="""The subtensor endpoint flag. If set, overrides the --network flag.""", + ) + parser.add_argument( + f"--{prefix_str}subtensor._mock", + default=False, + type=bool, + help="""If true, uses a mocked connection to the chain.""", + ) + + except argparse.ArgumentError: + # re-parsing arguments. + pass + + @staticmethod + def determine_chain_endpoint_and_network( + network: str, + ) -> tuple[Optional[str], Optional[str]]: + """Determines the chain endpoint and network from the passed network or chain_endpoint. + + Arguments: + network (str): The network flag. The choices are: ``finney`` (main network), ``archive`` (archive network + +300 blocks), ``local`` (local running network), ``test`` (test network). + + Returns: + tuple[Optional[str], Optional[str]]: The network and chain endpoint flag. If passed, overrides the + ``network`` argument. + """ + + if network is None: + return None, None + if network in settings.NETWORKS: + return network, settings.NETWORK_MAP[network] + + substrings_map = { + "entrypoint-finney.opentensor.ai": ("finney", settings.FINNEY_ENTRYPOINT), + "test.finney.opentensor.ai": ("test", settings.FINNEY_TEST_ENTRYPOINT), + "archive.chain.opentensor.ai": ("archive", settings.ARCHIVE_ENTRYPOINT), + "subvortex": ("subvortex", settings.SUBVORTEX_ENTRYPOINT), + "127.0.0.1": ("local", settings.LOCAL_ENTRYPOINT), + "localhost": ("local", settings.LOCAL_ENTRYPOINT), + } + + for substring, result in substrings_map.items(): + if substring in network: + return result + + return "unknown", network + + async def close(self): + """Close the connection.""" + if self.substrate: + await self.substrate.close() + async def __aenter__(self): logging.info( f"[magenta]Connecting to Substrate:[/magenta] [blue]{self}[/blue][magenta]...[/magenta]" @@ -166,8 +384,24 @@ async def __aenter__(self): async def __aexit__(self, exc_type, exc_val, exc_tb): await self.substrate.close() - async def close(self): - await self.substrate.close() + async def determine_block_hash( + self, + block: Optional[int], + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[str]: + # Ensure that only one of the parameters is specified. + if sum(bool(x) for x in [block, block_hash, reuse_block]) > 1: + raise ValueError( + "Only one of `block`, `block_hash`, or `reuse_block` can be specified." + ) + + # Return the appropriate value. + if block_hash: + return block_hash + if block: + return await self.get_block_hash(block) + return None async def encode_params( self, @@ -189,43 +423,6 @@ async def encode_params( return param_data.to_hex() - async def query_constant( - self, - module_name: str, - constant_name: str, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> Optional["ScaleType"]: - """ - Retrieves a constant from the specified module on the Bittensor blockchain. This function is used to access - fixed parameters or values defined within the blockchain's modules, which are essential for understanding - the network's configuration and rules. - - Args: - module_name: The name of the module containing the constant. - constant_name: The name of the constant to retrieve. - block: The blockchain block number at which to query the constant. Do not specify if using block_hash or - reuse_block - block_hash: the hash of th blockchain block at which to query the constant. Do not specify if using block - or reuse_block - reuse_block: Whether to reuse the blockchain block at which to query the constant. - - Returns: - Optional[scalecodec.ScaleType]: The value of the constant if found, `None` otherwise. - - Constants queried through this function can include critical network parameters such as inflation rates, - consensus rules, or validation thresholds, providing a deeper understanding of the Bittensor network's - operational parameters. - """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - return await self.substrate.get_constant( - module_name=module_name, - constant_name=constant_name, - block_hash=block_hash, - reuse_block_hash=reuse_block, - ) - async def get_hyperparameter( self, param_name: str, @@ -237,7 +434,7 @@ async def get_hyperparameter( """ Retrieves a specified hyperparameter for a specific subnet. - Args: + Arguments: param_name (str): The name of the hyperparameter to retrieve. netuid (int): The unique identifier of the subnet. block: the block number at which to retrieve the hyperparameter. Do not specify if using block_hash or @@ -249,8 +446,10 @@ async def get_hyperparameter( Returns: The value of the specified hyperparameter if the subnet exists, or None """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - if not await self.subnet_exists(netuid, block_hash, reuse_block=reuse_block): + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + if not await self.subnet_exists( + netuid, block_hash=block_hash, reuse_block=reuse_block + ): logging.error(f"subnet {netuid} does not exist") return None @@ -262,320 +461,158 @@ async def get_hyperparameter( reuse_block_hash=reuse_block, ) - return result - - # Common subtensor methods ========================================================================================= - - async def get_current_block(self) -> int: - """ - Returns the current block number on the Bittensor blockchain. This function provides the latest block number, - indicating the most recent state of the blockchain. - - Returns: - int: The current chain block number. - - Knowing the current block number is essential for querying real-time data and performing time-sensitive - operations on the blockchain. It serves as a reference point for network activities and data - synchronization. - """ - return await self.substrate.get_block_number(None) - - @a.lru_cache(maxsize=128) - async def _get_block_hash(self, block_id: int): - return await self.substrate.get_block_hash(block_id) - - async def get_block_hash(self, block_id: Optional[int] = None): - """ - Retrieves the hash of a specific block on the Bittensor blockchain. The block hash is a unique identifier - representing the cryptographic hash of the block's content, ensuring its integrity and immutability. - - Args: - block_id (int): The block number for which the hash is to be retrieved. - - Returns: - str: The cryptographic hash of the specified block. - - The block hash is a fundamental aspect of blockchain technology, providing a secure reference to each block's - data. It is crucial for verifying transactions, ensuring data consistency, and maintaining the - trustworthiness of the blockchain. - """ - if block_id: - return await self._get_block_hash(block_id) - else: - return await self.substrate.get_chain_head() + return getattr(result, "value", result) - async def _determine_block_hash( - self, block: Optional[int], block_hash: Optional[str], reuse_block: bool = False - ) -> Optional[str]: - if len([x for x in [block, block_hash, reuse_block] if x]) > 1: - raise ValueError( - "Of `block`, `block_hash`, or `reuse_block` only one can be specified." - ) - else: - if block_hash: - return block_hash - elif block: - return await self.get_block_hash(block) - else: - return None + # Subtensor queries =========================================================================================== - async def is_hotkey_registered_any( + async def query_constant( self, - hotkey_ss58: str, + module_name: str, + constant_name: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> bool: + ) -> Optional["ScaleType"]: """ - Checks if a neuron's hotkey is registered on any subnet within the Bittensor network. + Retrieves a constant from the specified module on the Bittensor blockchain. This function is used to access + fixed parameters or values defined within the blockchain's modules, which are essential for understanding + the network's configuration and rules. Args: - hotkey_ss58: The `SS58` address of the neuron's hotkey. - block: the blockchain block number on which to check. Do not specify if using block_hash or reuse_block - block_hash: The blockchain block_hash representation of block id. Do not specify if using block + module_name: The name of the module containing the constant. + constant_name: The name of the constant to retrieve. + block: The blockchain block number at which to query the constant. Do not specify if using block_hash or + reuse_block + block_hash: the hash of th blockchain block at which to query the constant. Do not specify if using block or reuse_block - reuse_block (bool): Whether to reuse the last-used block hash. Do not specify if using block_hash or block + reuse_block: Whether to reuse the blockchain block at which to query the constant. Returns: - bool: `True` if the hotkey is registered on any subnet, `False` otherwise. + Optional[scalecodec.ScaleType]: The value of the constant if found, `None` otherwise. - This function is essential for determining the network-wide presence and participation of a neuron. + Constants queried through this function can include critical network parameters such as inflation rates, + consensus rules, or validation thresholds, providing a deeper understanding of the Bittensor network's + operational parameters. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - return ( - len(await self.get_netuids_for_hotkey(hotkey_ss58, block_hash, reuse_block)) - > 0 + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + return await self.substrate.get_constant( + module_name=module_name, + constant_name=constant_name, + block_hash=block_hash, + reuse_block_hash=reuse_block, ) - async def get_subnet_burn_cost( + async def query_map( self, + module: str, + name: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[int]: + params: Optional[list] = None, + ) -> "QueryMapResult": """ - Retrieves the burn cost for registering a new subnet within the Bittensor network. This cost represents the - amount of Tao that needs to be locked or burned to establish a new subnet. + Queries map storage from any module on the Bittensor blockchain. This function retrieves data structures that + represent key-value mappings, essential for accessing complex and structured data within the blockchain + modules. Args: - block: The blockchain block number on which to check. Do not specify if using block_hash or reuse_block - block_hash: The blockchain block_hash of the block id. Do not specify if using block or + module: The name of the module from which to query the map storage. + name: The specific storage function within the module to query. + block: The blockchain block number at which to perform the query. + block_hash: The hash of the block to retrieve the parameter from. Do not specify if using block or reuse_block - reuse_block: Whether to reuse the last-used block hash. Do not specify if using block_hash or block + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + params: Parameters to be passed to the query. Returns: - The burn cost for subnet registration. + result: A data structure representing the map storage if found, `None` otherwise. - The subnet burn cost is an important economic parameter, reflecting the network's mechanisms for controlling - the proliferation of subnets and ensuring their commitment to the network's long-term viability. + This function is particularly useful for retrieving detailed and structured data from various blockchain + modules, offering insights into the network's state and the relationships between its different components. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - lock_cost = await self.query_runtime_api( - runtime_api="SubnetRegistrationRuntimeApi", - method="get_network_registration_cost", - params=[], + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query_map( + module=module, + storage_function=name, + params=params, block_hash=block_hash, - reuse_block=reuse_block, + reuse_block_hash=reuse_block, ) + return getattr(result, "value", None) - return lock_cost - - async def get_total_subnets( - self, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> Optional[int]: - """ - Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. - - Args: - block: The blockchain block number on which to check. Do not specify if using block_hash or reuse_block - block_hash (Optional[str]): The blockchain block_hash representation of block id. Do not specify if using - block or reuse_block - reuse_block: Whether to reuse the last-used block hash. Do not specify if using block_hash or block - - Returns: - The total number of subnets in the network. - - Understanding the total number of subnets is essential for assessing the network's growth and the extent of its - decentralized infrastructure. - """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - result = await self.substrate.query( - module="SubtensorModule", - storage_function="TotalNetworks", - params=[], - block_hash=block_hash, - reuse_block_hash=reuse_block, - ) - return result - - async def get_subnets( + async def query_map_subtensor( self, + name: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list[int]: + params: Optional[list] = None, + ) -> "QueryMapResult": """ - Retrieves the list of all subnet unique identifiers (netuids) currently present in the Bittensor network. + Queries map storage from the Subtensor module on the Bittensor blockchain. This function is designed to retrieve + a map-like data structure, which can include various neuron-specific details or network-wide attributes. Args: - block: The blockchain block number on which to check. Do not specify if using block_hash or reuse_block - block_hash (Optional[str]): The hash of the block to retrieve the subnet unique identifiers from. Do not - specify if using block or reuse_block - reuse_block (bool): Whether to reuse the last-used block hash. Do not set if using block_hash or block + name: The name of the map storage function to query. + block: The blockchain block number at which to perform the query. + block_hash: The hash of the block to retrieve the parameter 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. + params: A list of parameters to pass to the query function. Returns: - A list of subnet netuids. + An object containing the map-like data structure, or `None` if not found. - This function provides a comprehensive view of the subnets within the Bittensor network, - offering insights into its diversity and scale. + This function is particularly useful for analyzing and understanding complex network structures and + relationships within the Bittensor ecosystem, such as interneuronal connections and stake distributions. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - result = await self.substrate.query_map( + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + return await self.substrate.query_map( module="SubtensorModule", - storage_function="NetworksAdded", + storage_function=name, + params=params, block_hash=block_hash, reuse_block_hash=reuse_block, ) - return ( - [] - if result is None or not hasattr(result, "records") - else [netuid async for netuid, exists in result if exists] - ) - - async def is_hotkey_delegate( - self, - hotkey_ss58: str, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> bool: - """ - Determines whether a given hotkey (public key) is a delegate on the Bittensor network. This function checks if - the neuron associated with the hotkey is part of the network's delegation system. - - Args: - hotkey_ss58: The SS58 address of the neuron's hotkey. - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: The hash of the 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 - - Returns: - `True` if the hotkey is a delegate, `False` otherwise. - - Being a delegate is a significant status within the Bittensor network, indicating a neuron's involvement in - consensus and governance processes. - """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - delegates = await self.get_delegates( - block_hash=block_hash, reuse_block=reuse_block - ) - return hotkey_ss58 in [info.hotkey_ss58 for info in delegates] - - async def get_delegates( - self, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> list[DelegateInfo]: - """ - Fetches all delegates on the chain - - Args: - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: hash of the 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 - - Returns: - List of DelegateInfo objects, or an empty list if there are no delegates. - """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - hex_bytes_result = await self.query_runtime_api( - runtime_api="DelegateInfoRuntimeApi", - method="get_delegates", - params=[], - block_hash=block_hash, - reuse_block=reuse_block, - ) - if hex_bytes_result is not None: - return DelegateInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) - else: - return [] - async def get_stake_info_for_coldkey( + async def query_module( self, - coldkey_ss58: str, + module: str, + name: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list[StakeInfo]: + params: Optional[list] = None, + ) -> "ScaleType": """ - Retrieves stake information associated with a specific coldkey. This function provides details about the stakes - held by an account, including the staked amounts and associated delegates. + Queries any module storage on the Bittensor blockchain with the specified parameters and block number. This + function is a generic query interface that allows for flexible and diverse data retrieval from various + blockchain modules. Args: - coldkey_ss58 (str): The ``SS58`` address of the account's coldkey. - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: The hash of the blockchain block number for the query. Do not specify if using block or + module (str): The name of the module from which to query data. + name (str): The name of the storage function within the module. + block (Optional[int]): The blockchain block number at which to perform the query. + block_hash: The hash of the block to retrieve the parameter from. 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 + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + params (Optional[list[object]]): A list of parameters to pass to the query function. Returns: - A list of StakeInfo objects detailing the stake allocations for the account. - - Stake information is vital for account holders to assess their investment and participation in the network's - delegation and consensus processes. - """ - encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - hex_bytes_result = await self.query_runtime_api( - runtime_api="StakeInfoRuntimeApi", - method="get_stake_info_for_coldkey", - params=[encoded_coldkey], - block_hash=block_hash, - reuse_block=reuse_block, - ) - - if hex_bytes_result is None: - return [] - - return StakeInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) - - async def get_stake_for_coldkey_and_hotkey( - self, - hotkey_ss58: str, - coldkey_ss58: str, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> Balance: - """ - Retrieves stake information associated with a specific coldkey and hotkey. - - Args: - hotkey_ss58: the hotkey SS58 address to query - coldkey_ss58: the coldkey SS58 address to query - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: the hash of the 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 + An object containing the requested data if found, `None` otherwise. - Returns: - Stake Balance for the given coldkey and hotkey + This versatile query function is key to accessing a wide range of data and insights from different parts of the + Bittensor blockchain, enhancing the understanding and analysis of the network's state and dynamics. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - _result = await self.substrate.query( - module="SubtensorModule", - storage_function="Stake", - params=[hotkey_ss58, coldkey_ss58], + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + return await self.substrate.query( + module=module, + storage_function=name, + params=params, block_hash=block_hash, reuse_block_hash=reuse_block, ) - return Balance.from_rao(_result or 0) async def query_runtime_api( self, @@ -606,7 +643,7 @@ async def query_runtime_api( This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed and specific interactions with the network's runtime environment. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) call_definition = TYPE_REGISTRY["runtime_api"][runtime_api]["methods"][method] @@ -642,701 +679,737 @@ async def query_runtime_api( return obj.decode() - async def get_balance( + async def query_subtensor( self, - *addresses: str, + name: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict[str, Balance]: + params: Optional[list] = None, + ) -> "ScaleType": """ - Retrieves the balance for given coldkey(s) + Queries named storage from the Subtensor module on the Bittensor blockchain. This function is used to retrieve + specific data or parameters from the blockchain, such as stake, rank, or other neuron-specific attributes. Args: - addresses: coldkey addresses(s). - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: the block hash. Do not specify if using block or block - reuse_block: whether to reuse the last-used block hash. Do not set if using block_hash or block + name: The name of the storage function to query. + block: The blockchain block number at which to perform the query. + block_hash: The hash of the block to retrieve the parameter 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. + params: A list of parameters to pass to the query function. Returns: - Dict of {address: Balance objects}. + query_response (scalecodec.ScaleType): An object containing the requested data. + + This query function is essential for accessing detailed information about the network and its neurons, providing + valuable insights into the state and dynamics of the Bittensor ecosystem. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - if reuse_block: - block_hash = self.substrate.last_block_hash - else: - block_hash = await self.substrate.get_chain_head() - calls = [ - ( - await self.substrate.create_storage_key( - "System", "Account", [address], block_hash=block_hash - ) - ) - for address in addresses - ] - batch_call = await self.substrate.query_multi(calls, block_hash=block_hash) - results = {} - for item in batch_call: - value = item[1] or {"data": {"free": 0}} - results.update({item[0].params[0]: Balance(value["data"]["free"])}) - return results + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + return await self.substrate.query( + module="SubtensorModule", + storage_function=name, + params=params, + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) - async def get_transfer_fee( - self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] - ) -> "Balance": + async def state_call( + self, + method: str, + data: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[Any, Any]: """ - Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. This - function simulates the transfer to estimate the associated cost, taking into account the current network - conditions and transaction complexity. + Makes a state call to the Bittensor blockchain, allowing for direct queries of the blockchain's state. This + function is typically used for advanced queries that require specific method calls and data inputs. Args: - wallet: The wallet from which the transfer is initiated. - dest: The `SS58` address of the destination account. - value: The amount of tokens to be transferred, specified as a Balance object, or in Tao (float) or - Rao (int) units. + method: The method name for the state call. + data: The data to be passed to the method. + block: The blockchain block number at which to perform the state call. + block_hash: The hash of the block to retrieve the parameter 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 estimated transaction fee for the transfer, represented as a Balance object. + result (dict[Any, Any]): The result of the rpc call. - Estimating the transfer fee is essential for planning and executing token transactions, ensuring that the - wallet has sufficient funds to cover both the transfer amount and the associated costs. - This function provides a crucial tool for managing financial operations within the Bittensor network. + The state call function provides a more direct and flexible way of querying blockchain data, useful for specific + use cases where standard queries are insufficient. """ - if isinstance(value, float): - value = Balance.from_tao(value) - elif isinstance(value, int): - value = Balance.from_rao(value) + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + return await self.substrate.rpc_request( + method="state_call", + params=[method, data, block_hash] if block_hash else [method, data], + reuse_block_hash=reuse_block, + ) - if isinstance(value, Balance): - call = await self.substrate.compose_call( - call_module="Balances", - call_function="transfer_allow_death", - call_params={"dest": dest, "value": value.rao}, - ) - - try: - payment_info = await self.substrate.get_payment_info( - call=call, keypair=wallet.coldkeypub - ) - except Exception as e: - logging.error( - f":cross_mark: [red]Failed to get payment info: [/red]{e}" - ) - payment_info = {"partialFee": int(2e7)} # assume 0.02 Tao + # Common subtensor methods ========================================================================================= - return Balance.from_rao(payment_info["partialFee"]) - else: - fee = Balance.from_rao(int(2e7)) - logging.error( - "To calculate the transaction fee, the value must be Balance, float, or int. Received type: %s. Fee " - "is %s", - type(value), - 2e7, - ) - return fee + @property + async def block(self): + """Provides an asynchronous property to retrieve the current block.""" + return await self.get_current_block() - async def get_total_stake_for_coldkey( - self, - *ss58_addresses, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> dict[str, Balance]: + async def blocks_since_last_update(self, netuid: int, uid: int) -> Optional[int]: """ - Returns the total stake held on a coldkey. + Returns the number of blocks since the last update for a specific UID in the subnetwork. - Args: - ss58_addresses: The SS58 address(es) of the coldkey(s) - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: The hash of the block number to retrieve the stake from. 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 + Arguments: + netuid (int): The unique identifier of the subnetwork. + uid (int): The unique identifier of the neuron. Returns: - Dict in view {address: Balance objects}. + Optional[int]: The number of blocks since the last update, or ``None`` if the subnetwork or UID does not + exist. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - if reuse_block: - block_hash = self.substrate.last_block_hash - elif not block_hash: - block_hash = await self.substrate.get_chain_head() - calls = [ - ( - await self.substrate.create_storage_key( - "SubtensorModule", - "TotalColdkeyStake", - [address], - block_hash=block_hash, - ) - ) - for address in ss58_addresses - ] - batch_call = await self.substrate.query_multi(calls, block_hash=block_hash) - results = {} - for item in batch_call: - results.update({item[0].params[0]: Balance.from_rao(item[1] or 0)}) - return results + call = await self.get_hyperparameter(param_name="LastUpdate", netuid=netuid) + return None if call is None else await self.get_current_block() - int(call[uid]) - async def get_total_stake_for_hotkey( + async def bonds( self, - *ss58_addresses, + netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict[str, Balance]: + ) -> list[tuple[int, list[tuple[int, int]]]]: """ - Returns the total stake held on a hotkey. + Retrieves the bond distribution set by neurons within a specific subnet of the Bittensor network. + Bonds represent the investments or commitments made by neurons in one another, indicating a level of trust + and perceived value. This bonding mechanism is integral to the network's market-based approach to + measuring and rewarding machine intelligence. Args: - ss58_addresses: The SS58 address(es) of the hotkey(s) + netuid: The network UID of the subnet to query. block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: The hash of the block number to retrieve the stake from. Do not specify if using block - or reuse_block - reuse_block: Whether to reuse the last-used block hash when retrieving info. Do not set if using block_hash - or block + block_hash: The hash of the blockchain block number for the query. Do not specify if using reuse_block or + block. + reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or block. Returns: - Dict {address: Balance objects}. + List of tuples mapping each neuron's UID to its bonds with other neurons. + + Understanding bond distributions is crucial for analyzing the trust dynamics and market behavior within the + subnet. It reflects how neurons recognize and invest in each other's intelligence and contributions, + supporting diverse and niche systems within the Bittensor ecosystem. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - results = await self.substrate.query_multiple( - params=[s for s in ss58_addresses], + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + b_map_encoded = await self.substrate.query_map( module="SubtensorModule", - storage_function="TotalHotkeyStake", + storage_function="Bonds", + params=[netuid], block_hash=block_hash, reuse_block_hash=reuse_block, ) - return {k: Balance.from_rao(r or 0) for (k, r) in results.items()} + b_map = [(uid, b) async for uid, b in b_map_encoded] - async def get_netuids_for_hotkey( + return b_map + + async def commit(self, wallet: "Wallet", netuid: int, data: str) -> bool: + """ + Commits arbitrary data to the Bittensor network by publishing metadata. + + Arguments: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the data. + netuid (int): The unique identifier of the subnetwork. + data (str): The data to be committed to the network. + """ + return await publish_metadata( + subtensor=self, + wallet=wallet, + netuid=netuid, + data_type=f"Raw{len(data)}", + data=data.encode(), + ) + + async def commit_reveal_enabled( self, - hotkey_ss58: str, + netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list[int]: + ) -> bool: """ - Retrieves a list of subnet UIDs (netuids) for which a given hotkey is a member. This function identifies the - specific subnets within the Bittensor network where the neuron associated with the hotkey is active. + Check if commit-reveal mechanism is enabled for a given network at a specific block. - Args: - hotkey_ss58: The `SS58` address of the neuron's hotkey. - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: The hash of the blockchain block number at which to perform the query. Do not specify if using - block or reuse_block - reuse_block: Whether to reuse the last-used block hash when retrieving info. Do not set if using block_hash - or block. + Arguments: + netuid: The network identifier for which to check the commit-reveal mechanism. + block: The block number to query. Do not specify if using block_hash or reuse_block. + block_hash: The block hash of block at which to check the parameter. Do not set if using block or + reuse_block. + reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or + block. Returns: - A list of netuids where the neuron is a member. + Returns the integer value of the hyperparameter if available; otherwise, returns None. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - result = await self.substrate.query_map( - module="SubtensorModule", - storage_function="IsNetworkMember", - params=[hotkey_ss58], + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="CommitRevealWeightsEnabled", block_hash=block_hash, - reuse_block_hash=reuse_block, - ) - return ( - [record[0] async for record in result if record[1]] - if result and hasattr(result, "records") - else [] + netuid=netuid, + reuse_block=reuse_block, ) + return True if call is True else False - async def subnet_exists( + async def difficulty( self, netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> bool: + ) -> Optional[int]: """ - Checks if a subnet with the specified unique identifier (netuid) exists within the Bittensor network. + Retrieves the 'Difficulty' hyperparameter for a specified subnet in the Bittensor network. - Args: + This parameter is instrumental in determining the computational challenge required for neurons to participate in + consensus and validation processes. + + Arguments: netuid: The unique identifier of the subnet. - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: The hash of the blockchain block number at which to check the subnet existence. 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. + block: The blockchain block number for the query. Do not specify if using block_hash or reuse_block + block_hash: The hash of the block to retrieve the parameter 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: - `True` if the subnet exists, `False` otherwise. + Optional[int]: The value of the 'Difficulty' hyperparameter if the subnet exists, ``None`` otherwise. - This function is critical for verifying the presence of specific subnets in the network, - enabling a deeper understanding of the network's structure and composition. + The 'Difficulty' parameter directly impacts the network's security and integrity by setting the computational + effort required for validating transactions and participating in the network's consensus mechanism. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - result = await self.substrate.query( - module="SubtensorModule", - storage_function="NetworksAdded", - params=[netuid], + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="Difficulty", + netuid=netuid, block_hash=block_hash, - reuse_block_hash=reuse_block, + reuse_block=reuse_block, ) - return result + if call is None: + return None + return int(call) - async def filter_netuids_by_registered_hotkeys( + async def does_hotkey_exist( self, - all_netuids: Iterable[int], - filter_for_netuids: Iterable[int], - all_hotkeys: Iterable[Wallet], + hotkey_ss58: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list[int]: + ) -> bool: """ - Filters a given list of all netuids for certain specified netuids and hotkeys + Returns true if the hotkey is known by the chain and there are accounts. Args: - all_netuids: A list of netuids to filter. - filter_for_netuids: A subset of all_netuids to filter from the main list - all_hotkeys: Hotkeys to filter from the main list - block: the block number for this query. Do not specify if using block_hash or reuse_block. - block_hash: hash of the blockchain block number at which to perform the query. Do not specify if using block - or reuse_block - reuse_block: whether to reuse the last-used blockchain hash when retrieving info. Do not set if using - block_hash or block. + hotkey_ss58: The SS58 address of the hotkey. + block: the block number for this query. Do not specify if using block_hash or reuse_block + block_hash: The hash of the block number to check the hotkey against. Do not specify if using reuse_block + or block. + reuse_block: Whether to reuse the last-used blockchain hash. Do not set if using block_hash or block. Returns: - The filtered list of netuids. + `True` if the hotkey is known by the chain and there are accounts, `False` otherwise. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - netuids_with_registered_hotkeys = [ - item - for sublist in await asyncio.gather( - *[ - self.get_netuids_for_hotkey( - wallet.hotkey.ss58_address, - reuse_block=reuse_block, - block_hash=block_hash, - ) - for wallet in all_hotkeys - ] - ) - for item in sublist - ] - - if not filter_for_netuids: - all_netuids = netuids_with_registered_hotkeys - - else: - filtered_netuids = [ - netuid for netuid in all_netuids if netuid in filter_for_netuids - ] - - registered_hotkeys_filtered = [ - netuid - for netuid in netuids_with_registered_hotkeys - if netuid in filter_for_netuids - ] - - # Combine both filtered lists - all_netuids = filtered_netuids + registered_hotkeys_filtered - - return list(set(all_netuids)) + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + _result = await self.substrate.query( + module="SubtensorModule", + storage_function="Owner", + params=[hotkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + result = decode_account_id(_result[0]) + return_val = ( + False + if result is None + else result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" + ) + return return_val - async def get_existential_deposit( + async def get_all_subnets_info( self, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Balance: + ) -> list["SubnetInfo"]: """ - Retrieves the existential deposit amount for the Bittensor blockchain. - The existential deposit is the minimum amount of TAO required for an account to exist on the blockchain. - Accounts with balances below this threshold can be reaped to conserve network resources. + Retrieves detailed information about all subnets within the Bittensor network. This function provides + comprehensive data on each subnet, including its characteristics and operational parameters. - Args: - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: Block hash at which to query the deposit amount. Do not specify if using block or reuse_block - reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or block. + Arguments: + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The blockchain block_hash for the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. Returns: - The existential deposit amount. + list[SubnetInfo]: A list of SubnetInfo objects, each containing detailed information about a subnet. - The existential deposit is a fundamental economic parameter in the Bittensor network, ensuring efficient use of - storage and preventing the proliferation of dust accounts. + Gaining insights into the subnets' details assists in understanding the network's composition, the roles of + different subnets, and their unique features. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - result = await self.substrate.get_constant( - module_name="Balances", - constant_name="ExistentialDeposit", - block_hash=block_hash, - reuse_block_hash=reuse_block, + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + hex_bytes_result = await self.query_runtime_api( + "SubnetInfoRuntimeApi", "get_subnets_info", params=[], block_hash=block_hash ) + if not hex_bytes_result: + return [] + else: + return SubnetInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) - if result is None: - raise Exception("Unable to retrieve existential deposit amount.") - - return Balance.from_rao(result) - - async def neurons( + async def get_balance( self, - netuid: int, + address: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list[NeuronInfo]: + ) -> "Balance": """ - Retrieves a list of all neurons within a specified subnet of the Bittensor network. - This function provides a snapshot of the subnet's neuron population, including each neuron's attributes and - network interactions. + Retrieves the balance for given coldkey. - Args: - netuid: The unique identifier of the subnet. - block: the block number for this query. Do not specify if using block_hash or reuse_block. - block_hash: The hash of the blockchain block number for the query. Do not specify if using block - or reuse_block - reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or block. + Arguments: + address (str): coldkey address. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The blockchain block_hash for the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. Returns: - A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. - - Understanding the distribution and status of neurons within a subnet is key to comprehending the network's - decentralized structure and the dynamics of its consensus and governance processes. + Balance object. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - hex_bytes_result = await self.query_runtime_api( - runtime_api="NeuronInfoRuntimeApi", - method="get_neurons", - params=[netuid], + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + balance = await self.substrate.query( + "System", + "Account", + [address], block_hash=block_hash, - reuse_block=reuse_block, + reuse_block_hash=reuse_block, ) + return Balance(balance["data"]["free"]) - if hex_bytes_result is None: - return [] - - return NeuronInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) - - async def neurons_lite( + async def get_balances( self, - netuid: int, + *addresses: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list[NeuronInfoLite]: + ) -> dict[str, Balance]: """ - Retrieves a list of neurons in a 'lite' format from a specific subnet of the Bittensor network. - This function provides a streamlined view of the neurons, focusing on key attributes such as stake and network - participation. + Retrieves the balance for given coldkey(s) - Args: - netuid: The unique identifier of the subnet. - block: the block number for this query. Do not specify if using block_hash or reuse_block. - block_hash: The hash of the blockchain block number for the query. Do not specify if using block - or reuse_block - reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or block. + Arguments: + addresses (str): coldkey addresses(s). + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): the block hash, optional. + reuse_block (Optional[bool]): whether to reuse the last-used block hash. Returns: - A list of simplified neuron information for the subnet. + Dict of {address: Balance objects}. + """ + if reuse_block: + block_hash = self.substrate.last_block_hash + elif not block_hash: + block_hash = await self.get_block_hash() + else: + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + calls = [ + ( + await self.substrate.create_storage_key( + "System", "Account", [address], block_hash=block_hash + ) + ) + for address in addresses + ] + batch_call = await self.substrate.query_multi(calls, block_hash=block_hash) + results = {} + for item in batch_call: + value = item[1] or {"data": {"free": 0}} + results.update({item[0].params[0]: Balance(value["data"]["free"])}) + return results - This function offers a quick overview of the neuron population within a subnet, facilitating efficient analysis - of the network's decentralized structure and neuron dynamics. + async def get_current_block(self) -> int: """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - hex_bytes_result = await self.query_runtime_api( - runtime_api="NeuronInfoRuntimeApi", - method="get_neurons_lite", - params=[netuid], - block_hash=block_hash, - reuse_block=reuse_block, - ) + Returns the current block number on the Bittensor blockchain. This function provides the latest block number, + indicating the most recent state of the blockchain. - if hex_bytes_result is None: - return [] + Returns: + int: The current chain block number. - return NeuronInfoLite.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) + Knowing the current block number is essential for querying real-time data and performing time-sensitive + operations on the blockchain. It serves as a reference point for network activities and data + synchronization. + """ + return await self.substrate.get_block_number(None) - async def get_neuron_for_pubkey_and_subnet( + @a.lru_cache(maxsize=128) + async def _get_block_hash(self, block_id: int): + return await self.substrate.get_block_hash(block_id) + + async def get_block_hash(self, block: Optional[int] = None): + """ + Retrieves the hash of a specific block on the Bittensor blockchain. The block hash is a unique identifier + representing the cryptographic hash of the block's content, ensuring its integrity and immutability. + + Arguments: + block (int): The block number for which the hash is to be retrieved. + + Returns: + str: The cryptographic hash of the specified block. + + The block hash is a fundamental aspect of blockchain technology, providing a secure reference to each block's + data. It is crucial for verifying transactions, ensuring data consistency, and maintaining the + trustworthiness of the blockchain. + """ + if block: + return await self._get_block_hash(block) + else: + return await self.substrate.get_chain_head() + + async def get_children( self, - hotkey_ss58: str, + hotkey: str, netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> NeuronInfo: + ) -> tuple[bool, list, str]: """ - Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific subnet - UID (netuid). This function provides detailed neuron information for a particular subnet within the - Bittensor network. + This method retrieves the children of a given hotkey and netuid. It queries the SubtensorModule's ChildKeys + storage function to get the children and formats them before returning as a tuple. - Args: - hotkey_ss58: The `SS58` address of the neuron's hotkey. - netuid: The unique identifier of the subnet. - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: The blockchain block number at which to perform the query. Do not specify if using block - or reuse_block - reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or block. + Arguments: + hotkey (str): The hotkey value. + netuid (int): The netuid value. + block (Optional[int]): The block number for which the children are to be retrieved. + block_hash (Optional[str]): The hash of the block to retrieve the subnet unique identifiers from. + reuse_block (bool): Whether to reuse the last-used block hash. Returns: - Detailed information about the neuron if found, null neuron if not. + A tuple containing a boolean indicating success or failure, a list of formatted children, and an error + message (if applicable) + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + try: + children = await self.substrate.query( + module="SubtensorModule", + storage_function="ChildKeys", + params=[hotkey, netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + if children: + formatted_children = [] + for proportion, child in children: + # Convert U64 to int + formatted_child = decode_account_id(child[0]) + int_proportion = int(proportion) + formatted_children.append((int_proportion, formatted_child)) + return True, formatted_children, "" + else: + return True, [], "" + except SubstrateRequestException as e: + return False, [], format_error_message(e) - This function is crucial for accessing specific neuron data and understanding its status, stake, and other - attributes within a particular subnet of the Bittensor ecosystem. + async def get_commitment( + self, netuid: int, uid: int, block: Optional[int] = None + ) -> str: """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - uid = await self.substrate.query( - module="SubtensorModule", - storage_function="Uids", - params=[netuid, hotkey_ss58], - block_hash=block_hash, - reuse_block_hash=reuse_block, - ) - if uid is None: - return NeuronInfo.get_null_neuron() + Retrieves the on-chain commitment for a specific neuron in the Bittensor network. - params = [netuid, uid] - json_body = await self.substrate.rpc_request( - method="neuronInfo_getNeuron", - params=params, - ) + Arguments: + netuid (int): The unique identifier of the subnetwork. + uid (int): The unique identifier of the neuron. + block (Optional[int]): The block number to retrieve the commitment from. If None, the latest block is used. + Default is ``None``. - if not (result := json_body.get("result", None)): - return NeuronInfo.get_null_neuron() + Returns: + str: The commitment data as a string. + """ + metagraph = await self.metagraph(netuid) + hotkey = metagraph.hotkeys[uid] # type: ignore - return NeuronInfo.from_vec_u8(bytes(result)) + metadata = await get_metadata(self, netuid, hotkey, block) + try: + commitment = metadata["info"]["fields"][0] # type: ignore + hex_data = commitment[list(commitment.keys())[0]][2:] # type: ignore + return bytes.fromhex(hex_data).decode() - async def neuron_for_uid( + except TypeError: + return "" + + async def get_current_weight_commit_info( self, - uid: Optional[int], netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> NeuronInfo: + ) -> list: """ - Retrieves detailed information about a specific neuron identified by its unique identifier (UID) within a - specified subnet (netuid) of the Bittensor network. This function provides a comprehensive view of a - neuron's attributes, including its stake, rank, and operational status. + Retrieves CRV3 weight commit information for a specific subnet. - Args: - uid: The unique identifier of the neuron. - netuid: The unique identifier of the subnet. - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: The hash of the blockchain block number for the query. Do not specify if using block or - reuse_block - reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or block. + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. Default is ``None``. + block_hash (Optional[str]): The hash of the block to retrieve the subnet unique identifiers from. + reuse_block (bool): Whether to reuse the last-used block hash. Returns: - Detailed information about the neuron if found, a null neuron otherwise - - This function is crucial for analyzing individual neurons' contributions and status within a specific subnet, - offering insights into their roles in the network's consensus and validation mechanisms. + list: A list of commit details, where each entry is a dictionary with keys 'who', 'serialized_commit', and + 'reveal_round', or an empty list if no data is found. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - if uid is None: - return NeuronInfo.get_null_neuron() - - if reuse_block: - block_hash = self.substrate.last_block_hash - - params = [netuid, uid, block_hash] if block_hash else [netuid, uid] - json_body = await self.substrate.rpc_request( - method="neuronInfo_getNeuron", - params=params, # custom rpc method + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="CRV3WeightCommits", + params=[netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, ) - if not (result := json_body.get("result", None)): - return NeuronInfo.get_null_neuron() - bytes_result = bytes(result) - return NeuronInfo.from_vec_u8(bytes_result) + commits = result.records[0][1] if result.records else [] + return [WeightCommitInfo.from_vec_u8(commit) for commit in commits] - async def get_delegated( + async def get_delegate_by_hotkey( self, - coldkey_ss58: str, + hotkey_ss58: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list[tuple[DelegateInfo, Balance]]: + ) -> Optional[DelegateInfo]: """ - Retrieves a list of delegates and their associated stakes for a given coldkey. This function identifies the - delegates that a specific account has staked tokens on. + Retrieves detailed information about a delegate neuron based on its hotkey. This function provides a + comprehensive view of the delegate's status, including its stakes, nominators, and reward distribution. - Args: - coldkey_ss58: The `SS58` address of the account's coldkey. - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: The hash of the blockchain block number for the query. Do not specify if using block or - reuse_block - reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or block. + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the delegate's hotkey. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The hash of the block to retrieve the subnet unique identifiers from. + reuse_block (bool): Whether to reuse the last-used block hash. Returns: - A list of tuples, each containing a delegate's information and staked amount. + Optional[DelegateInfo]: Detailed information about the delegate neuron, ``None`` if not found. - This function is important for account holders to understand their stake allocations and their involvement in - the network's delegation and consensus mechanisms. + This function is essential for understanding the roles and influence of delegate neurons within the Bittensor + network's consensus and governance structures. """ - block_hash = ( - bh - if (bh := await self._determine_block_hash(block, block_hash, reuse_block)) - else (self.substrate.last_block_hash if reuse_block else None) - ) - encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) + + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + encoded_hotkey = ss58_to_vec_u8(hotkey_ss58) + json_body = await self.substrate.rpc_request( - method="delegateInfo_getDelegated", - params=([block_hash, encoded_coldkey] if block_hash else [encoded_coldkey]), + method="delegateInfo_getDelegate", # custom rpc method + params=([encoded_hotkey, block_hash] if block_hash else [encoded_hotkey]), + reuse_block_hash=reuse_block, ) - if not (result := json_body.get("result")): - return [] + if not (result := json_body.get("result", None)): + return None - return DelegateInfo.delegated_list_from_vec_u8(bytes(result)) + return DelegateInfo.from_vec_u8(bytes(result)) - async def query_identity( + async def get_delegate_identities( self, - key: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict: + ) -> dict[str, "DelegatesDetails"]: """ - Queries the identity of a neuron on the Bittensor blockchain using the given key. This function retrieves - detailed identity information about a specific neuron, which is a crucial aspect of the network's - decentralized identity and governance system. + Fetches delegates identities from the chain and GitHub. Preference is given to chain data, and missing info is + filled-in by the info from GitHub. At some point, we want to totally move away from fetching this info from + GitHub, but chain data is still limited in that regard. - Args: - key: The key used to query the neuron's identity, typically the neuron's SS58 address. - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: The hash of the blockchain block number at which to perform the query. Do not specify if using - block_hash or block. - reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or block. + Arguments: + block (Optional[int]): The blockchain block number for the query. + block_hash (str): the hash of the blockchain block for the query + reuse_block (bool): Whether to reuse the last-used blockchain block hash. Returns: - An object containing the identity information of the neuron if found, ``None`` otherwise. + Dict {ss58: DelegatesDetails, ...} - The identity information can include various attributes such as the neuron's stake, rank, and other - network-specific details, providing insights into the neuron's role and status within the Bittensor network. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + timeout = aiohttp.ClientTimeout(10.0) + async with aiohttp.ClientSession(timeout=timeout) as session: + identities_info, response = await asyncio.gather( + self.substrate.query_map( + module="Registry", + storage_function="IdentityOf", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ), + session.get(DELEGATES_DETAILS_URL), + ) - Note: - See the `Bittensor CLI documentation `_ for supported - identity parameters. + all_delegates_details = { + decode_account_id(ss58_address[0]): DelegatesDetails.from_chain_data( + decode_hex_identity_dict(identity["info"]) + ) + for ss58_address, identity in identities_info + } + + if response.ok: + all_delegates: dict[str, Any] = await response.json(content_type=None) + + for delegate_hotkey, delegate_details in all_delegates.items(): + delegate_info = all_delegates_details.setdefault( + delegate_hotkey, + DelegatesDetails( + display=delegate_details.get("name", ""), + web=delegate_details.get("url", ""), + additional=delegate_details.get("description", ""), + pgp_fingerprint=delegate_details.get("fingerprint", ""), + ), + ) + delegate_info.display = ( + delegate_info.display or delegate_details.get("name", "") + ) + delegate_info.web = delegate_info.web or delegate_details.get( + "url", "" + ) + delegate_info.additional = ( + delegate_info.additional + or delegate_details.get("description", "") + ) + delegate_info.pgp_fingerprint = ( + delegate_info.pgp_fingerprint + or delegate_details.get("fingerprint", "") + ) + + return all_delegates_details + + async def get_delegate_take( + self, + hotkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[float]: """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - identity_info = await self.substrate.query( - module="Registry", - storage_function="IdentityOf", - params=[key], + Retrieves the delegate 'take' percentage for a neuron identified by its hotkey. The 'take' represents the + percentage of rewards that the delegate claims from its nominators' stakes. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The hash of the block to retrieve the subnet unique identifiers from. + reuse_block (bool): Whether to reuse the last-used block hash. + + Returns: + Optional[float]: The delegate take percentage, None if not available. + + The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of + rewards among neurons and their nominators. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.query_subtensor( + name="Delegates", block_hash=block_hash, - reuse_block_hash=reuse_block, + reuse_block=reuse_block, + params=[hotkey_ss58], ) - try: - return _decode_hex_identity_dict(identity_info["info"]) - except TypeError: - return {} + return None if result is None else u16_normalized_float(result) - async def weights( + async def get_delegated( self, - netuid: int, + coldkey_ss58: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list[tuple[int, list[tuple[int, int]]]]: + ) -> list[tuple[DelegateInfo, Balance]]: """ - Retrieves the weight distribution set by neurons within a specific subnet of the Bittensor network. - This function maps each neuron's UID to the weights it assigns to other neurons, reflecting the network's trust - and value assignment mechanisms. + Retrieves a list of delegates and their associated stakes for a given coldkey. This function identifies the + delegates that a specific account has staked tokens on. - Args: - netuid: The network UID of the subnet to query. - block: the block number for this query. 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 reuse_block or block. - reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or block. + Arguments: + coldkey_ss58 (str): The `SS58` address of the account's coldkey. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The hash of the blockchain block number for the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. Returns: - A list of tuples mapping each neuron's UID to its assigned weights. + A list of tuples, each containing a delegate's information and staked amount. - 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. + This function is important for account holders to understand their stake allocations and their involvement in + the network's delegation and consensus mechanisms. """ - 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], - block_hash=block_hash, + + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) + json_body = await self.substrate.rpc_request( + method="delegateInfo_getDelegated", + params=([block_hash, encoded_coldkey] if block_hash else [encoded_coldkey]), reuse_block_hash=reuse_block, ) - w_map = [(uid, w or []) async for uid, w in w_map_encoded] - return w_map + if not (result := json_body.get("result")): + return [] - async def bonds( + return DelegateInfo.delegated_list_from_vec_u8(bytes(result)) + + async def get_delegates( self, - netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list[tuple[int, list[tuple[int, int]]]]: + ) -> list[DelegateInfo]: """ - Retrieves the bond distribution set by neurons within a specific subnet of the Bittensor network. - Bonds represent the investments or commitments made by neurons in one another, indicating a level of trust - and perceived value. This bonding mechanism is integral to the network's market-based approach to - measuring and rewarding machine intelligence. + Fetches all delegates on the chain - Args: - netuid: The network UID of the subnet to query. - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: The hash of the blockchain block number for the query. Do not specify if using reuse_block or - block. - reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or block. + Arguments: + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): hash of the blockchain block number for the query. + reuse_block (Optional[bool]): whether to reuse the last-used block hash. Returns: - List of tuples mapping each neuron's UID to its bonds with other neurons. - - Understanding bond distributions is crucial for analyzing the trust dynamics and market behavior within the - subnet. It reflects how neurons recognize and invest in each other's intelligence and contributions, - supporting diverse and niche systems within the Bittensor ecosystem. + List of DelegateInfo objects, or an empty list if there are no delegates. """ - 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], + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + hex_bytes_result = await self.query_runtime_api( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegates", + params=[], block_hash=block_hash, - reuse_block_hash=reuse_block, + reuse_block=reuse_block, ) - b_map = [(uid, b) async for uid, b in b_map_encoded] - - return b_map + if hex_bytes_result is not None: + return DelegateInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) + else: + return [] - async def does_hotkey_exist( + async def get_existential_deposit( self, - hotkey_ss58: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> bool: + ) -> Balance: """ - Returns true if the hotkey is known by the chain and there are accounts. + Retrieves the existential deposit amount for the Bittensor blockchain. + The existential deposit is the minimum amount of TAO required for an account to exist on the blockchain. + Accounts with balances below this threshold can be reaped to conserve network resources. - Args: - hotkey_ss58: The SS58 address of the hotkey. - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: The hash of the block number to check the hotkey against. Do not specify if using reuse_block - or block. - reuse_block: Whether to reuse the last-used blockchain hash. Do not set if using block_hash or block. + Arguments: + block (Optional[int]): The blockchain block number for the query. + block_hash (str): Block hash at which to query the deposit amount. If `None`, the current block is used. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. Returns: - `True` if the hotkey is known by the chain and there are accounts, `False` otherwise. + The existential deposit amount. + + The existential deposit is a fundamental economic parameter in the Bittensor network, ensuring efficient use of + storage and preventing the proliferation of dust accounts. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - _result = await self.substrate.query( - module="SubtensorModule", - storage_function="Owner", - params=[hotkey_ss58], + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.get_constant( + module_name="Balances", + constant_name="ExistentialDeposit", block_hash=block_hash, reuse_block_hash=reuse_block, ) - result = decode_account_id(_result[0]) - return_val = ( - False - if result is None - else result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" - ) - return return_val + + if result is None: + raise Exception("Unable to retrieve existential deposit amount.") + + return Balance.from_rao(getattr(result, "value", result)) async def get_hotkey_owner( self, @@ -1350,17 +1423,16 @@ async def get_hotkey_owner( This function queries the blockchain for the owner of the provided hotkey. If the hotkey does not exist at the specified block hash, it returns None. - Args: - hotkey_ss58: The SS58 address of the hotkey. - block: the block number for this query. Do not specify if using block_hash or reuse_block - block_hash: The hash of the block at which to check the hotkey ownership. Do not specify if using - reuse_block or block. - reuse_block: Whether to reuse the last-used blockchain hash. Do not set if using block_hash or block. + Arguments: + hotkey_ss58 (str): The SS58 address of the hotkey. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The hash of the block at which to check the hotkey ownership. + reuse_block (bool): Whether to reuse the last-used blockchain hash. Returns: Optional[str]: The SS58 address of the owner if the hotkey exists, or None if it doesn't. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) hk_owner_query = await self.substrate.query( module="SubtensorModule", storage_function="Owner", @@ -1376,788 +1448,1147 @@ async def get_hotkey_owner( hotkey_owner = val if exists else None return hotkey_owner - async def sign_and_send_extrinsic( + async def get_minimum_required_stake(self): + """ + Returns the minimum required stake for nominators in the Subtensor network. + This method retries the substrate call up to three times with exponential backoff in case of failures. + + Returns: + Balance: The minimum required stake as a Balance object. + + Raises: + Exception: If the substrate call fails after the maximum number of retries. + """ + result = await self.substrate.query( + module="SubtensorModule", storage_function="NominatorMinRequiredStake" + ) + + return Balance.from_rao(getattr(result, "value", None)) + + async def get_netuids_for_hotkey( self, - call: "GenericCall", - wallet: "Wallet", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - sign_with: str = "coldkey", - ) -> tuple[bool, str]: + hotkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[int]: """ - Helper method to sign and submit an extrinsic call to chain. + Retrieves a list of subnet UIDs (netuids) for which a given hotkey is a member. This function identifies the + specific subnets within the Bittensor network where the neuron associated with the hotkey is active. - Args: - call (scalecodec.types.GenericCall): a prepared Call object - wallet (bittensor_wallet.Wallet): the wallet whose coldkey will be used to sign the extrinsic - wait_for_inclusion (bool): whether to wait until the extrinsic call is included on the chain - wait_for_finalization (bool): whether to wait until the extrinsic call is finalized on the chain - sign_with: the wallet attr to sign the extrinsic call [coldkey (default), hotkey, or coldkeypub] + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The hash of the blockchain block number at which to perform the query. + reuse_block (Optional[bool]): Whether to reuse the last-used block hash when retrieving info. Returns: - (success, error message) + A list of netuids where the neuron is a member. """ - if sign_with not in ("coldkey", "hotkey", "coldkeypub"): - raise AttributeError( - f"'sign_with' must be either 'coldkey', 'hotkey' or 'coldkeypub', not '{sign_with}'" - ) - - extrinsic = await self.substrate.create_signed_extrinsic( - call=call, keypair=getattr(wallet, sign_with) + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="IsNetworkMember", + params=[hotkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, ) - try: - response = await self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, "" - if await response.is_success: - return True, "" - else: - return False, format_error_message(await response.error_message) - except SubstrateRequestException as e: - return False, format_error_message(e) + return [record[0] async for record in result if record[1]] if result else [] - async def get_children( + async def get_neuron_certificate( self, hotkey: str, netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> tuple[bool, list, str]: + ) -> Optional["Certificate"]: """ - This method retrieves the children of a given hotkey and netuid. It queries the SubtensorModule's ChildKeys - storage function to get the children and formats them before returning as a tuple. + Retrieves the TLS certificate for a specific neuron identified by its unique identifier (UID) within a + specified subnet (netuid) of the Bittensor network. - Args: - hotkey: The hotkey value. - netuid: The netuid value. - block: The block number to query. Do not specify if using block_hash or reuse_block. - block_hash: The hash of the blockchain block number for the query. Do not specify if using bloc or + Arguments: + hotkey: The hotkey to query. + 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 parameter from. Do not specify if using block or reuse_block. - reuse_block: Whether to reuse the last-used blockchain hash. Do not set if using block_hash or reuse_block. + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. Returns: - A tuple containing a boolean indicating success or failure, a list of formatted children, and an error - message (if applicable) + the certificate of the neuron if found, `None` otherwise. + + This function is used for certificate discovery for setting up mutual tls communication between neurons. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + certificate = await self.query_module( + module="SubtensorModule", + name="NeuronCertificates", + block_hash=block_hash, + reuse_block=reuse_block, + params=[netuid, hotkey], + ) try: - children = await self.substrate.query( - module="SubtensorModule", - storage_function="ChildKeys", - params=[hotkey, netuid], - block_hash=block_hash, - reuse_block_hash=reuse_block, - ) - if children: - formatted_children = [] - for proportion, child in children: - # Convert U64 to int - formatted_child = decode_account_id(child[0]) - int_proportion = int(proportion) - formatted_children.append((int_proportion, formatted_child)) - return True, formatted_children, "" - else: - return True, [], "" - except SubstrateRequestException as e: - return False, [], format_error_message(e) + if certificate: + return "".join( + chr(i) + for i in chain( + [certificate["algorithm"]], + certificate["public_key"][0], + ) + ) - async def get_subnet_hyperparameters( + except AttributeError: + return None + return None + + async def get_neuron_for_pubkey_and_subnet( self, + hotkey_ss58: str, netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[Union[list, SubnetHyperparameters]]: + ) -> "NeuronInfo": """ - Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters define - the operational settings and rules governing the subnet's behavior. + Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific subnet UID + (netuid). This function provides detailed neuron information for a particular subnet within the Bittensor + network. - Args: - netuid: The network UID of the subnet to query. - block: The block number to query. Do not specify if using block_hash or reuse_block. - block_hash: The hash of the blockchain block number for the query. Do not specify if using bloc or - reuse_block. - reuse_block: Whether to reuse the last-used blockchain hash. Do not set if using block_hash or reuse_block. + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[int]): The blockchain block number at which to perform the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. Returns: - The subnet's hyperparameters, or `None` if not available. + Optional[bittensor.core.chain_data.neuron_info.NeuronInfo]: Detailed information about the neuron if found, + ``None`` otherwise. - Understanding the hyperparameters is crucial for comprehending how subnets are configured and managed, and how - they interact with the network's consensus and incentive mechanisms. + This function is crucial for accessing specific neuron data and understanding its status, stake, and other + attributes within a particular subnet of the Bittensor ecosystem. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - hex_bytes_result = await self.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + uid = await self.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, hotkey_ss58], block_hash=block_hash, - reuse_block=reuse_block, + reuse_block_hash=reuse_block, ) + if uid is None: + return NeuronInfo.get_null_neuron() - if hex_bytes_result is None: - return [] + params = [netuid, uid] + json_body = await self.substrate.rpc_request( + method="neuronInfo_getNeuron", params=params, reuse_block_hash=reuse_block + ) - return SubnetHyperparameters.from_vec_u8(hex_to_bytes(hex_bytes_result)) + if not (result := json_body.get("result", None)): + return NeuronInfo.get_null_neuron() - async def get_vote_data( + return NeuronInfo.from_vec_u8(bytes(result)) + + async def get_stake_for_coldkey_and_hotkey( self, - proposal_hash: str, + hotkey_ss58: str, + coldkey_ss58: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[ProposalVoteData]: + ) -> Balance: """ - Retrieves the voting data for a specific proposal on the Bittensor blockchain. This data includes information - about how senate members have voted on the proposal. + Retrieves stake information associated with a specific coldkey and hotkey. - Args: - proposal_hash: The hash of the proposal for which voting data is requested. - block: The block number to query. Do not specify if using block_hash or reuse_block. - block_hash: The hash of the blockchain block number to query the voting data. Do not specify if using block - or reuse_block. - reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or - block. + Arguments: + hotkey_ss58 (str): the hotkey SS58 address to query + coldkey_ss58 (str): the coldkey SS58 address to query + block (Optional[int]): the block number to query + block_hash (Optional[str]): the hash of the blockchain block number for the query. + reuse_block (Optional[bool]): whether to reuse the last-used block hash. Returns: - An object containing the proposal's voting data, or `None` if not found. - - This function is important for tracking and understanding the decision-making processes within the Bittensor - network, particularly how proposals are received and acted upon by the governing body. + Stake Balance for the given coldkey and hotkey """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - vote_data = await self.substrate.query( - module="Triumvirate", - storage_function="Voting", - params=[proposal_hash], + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query( + module="SubtensorModule", + storage_function="Stake", + params=[hotkey_ss58, coldkey_ss58], block_hash=block_hash, reuse_block_hash=reuse_block, ) - if vote_data is None: - return None - else: - return ProposalVoteData(vote_data) + return Balance.from_rao(result or 0) - async def get_delegate_identities( + async def get_stake_info_for_coldkey( self, + coldkey_ss58: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict[str, DelegatesDetails]: + ) -> list[StakeInfo]: """ - Fetches delegates identities from the chain and GitHub. Preference is given to chain data, and missing info is - filled-in by the info from GitHub. At some point, we want to totally move away from fetching this info from - GitHub, but chain data is still limited in that regard. + Retrieves stake information associated with a specific coldkey. This function provides details about the stakes + held by an account, including the staked amounts and associated delegates. - Args: - block: The block number to query. 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 blockchain block hash. Do not set if using block_hash or block. + Arguments: + coldkey_ss58 (str): The ``SS58`` address of the account's coldkey. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The hash of the blockchain block number for the query. + reuse_block (bool): Whether to reuse the last-used block hash. Returns: - Dict {ss58: DelegatesDetails, ...} + A list of StakeInfo objects detailing the stake allocations for the account. + Stake information is vital for account holders to assess their investment and participation in the network's + delegation and consensus processes. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - timeout = aiohttp.ClientTimeout(10.0) - async with aiohttp.ClientSession(timeout=timeout) as session: - identities_info, response = await asyncio.gather( - self.substrate.query_map( - module="Registry", - storage_function="IdentityOf", - block_hash=block_hash, - reuse_block_hash=reuse_block, - ), - session.get(DELEGATES_DETAILS_URL), - ) - - all_delegates_details = { - decode_account_id(ss58_address[0]): DelegatesDetails.from_chain_data( - decode_hex_identity_dict(identity["info"]) - ) - for ss58_address, identity in identities_info - } + encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) - if response.ok: - all_delegates: dict[str, Any] = await response.json(content_type=None) + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + hex_bytes_result = await self.query_runtime_api( + runtime_api="StakeInfoRuntimeApi", + method="get_stake_info_for_coldkey", + params=[encoded_coldkey], + block_hash=block_hash, + reuse_block=reuse_block, + ) - for delegate_hotkey, delegate_details in all_delegates.items(): - delegate_info = all_delegates_details.setdefault( - delegate_hotkey, - DelegatesDetails( - display=delegate_details.get("name", ""), - web=delegate_details.get("url", ""), - additional=delegate_details.get("description", ""), - pgp_fingerprint=delegate_details.get("fingerprint", ""), - ), - ) - delegate_info.display = ( - delegate_info.display or delegate_details.get("name", "") - ) - delegate_info.web = delegate_info.web or delegate_details.get( - "url", "" - ) - delegate_info.additional = ( - delegate_info.additional - or delegate_details.get("description", "") - ) - delegate_info.pgp_fingerprint = ( - delegate_info.pgp_fingerprint - or delegate_details.get("fingerprint", "") - ) + if hex_bytes_result is None: + return [] - return all_delegates_details + return StakeInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) - async def is_hotkey_registered( + async def get_subnet_burn_cost( self, - hotkey_ss58: str, - netuid: Optional[int] = None, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> bool: + ) -> Optional[str]: """ - Determines whether a given hotkey (public key) is registered in the Bittensor network, either globally across - any subnet or specifically on a specified subnet. This function checks the registration status of a neuron - identified by its hotkey, which is crucial for validating its participation and activities within the - network. + Retrieves the burn cost for registering a new subnet within the Bittensor network. This cost represents the + amount of Tao that needs to be locked or burned to establish a new subnet. - Args: - hotkey_ss58: The SS58 address of the neuron's hotkey. - netuid: The unique identifier of the subnet to check the registration. If `None`, the - registration is checked across all subnets. - block: The blockchain block number at which to perform the query. - block_hash: The blockchain block_hash representation of the block id. Do not specify if using block or - reuse_block - reuse_block (bool): Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or - reuse_block. + Arguments: + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[int]): The blockchain block_hash of the block id. + reuse_block (bool): Whether to reuse the last-used block hash. Returns: - bool: `True` if the hotkey is registered in the specified context (either any subnet or a specific subnet), - `False` otherwise. + int: The burn cost for subnet registration. - This function is important for verifying the active status of neurons in the Bittensor network. It aids in - understanding whether a neuron is eligible to participate in network processes such as consensus, - validation, and incentive distribution based on its registration status. - """ - if netuid is None: - return await self.is_hotkey_registered_any( - hotkey_ss58, block, block_hash, reuse_block - ) - else: - return await self.is_hotkey_registered_on_subnet( - hotkey_ss58, netuid, block, block_hash, reuse_block - ) + The subnet burn cost is an important economic parameter, reflecting the network's mechanisms for controlling + the proliferation of subnets and ensuring their commitment to the network's long-term viability. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + lock_cost = await self.query_runtime_api( + runtime_api="SubnetRegistrationRuntimeApi", + method="get_network_registration_cost", + params=[], + block_hash=block_hash, + reuse_block=reuse_block, + ) - async def get_uid_for_hotkey_on_subnet( + return lock_cost + + async def get_subnet_hyperparameters( self, - hotkey_ss58: str, netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[int]: + ) -> Optional[Union[list, SubnetHyperparameters]]: """ - Retrieves the unique identifier (UID) for a neuron's hotkey on a specific subnet. + Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters define + the operational settings and rules governing the subnet's behavior. - Args: - hotkey_ss58: The ``SS58`` address of the neuron's hotkey. - netuid: The unique identifier of the subnet. - block: The block number to query. Do not specify if using block_hash or reuse_block. - block_hash: The blockchain block_hash representation of the block id. Do not specify if using block or - reuse_block - reuse_block (bool): Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or - reuse_block. + Arguments: + netuid (int): The network UID of the subnet to query. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The hash of the blockchain block number for the query. + reuse_block (bool): Whether to reuse the last-used blockchain hash. Returns: - The UID of the neuron if it is registered on the subnet, `None` otherwise. + The subnet's hyperparameters, or `None` if not available. - The UID is a critical identifier within the network, linking the neuron's hotkey to its operational and - governance activities on a particular subnet. + Understanding the hyperparameters is crucial for comprehending how subnets are configured and managed, and how + they interact with the network's consensus and incentive mechanisms. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - result = await self.substrate.query( - module="SubtensorModule", - storage_function="Uids", - params=[netuid, hotkey_ss58], + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + hex_bytes_result = await self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams", + params=[netuid], block_hash=block_hash, - reuse_block_hash=reuse_block, + reuse_block=reuse_block, ) - return result - async def weights_rate_limit( + if hex_bytes_result is None: + return [] + + return SubnetHyperparameters.from_vec_u8(hex_to_bytes(hex_bytes_result)) + + async def get_subnet_reveal_period_epochs( + self, netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None + ) -> int: + """Retrieve the SubnetRevealPeriodEpochs hyperparameter.""" + block_hash = await self.determine_block_hash(block, block_hash) + return await self.get_hyperparameter( + param_name="RevealPeriodEpochs", block_hash=block_hash, netuid=netuid + ) + + async def get_subnets( self, - netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[int]: + ) -> list[int]: """ - Returns network WeightsSetRateLimit hyperparameter. + Retrieves the list of all subnet unique identifiers (netuids) currently present in the Bittensor network. - Args: - netuid: The unique identifier of the subnetwork. - block: The block number to query. Do not specify if using block_hash or reuse_block. - block_hash: The blockchain block_hash representation of the block id. Do not specify if using block or - reuse_block - reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or block + Arguments: + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The hash of the block to retrieve the subnet unique identifiers from. + reuse_block (bool): Whether to reuse the last-used block hash. Returns: - The value of the WeightsSetRateLimit hyperparameter, or `None` if the subnetwork does not exist or the - parameter is not found. + A list of subnet netuids. + + This function provides a comprehensive view of the subnets within the Bittensor network, + offering insights into its diversity and scale. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - call = await self.get_hyperparameter( - param_name="WeightsSetRateLimit", - netuid=netuid, + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="NetworksAdded", block_hash=block_hash, - reuse_block=reuse_block, + reuse_block_hash=reuse_block, ) - return None if call is None else int(call) + return [netuid async for netuid, exists in result if exists] if result else [] - async def blocks_since_last_update(self, netuid: int, uid: int) -> Optional[int]: + async def get_total_stake_for_coldkey( + self, + ss58_address: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Balance: """ - Returns the number of blocks since the last update for a specific UID in the subnetwork. + Returns the total stake held on a coldkey. - Args: - netuid: The unique identifier of the subnetwork. - uid: The unique identifier of the neuron. + Arguments: + ss58_address (str): The SS58 address of the coldkey + block (Optional[int]): The blockchain block number for the query. + block_hash (str): The hash of the block number to retrieve the stake from. + reuse_block (bool): Whether to reuse the last-used block hash. Returns: - The number of blocks since the last update, or `None` if the subnetwork or UID does not exist. + Balance of the stake held on the coldkey. """ - call = await self.get_hyperparameter(param_name="LastUpdate", netuid=netuid) - return None if call is None else await self.get_current_block() - int(call[uid]) + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query( + "SubtensorModule", + "TotalColdkeyStake", + [ss58_address], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return Balance.from_rao(result or 0) - async def commit_reveal_enabled( + async def get_total_stake_for_coldkeys( self, - netuid: int, + *ss58_addresses: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> bool: + ) -> dict[str, Balance]: """ - Check if commit-reveal mechanism is enabled for a given network at a specific block. + Returns the total stake held on multiple coldkeys. Arguments: - netuid: The network identifier for which to check the commit-reveal mechanism. - block: The block number to query. Do not specify if using block_hash or reuse_block. - block_hash: The block hash of block at which to check the parameter. Do not set if using block or - reuse_block. - reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or - block. + ss58_addresses (tuple[str]): The SS58 address(es) of the coldkey(s) + block (Optional[int]): The blockchain block number for the query. + block_hash (str): The hash of the block number to retrieve the stake from. + reuse_block (bool): Whether to reuse the last-used block hash. Returns: - Returns the integer value of the hyperparameter if available; otherwise, returns None. + Dict in view {address: Balance objects}. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - call = await self.get_hyperparameter( - param_name="CommitRevealWeightsEnabled", - block_hash=block_hash, - netuid=netuid, - reuse_block=reuse_block, - ) - return True if call is True else False + if reuse_block: + block_hash = self.substrate.last_block_hash + elif not block_hash: + block_hash = await self.substrate.get_chain_head() + else: + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + calls = [ + ( + await self.substrate.create_storage_key( + "SubtensorModule", + "TotalColdkeyStake", + [address], + block_hash=block_hash, + ) + ) + for address in ss58_addresses + ] + batch_call = await self.substrate.query_multi(calls, block_hash=block_hash) + results = {} + for item in batch_call: + results.update({item[0].params[0]: Balance.from_rao(item[1] or 0)}) + return results - async def get_subnet_reveal_period_epochs( + async def get_total_stake_for_hotkey( self, - netuid: int, + ss58_address, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> int: - """Retrieve the SubnetRevealPeriodEpochs hyperparameter.""" - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - return await self.get_hyperparameter( - param_name="RevealPeriodEpochs", + ) -> Balance: + """ + Returns the total stake held on a hotkey. + + Arguments: + ss58_address (str): The SS58 address of the hotkey + block (Optional[int]): The blockchain block number for the query. + block_hash (str): The hash of the block number to retrieve the stake from. + reuse_block (bool): Whether to reuse the last-used block hash when retrieving info. + + Returns: + Balance of the stake held on the hotkey. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query( + module="SubtensorModule", + storage_function="TotalHotkeyStake", + params=[ss58_address], block_hash=block_hash, - netuid=netuid, - reuse_block=reuse_block, + reuse_block_hash=reuse_block, ) + return Balance.from_rao(result or 0) - # Extrinsics ======================================================================================================= - - async def transfer( + async def get_total_stake_for_hotkeys( self, - wallet: "Wallet", - destination: str, - amount: float, - transfer_all: bool, - ) -> bool: + *ss58_addresses, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[str, Balance]: """ - Transfer token of amount to destination. + Returns the total stake held on hotkeys. - Args: - wallet (bittensor_wallet.Wallet): Source wallet for the transfer. - destination (str): Destination address for the transfer. - amount (float): Amount of tokens to transfer. - transfer_all (bool): Flag to transfer all tokens. + Arguments: + ss58_addresses (tuple[str]): The SS58 address(es) of the hotkey(s) + block (Optional[int]): The blockchain block number for the query. + block_hash (str): The hash of the block number to retrieve the stake from. + reuse_block (bool): Whether to reuse the last-used block hash when retrieving info. Returns: - `True` if the transferring was successful, otherwise `False`. + Dict {address: Balance objects}. """ - return await transfer_extrinsic( - self, - wallet, - destination, - Balance.from_tao(amount), - transfer_all, + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + results = await self.substrate.query_multiple( + params=[s for s in ss58_addresses], + module="SubtensorModule", + storage_function="TotalHotkeyStake", + block_hash=block_hash, + reuse_block_hash=reuse_block, ) + return {k: Balance.from_rao(r or 0) for (k, r) in results.items()} - async def root_register( + async def get_total_subnets( self, - wallet: "Wallet", - netuid: int, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - ) -> bool: + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: """ - Register neuron by recycling some TAO. + Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. - Args: - wallet: Bittensor wallet instance. - netuid: Subnet uniq id. - 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`. + Arguments: + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The blockchain block_hash representation of block id. + reuse_block (bool): Whether to reuse the last-used block hash. Returns: - `True` if registration was successful, otherwise `False`. + Optional[str]: The total number of subnets in the network. + + Understanding the total number of subnets is essential for assessing the network's growth and the extent of its + decentralized infrastructure. """ - logging.info( - f"Registering on netuid [blue]0[/blue] on network: [blue]{self.network}[/blue]" + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query( + module="SubtensorModule", + storage_function="TotalNetworks", + params=[], + block_hash=block_hash, + reuse_block_hash=reuse_block, ) + return result - # Check current recycle amount - logging.info("Fetching recycle amount & balance.") - block_hash = await self.substrate.get_chain_head() - recycle_call, balance_ = await asyncio.gather( - self.get_hyperparameter( - param_name="Burn", netuid=netuid, block_hash=block_hash - ), - self.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), - ) - current_recycle = Balance.from_rao(int(recycle_call)) - try: - balance: Balance = balance_[wallet.coldkeypub.ss58_address] - except TypeError as e: - logging.error(f"Unable to retrieve current recycle. {e}") - return False - except KeyError: - logging.error("Unable to retrieve current balance.") - return False + async def get_transfer_fee( + self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] + ) -> "Balance": + """ + Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. This + function simulates the transfer to estimate the associated cost, taking into account the current network + conditions and transaction complexity. - # Check balance is sufficient - if balance < current_recycle: - logging.error( - f"[red]Insufficient balance {balance} to register neuron. " - f"Current recycle is {current_recycle} TAO[/red]." - ) - return False + Arguments: + wallet (bittensor_wallet.Wallet): The wallet from which the transfer is initiated. + dest (str): The ``SS58`` address of the destination account. + value (Union[bittensor.utils.balance.Balance, float, int]): The amount of tokens to be transferred, + specified as a Balance object, or in Tao (float) or Rao (int) units. - return await root_register_extrinsic( - subtensor=self, - wallet=wallet, + Returns: + bittensor.utils.balance.Balance: The estimated transaction fee for the transfer, represented as a Balance + object. + + Estimating the transfer fee is essential for planning and executing token transactions, ensuring that the wallet + has sufficient funds to cover both the transfer amount and the associated costs. This function provides a + crucial tool for managing financial operations within the Bittensor network. + """ + if isinstance(value, float): + value = Balance.from_tao(value) + elif isinstance(value, int): + value = Balance.from_rao(value) + + if isinstance(value, Balance): + call = await self.substrate.compose_call( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": dest, "value": str(value.rao)}, + ) + + try: + payment_info = await self.substrate.get_payment_info( + call=call, keypair=wallet.coldkeypub + ) + except Exception as e: + logging.error( + f":cross_mark: [red]Failed to get payment info: [/red]{e}" + ) + payment_info = {"partialFee": int(2e7)} # assume 0.02 Tao + + return Balance.from_rao(payment_info["partialFee"]) + else: + fee = Balance.from_rao(int(2e7)) + logging.error( + "To calculate the transaction fee, the value must be Balance, float, or int. Received type: %s. Fee " + "is %s", + type(value), + 2e7, + ) + return fee + + async def get_vote_data( + self, + proposal_hash: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional["ProposalVoteData"]: + """ + Retrieves the voting data for a specific proposal on the Bittensor blockchain. This data includes information + about how senate members have voted on the proposal. + + Arguments: + proposal_hash (str): The hash of the proposal for which voting data is requested. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The hash of the blockchain block number to query the voting data. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. + + Returns: + An object containing the proposal's voting data, or `None` if not found. + + This function is important for tracking and understanding the decision-making processes within the Bittensor + network, particularly how proposals are received and acted upon by the governing body. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + vote_data = await self.substrate.query( + module="Triumvirate", + storage_function="Voting", + params=[proposal_hash], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + if vote_data is None: + return None + else: + return ProposalVoteData(vote_data) + + async def get_uid_for_hotkey_on_subnet( + self, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """ + Retrieves the unique identifier (UID) for a neuron's hotkey on a specific subnet. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The blockchain block_hash representation of the block id. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. + + Returns: + Optional[int]: The UID of the neuron if it is registered on the subnet, ``None`` otherwise. + + The UID is a critical identifier within the network, linking the neuron's hotkey to its operational and + governance activities on a particular subnet. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, hotkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return getattr(result, "value", result) + + async def filter_netuids_by_registered_hotkeys( + self, + all_netuids: Iterable[int], + filter_for_netuids: Iterable[int], + all_hotkeys: Iterable["Wallet"], + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[int]: + """ + Filters a given list of all netuids for certain specified netuids and hotkeys + + Arguments: + all_netuids (Iterable[int]): A list of netuids to filter. + filter_for_netuids (Iterable[int]): A subset of all_netuids to filter from the main list. + all_hotkeys (Iterable[Wallet]): Hotkeys to filter from the main list. + block (Optional[int]): The blockchain block number for the query. + block_hash (str): hash of the blockchain block number at which to perform the query. + reuse_block (bool): whether to reuse the last-used blockchain hash when retrieving info. + + Returns: + The filtered list of netuids. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + netuids_with_registered_hotkeys = [ + item + for sublist in await asyncio.gather( + *[ + self.get_netuids_for_hotkey( + wallet.hotkey.ss58_address, + reuse_block=reuse_block, + block_hash=block_hash, + ) + for wallet in all_hotkeys + ] + ) + for item in sublist + ] + + if not filter_for_netuids: + all_netuids = netuids_with_registered_hotkeys + + else: + filtered_netuids = [ + netuid for netuid in all_netuids if netuid in filter_for_netuids + ] + + registered_hotkeys_filtered = [ + netuid + for netuid in netuids_with_registered_hotkeys + if netuid in filter_for_netuids + ] + + # Combine both filtered lists + all_netuids = filtered_netuids + registered_hotkeys_filtered + + return list(set(all_netuids)) + + async def immunity_period( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """ + Retrieves the 'ImmunityPeriod' hyperparameter for a specific subnet. This parameter defines the duration during + which new neurons are protected from certain network penalties or restrictions. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The blockchain block_hash representation of the block id. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. + + Returns: + Optional[int]: The value of the 'ImmunityPeriod' hyperparameter if the subnet exists, ``None`` otherwise. + + The 'ImmunityPeriod' is a critical aspect of the network's governance system, ensuring that new participants + have a grace period to establish themselves and contribute to the network without facing immediate + punitive actions. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="ImmunityPeriod", netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + block_hash=block_hash, + reuse_block=reuse_block, + ) + return None if call is None else int(call) + + async def is_hotkey_delegate( + self, + hotkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """ + Determines whether a given hotkey (public key) is a delegate on the Bittensor network. This function checks if + the neuron associated with the hotkey is part of the network's delegation system. + + Arguments: + hotkey_ss58 (str): The SS58 address of the neuron's hotkey. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The hash of the blockchain block number for the query. + reuse_block (Optional[bool]): Whether to reuse the last-used block hash. + + Returns: + `True` if the hotkey is a delegate, `False` otherwise. + + Being a delegate is a significant status within the Bittensor network, indicating a neuron's involvement in + consensus and governance processes. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + delegates = await self.get_delegates( + block_hash=block_hash, reuse_block=reuse_block + ) + return hotkey_ss58 in [info.hotkey_ss58 for info in delegates] + + async def is_hotkey_registered( + self, + hotkey_ss58: str, + netuid: Optional[int] = None, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """ + Determines whether a given hotkey (public key) is registered in the Bittensor network, either globally across + any subnet or specifically on a specified subnet. This function checks the registration status of a neuron + identified by its hotkey, which is crucial for validating its participation and activities within the + network. + + Args: + hotkey_ss58: The SS58 address of the neuron's hotkey. + netuid: The unique identifier of the subnet to check the registration. If `None`, the + registration is checked across all subnets. + block: The blockchain block number at which to perform the query. + block_hash: The blockchain block_hash representation of the block id. Do not specify if using block or + reuse_block + reuse_block (bool): Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or + reuse_block. + + Returns: + bool: `True` if the hotkey is registered in the specified context (either any subnet or a specific subnet), + `False` otherwise. + + This function is important for verifying the active status of neurons in the Bittensor network. It aids in + understanding whether a neuron is eligible to participate in network processes such as consensus, + validation, and incentive distribution based on its registration status. + """ + if netuid is None: + return await self.is_hotkey_registered_any( + hotkey_ss58, block, block_hash, reuse_block + ) + else: + return await self.is_hotkey_registered_on_subnet( + hotkey_ss58, netuid, block, block_hash, reuse_block + ) + + async def is_hotkey_registered_any( + self, + hotkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """ + Checks if a neuron's hotkey is registered on any subnet within the Bittensor network. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The blockchain block_hash representation of block id. + reuse_block (bool): Whether to reuse the last-used block hash. + + Returns: + bool: ``True`` if the hotkey is registered on any subnet, False otherwise. + + This function is essential for determining the network-wide presence and participation of a neuron. + """ + hotkeys = await self.get_netuids_for_hotkey( + hotkey_ss58, block, block_hash, reuse_block ) + return len(hotkeys) > 0 + + async def is_hotkey_registered_on_subnet( + self, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """Checks if the hotkey is registered on a given netuid.""" + return ( + await self.get_uid_for_hotkey_on_subnet( + hotkey_ss58, + netuid, + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + is not None + ) + + async def last_drand_round(self) -> Optional[int]: + """ + Retrieves the last drand round emitted in bittensor. This corresponds when committed weights will be revealed. + + Returns: + int: The latest Drand round emitted in bittensor. + """ + result = await self.substrate.query( + module="Drand", storage_function="LastStoredRound" + ) + return result if result is not None else None + + async def max_weight_limit( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[float]: + """ + Returns network MaxWeightsLimit hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The blockchain block_hash representation of block id. + reuse_block (bool): Whether to reuse the last-used block hash. + + Returns: + Optional[float]: The value of the MaxWeightsLimit hyperparameter, or ``None`` if the subnetwork does not + exist or the parameter is not found. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="MaxWeightsLimit", + netuid=netuid, + block_hash=block_hash, + reuse_block=reuse_block, + ) + return None if call is None else u16_normalized_float(int(call)) + + async def metagraph( + self, netuid: int, lite: bool = True, block: Optional[int] = None + ) -> "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. + + Arguments: + netuid (int): The network UID of the subnet to query. + lite (bool): If true, returns a metagraph using a lightweight sync (no weights, no bonds). Default is + ``True``. + block (Optional[int]): Block number for synchronization, or ``None`` for the latest block. + + Returns: + bittensor.core.metagraph.Metagraph: 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 = AsyncMetagraph( + network=self.chain_endpoint, + netuid=netuid, + lite=lite, + sync=False, + subtensor=self, + ) + await metagraph.sync(block=block, lite=lite, subtensor=self) + + return metagraph + + async def min_allowed_weights( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """ + Returns network MinAllowedWeights hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The blockchain block_hash representation of block id. + reuse_block (bool): Whether to reuse the last-used block hash. + + Returns: + Optional[int]: The value of the MinAllowedWeights hyperparameter, or ``None`` if the subnetwork does not + exist or the parameter is not found. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="MinAllowedWeights", + netuid=netuid, + block_hash=block_hash, + reuse_block=reuse_block, + ) + return None if call is None else int(call) + + async def neuron_for_uid( + self, + uid: Optional[int], + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> NeuronInfo: + """ + Retrieves detailed information about a specific neuron identified by its unique identifier (UID) within a + specified subnet (netuid) of the Bittensor network. This function provides a comprehensive view of a + neuron's attributes, including its stake, rank, and operational status. + + Arguments: + uid (int): The unique identifier of the neuron. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + block_hash (str): The hash of the blockchain block number for the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. + + Returns: + Detailed information about the neuron if found, a null neuron otherwise + + This function is crucial for analyzing individual neurons' contributions and status within a specific subnet, + offering insights into their roles in the network's consensus and validation mechanisms. + """ + if uid is None: + return NeuronInfo.get_null_neuron() + + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - async def register( - self: "AsyncSubtensor", - wallet: "Wallet", - netuid: int, - processors: int, - update_interval: int, - output_in_place: bool, - verbose: bool, - use_cuda: bool, - dev_id: Union[list[int], int], - threads_per_block: int, - ): - """Register neuron.""" - return await register_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - tpb=threads_per_block, - update_interval=update_interval, - num_processes=processors, - cuda=use_cuda, - dev_id=dev_id, - output_in_place=output_in_place, - log_verbose=verbose, + if reuse_block: + block_hash = self.substrate.last_block_hash + + params = [netuid, uid, block_hash] if block_hash else [netuid, uid] + json_body = await self.substrate.rpc_request( + method="neuronInfo_getNeuron", + params=params, # custom rpc method + reuse_block_hash=reuse_block, ) + if not (result := json_body.get("result", None)): + return NeuronInfo.get_null_neuron() - async def set_weights( + bytes_result = bytes(result) + return NeuronInfo.from_vec_u8(bytes_result) + + async def neurons( self, - wallet: "Wallet", netuid: int, - uids: Union[NDArray[np.int64], "torch.LongTensor", list], - weights: Union[NDArray[np.float32], "torch.FloatTensor", list], - version_key: int = version_as_int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - max_retries: int = 5, - ) -> tuple[bool, str]: + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[NeuronInfo]: """ - 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. + Retrieves a list of all neurons within a specified subnet of the Bittensor network. + This function provides a snapshot of the subnet's neuron population, including each neuron's attributes and + network interactions. - Args: - 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 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`. + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + block_hash (str): The hash of the blockchain block number for the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. Returns: - `True` if the setting of weights is successful, `False` otherwise. And `msg`, a string value describing the - success or potential error. + A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. - 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【81†source】. + Understanding the distribution and status of neurons within a subnet is key to comprehending the network's + decentralized structure and the dynamics of its consensus and governance processes. """ - retries = 0 - success = False - if ( - uid := await self.get_uid_for_hotkey_on_subnet( - wallet.hotkey.ss58_address, netuid - ) - ) is None: - return ( - False, - f"Hotkey {wallet.hotkey.ss58_address} not registered in subnet {netuid}", - ) + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + hex_bytes_result = await self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons", + params=[netuid], + block_hash=block_hash, + reuse_block=reuse_block, + ) - if (await self.commit_reveal_enabled(netuid=netuid)) is True: - # go with `commit reveal v3` extrinsic - return await commit_reveal_v3_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - message = "No attempt made. Perhaps it is too soon to commit weights!" - while ( - await self.blocks_since_last_update(netuid, uid) - > await self.weights_rate_limit(netuid) - and retries < max_retries - and success is False - ): - logging.info( - f"Committing weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." - ) - success, message = await commit_reveal_v3_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - retries += 1 - return success, message - else: - # go with classic `set weights extrinsic` - message = "No attempt made. Perhaps it is too soon to set weights!" - while ( - retries < max_retries - and await self.blocks_since_last_update(netuid, uid) - > await self.weights_rate_limit(netuid) - and success is False - ): - try: - logging.info( - f"Setting weights for subnet #[blue]{netuid}[/blue]. " - f"Attempt [blue]{retries + 1} of {max_retries}[/blue]." - ) - success, message = await set_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - except Exception as e: - logging.error(f"Error setting weights: {e}") - finally: - retries += 1 + if hex_bytes_result is None: + return [] - return success, message + return NeuronInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) - async def root_set_weights( + async def neurons_lite( self, - wallet: "Wallet", - netuids: list[int], - weights: list[float], - ) -> bool: + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[NeuronInfoLite]: """ - Set weights for root network. + Retrieves a list of neurons in a 'lite' format from a specific subnet of the Bittensor network. + This function provides a streamlined view of the neurons, focusing on key attributes such as stake and network + participation. - Args: - wallet (bittensor_wallet.Wallet): bittensor wallet instance. - netuids (list[int]): The list of subnet uids. - weights (list[float]): The list of weights to be set. + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + block_hash (str): The hash of the blockchain block number for the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. Returns: - `True` if the setting of weights is successful, `False` otherwise. + A list of simplified neuron information for the subnet. + + This function offers a quick overview of the neuron population within a subnet, facilitating efficient analysis + of the network's decentralized structure and neuron dynamics. """ - netuids_ = np.array(netuids, dtype=np.int64) - weights_ = np.array(weights, dtype=np.float32) - logging.info(f"Setting weights in network: [blue]{self.network}[/blue]") - # Run the set weights operation. - return await set_root_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuids=netuids_, - weights=weights_, - version_key=0, - wait_for_finalization=True, - wait_for_inclusion=True, + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + hex_bytes_result = await self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons_lite", + params=[ + netuid + ], # TODO check to see if this can accept more than one at a time + block_hash=block_hash, + reuse_block=reuse_block, ) - async def commit_weights( + if hex_bytes_result is None: + return [] + + return NeuronInfoLite.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) + + async def query_identity( self, - wallet: "Wallet", - netuid: int, - salt: list[int], - uids: Union[NDArray[np.int64], list], - weights: Union[NDArray[np.int64], list], - version_key: int = version_as_int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - max_retries: int = 5, - ) -> tuple[bool, str]: + key: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict: """ - 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. + Queries the identity of a neuron on the Bittensor blockchain using the given key. This function retrieves + detailed identity information about a specific neuron, which is a crucial aspect of the network's + decentralized identity and governance system. - Args: - 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: NumPy array of neuron UIDs for which weights are being committed. - weights: NumPy array of weight values corresponding to each UID. - version_key: Version key for compatibility with the network. Default is int representation of 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 commit weights. Default is `5`. + Arguments: + key (str): The key used to query the neuron's identity, typically the neuron's SS58 address. + block (Optional[int]): The blockchain block number for the query. + block_hash (str): The hash of the blockchain block number at which to perform the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. Returns: - (success, message) where message is a string value describing the success or potential error - - This function allows neurons to create a tamper-proof record of their weight distribution at a specific point - in time, enhancing transparency and accountability within the Bittensor network. - """ - retries = 0 - success = False - message = "No attempt made. Perhaps it is too soon to commit weights!" + An object containing the identity information of the neuron if found, ``None`` otherwise. - logging.info( - f"Committing weights with params: " - f"netuid={netuid}, uids={uids}, weights={weights}, version_key={version_key}" - ) + The identity information can include various attributes such as the neuron's stake, rank, and other + network-specific details, providing insights into the neuron's role and status within the Bittensor network. - # 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, + Note: + See the `Bittensor CLI documentation `_ for supported identity + parameters. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + identity_info = await self.substrate.query( + module="Registry", + storage_function="IdentityOf", + params=[key], + block_hash=block_hash, + reuse_block_hash=reuse_block, ) + try: + return _decode_hex_identity_dict(identity_info["info"]) + except TypeError: + return {} - while retries < max_retries: - try: - success, message = await commit_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - commit_hash=commit_hash, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - if success: - break - except Exception as e: - logging.error(f"Error committing weights: {e}") - finally: - retries += 1 - - return success, message - - async def get_all_subnets_info( + async def recycle( self, + netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ): + ) -> Optional["Balance"]: """ - Retrieves detailed information about all subnets within the Bittensor network. This function provides - comprehensive data on each subnet, including its characteristics and operational parameters. + Retrieves the 'Burn' hyperparameter for a specified subnet. The 'Burn' parameter represents the amount of Tao + that is effectively recycled within the Bittensor network. Args: - block: The blockchain block number for the query. Do not specify if using block_hash or reuse_block - block_hash: The hash of the blockchain block. Do not specify if using block or reuse_block + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + block_hash (str): The hash of the blockchain block number for the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. Returns: - list[SubnetInfo]: A list of SubnetInfo objects, each containing detailed information about a subnet. + Optional[Balance]: The value of the 'Burn' hyperparameter if the subnet exists, None otherwise. - Gaining insights into the subnets' details assists in understanding the network's composition, the roles of - different subnets, and their unique features. + Understanding the 'Burn' rate is essential for analyzing the network registration usage, particularly how it is + correlated with user activity and the overall cost of participation in a given subnet. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - hex_bytes_result = await self.query_runtime_api( - "SubnetInfoRuntimeApi", - "get_subnets_info", - params=[], + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="Burn", + netuid=netuid, block_hash=block_hash, reuse_block=reuse_block, ) - if not hex_bytes_result: - return [] - else: - return SubnetInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) + return None if call is None else Balance.from_rao(int(call)) - async def get_minimum_required_stake(self): + async def subnet_exists( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: """ - Returns the minimum required stake for nominators in the Subtensor network. + Checks if a subnet with the specified unique identifier (netuid) exists within the Bittensor network. - This method retries the substrate call up to three times with exponential backoff in case of failures. + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The hash of the blockchain block number at which to check the subnet existence. + reuse_block (bool): Whether to reuse the last-used block hash. Returns: - Balance: The minimum required stake as a Balance object. + `True` if the subnet exists, `False` otherwise. - Raises: - Exception: If the substrate call fails after the maximum number of retries. + This function is critical for verifying the presence of specific subnets in the network, + enabling a deeper understanding of the network's structure and composition. """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) result = await self.substrate.query( - module="SubtensorModule", storage_function="NominatorMinRequiredStake" + module="SubtensorModule", + storage_function="NetworksAdded", + params=[netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, ) - return Balance.from_rao(result) + return result - async def tempo( + async def subnetwork_n( self, netuid: int, block: Optional[int] = None, @@ -2165,29 +2596,28 @@ async def tempo( reuse_block: bool = False, ) -> Optional[int]: """ - Returns network Tempo hyperparameter. + Returns network SubnetworkN hyperparameter. Args: - netuid: The unique identifier of the subnetwork. - block: The blockchain block number. Do not specify if using block_hash or reuse_block - block_hash: The hash of the block to retrieve the parameter 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. + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The hash of the blockchain block number at which to check the subnet existence. + reuse_block (bool): Whether to reuse the last-used block hash. Returns: - The value of the Tempo hyperparameter, or `None` if the subnetwork does not exist or the parameter - is not found. + Optional[int]: The value of the SubnetworkN hyperparameter, or ``None`` if the subnetwork does not exist or + the parameter is not found. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) call = await self.get_hyperparameter( - param_name="Tempo", + param_name="SubnetworkN", netuid=netuid, block_hash=block_hash, reuse_block=reuse_block, ) return None if call is None else int(call) - async def difficulty( + async def tempo( self, netuid: int, block: Optional[int] = None, @@ -2195,525 +2625,752 @@ async def difficulty( reuse_block: bool = False, ) -> Optional[int]: """ - Retrieves the 'Difficulty' hyperparameter for a specified subnet in the Bittensor network. - - This parameter is instrumental in determining the computational challenge required for neurons to participate - in consensus and validation processes. + Returns network Tempo hyperparameter. Args: - netuid: The unique identifier of the subnet. - block: The blockchain block number for the query. Do not specify if using block_hash or reuse_block - block_hash: The hash of the block to retrieve the parameter 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. + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The hash of the blockchain block number at which to check the subnet existence. + reuse_block (bool): Whether to reuse the last-used block hash. Returns: - The value of the 'Difficulty' hyperparameter if the subnet exists, `None` otherwise. - - The 'Difficulty' parameter directly impacts the network's security and integrity by setting the computational - effort required for validating transactions and participating in the network's consensus mechanism. + Optional[int]: The value of the Tempo hyperparameter, or ``None`` if the subnetwork does not exist or the + parameter is not found. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) call = await self.get_hyperparameter( - param_name="Difficulty", + param_name="Tempo", netuid=netuid, block_hash=block_hash, reuse_block=reuse_block, ) - if call is None: - return None - return int(call) + return None if call is None else int(call) - async def query_subtensor( + async def tx_rate_limit( self, - name: str, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - params: Optional[list] = None, - ) -> "ScaleType": + ) -> Optional[int]: """ - Queries named storage from the Subtensor module on the Bittensor blockchain. This function is used to retrieve - specific data or parameters from the blockchain, such as stake, rank, or other neuron-specific attributes. + Retrieves the transaction rate limit for the Bittensor network as of a specific blockchain block. + This rate limit sets the maximum number of transactions that can be processed within a given time frame. Args: - name: The name of the storage function to query. - block: The blockchain block number at which to perform the query. - block_hash: The hash of the block to retrieve the parameter 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. - params: A list of parameters to pass to the query function. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The hash of the blockchain block number at which to check the subnet existence. + reuse_block (bool): Whether to reuse the last-used block hash. Returns: - query_response (scalecodec.ScaleType): An object containing the requested data. + Optional[int]: The transaction rate limit of the network, None if not available. - This query function is essential for accessing detailed information about the network and its neurons, providing - valuable insights into the state and dynamics of the Bittensor ecosystem. + The transaction rate limit is an essential parameter for ensuring the stability and scalability of the Bittensor + network. It helps in managing network load and preventing congestion, thereby maintaining efficient and + timely transaction processing. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - return await self.substrate.query( - module="SubtensorModule", - storage_function=name, - params=params, - block_hash=block_hash, - reuse_block_hash=reuse_block, + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.query_subtensor( + "TxRateLimit", block_hash=block_hash, reuse_block=reuse_block ) + return result if result is not None else None - async def query_module( + async def weights( self, - module: str, - name: str, + netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - params: Optional[list] = None, - ) -> "ScaleType": + ) -> list[tuple[int, list[tuple[int, int]]]]: """ - Queries any module storage on the Bittensor blockchain with the specified parameters and block number. This - function is a generic query interface that allows for flexible and diverse data retrieval from various - blockchain modules. + Retrieves the weight distribution set by neurons within a specific subnet of the Bittensor network. + This function maps each neuron's UID to the weights it assigns to other neurons, reflecting the network's trust + and value assignment mechanisms. - Args: - module (str): The name of the module from which to query data. - name (str): The name of the storage function within the module. - block (Optional[int]): The blockchain block number at which to perform the query. - block_hash: The hash of the block to retrieve the parameter 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. - params (Optional[list[object]]): A list of parameters to pass to the query function. + Arguments: + netuid (int): The network UID of the subnet to query. + block (Optional[int]): Block number for synchronization, or ``None`` for the latest block. + block_hash (str): The hash of the blockchain block for the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. Returns: - An object containing the requested data if found, `None` otherwise. + A list of tuples mapping each neuron's UID to its assigned weights. - This versatile query function is key to accessing a wide range of data and insights from different parts of the - Bittensor blockchain, enhancing the understanding and analysis of the network's state and dynamics. + 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. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - return await self.substrate.query( - module=module, - storage_function=name, - params=params, + 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], block_hash=block_hash, reuse_block_hash=reuse_block, ) + w_map = [(uid, w or []) async for uid, w in w_map_encoded] - async def query_map( + return w_map + + async def weights_rate_limit( self, - module: str, - name: str, + netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - params: Optional[list] = None, - ) -> "QueryMapResult": + ) -> Optional[int]: """ - Queries map storage from any module on the Bittensor blockchain. This function retrieves data structures that - represent key-value mappings, essential for accessing complex and structured data within the blockchain - modules. + Returns network WeightsSetRateLimit hyperparameter. - Args: - module: The name of the module from which to query the map storage. - name: The specific storage function within the module to query. - block: The blockchain block number at which to perform the query. - block_hash: The hash of the block to retrieve the parameter 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. - params: Parameters to be passed to the query. + Arguments: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + block_hash (Optional[str]): The blockchain block_hash representation of the block id. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. Returns: - result: A data structure representing the map storage if found, `None` otherwise. - - This function is particularly useful for retrieving detailed and structured data from various blockchain - modules, offering insights into the network's state and the relationships between its different components. + Optional[int]: The value of the WeightsSetRateLimit hyperparameter, or ``None`` if the subnetwork does not + exist or the parameter is not found. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - return await self.substrate.query_map( - module=module, - storage_function=name, - params=params, + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="WeightsSetRateLimit", + netuid=netuid, block_hash=block_hash, - reuse_block_hash=reuse_block, + reuse_block=reuse_block, ) + return None if call is None else int(call) - async def query_map_subtensor( + # Extrinsics helper ================================================================================================ + + async def sign_and_send_extrinsic( self, - name: str, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - params: Optional[list] = None, - ) -> "QueryMapResult": + call: "GenericCall", + wallet: "Wallet", + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + sign_with: str = "coldkey", + ) -> tuple[bool, str]: """ - Queries map storage from the Subtensor module on the Bittensor blockchain. This function is designed to retrieve - a map-like data structure, which can include various neuron-specific details or network-wide attributes. + Helper method to sign and submit an extrinsic call to chain. - Args: - name: The name of the map storage function to query. - block: The blockchain block number at which to perform the query. - block_hash: The hash of the block to retrieve the parameter 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. - params: A list of parameters to pass to the query function. + Arguments: + call (scalecodec.types.GenericCall): a prepared Call object + wallet (bittensor_wallet.Wallet): the wallet whose coldkey will be used to sign the extrinsic + wait_for_inclusion (bool): whether to wait until the extrinsic call is included on the chain + wait_for_finalization (bool): whether to wait until the extrinsic call is finalized on the chain + sign_with: the wallet's keypair to use for the signing. Options are "coldkey", "hotkey", "coldkeypub" Returns: - An object containing the map-like data structure, or `None` if not found. - - This function is particularly useful for analyzing and understanding complex network structures and - relationships within the Bittensor ecosystem, such as interneuronal connections and stake distributions. + (success, error message) """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - return await self.substrate.query_map( - module="SubtensorModule", - storage_function=name, - params=params, - block_hash=block_hash, - reuse_block_hash=reuse_block, + if sign_with not in ("coldkey", "hotkey", "coldkeypub"): + raise AttributeError( + f"'sign_with' must be either 'coldkey', 'hotkey' or 'coldkeypub', not '{sign_with}'" + ) + + extrinsic = await self.substrate.create_signed_extrinsic( + call=call, keypair=getattr(wallet, sign_with) ) + try: + response = await self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True, "" - async def get_neuron_certificate( + if await response.is_success: + return True, "" + + return False, format_error_message(await response.error_message) + + except SubstrateRequestException as e: + return False, format_error_message(e) + + # Extrinsics ======================================================================================================= + + async def add_stake( self, - hotkey: str, - netuid: int, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> Optional[Certificate]: + wallet: "Wallet", + hotkey_ss58: Optional[str] = None, + amount: Optional[Union["Balance", float]] = None, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: """ - Retrieves the TLS certificate for a specific neuron identified by its unique identifier (UID) - within a specified subnet (netuid) of the Bittensor network. + Adds the specified amount of stake to a neuron identified by the hotkey ``SS58`` address. + Staking is a fundamental process in the Bittensor network that enables neurons to participate actively and earn + incentives. Args: - hotkey: The hotkey to query. - 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 parameter 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. + wallet (bittensor_wallet.Wallet): The wallet to be used for staking. + hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey associated with the neuron. + amount (Union[Balance, float]): The amount of TAO to stake. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Returns: - the certificate of the neuron if found, `None` otherwise. + bool: ``True`` if the staking is successful, False otherwise. - This function is used for certificate discovery for setting up mutual tls communication between neurons + This function enables neurons to increase their stake in the network, enhancing their influence and potential + rewards in line with Bittensor's consensus and reward mechanisms. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - certificate = await self.query_module( - module="SubtensorModule", - name="NeuronCertificates", - block_hash=block_hash, - reuse_block=reuse_block, - params=[netuid, hotkey], + return await add_stake_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + amount=amount, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) - try: - if certificate: - return "".join( - chr(i) - for i in chain( - [certificate["algorithm"]], - certificate["public_key"][0], - ) - ) - - except AttributeError: - return None - return None - async def serve_axon( + async def add_stake_multiple( self, - netuid: int, - axon: "Axon", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - certificate: Optional[Certificate] = None, + wallet: "Wallet", + hotkey_ss58s: list[str], + amounts: Optional[list[Union["Balance", float]]] = None, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, ) -> bool: """ - Registers an `Axon` serving endpoint on the Bittensor network for a specific neuron. This function is used to - set up the Axon, a key component of a neuron that handles incoming queries and data processing tasks. + Adds stakes to multiple neurons identified by their hotkey SS58 addresses. + This bulk operation allows for efficient staking across different neurons from a single wallet. Args: - netuid: The unique identifier of the subnetwork. - axon: The Axon instance to be registered for serving. - 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 `True`. - certificate: the certificate of the neuron + wallet (bittensor_wallet.Wallet): The wallet used for staking. + hotkey_ss58s (list[str]): List of ``SS58`` addresses of hotkeys to stake to. + amounts (list[Union[Balance, float]]): Corresponding amounts of TAO to stake for each hotkey. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Returns: - `True` if the Axon serve registration is successful, `False` otherwise. + bool: ``True`` if the staking is successful for all specified neurons, False otherwise. - By registering an Axon, the neuron becomes an active part of the network's distributed computing infrastructure, - contributing to the collective intelligence of Bittensor. + This function is essential for managing stakes across multiple neurons, reflecting the dynamic and collaborative + nature of the Bittensor network. """ - return await serve_axon_extrinsic( - self, netuid, axon, wait_for_inclusion, wait_for_finalization, certificate + return await add_stake_multiple_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) - async def is_hotkey_registered_on_subnet( + async def burned_register( self, - hotkey_ss58: str, + wallet: "Wallet", netuid: int, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, ) -> bool: """ - Checks if a neuron's hotkey is registered on a specific subnet within the Bittensor network. + Registers a neuron on the Bittensor network by recycling TAO. This method of registration involves recycling + TAO tokens, allowing them to be re-mined by performing work on the network. Args: - hotkey_ss58 (str): The `SS58` address of the neuron's hotkey. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron to be registered. netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number at which to perform the check. - block_hash: The hash of the block to retrieve the parameter 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. + wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Defaults to + `False`. + wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. + Defaults to `True`. Returns: - bool: `True` if the hotkey is registered on the specified subnet, False otherwise. - - This function helps in assessing the participation of a neuron in a particular subnet, indicating its specific - area of operation or influence within the network. + bool: ``True`` if the registration is successful, False otherwise. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - return ( - await self.get_uid_for_hotkey_on_subnet( - hotkey_ss58, netuid, block_hash=block_hash, reuse_block=reuse_block + async with self: + return await burned_register_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) - ) is not None - @property - async def block(self) -> int: + async def commit_weights( + self, + wallet: "Wallet", + netuid: int, + salt: list[int], + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.int64], list], + version_key: int = version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + max_retries: int = 5, + ) -> tuple[bool, str]: """ - Returns current chain block. + 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 + 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``. Returns: - block: Current chain block. + tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string + value describing the success or potential error. + + This function allows neurons to create a tamper-proof record of their weight distribution at a specific point + in time, enhancing transparency and accountability within the Bittensor network. """ - return await self.get_current_block() + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to commit weights!" - async def metagraph( - self, netuid: int, lite: bool = True, block: Optional[int] = None - ) -> Metagraph: + logging.info( + f"Committing weights with params: netuid={netuid}, uids={uids}, weights={weights}, version_key={version_key}" + ) + + # 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( + subtensor=self, + wallet=wallet, + netuid=netuid, + commit_hash=commit_hash, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + if success: + break + except Exception as e: + logging.error(f"Error committing weights: {e}") + finally: + retries += 1 + + return success, message + + async def register( + self: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + max_allowed_attempts: int = 3, + output_in_place: bool = False, + cuda: bool = False, + dev_id: Union[list[int], int] = 0, + tpb: int = 256, + num_processes: Optional[int] = None, + update_interval: Optional[int] = None, + log_verbose: bool = False, + ): """ - Returns a synced metagraph for a specified subnet within the Bittensor network. The metagraph represents the - network's structure, including neuron connections and interactions. + Registers a neuron on the Bittensor network using the provided wallet. + + Registration is a critical step for a neuron to become an active participant in the network, enabling it to + stake, set weights, and receive incentives. Args: - 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`. - block: Block number for synchronization, or `None` for the latest block. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron to be registered. + netuid (int): The unique identifier of the subnet. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. Defaults to `False`. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Defaults to + `True`. + max_allowed_attempts (int): Maximum number of attempts to register the wallet. + output_in_place (bool): If true, prints the progress of the proof of work to the console in-place. Meaning + the progress is printed on the same lines. Defaults to `True`. + cuda (bool): If ``true``, the wallet should be registered using CUDA device(s). Defaults to `False`. + dev_id (Union[List[int], int]): The CUDA device id to use, or a list of device ids. Defaults to `0` (zero). + tpb (int): The number of threads per block (CUDA). Default to `256`. + num_processes (Optional[int]): The number of processes to use to register. Default to `None`. + update_interval (Optional[int]): The number of nonces to solve between updates. Default to `None`. + log_verbose (bool): If ``true``, the registration process will log more information. Default to `False`. Returns: - The metagraph representing the subnet's structure and neuron relationships. + bool: ``True`` if the registration is successful, False otherwise. - 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. + This function facilitates the entry of new neurons into the network, supporting the decentralized + growth and scalability of the Bittensor ecosystem. """ - metagraph = Metagraph( - network=self.chain_endpoint, - netuid=netuid, - lite=lite, - sync=False, + return await register_extrinsic( subtensor=self, + wallet=wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + max_allowed_attempts=max_allowed_attempts, + tpb=tpb, + update_interval=update_interval, + num_processes=num_processes, + cuda=cuda, + dev_id=dev_id, + output_in_place=output_in_place, + log_verbose=log_verbose, ) - await metagraph.sync(block=block, lite=lite, subtensor=self) - - return metagraph - async def get_delegate_take( + async def reveal_weights( self, - hotkey_ss58: str, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> Optional[float]: + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.int64], list], + salt: Union[NDArray[np.int64], list], + version_key: int = version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + max_retries: int = 5, + ) -> tuple[bool, str]: """ - Retrieves the delegate 'take' percentage for a neuron identified by its hotkey. The 'take' represents the - percentage of rewards that the delegate claims from its nominators' stakes. + 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: - hotkey_ss58: The `SS58` address of the neuron's hotkey. - block: The blockchain block number for the query. - block_hash: The hash of the block to retrieve the parameter 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. + 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 + 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``. Returns: - The delegate take percentage, `None` if not available. + tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string + value describing the success or potential error. - The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of - rewards among neurons and their nominators. + This function allows neurons to reveal their previously committed weight distribution, ensuring transparency + and accountability within the Bittensor network. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - _result = await self.query_subtensor( - "Delegates", - block_hash=block_hash, - reuse_block=reuse_block, - params=[hotkey_ss58], - ) - return None if _result is None else u16_normalized_float(_result) + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to reveal weights!" + + while retries < max_retries and success is False: + try: + success, message = await reveal_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=list(uids), + weights=list(weights), + salt=list(salt), + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + if success: + break + except Exception as e: + logging.error(f"Error revealing weights: {e}") + finally: + retries += 1 + + return success, message - async def get_prometheus_info( + async def root_register( self, - netuid: int, - hotkey_ss58: str, - block: Optional[int] = None, + wallet: "Wallet", + netuid: int = 0, block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> Optional[PrometheusInfo]: + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: """ - Returns the prometheus information for this hotkey account. + Register neuron by recycling some TAO. - Args: - netuid: The unique identifier of the subnetwork. - hotkey_ss58: The SS58 address of the hotkey. - block: The block number to retrieve the prometheus information from. If `None`, the latest block is used. - Default is `None`. - block_hash: The hash of the block to retrieve the parameter 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. + Arguments: + wallet (bittensor_wallet.Wallet): Bittensor wallet instance. + netuid (int): Subnet uniq id. Root subnet uid is 0. + block_hash (Optional[str]): The hash of the blockchain block for the query. + 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``. + + Returns: + `True` if registration was successful, otherwise `False`. + """ + logging.info( + f"Registering on netuid [blue]0[/blue] on network: [blue]{self.network}[/blue]" + ) + + # Check current recycle amount + logging.info("Fetching recycle amount & balance.") + block_hash = block_hash if block_hash else await self.get_block_hash() + + try: + recycle_call, balance = await asyncio.gather( + self.get_hyperparameter( + param_name="Burn", netuid=netuid, block_hash=block_hash + ), + self.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), + ) + except TypeError as e: + logging.error(f"Unable to retrieve current recycle. {e}") + return False + except KeyError: + logging.error("Unable to retrieve current balance.") + return False + + current_recycle = Balance.from_rao(int(recycle_call)) + + # Check balance is sufficient + if balance < current_recycle: + logging.error( + f"[red]Insufficient balance {balance} to register neuron. " + f"Current recycle is {current_recycle} TAO[/red]." + ) + return False - Returns: - A PrometheusInfo object containing the prometheus information, or `None` if the prometheus information is - not found. - """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - result = await self.query_subtensor( - "Prometheus", - block_hash=block_hash, - reuse_block=reuse_block, - params=[netuid, hotkey_ss58], + return await root_register_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) - if result is not None: - return PrometheusInfo( - ip=networking.int_to_ip(result["ip"]), - ip_type=result["ip_type"], - port=result["port"], - version=result["version"], - block=result["block"], - ) - return None - async def tx_rate_limit( + async def root_set_weights( self, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> Optional[int]: + wallet: "Wallet", + netuids: list[int], + weights: list[float], + version_key: int = 0, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: """ - Retrieves the transaction rate limit for the Bittensor network as of a specific blockchain block. - This rate limit sets the maximum number of transactions that can be processed within a given time frame. + Set weights for root network. - Args: - block: The blockchain block number at which to perform the query. - block_hash: The hash of the block to retrieve the parameter 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. + Arguments: + wallet (bittensor_wallet.Wallet): bittensor wallet instance. + netuids (list[int]): The list of subnet uids. + weights (list[float]): The list of weights to be set. + version_key (int, optional): Version key for compatibility with the network. Default is ``0``. + wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Defaults to + ``False``. + wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. + Defaults to ``False``. Returns: - The transaction rate limit of the network, None if not available. - - The transaction rate limit is an essential parameter for ensuring the stability and scalability of the Bittensor - network. It helps in managing network load and preventing congestion, thereby maintaining efficient and - timely transaction processing. + `True` if the setting of weights is successful, `False` otherwise. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - result = await self.query_subtensor( - "TxRateLimit", block_hash=block_hash, reuse_block=reuse_block + netuids_ = np.array(netuids, dtype=np.int64) + weights_ = np.array(weights, dtype=np.float32) + logging.info(f"Setting weights in network: [blue]{self.network}[/blue]") + # Run the set weights operation. + return await set_root_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuids=netuids_, + weights=weights_, + version_key=version_key, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, ) - return result - async def max_weight_limit( + async def set_weights( self, + wallet: "Wallet", netuid: int, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> Optional[float]: + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + version_key: int = version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + max_retries: int = 5, + ): """ - Returns network MaxWeightsLimit hyperparameter. + Sets the inter-neuronal 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. - Args: - netuid: The unique identifier of the subnetwork. - block: The block number to retrieve the parameter from. If `None`, the latest block is used. - Default is `None`. - block_hash: The hash of the block to retrieve the parameter 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. + Arguments: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. + netuid (int): The unique identifier of the subnet. + uids (Union[NDArray[np.int64], torch.LongTensor, list]): The list of neuron UIDs that the weights are being + set for. + weights (Union[NDArray[np.float32], torch.FloatTensor, list]): The corresponding weights to be set for each + UID. + version_key (int): Version key for compatibility with the network. Default is int representation of + 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 set weights. Default is ``5``. Returns: - The value of the MaxWeightsLimit hyperparameter, or `None` if the subnetwork does not exist or the parameter - is not found. + tuple[bool, str]: ``True`` if the setting of weights is successful, False otherwise. And `msg`, 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【81†source】. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - call = await self.get_hyperparameter( - param_name="MaxWeightsLimit", - block_hash=block_hash, - netuid=netuid, - reuse_block=reuse_block, - ) - return None if call is None else u16_normalized_float(int(call)) - async def subnetwork_n( + async def _blocks_weight_limit() -> bool: + bslu, wrl = await asyncio.gather( + self.blocks_since_last_update(netuid, uid), + self.weights_rate_limit(netuid), + ) + return bslu > wrl + + retries = 0 + success = False + if ( + uid := await self.get_uid_for_hotkey_on_subnet( + wallet.hotkey.ss58_address, netuid + ) + ) is None: + return ( + False, + f"Hotkey {wallet.hotkey.ss58_address} not registered in subnet {netuid}", + ) + + if (await self.commit_reveal_enabled(netuid=netuid)) is True: + # go with `commit reveal v3` extrinsic + message = "No attempt made. Perhaps it is too soon to commit weights!" + while ( + retries < max_retries + and success is False + and await _blocks_weight_limit() + ): + logging.info( + f"Committing weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." + ) + success, message = await commit_reveal_v3_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + retries += 1 + return success, message + else: + # go with classic `set weights extrinsic` + message = "No attempt made. Perhaps it is too soon to set weights!" + while ( + retries < max_retries + and success is False + and await _blocks_weight_limit() + ): + try: + logging.info( + f"Setting weights for subnet #[blue]{netuid}[/blue]. " + f"Attempt [blue]{retries + 1} of {max_retries}[/blue]." + ) + success, message = await set_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + except Exception as e: + logging.error(f"Error setting weights: {e}") + finally: + retries += 1 + + return success, message + + async def serve_axon( self, netuid: int, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> Optional[int]: + axon: "Axon", + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + certificate: Optional["Certificate"] = None, + ) -> bool: """ - Returns network SubnetworkN hyperparameter. + Registers an ``Axon`` serving endpoint on the Bittensor network for a specific neuron. This function is used to + set up the Axon, a key component of a neuron that handles incoming queries and data processing tasks. Args: - netuid: The unique identifier of the subnetwork. - block: The block number to retrieve the parameter from. If `None`, the latest block is used. - Default is `None`. - block_hash: The hash of the block to retrieve the parameter 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. + netuid (int): The unique identifier of the subnetwork. + axon (bittensor.core.axon.Axon): The Axon instance to be registered for serving. + 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 + ``True``. + certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. + Defaults to ``None``. Returns: - The value of the SubnetworkN hyperparameter, or `None` if the subnetwork does not exist or the parameter is - not found. + bool: ``True`` if the Axon serve registration is successful, False otherwise. + + By registering an Axon, the neuron becomes an active part of the network's distributed computing infrastructure, + contributing to the collective intelligence of Bittensor. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - call = await self.get_hyperparameter( - param_name="SubnetworkN", + return await serve_axon_extrinsic( + subtensor=self, netuid=netuid, - block_hash=block_hash, - reuse_block=reuse_block, + axon=axon, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + certificate=certificate, ) - return None if call is None else int(call) - async def recycle( + async def transfer( self, - netuid: int, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> Optional["Balance"]: + wallet: "Wallet", + destination: str, + amount: Union["Balance", float], + transfer_all: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + keep_alive: bool = True, + ) -> bool: """ - Retrieves the 'Burn' hyperparameter for a specified subnet. The 'Burn' parameter represents the amount of Tao - that is effectively recycled within the Bittensor network. + Transfer token of amount to destination. - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - block_hash: The hash of the block to retrieve the parameter 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. + Arguments: + wallet (bittensor_wallet.Wallet): Source wallet for the transfer. + destination (str): Destination address for the transfer. + amount (float): Amount of tokens to transfer. + transfer_all (bool): Flag to transfer all tokens. Default is ``False``. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``True``. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is + ``False``. + keep_alive (bool): Flag to keep the connection alive. Default is ``True``. Returns: - Optional[Balance]: The value of the 'Burn' hyperparameter if the subnet exists, None otherwise. - - Understanding the 'Burn' rate is essential for analyzing the network registration usage, particularly how it is - correlated with user activity and the overall cost of participation in a given subnet. + `True` if the transferring was successful, otherwise `False`. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - call = await self.get_hyperparameter( - param_name="Burn", - netuid=netuid, - block_hash=block_hash, - reuse_block=reuse_block, + if isinstance(amount, float): + amount = Balance.from_tao(amount) + + return await transfer_extrinsic( + subtensor=self, + wallet=wallet, + destination=destination, + amount=amount, + transfer_all=transfer_all, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + keep_alive=keep_alive, ) - return None if call is None else Balance.from_rao(int(call)) async def unstake( self, @@ -2728,19 +3385,20 @@ async def unstake( individual neuron stakes within the Bittensor network. Args: - wallet: The wallet associated with the neuron from which the stake is being removed. - hotkey_ss58: The `SS58` address of the hotkey account to unstake from. - amount: The amount of TAO to unstake. If not specified, unstakes all. - 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. + wallet (bittensor_wallet.wallet): The wallet associated with the neuron from which the stake is being + removed. + hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey account to unstake from. + amount (Union[Balance, float]): The amount of TAO to unstake. If not specified, unstakes all. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Returns: - `True` if the unstaking process is successful, `False` otherwise. + bool: ``True`` if the unstaking process is successful, False otherwise. This function supports flexible stake management, allowing neurons to adjust their network participation and potential reward accruals. """ - return unstake_extrinsic( + return await unstake_extrinsic( subtensor=self, wallet=wallet, hotkey_ss58=hotkey_ss58, @@ -2749,35 +3407,38 @@ async def unstake( wait_for_finalization=wait_for_finalization, ) - async def state_call( + async def unstake_multiple( self, - method: str, - data: str, - block: Optional[int] = None, - block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> dict[Any, Any]: + wallet: "Wallet", + hotkey_ss58s: list[str], + amounts: Optional[list[Union["Balance", float]]] = None, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: """ - Makes a state call to the Bittensor blockchain, allowing for direct queries of the blockchain's state. This - function is typically used for advanced queries that require specific method calls and data inputs. + Performs batch unstaking from multiple hotkey accounts, allowing a neuron to reduce its staked amounts + efficiently. This function is useful for managing the distribution of stakes across multiple neurons. Args: - method: The method name for the state call. - data: The data to be passed to the method. - block: The blockchain block number at which to perform the state call. - block_hash: The hash of the block to retrieve the parameter 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. + wallet (bittensor_wallet.Wallet): The wallet linked to the coldkey from which the stakes are being + withdrawn. + hotkey_ss58s (List[str]): A list of hotkey ``SS58`` addresses to unstake from. + amounts (List[Union[Balance, float]]): The amounts of TAO to unstake from each hotkey. If not provided, + unstakes all available stakes. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Returns: - result (dict[Any, Any]): The result of the rpc call. + bool: ``True`` if the batch unstaking is successful, False otherwise. - The state call function provides a more direct and flexible way of querying blockchain data, useful for specific - use cases where standard queries are insufficient. + This function allows for strategic reallocation or withdrawal of stakes, aligning with the dynamic stake + management aspect of the Bittensor network. """ - block_hash = await self._determine_block_hash(block, block_hash, reuse_block) - return self.substrate.rpc_request( - method="state_call", - params=[method, data, block_hash] if block_hash else [method, data], - reuse_block_hash=reuse_block, + return await unstake_multiple_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) diff --git a/bittensor/core/chain_data/__init__.py b/bittensor/core/chain_data/__init__.py index 760eaa3354..45f3ada0c1 100644 --- a/bittensor/core/chain_data/__init__.py +++ b/bittensor/core/chain_data/__init__.py @@ -18,6 +18,7 @@ from .stake_info import StakeInfo from .subnet_hyperparameters import SubnetHyperparameters from .subnet_info import SubnetInfo +from .weight_commit_info import WeightCommitInfo from .utils import custom_rpc_type_registry, decode_account_id, process_stake_data ProposalCallData = GenericCall diff --git a/bittensor/core/chain_data/neuron_info.py b/bittensor/core/chain_data/neuron_info.py index ecc1b2488c..ec9df2b671 100644 --- a/bittensor/core/chain_data/neuron_info.py +++ b/bittensor/core/chain_data/neuron_info.py @@ -37,7 +37,7 @@ class NeuronInfo: dividends (float): The dividends value. last_update (int): The timestamp of the last update. validator_permit (bool): Validator permit status. - weights (list[list[int]]): List of weights associated with the neuron. + weights (list[tuple[int]]): List of weights associated with the neuron. bonds (list[list[int]]): List of bonds associated with the neuron. pruning_score (int): The pruning score of the neuron. prometheus_info (Optional[PrometheusInfo]): Information related to Prometheus. @@ -63,7 +63,7 @@ class NeuronInfo: dividends: float last_update: int validator_permit: bool - weights: list[list[int]] + weights: list[tuple[int, int]] bonds: list[list[int]] pruning_score: int prometheus_info: Optional["PrometheusInfo"] = None @@ -151,7 +151,7 @@ def from_vec_u8(cls, vec_u8: bytes) -> "NeuronInfo": dividends=u16_normalized_float(n.dividends), last_update=n.last_update, validator_permit=n.validator_permit, - weights=[[e[0], e[1]] for e in n.weights], + weights=[(e[0], e[1]) for e in n.weights], bonds=[[e[0], e[1]] for e in n.bonds], pruning_score=n.pruning_score, prometheus_info=PrometheusInfo( @@ -203,7 +203,7 @@ def fix(n): dividends=u16_normalized_float(n.dividends), last_update=n.last_update, validator_permit=n.validator_permit, - weights=[[e[0], e[1]] for e in n.weights], + weights=[(e[0], e[1]) for e in n.weights], bonds=[[e[0], e[1]] for e in n.bonds], pruning_score=n.pruning_score, prometheus_info=PrometheusInfo( diff --git a/bittensor/core/chain_data/weight_commit_info.py b/bittensor/core/chain_data/weight_commit_info.py new file mode 100644 index 0000000000..db253f291c --- /dev/null +++ b/bittensor/core/chain_data/weight_commit_info.py @@ -0,0 +1,37 @@ +from dataclasses import dataclass +from bittensor.core.chain_data.utils import decode_account_id + + +@dataclass +class WeightCommitInfo: + """ + Data class representing weight commit information. + + Attributes: + ss58 (str): The SS58 address of the committer + commit_hex (str): The serialized weight commit data as hex string + reveal_round (int): The round number for reveal + """ + + ss58: str + commit_hex: str + reveal_round: int + + @classmethod + def from_vec_u8(cls, data: tuple) -> tuple[str, str, int]: + """ + Creates a WeightCommitInfo instance + + Args: + data (tuple): Tuple containing ((AccountId,), (commit_data,), round_number) + + Returns: + WeightCommitInfo: A new instance with the decoded data + """ + account_id, commit_data, round_number = data + + account_id_ = account_id[0] if isinstance(account_id, tuple) else account_id + commit_data = commit_data[0] if isinstance(commit_data, tuple) else commit_data + commit_hex = "0x" + "".join(format(x, "02x") for x in commit_data) + + return decode_account_id(account_id_), commit_hex, round_number diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index 9543a837f7..5151a3e0ca 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -31,7 +31,7 @@ from bittensor.core.settings import version_as_int from bittensor.core.stream import StreamingSynapse from bittensor.core.synapse import Synapse, TerminalInfo -from bittensor.utils import networking +from bittensor.utils import networking, event_loop_is_running from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch @@ -48,14 +48,6 @@ DENDRITE_DEFAULT_ERROR = ("422", "Failed to parse response") -def event_loop_is_running(): - try: - asyncio.get_running_loop() - return True - except RuntimeError: - return False - - class DendriteMixin: """ The Dendrite class represents the abstracted implementation of a network client module. diff --git a/bittensor/core/extrinsics/async_commit_reveal.py b/bittensor/core/extrinsics/async_commit_reveal.py deleted file mode 100644 index b1c2bea094..0000000000 --- a/bittensor/core/extrinsics/async_commit_reveal.py +++ /dev/null @@ -1,152 +0,0 @@ -from typing import Optional, Union, TYPE_CHECKING - -import numpy as np -from bittensor_commit_reveal import get_encrypted_commit -from numpy.typing import NDArray - -from bittensor.core.settings import version_as_int -from bittensor.utils import format_error_message -from bittensor.utils.btlogging import logging -from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit - -if TYPE_CHECKING: - from bittensor_wallet import Wallet - from bittensor.core.async_subtensor import AsyncSubtensor - from bittensor.utils.registration import torch - - -async def _do_commit_reveal_v3( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - netuid: int, - commit: bytes, - reveal_round: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, -) -> tuple[bool, Optional[str]]: - """ - Executes the commit-reveal phase 3 for a given netuid and commit, and optionally waits for extrinsic inclusion or finalization. - - Arguments: - subtensor: An instance of the Subtensor class. - wallet: Wallet An instance of the Wallet class containing the user's keypair. - netuid: int The network unique identifier. - commit bytes The commit data in bytes format. - reveal_round: int The round number for the reveal phase. - wait_for_inclusion: bool, optional Flag indicating whether to wait for the extrinsic to be included in a block. - wait_for_finalization: bool, optional Flag indicating whether to wait for the extrinsic to be finalized. - - Returns: - A tuple where the first element is a boolean indicating success or failure, and the second element is an optional string containing error message if any. - """ - logging.info( - f"Committing weights hash [blue]{commit.hex()}[/blue] for subnet #[blue]{netuid}[/blue] with " - f"reveal round [blue]{reveal_round}[/blue]..." - ) - - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="commit_crv3_weights", - call_params={ - "netuid": netuid, - "commit": commit, - "reveal_round": reveal_round, - }, - ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - ) - - response = await subtensor.substrate.submit_extrinsic( - subtensor=subtensor, - extrinsic=extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." - - if await response.is_success: - return True, None - - return False, format_error_message(await response.error_message) - - -async def commit_reveal_v3_extrinsic( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - netuid: int, - uids: Union[NDArray[np.int64], "torch.LongTensor", list], - weights: Union[NDArray[np.float32], "torch.FloatTensor", list], - version_key: int = version_as_int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, -) -> tuple[bool, str]: - """ - Commits and reveals weights for given subtensor and wallet with provided uids and weights. - - Arguments: - subtensor: The Subtensor instance. - wallet: The wallet to use for committing and revealing. - netuid: The id of the network. - uids: The uids to commit. - weights: The weights associated with the uids. - version_key: The version key to use for committing and revealing. Default is version_as_int. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. Default is False. - wait_for_finalization: Whether to wait for the finalization of the transaction. Default is False. - - Returns: - tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure, and the second element is a message associated with the result. - """ - try: - # Convert uids and weights - if isinstance(uids, list): - uids = np.array(uids, dtype=np.int64) - if isinstance(weights, list): - weights = np.array(weights, dtype=np.float32) - - # Reformat and normalize. - uids, weights = convert_weights_and_uids_for_emit(uids, weights) - - current_block = await subtensor.substrate.get_block_number(None) - subnet_hyperparameters = await subtensor.get_subnet_hyperparameters(netuid) - tempo = subnet_hyperparameters.tempo - subnet_reveal_period_epochs = ( - subnet_hyperparameters.commit_reveal_weights_interval - ) - - # 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=netuid, - subnet_reveal_period_epochs=subnet_reveal_period_epochs, - ) - - success, message = await _do_commit_reveal_v3( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - commit=commit_for_reveal, - reveal_round=reveal_round, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if success is not True: - logging.error(message) - return False, message - - logging.success( - f"[green]Finalized![/green] Weights commited with reveal round [blue]{reveal_round}[/blue]." - ) - return True, f"reveal_round:{reveal_round}" - - except Exception as e: - logging.error(f":cross_mark: [red]Failed. Error:[/red] {e}") - return False, str(e) diff --git a/bittensor/core/extrinsics/asyncex/commit_reveal.py b/bittensor/core/extrinsics/asyncex/commit_reveal.py index 884541b065..e26e1fb9cc 100644 --- a/bittensor/core/extrinsics/asyncex/commit_reveal.py +++ b/bittensor/core/extrinsics/asyncex/commit_reveal.py @@ -100,8 +100,8 @@ async def commit_reveal_v3_extrinsic( wait_for_finalization: Whether to wait for the finalization of the transaction. Default is False. Returns: - A tuple where the first element is a boolean indicating success or failure, and the second element is a message - associated with the result. + tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure, and the second + element is a message associated with the result """ try: # Convert uids and weights @@ -113,9 +113,9 @@ async def commit_reveal_v3_extrinsic( # Reformat and normalize. uids, weights = convert_weights_and_uids_for_emit(uids, weights) - current_block = await subtensor.get_current_block() + current_block = await subtensor.substrate.get_block(None) subnet_hyperparameters = await subtensor.get_subnet_hyperparameters( - netuid, block=current_block + netuid, block_hash=current_block["header"]["hash"] ) tempo = subnet_hyperparameters.tempo subnet_reveal_period_epochs = ( @@ -128,7 +128,7 @@ async def commit_reveal_v3_extrinsic( weights=weights, version_key=version_key, tempo=tempo, - current_block=current_block, + current_block=current_block["header"]["number"], netuid=netuid, subnet_reveal_period_epochs=subnet_reveal_period_epochs, ) diff --git a/bittensor/core/extrinsics/asyncex/registration.py b/bittensor/core/extrinsics/asyncex/registration.py index c960151ea9..371b52e834 100644 --- a/bittensor/core/extrinsics/asyncex/registration.py +++ b/bittensor/core/extrinsics/asyncex/registration.py @@ -309,6 +309,7 @@ async def register_extrinsic( if cuda: if not torch.cuda.is_available(): return False + pow_result = await create_pow_async( subtensor=subtensor, wallet=wallet, diff --git a/bittensor/core/extrinsics/asyncex/root.py b/bittensor/core/extrinsics/asyncex/root.py index 66a6de020f..fe57ae63a3 100644 --- a/bittensor/core/extrinsics/asyncex/root.py +++ b/bittensor/core/extrinsics/asyncex/root.py @@ -173,8 +173,8 @@ async def _do_set_root_weights( if await response.is_success: return True, "Successfully set weights." - else: - return False, format_error_message(await response.error_message) + + return False, format_error_message(await response.error_message) async def set_root_weights_extrinsic( diff --git a/bittensor/core/extrinsics/asyncex/serving.py b/bittensor/core/extrinsics/asyncex/serving.py index 82551f1367..a443ddd7a7 100644 --- a/bittensor/core/extrinsics/asyncex/serving.py +++ b/bittensor/core/extrinsics/asyncex/serving.py @@ -1,3 +1,4 @@ +import asyncio from typing import Optional, TYPE_CHECKING from bittensor.core.errors import MetadataError @@ -26,19 +27,19 @@ async def do_serve_axon( ) -> tuple[bool, Optional[dict]]: """ Internal method to submit a serve axon transaction to the Bittensor blockchain. This method creates and submits a - transaction, enabling a neuron's `Axon` to serve requests on the network. + transaction, enabling a neuron's ``Axon`` to serve requests on the network. Args: - subtensor: Subtensor instance object. - wallet: The wallet associated with the neuron. - call_params: Parameters required for the serve axon call. - 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. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): Subtensor instance object. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron. + call_params (bittensor.core.types.AxonServeCallParams): Parameters required for the serve axon call. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Returns: - A tuple containing a success flag and an optional error message. + tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - This function is crucial for initializing and announcing a neuron's `Axon` service on the network, enhancing the + This function is crucial for initializing and announcing a neuron's ``Axon`` service on the network, enhancing the decentralized computation capabilities of Bittensor. """ @@ -64,10 +65,10 @@ async def do_serve_axon( if wait_for_inclusion or wait_for_finalization: if await response.is_success: return True, None - else: - return False, await response.error_message - else: - return True, None + + return False, await response.error_message + + return True, None async def serve_extrinsic( @@ -86,23 +87,24 @@ async def serve_extrinsic( """Subscribes a Bittensor endpoint to the subtensor chain. Args: - subtensor: Subtensor instance object. - wallet: Bittensor wallet object. - ip: Endpoint host port i.e., `192.122.31.4`. - port: Endpoint port number i.e., `9221`. - protocol: An `int` representation of the protocol. - netuid: The network uid to serve on. - placeholder1: A placeholder for future use. - placeholder2: A placeholder for future use. - wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns - `False` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, - or returns `False` if the extrinsic fails to be finalized within the timeout. - certificate: Certificate to use for TLS. If ``None``, no TLS will be used. Defaults to `None`. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): Subtensor instance object. + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + ip (str): Endpoint host port i.e., ``192.122.31.4``. + port (int): Endpoint port number i.e., ``9221``. + protocol (int): An ``int`` representation of the protocol. + netuid (int): The network uid to serve on. + placeholder1 (int): A placeholder for future use. + placeholder2 (int): A placeholder for future use. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or + returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. + certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. + Defaults to ``None``. Returns: - success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for - finalization/inclusion, the response is `True`. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for + finalization / inclusion, the response is ``true``. """ # Decrypt hotkey if not (unlock := unlock_key(wallet, "hotkey")).success: @@ -179,22 +181,22 @@ async def serve_axon_extrinsic( wait_for_finalization: bool = True, certificate: Optional[Certificate] = None, ) -> bool: - """ - Serves the axon to the network. + """Serves the axon to the network. Args: - subtensor: Subtensor instance object. - netuid: The `netuid` being served on. - axon: Axon to serve. - wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns - `False` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, - or returns `False` if the extrinsic fails to be finalized within the timeout. - certificate: Certificate to use for TLS. If `None`, no TLS will be used. Defaults to `None`. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): Subtensor instance object. + netuid (int): The ``netuid`` being served on. + axon (bittensor.core.axon.Axon): Axon to serve. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or + returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. + certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. + Defaults to ``None``. Returns: - success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for - finalization/inclusion, the response is `True`. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for + finalization / inclusion, the response is ``true``. """ if not (unlock := unlock_key(axon.wallet, "hotkey")).success: logging.error(unlock.message) @@ -245,31 +247,31 @@ async def publish_metadata( Publishes metadata on the Bittensor network using the specified wallet and network identifier. Args: - subtensor: The subtensor instance representing the Bittensor blockchain connection. - wallet: The wallet object used for authentication in the transaction. - netuid: Network UID on which the metadata is to be published. - data_type: The data type of the information being submitted. It should be one of the following: 'Sha256', - 'Blake256', 'Keccak256', or 'Raw0-128'. This specifies the format or hashing algorithm used for the data. - data: The actual metadata content to be published. This should be formatted or hashed according to the `type` - specified. (Note: max `str` length is 128 bytes) - wait_for_inclusion: If `True`, the function will wait for the extrinsic to be included in a block before - returning. Defaults to `False`. - wait_for_finalization: If `True`, the function will wait for the extrinsic to be finalized on the chain before - returning. Defaults to `True`. + subtensor (bittensor.subtensor): The subtensor instance representing the Bittensor blockchain connection. + wallet (bittensor.wallet): The wallet object used for authentication in the transaction. + netuid (int): Network UID on which the metadata is to be published. + data_type (str): The data type of the information being submitted. It should be one of the following: + ``'Sha256'``, ``'Blake256'``, ``'Keccak256'``, or ``'Raw0-128'``. This specifies the format or hashing + algorithm used for the data. + data (str): The actual metadata content to be published. This should be formatted or hashed according to the + ``type`` specified. (Note: max ``str`` length is 128 bytes) + wait_for_inclusion (bool, optional): If ``True``, the function will wait for the extrinsic to be included in a + block before returning. Defaults to ``False``. + wait_for_finalization (bool, optional): If ``True``, the function will wait for the extrinsic to be finalized + on the chain before returning. Defaults to ``True``. Returns: - success: `True` if the metadata was successfully published (and finalized if specified). `False` otherwise. + bool: ``True`` if the metadata was successfully published (and finalized if specified). ``False`` otherwise. Raises: - MetadataError: If there is an error in submitting the extrinsic or if the response from the blockchain - indicates failure. + MetadataError: If there is an error in submitting the extrinsic or if the response from the blockchain indicates + failure. """ if not (unlock := unlock_key(wallet, "hotkey")).success: logging.error(unlock.message) return False - substrate: "AsyncSubstrateInterface" async with subtensor.substrate as substrate: call = await substrate.compose_call( call_module="Commitments", @@ -291,10 +293,10 @@ async def publish_metadata( # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: return True + if await response.is_success: return True - else: - raise MetadataError(format_error_message(await response.error_message)) + raise MetadataError(format_error_message(await response.error_message)) async def get_metadata( diff --git a/bittensor/core/extrinsics/asyncex/transfer.py b/bittensor/core/extrinsics/asyncex/transfer.py index 02c3992b28..f1d3bc65e2 100644 --- a/bittensor/core/extrinsics/asyncex/transfer.py +++ b/bittensor/core/extrinsics/asyncex/transfer.py @@ -59,12 +59,8 @@ async def _do_transfer( if await response.is_success: block_hash_ = response.block_hash return True, block_hash_, "Success with response." - else: - return ( - False, - "", - format_error_message(await response.error_message), - ) + + return False, "", format_error_message(await response.error_message) async def transfer_extrinsic( diff --git a/bittensor/core/extrinsics/asyncex/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index be4e65b8b0..f9e8fb58a3 100644 --- a/bittensor/core/extrinsics/asyncex/unstaking.py +++ b/bittensor/core/extrinsics/asyncex/unstaking.py @@ -1,4 +1,4 @@ -from asyncio import sleep +import asyncio from typing import Union, Optional, TYPE_CHECKING from bittensor.core.errors import StakeError, NotRegisteredError @@ -43,18 +43,17 @@ async def _do_unstake( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - """ - Sends an unstake extrinsic to the chain. + """Sends an unstake extrinsic to the chain. Args: - wallet: Wallet object that can sign the extrinsic. - hotkey_ss58: Hotkey `ss58` address to unstake from. - amount: Amount to unstake. - wait_for_inclusion: If `True`, waits for inclusion before returning. - wait_for_finalization: If `True`, waits for finalization before returning. + wallet (bittensor_wallet.Wallet): Wallet object that can sign the extrinsic. + hotkey_ss58 (str): Hotkey ``ss58`` address to unstake from. + amount (bittensor.utils.balance.Balance): Amount to unstake. + wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. + wait_for_finalization (bool): If ``true``, waits for finalization before returning. Returns: - success: `True` if the extrinsic was successful. + success (bool): ``True`` if the extrinsic was successful. Raises: StakeError: If the extrinsic failed. @@ -95,21 +94,21 @@ async def __do_remove_stake_single( Executes an unstake call to the chain using the wallet and the amount specified. Args: - wallet: Bittensor wallet object. - hotkey_ss58: Hotkey address to unstake from. - amount: Amount to unstake as Bittensor balance object. - wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` - if the extrinsic fails to enter the block within the timeout. - wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, or - returns `False` if the extrinsic fails to be finalized within the timeout. + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + hotkey_ss58 (str): Hotkey address to unstake from. + amount (bittensor.utils.balance.Balance): Amount to unstake as Bittensor balance object. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or + returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. Returns: - success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for - finalization/inclusion, the response is `True`. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for + finalization / inclusion, the response is ``true``. Raises: - StakeError: If the extrinsic fails to be finalized or included in the block. - NotRegisteredError: If the hotkey is not registered in any subnets. + bittensor.core.errors.StakeError: If the extrinsic fails to be finalized or included in the block. + bittensor.core.errors.NotRegisteredError: If the hotkey is not registered in any subnets. """ if not (unlock := unlock_key(wallet)).success: @@ -127,8 +126,6 @@ async def __do_remove_stake_single( if success: return True - else: - raise StakeError(format_error_message(err_msg)) async def unstake_extrinsic( @@ -139,22 +136,22 @@ async def unstake_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - """ - Removes stake into the wallet coldkey from the specified hotkey `uid`. + """Removes stake into the wallet coldkey from the specified hotkey ``uid``. Args: - subtensor: AsyncSubtensor instance. - wallet: Bittensor wallet object. - hotkey_ss58: The `ss58` address of the hotkey to unstake from. By default, the wallet hotkey is used. - amount: Amount to stake as Bittensor balance, or `float` interpreted as Tao. - wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns - `False` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, - or returns `False` if the extrinsic fails to be finalized within the timeout. + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey to unstake from. By default, the wallet hotkey + is used. + amount (Union[Balance, float]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or + returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. Returns: - success: Flag is `True`` if extrinsic was finalized or included in the block. If we did not wait for - finalization/inclusion, the response is `True`. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for + finalization / inclusion, the response is ``true``. """ # Decrypt keys, if not (unlock := unlock_key(wallet)).success: @@ -271,21 +268,21 @@ async def unstake_multiple_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - """Removes stake from each `hotkey_ss58` in the list, using each amount, to a common coldkey. + """Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. Args: - subtensor: Subtensor instance. - wallet: The wallet with the coldkey to unstake to. - hotkey_ss58s: List of hotkeys to unstake from. - amounts: List of amounts to unstake. If `None`, unstake all. - wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` - if the extrinsic fails to enter the block within the timeout. - wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, or - returns `False` if the extrinsic fails to be finalized within the timeout. + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. + wallet (bittensor_wallet.Wallet): The wallet with the coldkey to unstake to. + hotkey_ss58s (List[str]): List of hotkeys to unstake from. + amounts (List[Union[Balance, float]]): List of amounts to unstake. If ``None``, unstake all. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or + returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. Returns: - success: Flag is `True` if extrinsic was finalized or included in the block. Flag is `True` if any wallet was - unstaked. If we did not wait for finalization/inclusion, the response is `True`. + success (bool): Flag is ``true`` if extrinsic was finalized or included in the block. Flag is ``true`` if any + wallet was unstaked. If we did not wait for finalization / inclusion, the response is ``true``. """ if not isinstance(hotkey_ss58s, list) or not all( isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s @@ -323,6 +320,8 @@ async def unstake_multiple_extrinsic( logging.error(unlock.message) return False + old_stakes = [] + own_hotkeys = [] logging.info( f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" ) @@ -406,7 +405,9 @@ async def unstake_multiple_extrinsic( f":hourglass: [yellow]Waiting for tx rate limit: " f"[white]{tx_rate_limit_blocks}[/white] blocks[/yellow]" ) - await sleep(tx_rate_limit_blocks * 12) # 12 seconds per block + await asyncio.sleep( + tx_rate_limit_blocks * 12 + ) # 12 seconds per block if not wait_for_finalization and not wait_for_inclusion: successful_unstakes += 1 diff --git a/bittensor/core/extrinsics/asyncex/weights.py b/bittensor/core/extrinsics/asyncex/weights.py index 1e2c45d406..ea4c8a1301 100644 --- a/bittensor/core/extrinsics/asyncex/weights.py +++ b/bittensor/core/extrinsics/asyncex/weights.py @@ -70,8 +70,8 @@ async def _do_commit_weights( if await response.is_success: return True, None - else: - return False, format_error_message(response.error_message) + + return False, format_error_message(response.error_message) async def commit_weights_extrinsic( @@ -114,9 +114,9 @@ async def commit_weights_extrinsic( success_message = "Successfully committed weights." logging.info(success_message) return True, success_message - else: - logging.error(f"Failed to commit weights: {error_message}") - return False, error_message + + logging.error(f"Failed to commit weights: {error_message}") + return False, error_message async def _do_reveal_weights( @@ -182,8 +182,8 @@ async def _do_reveal_weights( if await response.is_success: return True, None - else: - return False, await response.error_message + + return False, await response.error_message async def reveal_weights_extrinsic( @@ -234,10 +234,10 @@ async def reveal_weights_extrinsic( success_message = "Successfully revealed weights." logging.info(success_message) return True, success_message - else: - error_message = format_error_message(error_message) - logging.error(f"Failed to reveal weights: {error_message}") - return False, error_message + + error_message = format_error_message(error_message) + logging.error(f"Failed to reveal weights: {error_message}") + return False, error_message async def _do_set_weights( @@ -307,8 +307,8 @@ async def _do_set_weights( if await response.is_success: return True, "Successfully set weights." - else: - return False, format_error_message(response.error_message) + + return False, format_error_message(response.error_message) async def set_weights_extrinsic( @@ -369,9 +369,9 @@ async def set_weights_extrinsic( message = "Successfully set weights and Finalized." logging.success(f":white_heavy_check_mark: [green]{message}[/green]") return True, message - else: - logging.error(f"[red]Failed[/red] set weights. Error: {error_message}") - return False, error_message + + logging.error(f"[red]Failed[/red] set weights. Error: {error_message}") + return False, error_message except Exception as error: logging.error(f":cross_mark: [red]Failed[/red] set weights. Error: {error}") diff --git a/bittensor/core/extrinsics/commit_reveal.py b/bittensor/core/extrinsics/commit_reveal.py index 82d2d981f7..eac792f897 100644 --- a/bittensor/core/extrinsics/commit_reveal.py +++ b/bittensor/core/extrinsics/commit_reveal.py @@ -1,14 +1,15 @@ -from typing import Optional, Union, TYPE_CHECKING +"""This module provides sync functionality for commit reveal in the Bittensor network.""" + +from typing import Union, TYPE_CHECKING import numpy as np -from bittensor_commit_reveal import get_encrypted_commit from numpy.typing import NDArray -from bittensor.core.extrinsics.utils import submit_extrinsic +from bittensor.core.extrinsics.asyncex.commit_reveal import ( + commit_reveal_v3_extrinsic as async_commit_reveal_v3_extrinsic, +) from bittensor.core.settings import version_as_int -from bittensor.utils import format_error_message -from bittensor.utils.btlogging import logging -from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit +from bittensor.utils import execute_coroutine if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -16,64 +17,6 @@ from bittensor.utils.registration import torch -def _do_commit_reveal_v3( - self: "Subtensor", - wallet: "Wallet", - netuid: int, - commit: bytes, - reveal_round: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, -) -> tuple[bool, Optional[str]]: - """ - Executes the commit-reveal phase 3 for a given netuid and commit, and optionally waits for extrinsic inclusion or finalization. - - Arguments: - wallet: Wallet An instance of the Wallet class containing the user's keypair. - netuid: int The network unique identifier. - commit bytes The commit data in bytes format. - reveal_round: int The round number for the reveal phase. - wait_for_inclusion: bool, optional Flag indicating whether to wait for the extrinsic to be included in a block. - wait_for_finalization: bool, optional Flag indicating whether to wait for the extrinsic to be finalized. - - Returns: - A tuple where the first element is a boolean indicating success or failure, and the second element is an optional string containing error message if any. - """ - logging.info( - f"Committing weights hash [blue]{commit.hex()}[/blue] for subnet #[blue]{netuid}[/blue] with " - f"reveal round [blue]{reveal_round}[/blue]..." - ) - - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="commit_crv3_weights", - call_params={ - "netuid": netuid, - "commit": commit, - "reveal_round": reveal_round, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - ) - - response = submit_extrinsic( - subtensor=self, - extrinsic=extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." - - if response.is_success: - return True, None - else: - return False, format_error_message(response.error_message) - - def commit_reveal_v3_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -84,71 +27,16 @@ def commit_reveal_v3_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, ) -> tuple[bool, str]: - """ - Commits and reveals weights for given subtensor and wallet with provided uids and weights. - - Arguments: - subtensor: The Subtensor instance. - wallet: The wallet to use for committing and revealing. - netuid: The id of the network. - uids: The uids to commit. - weights: The weights associated with the uids. - version_key: The version key to use for committing and revealing. Default is version_as_int. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. Default is False. - wait_for_finalization: Whether to wait for the finalization of the transaction. Default is False. - - Returns: - tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure, and the second element is a message associated with the result. - """ - try: - # Convert uids and weights - if isinstance(uids, list): - uids = np.array(uids, dtype=np.int64) - if isinstance(weights, list): - weights = np.array(weights, dtype=np.float32) - - # Reformat and normalize. - uids, weights = convert_weights_and_uids_for_emit(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_weights_interval - ) - - # Encrypt `commit_hash` with t-lock and `get reveal_round` - commit_for_reveal, reveal_round = get_encrypted_commit( + return execute_coroutine( + coroutine=async_commit_reveal_v3_extrinsic( + subtensor=subtensor.async_subtensor, + wallet=wallet, + netuid=netuid, uids=uids, weights=weights, version_key=version_key, - tempo=tempo, - current_block=current_block, - netuid=netuid, - subnet_reveal_period_epochs=subnet_reveal_period_epochs, - ) - - success, message = _do_commit_reveal_v3( - self=subtensor, - wallet=wallet, - netuid=netuid, - commit=commit_for_reveal, - reveal_round=reveal_round, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - ) - - if success is True: - logging.success( - f"[green]Finalized![/green] Weights commited with reveal round [blue]{reveal_round}[/blue]." - ) - return True, f"reveal_round:{reveal_round}" - else: - logging.error(message) - return False, message - - except Exception as e: - logging.error(f":cross_mark: [red]Failed. Error:[/red] {e}") - return False, str(e) + ), + event_loop=subtensor.event_loop, + ) diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index 3626eaf9ff..bd98f32ecc 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -1,94 +1,18 @@ -# The MIT License (MIT) -# Copyright © 2024 Opentensor Foundation -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. +"""Module sync commit weights and reveal weights extrinsic.""" -"""Module commit weights and reveal weights extrinsic.""" +from typing import TYPE_CHECKING -from typing import Optional, TYPE_CHECKING +from bittensor.core.extrinsics.asyncex.weights import ( + reveal_weights_extrinsic as async_reveal_weights_extrinsic, + commit_weights_extrinsic as async_commit_weights_extrinsic, +) +from bittensor.utils import execute_coroutine -from bittensor.core.extrinsics.utils import submit_extrinsic -from bittensor.utils import format_error_message -from bittensor.utils.btlogging import logging - -# For annotation purposes if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.subtensor import Subtensor -# Chain call for `commit_weights_extrinsic` - - -def do_commit_weights( - self: "Subtensor", - wallet: "Wallet", - netuid: int, - commit_hash: str, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, -) -> tuple[bool, Optional[dict]]: - """ - Internal method to send a transaction to the Bittensor blockchain, committing the hash of a neuron's weights. - This method constructs and submits the transaction, handling retries and blockchain communication. - - Args: - self (bittensor.core.subtensor.Subtensor): The subtensor instance used for blockchain interaction. - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the weights. - netuid (int): The unique identifier of the subnet. - commit_hash (str): The hash of the neuron's weights to be committed. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - - Returns: - tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - - This method ensures that the weight commitment is securely recorded on the Bittensor blockchain, providing a verifiable record of the neuron's weight distribution at a specific point in time. - """ - - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="commit_weights", - call_params={ - "netuid": netuid, - "commit_hash": commit_hash, - }, - ) - next_nonce = self.get_account_next_index(wallet.hotkey.ss58_address) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - nonce=next_nonce, - ) - response = submit_extrinsic( - subtensor=self, - extrinsic=extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - response.process_events() - if response.is_success: - return True, None - else: - return False, response.error_message - - def commit_weights_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -97,109 +21,18 @@ def commit_weights_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, ) -> tuple[bool, str]: - """ - Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. - This function is a wrapper around the `do_commit_weights` method. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for blockchain interaction. - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the weights. - netuid (int): The unique identifier of the subnet. - commit_hash (str): The hash of the neuron's weights to be committed. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - - Returns: - tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string value describing the success or potential error. - - This function provides a user-friendly interface for committing weights to the Bittensor blockchain, ensuring proper error handling and user interaction when required. - """ - - success, error_message = do_commit_weights( - self=subtensor, - wallet=wallet, - netuid=netuid, - commit_hash=commit_hash, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + return execute_coroutine( + coroutine=async_commit_weights_extrinsic( + subtensor=subtensor.async_subtensor, + wallet=wallet, + netuid=netuid, + commit_hash=commit_hash, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), + event_loop=subtensor.event_loop, ) - if success: - success_message = "Successfully committed weights." - logging.info(success_message) - return True, success_message - else: - error_message = format_error_message(error_message) - logging.error(f"Failed to commit weights: {error_message}") - return False, error_message - - -# Chain call for `reveal_weights_extrinsic` - - -def do_reveal_weights( - self: "Subtensor", - wallet: "Wallet", - netuid: int, - uids: list[int], - values: list[int], - salt: list[int], - version_key: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, -) -> tuple[bool, Optional[dict]]: - """ - Internal method to send a transaction to the Bittensor blockchain, revealing the weights for a specific subnet. - This method constructs and submits the transaction, handling retries and blockchain communication. - - Args: - self (bittensor.core.subtensor.Subtensor): The subtensor instance used for blockchain interaction. - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron revealing the weights. - netuid (int): The unique identifier of the subnet. - uids (list[int]): List of neuron UIDs for which weights are being revealed. - values (list[int]): List of weight values corresponding to each UID. - salt (list[int]): List of salt values corresponding to the hash function. - version_key (int): Version key for compatibility with the network. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - - Returns: - tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - - This method ensures that the weight revelation is securely recorded on the Bittensor blockchain, providing transparency and accountability for the neuron's weight distribution. - """ - - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="reveal_weights", - call_params={ - "netuid": netuid, - "uids": uids, - "values": values, - "salt": salt, - "version_key": version_key, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - ) - response = submit_extrinsic( - subtensor=self, - extrinsic=extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - response.process_events() - if response.is_success: - return True, None - else: - return False, response.error_message - def reveal_weights_extrinsic( subtensor: "Subtensor", @@ -212,44 +45,17 @@ def reveal_weights_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, ) -> tuple[bool, str]: - """ - Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. - This function is a wrapper around the `_do_reveal_weights` method. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for blockchain interaction. - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron revealing the weights. - netuid (int): The unique identifier of the subnet. - uids (list[int]): List of neuron UIDs for which weights are being revealed. - weights (list[int]): List of weight values corresponding to each UID. - salt (list[int]): List of salt values corresponding to the hash function. - version_key (int): Version key for compatibility with the network. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - - Returns: - tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string value describing the success or potential error. - - This function provides a user-friendly interface for revealing weights on the Bittensor blockchain, ensuring proper error handling and user interaction when required. - """ - - success, error_message = do_reveal_weights( - self=subtensor, - wallet=wallet, - netuid=netuid, - uids=uids, - values=weights, - salt=salt, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + return execute_coroutine( + coroutine=async_reveal_weights_extrinsic( + subtensor=subtensor.async_subtensor, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + salt=salt, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), + event_loop=subtensor.event_loop, ) - - if success: - success_message = "Successfully revealed weights." - logging.info(success_message) - return True, success_message - else: - error_message = format_error_message(error_message) - logging.error(f"Failed to reveal weights: {error_message}") - return False, error_message diff --git a/bittensor/core/extrinsics/registration.py b/bittensor/core/extrinsics/registration.py index 890465ffa4..5fd231fa35 100644 --- a/bittensor/core/extrinsics/registration.py +++ b/bittensor/core/extrinsics/registration.py @@ -6,83 +6,38 @@ - burned_register_extrinsic: Registers the wallet to chain by recycling TAO. """ -import time from typing import Union, Optional, TYPE_CHECKING - -from bittensor.utils import format_error_message, unlock_key -from bittensor.utils.btlogging import logging -from bittensor.utils.registration import create_pow, torch, log_no_torch_error -from bittensor.core.extrinsics.utils import submit_extrinsic +from bittensor.core.extrinsics.asyncex.registration import ( + burned_register_extrinsic as async_burned_register_extrinsic, + register_extrinsic as async_register_extrinsic, +) +from bittensor.utils import execute_coroutine # For annotation and lazy import purposes if TYPE_CHECKING: - import torch from bittensor_wallet import Wallet from bittensor.core.subtensor import Subtensor - from bittensor.utils.registration import POWSolution -else: - from bittensor.utils.registration.pow import LazyLoadedTorch - - torch = LazyLoadedTorch() -def _do_pow_register( - self: "Subtensor", - netuid: int, +def burned_register_extrinsic( + subtensor: "Subtensor", wallet: "Wallet", - pow_result: "POWSolution", + netuid: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, -) -> tuple[bool, Optional[str]]: - """Sends a (POW) register extrinsic to the chain. - - Args: - self (bittensor.core.subtensor.Subtensor): The subtensor to send the extrinsic to. - netuid (int): The subnet to register on. - wallet (bittensor.wallet): The wallet to register. - pow_result (POWSolution): The PoW result to register. - wait_for_inclusion (bool): If ``True``, waits for the extrinsic to be included in a block. Default to `False`. - wait_for_finalization (bool): If ``True``, waits for the extrinsic to be finalized. Default to `True`. - - Returns: - success (bool): ``True`` if the extrinsic was included in a block. - error (Optional[str]): ``None`` on success or not waiting for inclusion/finalization, otherwise the error message. - """ - # create extrinsic call - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="register", - call_params={ - "netuid": netuid, - "block_number": pow_result.block_number, - "nonce": pow_result.nonce, - "work": [int(byte_) for byte_ in pow_result.seal], - "hotkey": wallet.hotkey.ss58_address, - "coldkey": wallet.coldkeypub.ss58_address, - }, - ) - # TODO sign for period length adjustmentInterval(netuid) and lastAdjustmentBlock(netuid) - extrinsic = self.substrate.create_signed_extrinsic(call=call, keypair=wallet.hotkey) - response = submit_extrinsic( - self, - extrinsic=extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, +) -> bool: + return execute_coroutine( + coroutine=async_burned_register_extrinsic( + subtensor=subtensor.async_subtensor, + wallet=wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), + event_loop=subtensor.event_loop, ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - return False, format_error_message(response.error_message) - # Successful registration - else: - return True, None - def register_extrinsic( subtensor: "Subtensor", @@ -99,298 +54,21 @@ def register_extrinsic( update_interval: Optional[int] = None, log_verbose: bool = False, ) -> bool: - """Registers the wallet to the chain. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor interface. - wallet (bittensor_wallet.wallet): Bittensor wallet object. - netuid (int): The ``netuid`` of the subnet to register on. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - max_allowed_attempts (int): Maximum number of attempts to register the wallet. - output_in_place (bool): If true, prints the progress of the proof of work to the console in-place. Meaning the progress is printed on the same lines. Defaults to `True`. - cuda (bool): If ``true``, the wallet should be registered using CUDA device(s). - dev_id (Union[List[int], int]): The CUDA device id to use, or a list of device ids. - tpb (int): The number of threads per block (CUDA). - num_processes (int): The number of processes to use to register. - update_interval (int): The number of nonces to solve between updates. - log_verbose (bool): If ``true``, the registration process will log more information. - - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if not subtensor.subnet_exists(netuid): - logging.error( - f":cross_mark: [red]Failed: [/red] Subnet [blue]{netuid}[/blue] does not exist." - ) - return False - - logging.info( - f":satellite: [magenta]Checking Account on subnet[/magenta] [blue]{netuid}[/blue][magenta]...[/magenta]" + return execute_coroutine( + coroutine=async_register_extrinsic( + subtensor=subtensor.async_subtensor, + wallet=wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + max_allowed_attempts=max_allowed_attempts, + output_in_place=output_in_place, + cuda=cuda, + dev_id=dev_id, + tpb=tpb, + num_processes=num_processes, + update_interval=update_interval, + log_verbose=log_verbose, + ), + event_loop=subtensor.event_loop, ) - neuron = subtensor.get_neuron_for_pubkey_and_subnet( - wallet.hotkey.ss58_address, netuid=netuid - ) - - if not neuron.is_null: - logging.debug( - f"Wallet [green]{wallet}[/green] is already registered on [blue]{neuron.netuid}[/blue] with [blue]{neuron.uid}[/blue]." - ) - return True - - logging.debug( - f"Registration hotkey: [blue]{wallet.hotkey.ss58_address}[/blue], [green]Public[/green] coldkey: [blue]{wallet.coldkey.ss58_address}[/blue] in the network: [blue]{subtensor.network}[/blue]." - ) - - if not torch: - log_no_torch_error() - return False - - # Attempt rolling registration. - attempts = 1 - while True: - logging.info( - f":satellite: [magenta]Registering...[/magenta] [blue]({attempts}/{max_allowed_attempts})[/blue]" - ) - # Solve latest POW. - if cuda: - if not torch.cuda.is_available(): - return False - pow_result: Optional["POWSolution"] = create_pow( - subtensor, - wallet, - netuid, - output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - pow_result: Optional["POWSolution"] = create_pow( - subtensor, - wallet, - netuid, - output_in_place, - cuda=cuda, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - # pow failed - if not pow_result: - # might be registered already on this subnet - is_registered = subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - logging.info( - f":white_heavy_check_mark: [green]Already registered on netuid:[/green] [blue]{netuid}[/blue]." - ) - return True - - # pow successful, proceed to submit pow to chain for registration - else: - logging.info(":satellite: [magenta]Submitting POW...[/magenta]") - # check if pow result is still valid - while not pow_result.is_stale(subtensor=subtensor): - result: tuple[bool, Optional[str]] = _do_pow_register( - self=subtensor, - netuid=netuid, - wallet=wallet, - pow_result=pow_result, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - success, err_msg = result - - if not success: - # Look error here - # https://github.com/opentensor/subtensor/blob/development/pallets/subtensor/src/errors.rs - if "HotKeyAlreadyRegisteredInSubNet" in err_msg: - logging.info( - f":white_heavy_check_mark: [green]Already Registered on subnet [/green][blue]{netuid}[/blue]." - ) - return True - - logging.error(f":cross_mark: [red]Failed:[/red] {err_msg}") - time.sleep(0.5) - - # Successful registration, final check for neuron and pubkey - else: - logging.info(":satellite: [magenta]Checking Balance...[/magenta]") - is_registered = subtensor.is_hotkey_registered( - hotkey_ss58=wallet.hotkey.ss58_address, - netuid=netuid, - ) - if is_registered: - logging.info( - ":white_heavy_check_mark: [green]Registered[/green]" - ) - return True - else: - # neuron not found, try again - logging.error( - ":cross_mark: [red]Unknown error. Neuron not found.[/red]" - ) - # keep commented due to this line brings loop to infinitive one - # continue - else: - # Exited loop because pow is no longer valid. - logging.error("[red]POW is stale.[/red]") - # Try again. - continue - - if attempts < max_allowed_attempts: - # Failed registration, retry pow - attempts += 1 - logging.info( - f":satellite: [magenta]Failed registration, retrying pow ...[/magenta] [blue]({attempts}/{max_allowed_attempts})[/blue]" - ) - else: - # Failed to register after max attempts. - logging.error("[red]No more attempts.[/red]") - return False - - -def _do_burned_register( - self, - netuid: int, - wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, -) -> tuple[bool, Optional[str]]: - """ - Performs a burned register extrinsic call to the Subtensor chain. - - This method sends a registration transaction to the Subtensor blockchain using the burned register mechanism. - - Args: - self (bittensor.core.subtensor.Subtensor): Subtensor instance. - netuid (int): The network unique identifier to register on. - wallet (bittensor_wallet.Wallet): The wallet to be registered. - wait_for_inclusion (bool): Whether to wait for the transaction to be included in a block. Default is False. - wait_for_finalization (bool): Whether to wait for the transaction to be finalized. Default is True. - - Returns: - Tuple[bool, Optional[str]]: A tuple containing a boolean indicating success or failure, and an optional error message. - """ - - # create extrinsic call - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="burned_register", - call_params={ - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = submit_extrinsic( - self, - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - return False, format_error_message(response.error_message) - # Successful registration - else: - return True, None - - -def burned_register_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, -) -> bool: - """Registers the wallet to chain by recycling TAO. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. - wallet (bittensor.wallet): Bittensor wallet object. - netuid (int): The ``netuid`` of the subnet to register on. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if not subtensor.subnet_exists(netuid): - logging.error( - f":cross_mark: [red]Failed error:[/red] subnet [blue]{netuid}[/blue] does not exist." - ) - return False - - if not (unlock := unlock_key(wallet)).success: - logging.error(unlock.message) - return False - - logging.info( - f":satellite: [magenta]Checking Account on subnet[/magenta] [blue]{netuid}[/blue][magenta] ...[/magenta]" - ) - neuron = subtensor.get_neuron_for_pubkey_and_subnet( - wallet.hotkey.ss58_address, netuid=netuid - ) - - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - - if not neuron.is_null: - logging.info(":white_heavy_check_mark: [green]Already Registered[/green]") - logging.info(f"\t\tuid: [blue]{neuron.uid}[/blue]") - logging.info(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") - logging.info(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") - logging.info(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") - return True - - logging.info(":satellite: [magenta]Recycling TAO for Registration...[/magenta]") - - recycle_amount = subtensor.recycle(netuid=netuid) - logging.info(f"Recycling {recycle_amount} to register on subnet:{netuid}") - - success, err_msg = _do_burned_register( - self=subtensor, - netuid=netuid, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not success: - logging.error(f":cross_mark: [red]Failed error:[/red] {err_msg}") - time.sleep(0.5) - return False - # Successful registration, final check for neuron and pubkey - else: - logging.info(":satellite: [magenta]Checking Balance...[/magenta]") - block = subtensor.get_current_block() - new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address, block=block) - - logging.info( - f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" - ) - is_registered = subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - logging.info(":white_heavy_check_mark: [green]Registered[/green]") - return True - else: - # neuron not found, try again - logging.error(":cross_mark: [red]Unknown error. Neuron not found.[/red]") - return False diff --git a/bittensor/core/extrinsics/root.py b/bittensor/core/extrinsics/root.py index 9e4763ccb2..a0312a4211 100644 --- a/bittensor/core/extrinsics/root.py +++ b/bittensor/core/extrinsics/root.py @@ -1,176 +1,38 @@ -import time -from typing import Optional, Union, TYPE_CHECKING +from typing import Union, TYPE_CHECKING import numpy as np from numpy.typing import NDArray -from bittensor.core.settings import version_as_int -from bittensor.core.extrinsics.utils import submit_extrinsic -from bittensor.utils import format_error_message, weight_utils, unlock_key -from bittensor.utils.btlogging import logging -from bittensor.utils.registration import torch, legacy_torch_api_compat +from bittensor.core.extrinsics.asyncex.root import ( + root_register_extrinsic as async_root_register_extrinsic, + set_root_weights_extrinsic as async_set_root_weights_extrinsic, +) +from bittensor.utils import execute_coroutine +from bittensor.utils.registration import torch if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.subtensor import Subtensor -def _do_root_register( - self: "Subtensor", - wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, -) -> tuple[bool, Optional[str]]: - # create extrinsic call - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="root_register", - call_params={"hotkey": wallet.hotkey.ss58_address}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = submit_extrinsic( - self, - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - return False, format_error_message(response.error_message) - # Successful registration - else: - return True, None - - def root_register_extrinsic( subtensor: "Subtensor", wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> bool: - """Registers the wallet to root network. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. - wallet (bittensor_wallet.Wallet): Bittensor wallet object. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. Default is ``False``. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. Default is ``True``. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - - if not (unlock := unlock_key(wallet)).success: - logging.error(unlock.message) - return False - - is_registered = subtensor.is_hotkey_registered( - netuid=0, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - logging.info( - ":white_heavy_check_mark: [green]Already registered on root network.[/green]" - ) - return True - - logging.info(":satellite: [magenta]Registering to root network...[/magenta]") - success, err_msg = _do_root_register( - self=subtensor, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not success: - logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}") - time.sleep(0.5) - - # Successful registration, final check for neuron and pubkey - else: - is_registered = subtensor.is_hotkey_registered( - netuid=0, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - logging.success(":white_heavy_check_mark: [green]Registered[/green]") - return True - else: - # neuron not found, try again - logging.error(":cross_mark: [red]Unknown error. Neuron not found.[/red]") - - -def _do_set_root_weights( - self: "Subtensor", - wallet: "Wallet", - uids: list[int], - vals: list[int], - netuid: int = 0, - version_key: int = version_as_int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - period: int = 5, -) -> tuple[bool, Optional[str]]: - """ - Internal method to send a transaction to the Bittensor blockchain, setting weights for specified neurons on root. This method constructs and submits the transaction, handling retries and blockchain communication. - - Args: - self (bittensor.core.subtensor.Subtensor): Subtensor instance. - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. - uids (List[int]): List of neuron UIDs for which weights are being set. - vals (List[int]): List of weight values corresponding to each UID. - netuid (int): Unique identifier for the network. - version_key (int, optional): Version key for compatibility with the network. Defaults is a current ``version_as_int``. - wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Defaults is ``False``. - wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. Defaults is ``False``. - - Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - - This method is vital for the dynamic weighting mechanism in Bittensor, where neurons adjust their trust in other neurons based on observed performance and contributions on the root network. - """ - - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="set_root_weights", - call_params={ - "dests": uids, - "weights": vals, - "netuid": netuid, - "version_key": version_key, - "hotkey": wallet.hotkey.ss58_address, - }, - ) - # Period dictates how long the extrinsic will stay as part of waiting pool - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.coldkey, - era={"period": period}, - ) - response = submit_extrinsic( - self, - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + return execute_coroutine( + coroutine=async_root_register_extrinsic( + subtensor=subtensor.async_subtensor, + wallet=wallet, + netuid=0, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), + event_loop=subtensor.event_loop, ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalziation or inclusion." - - response.process_events() - if response.is_success: - return True, "Successfully set weights." - else: - return False, response.error_message -@legacy_torch_api_compat def set_root_weights_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -180,84 +42,15 @@ def set_root_weights_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, ) -> bool: - """Sets the given weights and values on chain for wallet hotkey account. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. - wallet (bittensor_wallet.Wallet): Bittensor wallet object. Bittensor wallet object. - netuids (Union[NDArray[np.int64], torch.LongTensor, list[int]]): The ``netuid`` of the subnet to set weights for. - weights (Union[NDArray[np.float32], torch.FloatTensor, list[float]]): Weights to set. These must be ``float`` s and must correspond to the passed ``netuid`` s. - version_key (int): The version key of the validator. Default is ``0``. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. Default is ``False``. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. Default is ``False``. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if not (unlock := unlock_key(wallet)).success: - logging.error(unlock.message) - return False - - # First convert types. - if isinstance(netuids, list): - netuids = np.array(netuids, dtype=np.int64) - if isinstance(weights, list): - weights = np.array(weights, dtype=np.float32) - - # Get weight restrictions. - min_allowed_weights = subtensor.min_allowed_weights(netuid=0) - max_weight_limit = subtensor.max_weight_limit(netuid=0) - - # Get non zero values. - non_zero_weight_idx = np.argwhere(weights > 0).squeeze(axis=1) - non_zero_weight_uids = netuids[non_zero_weight_idx] - non_zero_weights = weights[non_zero_weight_idx] - if non_zero_weights.size < min_allowed_weights: - raise ValueError( - "The minimum number of weights required to set weights is {}, got {}".format( - min_allowed_weights, non_zero_weights.size - ) - ) - - # Normalize the weights to max value. - formatted_weights = weight_utils.normalize_max_weight( - x=weights, limit=max_weight_limit - ) - logging.info( - f"Raw Weights -> Normalized weights: [blue]{weights}[/blue] -> [green]{formatted_weights}[/green]" - ) - - logging.info( - f":satellite: [magenta]Setting root weights on[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" - ) - try: - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - netuids, weights - ) - success, error_message = _do_set_root_weights( - self=subtensor, + return execute_coroutine( + coroutine=async_set_root_weights_extrinsic( + subtensor=subtensor.async_subtensor, wallet=wallet, - netuid=0, - uids=weight_uids, - vals=weight_vals, + netuids=netuids, + weights=weights, version_key=version_key, - wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return True - - if success is True: - logging.info(":white_heavy_check_mark: [green]Finalized[/green]") - logging.success(f"Set weights {str(success)}") - return True - else: - logging.error( - f":cross_mark: [red]Failed [/red] set weights. {str(error_message)}" - ) - return False - - except Exception as e: - logging.error(f":cross_mark: [red]Failed [/red] set weights. {str(e)}") - return False + wait_for_finalization=wait_for_finalization, + ), + event_loop=subtensor.event_loop, + ) diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index d13712e188..8a8d9c82d1 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -1,42 +1,19 @@ -# The MIT License (MIT) -# Copyright © 2024 Opentensor Foundation -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - from typing import Optional, TYPE_CHECKING -from bittensor.core.errors import MetadataError -from bittensor.core.extrinsics.utils import submit_extrinsic -from bittensor.core.settings import version_as_int -from bittensor.utils import ( - format_error_message, - networking as net, - unlock_key, - Certificate, +from bittensor.core.extrinsics.asyncex.serving import ( + do_serve_axon as async_do_serve_axon, + serve_axon_extrinsic as async_serve_axon_extrinsic, + publish_metadata as async_publish_metadata, + get_metadata as async_get_metadata, ) -from bittensor.utils.btlogging import logging +from bittensor.utils import execute_coroutine -# For annotation purposes if TYPE_CHECKING: + from bittensor_wallet import Wallet from bittensor.core.axon import Axon from bittensor.core.subtensor import Subtensor from bittensor.core.types import AxonServeCallParams - from bittensor_wallet import Wallet - - -# Chain call for `serve_extrinsic` and `serve_axon_extrinsic` + from bittensor.utils import Certificate def do_serve_axon( @@ -46,146 +23,17 @@ def do_serve_axon( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> tuple[bool, Optional[dict]]: - """ - Internal method to submit a serve axon transaction to the Bittensor blockchain. This method creates and submits a transaction, enabling a neuron's ``Axon`` to serve requests on the network. - - Args: - self (bittensor.core.subtensor.Subtensor): Subtensor instance object. - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron. - call_params (bittensor.core.types.AxonServeCallParams): Parameters required for the serve axon call. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - - Returns: - tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - - This function is crucial for initializing and announcing a neuron's ``Axon`` service on the network, enhancing the decentralized computation capabilities of Bittensor. - """ - - if call_params["certificate"] is None: - del call_params["certificate"] - call_function = "serve_axon" - else: - call_function = "serve_axon_tls" - - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function=call_function, - call_params=call_params, - ) - extrinsic = self.substrate.create_signed_extrinsic(call=call, keypair=wallet.hotkey) - response = submit_extrinsic( - self, - extrinsic=extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - if wait_for_inclusion or wait_for_finalization: - response.process_events() - if response.is_success: - return True, None - else: - return False, response.error_message - else: - return True, None - - -def serve_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - ip: str, - port: int, - protocol: int, - netuid: int, - placeholder1: int = 0, - placeholder2: int = 0, - wait_for_inclusion: bool = False, - wait_for_finalization=True, - certificate: Optional[Certificate] = None, -) -> bool: - """Subscribes a Bittensor endpoint to the subtensor chain. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance object. - wallet (bittensor_wallet.Wallet): Bittensor wallet object. - ip (str): Endpoint host port i.e., ``192.122.31.4``. - port (int): Endpoint port number i.e., ``9221``. - protocol (int): An ``int`` representation of the protocol. - netuid (int): The network uid to serve on. - placeholder1 (int): A placeholder for future use. - placeholder2 (int): A placeholder for future use. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - # Decrypt hotkey - if not (unlock := unlock_key(wallet, "hotkey")).success: - logging.error(unlock.message) - return False - - params: "AxonServeCallParams" = { - "version": version_as_int, - "ip": net.ip_to_int(ip), - "port": port, - "ip_type": net.ip_version(ip), - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - "coldkey": wallet.coldkeypub.ss58_address, - "protocol": protocol, - "placeholder1": placeholder1, - "placeholder2": placeholder2, - "certificate": certificate, - } - logging.debug("Checking axon ...") - neuron = subtensor.get_neuron_for_pubkey_and_subnet( - wallet.hotkey.ss58_address, netuid=netuid - ) - neuron_up_to_date = not neuron.is_null and params == { - "version": neuron.axon_info.version, - "ip": net.ip_to_int(neuron.axon_info.ip), - "port": neuron.axon_info.port, - "ip_type": neuron.axon_info.ip_type, - "netuid": neuron.netuid, - "hotkey": neuron.hotkey, - "coldkey": neuron.coldkey, - "protocol": neuron.axon_info.protocol, - "placeholder1": neuron.axon_info.placeholder1, - "placeholder2": neuron.axon_info.placeholder2, - } - output = params.copy() - output["coldkey"] = wallet.coldkeypub.ss58_address - output["hotkey"] = wallet.hotkey.ss58_address - if neuron_up_to_date: - logging.debug( - f"Axon already served on: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) " - ) - return True - - logging.debug( - f"Serving axon with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) -> {subtensor.network}:{netuid}" - ) - success, error_message = do_serve_axon( - self=subtensor, - wallet=wallet, - call_params=params, - wait_for_finalization=wait_for_finalization, - wait_for_inclusion=wait_for_inclusion, + return execute_coroutine( + coroutine=async_do_serve_axon( + subtensor=self.async_subtensor, + wallet=wallet, + call_params=call_params, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), + event_loop=self.event_loop, ) - if wait_for_inclusion or wait_for_finalization: - if success is True: - logging.debug( - f"Axon served with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) on {subtensor.network}:{netuid} " - ) - return True - else: - logging.error(f"Failed: {format_error_message(error_message)}") - return False - else: - return True - def serve_axon_extrinsic( subtensor: "Subtensor", @@ -193,55 +41,19 @@ def serve_axon_extrinsic( axon: "Axon", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, - certificate: Optional[Certificate] = None, + certificate: Optional["Certificate"] = None, ) -> bool: - """Serves the axon to the network. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance object. - netuid (int): The ``netuid`` being served on. - axon (bittensor.core.axon.Axon): Axon to serve. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if not (unlock := unlock_key(axon.wallet, "hotkey")).success: - logging.error(unlock.message) - return False - external_port = axon.external_port - - # ---- Get external ip ---- - if axon.external_ip is None: - try: - external_ip = net.get_external_ip() - logging.success( - f":white_heavy_check_mark: [green]Found external ip:[/green] [blue]{external_ip}[/blue]" - ) - except Exception as e: - raise RuntimeError( - f"Unable to attain your external ip. Check your internet connection. error: {e}" - ) from e - else: - external_ip = axon.external_ip - - # ---- Subscribe to chain ---- - serve_success = serve_extrinsic( - subtensor=subtensor, - wallet=axon.wallet, - ip=external_ip, - port=external_port, - netuid=netuid, - protocol=4, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - certificate=certificate, + return execute_coroutine( + coroutine=async_serve_axon_extrinsic( + subtensor=subtensor.async_subtensor, + netuid=netuid, + axon=axon, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + certificate=certificate, + ), + event_loop=subtensor.event_loop, ) - return serve_success - - -# Community uses this extrinsic directly and via `subtensor.commit` def publish_metadata( @@ -253,64 +65,29 @@ def publish_metadata( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> bool: - """ - Publishes metadata on the Bittensor network using the specified wallet and network identifier. - - Args: - self (bittensor.core.subtensor.Subtensor): The subtensor instance representing the Bittensor blockchain connection. - wallet (bittensor_wallet.Wallet): The wallet object used for authentication in the transaction. - netuid (int): Network UID on which the metadata is to be published. - data_type (str): The data type of the information being submitted. It should be one of the following: ``'Sha256'``, ``'Blake256'``, ``'Keccak256'``, or ``'Raw0-128'``. This specifies the format or hashing algorithm used for the data. - data (str): The actual metadata content to be published. This should be formatted or hashed according to the ``type`` specified. (Note: max ``str`` length is 128 bytes) - wait_for_inclusion (bool): If ``True``, the function will wait for the extrinsic to be included in a block before returning. Defaults to ``False``. - wait_for_finalization (bool): If ``True``, the function will wait for the extrinsic to be finalized on the chain before returning. Defaults to ``True``. - - Returns: - bool: ``True`` if the metadata was successfully published (and finalized if specified). ``False`` otherwise. - - Raises: - MetadataError: If there is an error in submitting the extrinsic or if the response from the blockchain indicates failure. - """ - - if not (unlock := unlock_key(wallet, "hotkey")).success: - logging.error(unlock.message) - return False - - with self.substrate as substrate: - call = substrate.compose_call( - call_module="Commitments", - call_function="set_commitment", - call_params={ - "netuid": netuid, - "info": {"fields": [[{f"{data_type}": data}]]}, - }, - ) - - extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.hotkey) - response = submit_extrinsic( - self, - extrinsic, + return execute_coroutine( + coroutine=async_publish_metadata( + subtensor=self.async_subtensor, + wallet=wallet, + netuid=netuid, + data_type=data_type, + data=data, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise MetadataError(format_error_message(response.error_message)) - - -# Community uses this function directly + ), + event_loop=self.event_loop, + ) -def get_metadata(self, netuid: int, hotkey: str, block: Optional[int] = None) -> str: - with self.substrate as substrate: - return substrate.query( - module="Commitments", - storage_function="CommitmentOf", - params=[netuid, hotkey], - block_hash=None if block is None else substrate.get_block_hash(block), - ).value +def get_metadata( + self: "Subtensor", netuid: int, hotkey: str, block: Optional[int] = None +) -> str: + return execute_coroutine( + coroutine=async_get_metadata( + subtensor=self.async_subtensor, + netuid=netuid, + hotkey=hotkey, + block=block, + ), + event_loop=self.event_loop, + ) diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index cb4c33bb58..913ae29a8b 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -1,105 +1,21 @@ -# The MIT License (MIT) -# Copyright © 2024 Opentensor Foundation -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. +"""Module sync setting weights extrinsic.""" -from typing import Union, Optional, TYPE_CHECKING +from typing import Union, TYPE_CHECKING import numpy as np from numpy.typing import NDArray -from bittensor.core.extrinsics.utils import submit_extrinsic -from bittensor.core.settings import version_as_int -from bittensor.utils import format_error_message, weight_utils -from bittensor.utils.btlogging import logging -from bittensor.utils.registration import torch, use_torch +from bittensor.core.extrinsics.asyncex.weights import ( + set_weights_extrinsic as async_set_weights_extrinsic, +) +from bittensor.utils import execute_coroutine +from bittensor.utils.registration import torch -# For annotation purposes if TYPE_CHECKING: from bittensor.core.subtensor import Subtensor from bittensor_wallet import Wallet -# Chain call for `do_set_weights` -def do_set_weights( - self: "Subtensor", - wallet: "Wallet", - uids: list[int], - vals: list[int], - netuid: int, - version_key: int = version_as_int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - period: int = 5, -) -> tuple[bool, Optional[str]]: # (success, error_message) - """ - Internal method to send a transaction to the Bittensor blockchain, setting weights for specified neurons. This method constructs and submits the transaction, handling retries and blockchain communication. - - Args: - self (bittensor.core.subtensor.Subtensor): Subtensor interface - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. - uids (list[int]): List of neuron UIDs for which weights are being set. - vals (list[int]): List of weight values corresponding to each UID. - netuid (int): Unique identifier for the network. - version_key (int): Version key for compatibility with the network. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - period (int): Period dictates how long the extrinsic will stay as part of waiting pool. - - Returns: - tuple[bool, Optional[str]]: A tuple containing a success flag and an optional response message. - - This method is vital for the dynamic weighting mechanism in Bittensor, where neurons adjust their trust in other neurons based on observed performance and contributions. - """ - - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="set_weights", - call_params={ - "dests": uids, - "weights": vals, - "netuid": netuid, - "version_key": version_key, - }, - ) - next_nonce = self.get_account_next_index(wallet.hotkey.ss58_address) - # Period dictates how long the extrinsic will stay as part of waiting pool - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - era={"period": period}, - nonce=next_nonce, - ) - response = submit_extrinsic( - self, - extrinsic=extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." - - response.process_events() - if response.is_success: - return True, "Successfully set weights." - else: - return False, format_error_message(response.error_message) - - -# Community uses this extrinsic directly and via `subtensor.set_weights` def set_weights_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -110,66 +26,16 @@ def set_weights_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, ) -> tuple[bool, str]: - """Sets the given weights and values on chain for wallet hotkey account. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor endpoint to use. - wallet (bittensor_wallet.Wallet): Bittensor wallet object. - netuid (int): The ``netuid`` of the subnet to set weights for. - uids (Union[NDArray[np.int64], torch.LongTensor, list]): The ``uint64`` uids of destination neurons. - weights (Union[NDArray[np.float32], torch.FloatTensor, list]): The weights to set. These must be ``float`` s and correspond to the passed ``uid`` s. - version_key (int): The version key of the validator. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - - Returns: - tuple[bool, str]: A tuple containing a success flag and an optional response message. - """ - # First convert types. - if use_torch(): - if isinstance(uids, list): - uids = torch.tensor(uids, dtype=torch.int64) - if isinstance(weights, list): - weights = torch.tensor(weights, dtype=torch.float32) - else: - if isinstance(uids, list): - uids = np.array(uids, dtype=np.int64) - if isinstance(weights, list): - weights = np.array(weights, dtype=np.float32) - - # Reformat and normalize. - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - uids, weights - ) - - logging.info( - f":satellite: [magenta]Setting weights on [/magenta][blue]{subtensor.network}[blue] [magenta]...[/magenta]" - ) - logging.debug(f"Weights: {[float(v / 65535) for v in weight_vals]}") - - try: - success, message = do_set_weights( - self=subtensor, + return execute_coroutine( + coroutine=async_set_weights_extrinsic( + subtensor=subtensor.async_subtensor, wallet=wallet, netuid=netuid, - uids=weight_uids, - vals=weight_vals, + uids=uids, + weights=weights, version_key=version_key, - wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." - - if success is True: - logging.success(f"[green]Finalized![/green] Set weights: {str(success)}") - return True, "Successfully set weights and Finalized." - else: - logging.error(message) - return False, message - - except Exception as e: - logging.error(f":cross_mark: [red]Failed.[/red]: Error: {e}") - logging.debug(str(e)) - return False, str(e) + wait_for_finalization=wait_for_finalization, + ), + event_loop=subtensor.event_loop, + ) diff --git a/bittensor/core/extrinsics/staking.py b/bittensor/core/extrinsics/staking.py index 6162693c81..7245ce98e9 100644 --- a/bittensor/core/extrinsics/staking.py +++ b/bittensor/core/extrinsics/staking.py @@ -1,474 +1,54 @@ -from time import sleep from typing import Union, Optional, TYPE_CHECKING -from bittensor.core.errors import NotDelegateError, StakeError, NotRegisteredError -from bittensor.utils import format_error_message, unlock_key -from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging +from bittensor.core.extrinsics.asyncex.staking import ( + add_stake_extrinsic as async_add_stake_extrinsic, + add_stake_multiple_extrinsic as async_add_stake_multiple_extrinsic, +) +from bittensor.utils import execute_coroutine if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.subtensor import Subtensor - - -def _do_stake( - self: "Subtensor", - wallet: "Wallet", - hotkey_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> bool: - """Sends a stake extrinsic to the chain. - - Args: - self (subtensor): Subtensor instance. - wallet (bittensor_wallet.Wallet): Wallet object that can sign the extrinsic. - hotkey_ss58 (str): Hotkey ``ss58`` address to stake to. - amount (bittensor.utils.balance.Balance): Amount to stake. - wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. - wait_for_finalization (bool): If ``true``, waits for finalization before returning. - - Returns: - success (bool): ``True`` if the extrinsic was successful. - - Raises: - bittensor.core.errors.StakeError: If the extrinsic failed. - """ - - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="add_stake", - call_params={"hotkey": hotkey_ss58, "amount_staked": amount.rao}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - if response.is_success: - return True - else: - raise StakeError(format_error_message(response.error_message)) - - -def _check_threshold_amount( - subtensor: "Subtensor", stake_balance: Balance -) -> tuple[bool, Balance]: - """ - Checks if the new stake balance will be above the minimum required stake threshold. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. - stake_balance (Balance): the balance to check for threshold limits. - - Returns: - success, threshold (bool, Balance): ``true`` if the staking balance is above the threshold, or ``false`` if the staking balance is below the threshold. The threshold balance required to stake. - """ - min_req_stake: Balance = subtensor.get_minimum_required_stake() - if min_req_stake > stake_balance: - return False, min_req_stake - else: - return True, min_req_stake - - -def __do_add_stake_single( - subtensor: "Subtensor", - wallet: "Wallet", - hotkey_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> bool: - """ - Executes a stake call to the chain using the wallet and the amount specified. - - Args: - wallet (bittensor_wallet.Wallet): Bittensor wallet object. - hotkey_ss58 (str): Hotkey to stake to. - amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance object. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - - Raises: - bittensor.core.errors.StakeError: If the extrinsic fails to be finalized or included in the block. - bittensor.core.errors.NotDelegateError: If the hotkey is not a delegate. - bittensor.core.errors.NotRegisteredError: If the hotkey is not registered in any subnets. - - """ - # Decrypt keys, - if not (unlock := unlock_key(wallet)).success: - logging.error(unlock.message) - return False - - hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) - own_hotkey = wallet.coldkeypub.ss58_address == hotkey_owner - if not own_hotkey: - # We are delegating. Verify that the hotkey is a delegate. - if not subtensor.is_hotkey_delegate(hotkey_ss58=hotkey_ss58): - raise NotDelegateError("Hotkey: {} is not a delegate.".format(hotkey_ss58)) - - success = _do_stake( - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - return success + from bittensor.utils.balance import Balance def add_stake_extrinsic( subtensor: "Subtensor", wallet: "Wallet", hotkey_ss58: Optional[str] = None, - amount: Optional[Union[Balance, float]] = None, + amount: Optional[Union["Balance", float]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - """Adds the specified amount of stake to passed hotkey ``uid``. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. - wallet (Wallet): Bittensor wallet object. - hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. - amount (Union[Balance, float]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - - Raises: - bittensor.core.errors.NotRegisteredError: If the wallet is not registered on the chain. - bittensor.core.errors.NotDelegateError: If the hotkey is not a delegate on the chain. - """ - # Decrypt keys, - if not (unlock := unlock_key(wallet)).success: - logging.error(unlock.message) - return False - - # Default to wallet's own hotkey if the value is not passed. - if hotkey_ss58 is None: - hotkey_ss58 = wallet.hotkey.ss58_address - - # Flag to indicate if we are using the wallet's own hotkey. - own_hotkey: bool - - logging.info( - f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" - ) - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - # Get hotkey owner - hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) - own_hotkey = wallet.coldkeypub.ss58_address == hotkey_owner - if not own_hotkey: - # This is not the wallet's own hotkey so we are delegating. - if not subtensor.is_hotkey_delegate(hotkey_ss58): - raise NotDelegateError("Hotkey: {} is not a delegate.".format(hotkey_ss58)) - - # Get current stake - old_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) - - # Grab the existential deposit. - existential_deposit = subtensor.get_existential_deposit() - - # Convert to bittensor.Balance - if amount is None: - # Stake it all. - staking_balance = Balance.from_tao(old_balance.tao) - elif not isinstance(amount, Balance): - staking_balance = Balance.from_tao(amount) - else: - staking_balance = amount - - # Leave existential balance to keep key alive. - if staking_balance > old_balance - existential_deposit: - # If we are staking all, we need to leave at least the existential deposit. - staking_balance = old_balance - existential_deposit - else: - staking_balance = staking_balance - - # Check enough to stake. - if staking_balance > old_balance: - logging.error(":cross_mark: [red]Not enough stake:[/red]") - logging.error(f"\t\tbalance:{old_balance}") - logging.error(f"\t\tamount: {staking_balance}") - logging.error(f"\t\twallet: {wallet.name}") - return False - - # If nominating, we need to check if the new stake balance will be above the minimum required stake threshold. - if not own_hotkey: - new_stake_balance = old_stake + staking_balance - is_above_threshold, threshold = _check_threshold_amount( - subtensor, new_stake_balance - ) - if not is_above_threshold: - logging.error( - f":cross_mark: [red]New stake balance of {new_stake_balance} is below the minimum required nomination stake threshold {threshold}.[/red]" - ) - return False - - try: - logging.info( - f":satellite: [magenta]Staking to:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" - ) - staking_response: bool = __do_add_stake_single( - subtensor=subtensor, + return execute_coroutine( + coroutine=async_add_stake_extrinsic( + subtensor=subtensor.async_subtensor, wallet=wallet, hotkey_ss58=hotkey_ss58, - amount=staking_balance, + amount=amount, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - ) - - if staking_response is True: # If we successfully staked. - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - logging.success(":white_heavy_check_mark: [green]Finalized[/green]") - - logging.info( - f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" - ) - new_balance = subtensor.get_balance(address=wallet.coldkeypub.ss58_address) - block = subtensor.get_current_block() - new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - block=block, - ) # Get current stake - - logging.info("Balance:") - logging.info( - f"[blue]{old_balance}[/blue] :arrow_right: {new_balance}[/green]" - ) - logging.info("Stake:") - logging.info( - f"[blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]" - ) - return True - else: - logging.error(":cross_mark: [red]Failed[/red]: Error unknown.") - return False - - except NotRegisteredError: - logging.error( - ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( - wallet.hotkey_str - ) - ) - return False - except StakeError as e: - logging.error(f":cross_mark: [red]Stake Error: {e}[/red]") - return False + ), + event_loop=subtensor.event_loop, + ) def add_stake_multiple_extrinsic( subtensor: "Subtensor", wallet: "Wallet", hotkey_ss58s: list[str], - amounts: Optional[list[Union[Balance, float]]] = None, + amounts: Optional[list[Union["Balance", float]]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - """Adds stake to each ``hotkey_ss58`` in the list, using each amount, from a common coldkey. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. - wallet (bittensor_wallet.Wallet): Bittensor wallet object for the coldkey. - hotkey_ss58s (List[str]): List of hotkeys to stake to. - amounts (List[Union[Balance, float]]): List of amounts to stake. If ``None``, stake all to the first hotkey. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or included in the block. Flag is ``true`` if any wallet was staked. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if not isinstance(hotkey_ss58s, list) or not all( - isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s - ): - raise TypeError("hotkey_ss58s must be a list of str") - - if len(hotkey_ss58s) == 0: - return True - - if amounts is not None and len(amounts) != len(hotkey_ss58s): - raise ValueError("amounts must be a list of the same length as hotkey_ss58s") - - if amounts is not None and not all( - isinstance(amount, (Balance, float)) for amount in amounts - ): - raise TypeError( - "amounts must be a [list of bittensor.Balance or float] or None" - ) - - if amounts is None: - amounts = [None] * len(hotkey_ss58s) - else: - # Convert to Balance - amounts = [ - Balance.from_tao(amount) if isinstance(amount, float) else amount - for amount in amounts - ] - - if sum(amount.tao for amount in amounts) == 0: - # Staking 0 tao - return True - - # Decrypt keys, - if not (unlock := unlock_key(wallet)).success: - logging.error(unlock.message) - return False - - old_stakes = [] - - logging.info( - f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" - ) - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - - # Get the old stakes. - for hotkey_ss58 in hotkey_ss58s: - old_stakes.append( - subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) - ) - - # Remove existential balance to keep key alive. - # Keys must maintain a balance of at least 1000 rao to stay alive. - total_staking_rao = sum( - [amount.rao if amount is not None else 0 for amount in amounts] + return execute_coroutine( + coroutine=async_add_stake_multiple_extrinsic( + subtensor=subtensor.async_subtensor, + wallet=wallet, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), + event_loop=subtensor.event_loop, ) - if total_staking_rao == 0: - # Staking all to the first wallet. - if old_balance.rao > 1000: - old_balance -= Balance.from_rao(1000) - - elif total_staking_rao < 1000: - # Staking less than 1000 rao to the wallets. - pass - else: - # Staking more than 1000 rao to the wallets. - # Reduce the amount to stake to each wallet to keep the balance above 1000 rao. - percent_reduction = 1 - (1000 / total_staking_rao) - amounts = [ - Balance.from_tao(amount.tao * percent_reduction) for amount in amounts - ] - - successful_stakes = 0 - for idx, (hotkey_ss58, amount, old_stake) in enumerate( - zip(hotkey_ss58s, amounts, old_stakes) - ): - staking_all = False - # Convert to bittensor.Balance - if amount is None: - # Stake it all. - staking_balance = Balance.from_tao(old_balance.tao) - staking_all = True - else: - # Amounts are cast to balance earlier in the function - assert isinstance(amount, Balance) - staking_balance = amount - - # Check enough to stake - if staking_balance > old_balance: - logging.error( - f":cross_mark: [red]Not enough balance[/red]: [green]{old_balance}[/green] to stake: [blue]{staking_balance}[/blue] from wallet: [white]{wallet.name}[/white]" - ) - continue - - try: - staking_response: bool = __do_add_stake_single( - subtensor=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=staking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # If we successfully staked. - if staking_response: - # We only wait here if we expect finalization. - - if idx < len(hotkey_ss58s) - 1: - # Wait for tx rate limit. - tx_rate_limit_blocks = subtensor.tx_rate_limit() - if tx_rate_limit_blocks > 0: - logging.error( - f":hourglass: [yellow]Waiting for tx rate limit: [white]{tx_rate_limit_blocks}[/white] blocks[/yellow]" - ) - sleep(tx_rate_limit_blocks * 12) # 12 seconds per block - - if not wait_for_finalization and not wait_for_inclusion: - old_balance -= staking_balance - successful_stakes += 1 - if staking_all: - # If staked all, no need to continue - break - - continue - - logging.success(":white_heavy_check_mark: [green]Finalized[/green]") - - block = subtensor.get_current_block() - new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - block=block, - ) - new_balance = subtensor.get_balance( - wallet.coldkeypub.ss58_address, block=block - ) - logging.info( - "Stake ({}): [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - hotkey_ss58, old_stake, new_stake - ) - ) - old_balance = new_balance - successful_stakes += 1 - if staking_all: - # If staked all, no need to continue - break - - else: - logging.error(":cross_mark: [red]Failed[/red]: Error unknown.") - continue - - except NotRegisteredError: - logging.error( - ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( - hotkey_ss58 - ) - ) - continue - except StakeError as e: - logging.error(":cross_mark: [red]Stake Error: {}[/red]".format(e)) - continue - - if successful_stakes != 0: - logging.info( - f":satellite: [magenta]Checking Balance on:[/magenta] ([blue]{subtensor.network}[/blue] [magenta]...[/magenta]" - ) - new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - logging.info( - f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" - ) - return True - - return False diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index b00feeb092..fbf6267b19 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -1,199 +1,36 @@ -# The MIT License (MIT) -# Copyright © 2024 Opentensor Foundation -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. +from typing import Union, TYPE_CHECKING -from typing import Optional, Union, TYPE_CHECKING - -from bittensor.core.extrinsics.utils import submit_extrinsic -from bittensor.core.settings import NETWORK_EXPLORER_MAP -from bittensor.utils import ( - get_explorer_url_for_network, - format_error_message, - is_valid_bittensor_address_or_public_key, - unlock_key, +from bittensor.core.extrinsics.asyncex.transfer import ( + transfer_extrinsic as async_transfer_extrinsic, ) -from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging +from bittensor.utils import execute_coroutine -# For annotation purposes if TYPE_CHECKING: - from bittensor.core.subtensor import Subtensor from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + from bittensor.utils.balance import Balance -# Chain call for `transfer_extrinsic` - - -def do_transfer( - self: "Subtensor", - wallet: "Wallet", - dest: str, - transfer_balance: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> tuple[bool, Optional[str], Optional[dict]]: - """Sends a transfer extrinsic to the chain. - - Args: - self (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (bittensor_wallet.Wallet): Wallet object. - dest (str): Destination public key address. - transfer_balance (bittensor.utils.balance.Balance): Amount to transfer. - wait_for_inclusion (bool): If ``true``, waits for inclusion. - wait_for_finalization (bool): If ``true``, waits for finalization. - - Returns: - success (bool): ``True`` if transfer was successful. - block_hash (str): Block hash of the transfer. On success and if wait_for_ finalization/inclusion is ``True``. - error (dict): Error message from subtensor if transfer failed. - """ - - call = self.substrate.compose_call( - call_module="Balances", - call_function="transfer_allow_death", - call_params={"dest": dest, "value": transfer_balance.rao}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = submit_extrinsic( - self, - extrinsic=extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None, None - - # Otherwise continue with finalization. - response.process_events() - if response.is_success: - block_hash = response.block_hash - return True, block_hash, None - else: - return False, None, response.error_message - - -# Community uses this extrinsic directly and via `subtensor.transfer` def transfer_extrinsic( subtensor: "Subtensor", wallet: "Wallet", dest: str, amount: Union["Balance", float], + transfer_all: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, keep_alive: bool = True, ) -> bool: - """Transfers funds from this wallet to the destination public key address. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. - dest (str, ss58_address or ed25519): Destination public key address of receiver. - amount (Union[Balance, int]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - keep_alive (bool): If set, keeps the account alive by keeping the balance above the existential deposit. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - # Validate destination address. - if not is_valid_bittensor_address_or_public_key(dest): - logging.error(f"[red]Invalid destination address: {dest}[/red]") - return False - - if isinstance(dest, bytes): - # Convert bytes to hex string. - dest = "0x" + dest.hex() - - if not (unlock := unlock_key(wallet)).success: - logging.error(unlock.message) - return False - - # Convert to bittensor.Balance - if not isinstance(amount, Balance): - transfer_balance = Balance.from_tao(amount) - else: - transfer_balance = amount - - # Check balance. - logging.info(":satellite: [magenta]Checking Balance...[/magenta]") - account_balance = subtensor.get_balance(wallet.coldkey.ss58_address) - # check existential deposit. - existential_deposit = subtensor.get_existential_deposit() - - logging.info(":satellite: [magenta]Transferring...[/magenta]") - fee = subtensor.get_transfer_fee( - wallet=wallet, dest=dest, value=transfer_balance.rao + return execute_coroutine( + coroutine=async_transfer_extrinsic( + subtensor=subtensor.async_subtensor, + wallet=wallet, + destination=dest, + amount=amount, + transfer_all=transfer_all, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + keep_alive=keep_alive, + ), + event_loop=subtensor.event_loop, ) - - if not keep_alive: - # Check if the transfer should keep_alive the account - existential_deposit = Balance(0) - - # Check if we have enough balance. - if account_balance < (transfer_balance + fee + existential_deposit): - logging.error(":cross_mark: [red]Not enough balance[/red]:") - logging.info(f"\t\tBalance: \t[blue]{account_balance}[/blue]") - logging.info(f"\t\tAmount: \t[blue]{transfer_balance}[/blue]") - logging.info(f"\t\tFor fee: \t[blue]{fee}[/blue]") - return False - - logging.info(":satellite: [magenta]Transferring...[/magenta]") - logging.info(f"\tAmount: [blue]{transfer_balance}[/blue]") - logging.info(f"\tfrom: [blue]{wallet.name}:{wallet.coldkey.ss58_address}[/blue]") - logging.info(f"\tTo: [blue]{dest}[/blue]") - logging.info(f"\tFor fee: [blue]{fee}[/blue]") - - success, block_hash, error_message = do_transfer( - self=subtensor, - wallet=wallet, - dest=dest, - transfer_balance=transfer_balance, - wait_for_finalization=wait_for_finalization, - wait_for_inclusion=wait_for_inclusion, - ) - - if success: - logging.success(":white_heavy_check_mark: [green]Finalized[/green]") - logging.info(f"[green]Block Hash:[/green] [blue]{block_hash}[/blue]") - - explorer_urls = get_explorer_url_for_network( - subtensor.network, block_hash, NETWORK_EXPLORER_MAP - ) - if explorer_urls != {} and explorer_urls: - logging.info( - f"[green]Opentensor Explorer Link: {explorer_urls.get('opentensor')}[/green]" - ) - logging.info( - f"[green]Taostats Explorer Link: {explorer_urls.get('taostats')}[/green]" - ) - else: - logging.error( - f":cross_mark: [red]Failed[/red]: {format_error_message(error_message)}" - ) - - if success: - logging.info(":satellite: [magenta]Checking Balance...[/magenta]") - new_balance = subtensor.get_balance(wallet.coldkey.ss58_address) - logging.success( - f"Balance: [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" - ) - return True - - return False diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index 1dea45cae3..0b7a425f34 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -1,127 +1,17 @@ -from time import sleep from typing import Union, Optional, TYPE_CHECKING -from bittensor.core.errors import StakeError, NotRegisteredError -from bittensor.utils import format_error_message, unlock_key +from bittensor.core.extrinsics.asyncex.unstaking import ( + unstake_extrinsic as async_unstake_extrinsic, + unstake_multiple_extrinsic as async_unstake_multiple_extrinsic, +) +from bittensor.utils import execute_coroutine from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.subtensor import Subtensor -def _do_unstake( - self: "Subtensor", - wallet: "Wallet", - hotkey_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> bool: - """Sends an unstake extrinsic to the chain. - - Args: - wallet (bittensor_wallet.Wallet): Wallet object that can sign the extrinsic. - hotkey_ss58 (str): Hotkey ``ss58`` address to unstake from. - amount (bittensor.utils.balance.Balance): Amount to unstake. - wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. - wait_for_finalization (bool): If ``true``, waits for finalization before returning. - - Returns: - success (bool): ``True`` if the extrinsic was successful. - - Raises: - StakeError: If the extrinsic failed. - """ - - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake", - call_params={"hotkey": hotkey_ss58, "amount_unstaked": amount.rao}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - if response.is_success: - return True - else: - raise StakeError(format_error_message(response.error_message)) - - -def _check_threshold_amount(subtensor: "Subtensor", stake_balance: "Balance") -> bool: - """ - Checks if the remaining stake balance is above the minimum required stake threshold. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. - stake_balance (bittensor.utils.balance.Balance): the balance to check for threshold limits. - - Returns: - success (bool): ``true`` if the unstaking is above the threshold or 0, or ``false`` if the unstaking is below the threshold, but not 0. - """ - min_req_stake: Balance = subtensor.get_minimum_required_stake() - - if min_req_stake > stake_balance > 0: - logging.warning( - f":cross_mark: [yellow]Remaining stake balance of {stake_balance} less than minimum of {min_req_stake} TAO[/yellow]" - ) - return False - else: - return True - - -def __do_remove_stake_single( - subtensor: "Subtensor", - wallet: "Wallet", - hotkey_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> bool: - """ - Executes an unstake call to the chain using the wallet and the amount specified. - - Args: - wallet (bittensor_wallet.Wallet): Bittensor wallet object. - hotkey_ss58 (str): Hotkey address to unstake from. - amount (bittensor.utils.balance.Balance): Amount to unstake as Bittensor balance object. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - - Raises: - bittensor.core.errors.StakeError: If the extrinsic fails to be finalized or included in the block. - bittensor.core.errors.NotRegisteredError: If the hotkey is not registered in any subnets. - - """ - if not (unlock := unlock_key(wallet)).success: - logging.error(unlock.message) - return False - - success = _do_unstake( - self=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - return success - - def unstake_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -130,112 +20,17 @@ def unstake_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - """Removes stake into the wallet coldkey from the specified hotkey ``uid``. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. - wallet (bittensor_wallet.Wallet): Bittensor wallet object. - hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey to unstake from. By default, the wallet hotkey is used. - amount (Union[Balance, float]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - # Decrypt keys, - if not (unlock := unlock_key(wallet)).success: - logging.error(unlock.message) - return False - - if hotkey_ss58 is None: - hotkey_ss58 = wallet.hotkey.ss58_address # Default to wallet's own hotkey. - - logging.info( - f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" - ) - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - old_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) - - hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) - own_hotkey: bool = wallet.coldkeypub.ss58_address == hotkey_owner - - # Convert to bittensor.Balance - if amount is None: - # Unstake it all. - unstaking_balance = old_stake - elif not isinstance(amount, Balance): - unstaking_balance = Balance.from_tao(amount) - else: - unstaking_balance = amount - - # Check enough to unstake. - stake_on_uid = old_stake - if unstaking_balance > stake_on_uid: - logging.error( - f":cross_mark: [red]Not enough stake[/red]: [green]{stake_on_uid}[/green] to unstake: [blue]{unstaking_balance}[/blue] from hotkey: [yellow]{wallet.hotkey_str}[/yellow]" - ) - return False - - # If nomination stake, check threshold. - if not own_hotkey and not _check_threshold_amount( - subtensor=subtensor, stake_balance=(stake_on_uid - unstaking_balance) - ): - logging.warning( - ":warning: [yellow]This action will unstake the entire staked balance![/yellow]" - ) - unstaking_balance = stake_on_uid - - try: - logging.info( - f":satellite: [magenta]Unstaking from chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" - ) - staking_response: bool = __do_remove_stake_single( - subtensor=subtensor, + return execute_coroutine( + coroutine=async_unstake_extrinsic( + subtensor=subtensor.async_subtensor, wallet=wallet, hotkey_ss58=hotkey_ss58, - amount=unstaking_balance, + amount=amount, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - ) - - if staking_response is True: # If we successfully unstaked. - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - logging.success(":white_heavy_check_mark: [green]Finalized[/green]") - - logging.info( - f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" - ) - new_balance = subtensor.get_balance(address=wallet.coldkeypub.ss58_address) - new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) # Get stake on hotkey. - logging.info(f"Balance:") - logging.info( - f"\t\t[blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" - ) - logging.info("Stake:") - logging.info( - f"\t\t[blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]" - ) - return True - else: - logging.error(":cross_mark: [red]Failed[/red]: Unknown Error.") - return False - - except NotRegisteredError: - logging.error( - f":cross_mark: [red]Hotkey: {wallet.hotkey_str} is not registered.[/red]" - ) - return False - except StakeError as e: - logging.error(":cross_mark: [red]Stake Error: {}[/red]".format(e)) - return False + ), + event_loop=subtensor.event_loop, + ) def unstake_multiple_extrinsic( @@ -246,166 +41,14 @@ def unstake_multiple_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - """Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. - wallet (bittensor_wallet.Wallet): The wallet with the coldkey to unstake to. - hotkey_ss58s (List[str]): List of hotkeys to unstake from. - amounts (List[Union[Balance, float]]): List of amounts to unstake. If ``None``, unstake all. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or included in the block. Flag is ``true`` if any wallet was unstaked. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if not isinstance(hotkey_ss58s, list) or not all( - isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s - ): - raise TypeError("hotkey_ss58s must be a list of str") - - if len(hotkey_ss58s) == 0: - return True - - if amounts is not None and len(amounts) != len(hotkey_ss58s): - raise ValueError("amounts must be a list of the same length as hotkey_ss58s") - - if amounts is not None and not all( - isinstance(amount, (Balance, float)) for amount in amounts - ): - raise TypeError( - "amounts must be a [list of bittensor.Balance or float] or None" - ) - - if amounts is None: - amounts = [None] * len(hotkey_ss58s) - else: - # Convert to Balance - amounts = [ - Balance.from_tao(amount) if isinstance(amount, float) else amount - for amount in amounts - ] - - if sum(amount.tao for amount in amounts) == 0: - # Staking 0 tao - return True - - # Unlock coldkey. - if not (unlock := unlock_key(wallet)).success: - logging.error(unlock.message) - return False - - old_stakes = [] - own_hotkeys = [] - logging.info( - f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + return execute_coroutine( + coroutine=async_unstake_multiple_extrinsic( + subtensor=subtensor.async_subtensor, + wallet=wallet, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), + event_loop=subtensor.event_loop, ) - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - - for hotkey_ss58 in hotkey_ss58s: - old_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) # Get stake on hotkey. - old_stakes.append(old_stake) # None if not registered. - - hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) - own_hotkeys.append(wallet.coldkeypub.ss58_address == hotkey_owner) - - successful_unstakes = 0 - for idx, (hotkey_ss58, amount, old_stake, own_hotkey) in enumerate( - zip(hotkey_ss58s, amounts, old_stakes, own_hotkeys) - ): - # Covert to bittensor.Balance - if amount is None: - # Unstake it all. - unstaking_balance = old_stake - else: - unstaking_balance = ( - amount if isinstance(amount, Balance) else Balance.from_tao(amount) - ) - - # Check enough to unstake. - stake_on_uid = old_stake - if unstaking_balance > stake_on_uid: - logging.error( - f":cross_mark: [red]Not enough stake[/red]: [green]{stake_on_uid}[/green] to unstake: [blue]{unstaking_balance}[/blue] from hotkey: [blue]{wallet.hotkey_str}[/blue]." - ) - continue - - # If nomination stake, check threshold. - if not own_hotkey and not _check_threshold_amount( - subtensor=subtensor, stake_balance=(stake_on_uid - unstaking_balance) - ): - logging.warning( - f":warning: [yellow]This action will unstake the entire staked balance![/yellow]" - ) - unstaking_balance = stake_on_uid - - try: - logging.info( - f":satellite: [magenta]Unstaking from chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" - ) - staking_response: bool = __do_remove_stake_single( - subtensor=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=unstaking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if staking_response is True: # If we successfully unstaked. - # We only wait here if we expect finalization. - - if idx < len(hotkey_ss58s) - 1: - # Wait for tx rate limit. - tx_rate_limit_blocks = subtensor.tx_rate_limit() - if tx_rate_limit_blocks > 0: - logging.info( - f":hourglass: [yellow]Waiting for tx rate limit: [white]{tx_rate_limit_blocks}[/white] blocks[/yellow]" - ) - sleep(tx_rate_limit_blocks * 12) # 12 seconds per block - - if not wait_for_finalization and not wait_for_inclusion: - successful_unstakes += 1 - continue - - logging.info(":white_heavy_check_mark: [green]Finalized[/green]") - - logging.info( - f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]..." - ) - block = subtensor.get_current_block() - new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - block=block, - ) - logging.info( - f"Stake ({hotkey_ss58}): [blue]{stake_on_uid}[/blue] :arrow_right: [green]{new_stake}[/green]" - ) - successful_unstakes += 1 - else: - logging.error(":cross_mark: [red]Failed: Unknown Error.[/red]") - continue - - except NotRegisteredError: - logging.error( - f":cross_mark: [red]Hotkey[/red] [blue]{hotkey_ss58}[/blue] [red]is not registered.[/red]" - ) - continue - except StakeError as e: - logging.error(":cross_mark: [red]Stake Error: {}[/red]".format(e)) - continue - - if successful_unstakes != 0: - logging.info( - f":satellite: [magenta]Checking Balance on:[/magenta] ([blue]{subtensor.network}[/blue] [magenta]...[/magenta]" - ) - new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - logging.info( - f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" - ) - return True - - return False diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 4da95852be..b5b958b93f 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -1,3 +1,4 @@ +import asyncio import copy import os import pickle @@ -17,12 +18,15 @@ convert_bond_uids_and_vals_to_tensor, convert_root_weight_uids_and_vals_to_tensor, ) -from . import settings -from .chain_data import AxonInfo +from bittensor.core import settings +from bittensor.core.chain_data import AxonInfo +from bittensor.utils import execute_coroutine # For annotation purposes if typing.TYPE_CHECKING: from bittensor.core.subtensor import Subtensor + from bittensor.core.async_subtensor import AsyncSubtensor + from bittensor.core.chain_data import NeuronInfo, NeuronInfoLite METAGRAPH_STATE_DICT_NDARRAY_KEYS = [ @@ -45,7 +49,8 @@ ] """List of keys for the metagraph state dictionary used in NDArray serialization. -This list defines the set of keys expected in the metagraph's state dictionary when serializing and deserializing NumPy ndarray objects. Each key corresponds to a specific attribute or metric associated with the nodes in the metagraph. +This list defines the set of keys expected in the metagraph's state dictionary when serializing and deserializing NumPy +ndarray objects. Each key corresponds to a specific attribute or metric associated with the nodes in the metagraph. - **version** (`str`): The version identifier of the metagraph state. - **n** (`int`): The total number of nodes in the metagraph. @@ -66,22 +71,25 @@ """ -def get_save_dir(network: str, netuid: int) -> str: +def get_save_dir( + network: str, netuid: int, root_dir: Optional[list[str]] = None +) -> str: """ Returns a directory path given ``network`` and ``netuid`` inputs. Args: network (str): Network name. netuid (int): Network UID. + root_dir: list to the file path for the root directory of your metagraph saves (i.e. ['/', 'tmp', 'metagraphs'], + defaults to ["~", ".bittensor", "metagraphs"] Returns: str: Directory path. """ + _root_dir = root_dir or ["~", ".bittensor", "metagraphs"] return os.path.expanduser( os.path.join( - "~", - ".bittensor", - "metagraphs", + *_root_dir, f"network-{str(network)}", f"netuid-{str(netuid)}", ) @@ -135,15 +143,21 @@ def determine_chain_endpoint_and_network(network: str) -> tuple[str, str]: return "unknown", network -class MetagraphMixin(ABC): +class AsyncMetagraphMixin(ABC): """ - The metagraph class is a core component of the Bittensor network, representing the neural graph that forms the backbone of the decentralized machine learning system. + The metagraph class is a core component of the Bittensor network, representing the neural graph that forms the + backbone of the decentralized machine learning system. - The metagraph is a dynamic representation of the network's state, capturing the interconnectedness and attributes of neurons (participants) in the Bittensor ecosystem. This class is not just a static structure but a live reflection of the network, constantly updated and synchronized with the state of the blockchain. + The metagraph is a dynamic representation of the network's state, capturing the interconnectedness and attributes of + neurons (participants) in the Bittensor ecosystem. This class is not just a static structure but a live + reflection of the network, constantly updated and synchronized with the state of the blockchain. - In Bittensor, neurons are akin to nodes in a distributed system, each contributing computational resources and participating in the network's collective intelligence. The metagraph tracks various attributes of these neurons, such as stake, trust, and consensus, which are crucial for the network's incentive mechanisms and the Yuma Consensus algorithm as outlined in the `NeurIPS paper `_. These attributes - govern how neurons interact, how they are incentivized, and their roles within the network's - decision-making processes. + In Bittensor, neurons are akin to nodes in a distributed system, each contributing computational resources and + participating in the network's collective intelligence. The metagraph tracks various attributes of these + neurons, such as stake, trust, and consensus, which are crucial for the network's incentive mechanisms and the + Yuma Consensus algorithm as outlined in the `NeurIPS paper + `_. These attributes govern how neurons + interact, how they are incentivized, and their roles within the network's decision-making processes. Args: netuid (int): A unique identifier that distinguishes between different instances or versions of the Bittensor network. @@ -168,8 +182,9 @@ class MetagraphMixin(ABC): uids: Unique identifiers for each neuron, essential for network operations. axons (List): Details about each neuron's axon, critical for facilitating network communication. - The metagraph plays a pivotal role in Bittensor's decentralized AI operations, influencing everything from data propagation to reward distribution. It embodies the principles of decentralized governance - and collaborative intelligence, ensuring that the network remains adaptive, secure, and efficient. + The metagraph plays a pivotal role in Bittensor's decentralized AI operations, influencing everything from data + propagation to reward distribution. It embodies the principles of decentralized governance and collaborative + intelligence, ensuring that the network remains adaptive, secure, and efficient. Example: Initializing the metagraph to represent the current state of the Bittensor network:: @@ -198,6 +213,7 @@ class MetagraphMixin(ABC): network: str version: Union["torch.nn.Parameter", tuple[NDArray]] n: Union["torch.nn.Parameter", NDArray] + neurons: list[Union["NeuronInfo", "NeuronInfoLite"]] block: Union["torch.nn.Parameter", NDArray] stake: Union["torch.nn.Parameter", NDArray] total_stake: Union["torch.nn.Parameter", NDArray] @@ -216,7 +232,7 @@ class MetagraphMixin(ABC): uids: Union["torch.nn.Parameter", NDArray] axons: list[AxonInfo] chain_endpoint: Optional[str] - subtensor: Optional["Subtensor"] + subtensor: Optional["AsyncSubtensor"] @property def S(self) -> Union[NDArray, "torch.nn.Parameter"]: @@ -227,7 +243,8 @@ def S(self) -> Union[NDArray, "torch.nn.Parameter"]: from the network, playing a crucial role in the distribution of incentives and decision-making processes. Returns: - NDArray: A tensor representing the stake of each neuron in the network. Higher values signify a greater stake held by the respective neuron. + NDArray: A tensor representing the stake of each neuron in the network. Higher values signify a greater + stake held by the respective neuron. """ return self.total_stake @@ -240,7 +257,8 @@ def R(self) -> Union[NDArray, "torch.nn.Parameter"]: incentives within the network, with higher-ranked neurons receiving more incentive. Returns: - NDArray: A tensor where each element represents the rank of a neuron. Higher values indicate higher ranks within the network. + NDArray: A tensor where each element represents the rank of a neuron. Higher values indicate higher ranks + within the network. """ return self.ranks @@ -253,7 +271,8 @@ def I(self) -> Union[NDArray, "torch.nn.Parameter"]: trusted contributions are incentivized. Returns: - NDArray: A tensor of incentive values, indicating the rewards or benefits accrued by each neuron based on their contributions and network consensus. + NDArray: A tensor of incentive values, indicating the rewards or benefits accrued by each neuron based on + their contributions and network consensus. """ return self.incentive @@ -266,7 +285,8 @@ def E(self) -> Union[NDArray, "torch.nn.Parameter"]: contributing neurons are appropriately rewarded. Returns: - NDArray: A tensor where each element represents the emission value for a neuron, indicating the amount of reward distributed to that neuron. + NDArray: A tensor where each element represents the emission value for a neuron, indicating the amount of + reward distributed to that neuron. """ return self.emission @@ -280,7 +300,8 @@ def C(self) -> Union[NDArray, "torch.nn.Parameter"]: are more widely trusted and valued across the network. Returns: - NDArray: A tensor of consensus values, where each element reflects the level of trust and agreement a neuron has achieved within the network. + NDArray: A tensor of consensus values, where each element reflects the level of trust and agreement a neuron + has achieved within the network. """ return self.consensus @@ -297,7 +318,8 @@ def T(self) -> Union[NDArray, "torch.nn.Parameter"]: has in others. A higher value in the trust matrix suggests a stronger trust relationship between neurons. Returns: - NDArray: A tensor of trust values, where each element represents the trust level of a neuron. Higher values denote a higher level of trust within the network. + NDArray: A tensor of trust values, where each element represents the trust level of a neuron. Higher values + denote a higher level of trust within the network. """ return self.trust @@ -313,7 +335,8 @@ def Tv(self) -> Union[NDArray, "torch.nn.Parameter"]: determining the validators' influence and responsibilities in these critical functions. Returns: - NDArray: A tensor of validator trust values, specifically applicable to neurons serving as validators, where higher values denote greater trustworthiness in their validation roles. + NDArray: A tensor of validator trust values, specifically applicable to neurons serving as validators, where + higher values denote greater trustworthiness in their validation roles. """ return self.validator_trust @@ -325,7 +348,8 @@ def D(self) -> Union[NDArray, "torch.nn.Parameter"]: They are an integral part of the network's incentive structure, encouraging active and beneficial participation. Returns: - NDArray: A tensor of dividend values, where each element indicates the dividends received by a neuron, reflecting their share of network rewards. + NDArray: A tensor of dividend values, where each element indicates the dividends received by a neuron, + reflecting their share of network rewards. """ return self.dividends @@ -338,7 +362,8 @@ def B(self) -> Union[NDArray, "torch.nn.Parameter"]: among neurons while providing an additional layer of incentive. Returns: - NDArray: A tensor representing the bonds held by each neuron, where each value signifies the proportion of bonds owned by one neuron in another. + NDArray: A tensor representing the bonds held by each neuron, where each value signifies the proportion of + bonds owned by one neuron in another. """ return self.bonds @@ -350,13 +375,15 @@ def W(self) -> Union[NDArray, "torch.nn.Parameter"]: for setting its weights, which are then recorded on a digital ledger. These weights are reflective of the neuron's assessment or judgment of other neurons in the network. - The weight matrix :math:`W = [w_{ij}]` is a key component of the network's architecture, where the :math:`i^{th}` row is set by - neuron :math:`i` and represents its weights towards other neurons. These weights influence the ranking and incentive - mechanisms within the network. Higher weights from a neuron towards another can imply greater trust or value - placed on that neuron's contributions. + The weight matrix :math:`W = [w_{ij}]` is a key component of the network's architecture, where the :math: + `i^{th}` row is set by neuron :math:`i` and represents its weights towards other neurons. These weights + influence the ranking and incentive mechanisms within the network. Higher weights from a neuron towards another + can imply greater trust or value placed on that neuron's contributions. Returns: - NDArray: A tensor of inter-peer weights, where each element :math:`w_{ij}` represents the weight assigned by neuron :math:`i` to neuron :math:`j`. This matrix is fundamental to the network's functioning, influencing the distribution of incentives and the inter-neuronal dynamics. + NDArray: A tensor of inter-peer weights, where each element :math:`w_{ij}` represents the weight assigned by + neuron :math:`i` to neuron :math:`j`. This matrix is fundamental to the network's functioning, + influencing the distribution of incentives and the inter-neuronal dynamics. """ return self.weights @@ -365,16 +392,20 @@ def hotkeys(self) -> list[str]: """ Represents a list of ``hotkeys`` for each neuron in the Bittensor network. - Hotkeys are unique identifiers used by neurons for active participation in the network, such as sending and receiving information or - transactions. They are akin to public keys in cryptographic systems and are essential for identifying and authenticating neurons within the network's operations. + Hotkeys are unique identifiers used by neurons for active participation in the network, such as sending and + receiving information or transactions. They are akin to public keys in cryptographic systems and are essential + for identifying and authenticating neurons within the network's operations. Returns: List[str]: A list of hotkeys, with each string representing the hotkey of a corresponding neuron. - These keys are crucial for the network's security and integrity, ensuring proper identification and authorization of network participants. + These keys are crucial for the network's security and integrity, ensuring proper identification and + authorization of network participants. Note: - While the `NeurIPS paper `_ may not explicitly detail the concept of hotkeys, they are a fundamental of decentralized networks for secure and authenticated interactions. + While the `NeurIPS paper `_ may not + explicitly detail the concept of hotkeys, they are a fundamental of decentralized networks for secure + and authenticated interactions. """ return [axon.hotkey for axon in self.axons] @@ -383,14 +414,17 @@ def coldkeys(self) -> list[str]: """ Contains a list of ``coldkeys`` for each neuron in the Bittensor network. - Coldkeys are similar to hotkeys but are typically used for more secure, offline activities such as storing assets or offline signing of transactions. They are an important aspect of a neuron's security, providing an additional layer of protection for sensitive operations and assets. + Coldkeys are similar to hotkeys but are typically used for more secure, offline activities such as storing + assets or offline signing of transactions. They are an important aspect of a neuron's security, providing an + additional layer of protection for sensitive operations and assets. Returns: - List[str]: A list of coldkeys, each string representing the coldkey of a neuron. These keys play a vital role in the secure management of assets and sensitive operations within the network. + List[str]: A list of coldkeys, each string representing the coldkey of a neuron. These keys play a vital + role in the secure management of assets and sensitive operations within the network. Note: The concept of coldkeys, while not explicitly covered in the NeurIPS paper, is a standard practice in - blockchain and decentralized networks for enhanced security and asset protection. + blockchain and decentralized networks for enhanced security and asset protection. """ return [axon.coldkey for axon in self.axons] @@ -402,12 +436,15 @@ def addresses(self) -> list[str]: IP addresses are fundamental for the network's peer-to-peer communication infrastructure. Returns: - List[str]: A list of IP addresses, with each string representing the address of a neuron. These addresses enable the decentralized, distributed nature of the network, facilitating direct communication and data exchange among neurons. + List[str]: A list of IP addresses, with each string representing the address of a neuron. These addresses + enable the decentralized, distributed nature of the network, facilitating direct communication and data + exchange among neurons. Note: While IP addresses are a basic aspect of network communication, specific details about their use in - the Bittensor network may not be covered in the `NeurIPS paper `_. They are, however, integral to the - functioning of any distributed network. + the Bittensor network may not be covered in the `NeurIPS paper + `_. They are, however, integral to + the functioning of any distributed network. """ return [axon.ip_str() for axon in self.axons] @@ -418,18 +455,22 @@ def __init__( network: str = settings.DEFAULT_NETWORK, lite: bool = True, sync: bool = True, - subtensor: "Subtensor" = None, + subtensor: "AsyncSubtensor" = None, ): """ - Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the provided arguments. - This method is the entry point for creating a metagraph object, - which is a central component in representing the state of the Bittensor network. + Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the + provided arguments. 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. Synchronization involves updating the metagraph's parameters to reflect the current state of the network. + 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. + 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:: @@ -440,11 +481,14 @@ def __init__( def __str__(self) -> str: """ - Provides a human-readable string representation of the metagraph object. This representation includes key identifiers and attributes of the metagraph, making it easier to quickly understand - the state and configuration of the metagraph in a simple format. + Provides a human-readable string representation of the metagraph object. This representation includes key + identifiers and attributes of the metagraph, making it easier to quickly understand the state and configuration + of the metagraph in a simple format. Returns: - str: A string that succinctly represents the metagraph, including its network UID, the total number of neurons (n), the current block number, and the network's name. This format is particularly useful for logging, debugging, and displaying the metagraph in a concise manner. + str: A string that succinctly represents the metagraph, including its network UID, the total number of + neurons (n), the current block number, and the network's name. This format is particularly useful + for logging, debugging, and displaying the metagraph in a concise manner. Example: When printing the metagraph object or using it in a string context, this method is automatically invoked:: @@ -455,11 +499,13 @@ def __str__(self) -> str: def __repr__(self) -> str: """ - Provides a detailed string representation of the metagraph object, intended for unambiguous understanding and debugging purposes. This method simply calls the :func:`__str__` method, ensuring - consistency between the informal and formal string representations of the metagraph. + Provides a detailed string representation of the metagraph object, intended for unambiguous understanding and + debugging purposes. This method simply calls the :func:`__str__` method, ensuring consistency between the + informal and formal string representations of the metagraph. Returns: - str: The same string representation as provided by the :func:`__str__` method, detailing the metagraph's key attributes including network UID, number of neurons, block number, and network name. + str: The same string representation as provided by the :func:`__str__` method, detailing the metagraph's key + attributes including network UID, number of neurons, block number, and network name. Example: The :func:`__repr__` output can be used in debugging to get a clear and concise description of the metagraph:: @@ -471,10 +517,9 @@ def __repr__(self) -> str: def metadata(self) -> dict: """ - Retrieves the metadata of the metagraph, providing key information about the current state of the - Bittensor network. This metadata includes details such as the network's unique identifier (``netuid``), - the total number of neurons (``n``), the current block number, the network's name, and the version of - the Bittensor network. + Retrieves the metadata of the metagraph, providing key information about the current state of the Bittensor + network. This metadata includes details such as the network's unique identifier (``netuid``), the total number + of neurons (``n``), the current block number, the network's name, and the version of the Bittensor network. Returns: dict: A dictionary containing essential metadata about the metagraph, including: @@ -486,7 +531,8 @@ def metadata(self) -> dict: - ``version``: The version number of the Bittensor software. Note: - This metadata is crucial for understanding the current state and configuration of the network, as well as for tracking its evolution over time. + This metadata is crucial for understanding the current state and configuration of the network, as well as + for tracking its evolution over time. """ return { "netuid": self.netuid, @@ -522,19 +568,25 @@ def state_dict(self): "neurons": self.neurons, } - def sync( + async def sync( self, block: Optional[int] = None, lite: bool = True, - subtensor: Optional["Subtensor"] = None, + subtensor: Optional["AsyncSubtensor"] = None, ): """ - Synchronizes the metagraph with the Bittensor network's current state. It updates the metagraph's attributes to reflect the latest data from the network, ensuring the metagraph represents the most current state of the network. + Synchronizes the metagraph with the Bittensor network's current state. It updates the metagraph's attributes to + reflect the latest data from the network, ensuring the metagraph represents the most current state of the + network. Args: - block (Optional[int]): A specific block number to synchronize with. If None, the metagraph syncs with the latest block. This allows for historical analysis or specific state examination of the network. - lite (bool): If True, a lite version of the metagraph is used for quicker synchronization. This is beneficial when full detail is not necessary, allowing for reduced computational and time overhead. - subtensor (Optional[bittensor.core.subtensor.Subtensor]): An instance of the subtensor class from Bittensor, providing an interface to the underlying blockchain data. If provided, this instance is used for data retrieval during synchronization. + block (Optional[int]): A specific block number to synchronize with. If None, the metagraph syncs with the + latest block. This allows for historical analysis or specific state examination of the network. + lite (bool): If True, a lite version of the metagraph is used for quicker synchronization. This is + beneficial when full detail is not necessary, allowing for reduced computational and time overhead. + subtensor (Optional[bittensor.core.subtensor.Subtensor]): An instance of the subtensor class from Bittensor, + providing an interface to the underlying blockchain data. If provided, this instance is used for data + retrieval during synchronization. Example: Sync the metagraph with the latest block from the subtensor, using the lite version for efficiency:: @@ -552,7 +604,9 @@ def sync( metagraph.sync(block=12345, lite=False, subtensor=subtensor) NOTE: - If attempting to access data beyond the previous 300 blocks, you **must** use the ``archive`` network for subtensor. Light nodes are configured only to store the previous 300 blocks if connecting to finney or test networks. + If attempting to access data beyond the previous 300 blocks, you **must** use the ``archive`` network for + subtensor. Light nodes are configured only to store the previous 300 blocks if connecting to finney or + test networks. For example:: @@ -564,7 +618,6 @@ def sync( metagraph.sync(block=history_block, lite=False, subtensor=subtensor) """ - # Initialize subtensor subtensor = self._initialize_subtensor(subtensor) @@ -572,7 +625,7 @@ def sync( subtensor.chain_endpoint != settings.ARCHIVE_ENTRYPOINT or subtensor.network != "archive" ): - cur_block = subtensor.get_current_block() + cur_block = await subtensor.get_current_block() if block and block < (cur_block - 300): logging.warning( "Attempting to sync longer than 300 blocks ago on a non-archive node. Please use the 'archive' " @@ -580,28 +633,32 @@ def sync( ) # Assign neurons based on 'lite' flag - self._assign_neurons(block, lite, subtensor) + await self._assign_neurons(block, lite, subtensor) # Set attributes for metagraph - self._set_metagraph_attributes(block, subtensor) + await self._set_metagraph_attributes(block, subtensor) # If not a 'lite' version, compute and set weights and bonds for each neuron if not lite: - self._set_weights_and_bonds(subtensor=subtensor) + await self._set_weights_and_bonds(subtensor=subtensor) - def _initialize_subtensor(self, subtensor: "Subtensor"): + def _initialize_subtensor(self, subtensor: "AsyncSubtensor") -> "AsyncSubtensor": """ Initializes the subtensor to be used for syncing the metagraph. - This method ensures that a subtensor instance is available and properly set up for data retrieval during the synchronization process. + This method ensures that a subtensor instance is available and properly set up for data retrieval during the + synchronization process. - If no subtensor is provided, this method is responsible for creating a new instance of the subtensor, configured according to the current network settings. + If no subtensor is provided, this method is responsible for creating a new instance of the subtensor, configured + according to the current network settings. Args: - subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance provided for initialization. If ``None``, a new subtensor instance is created using the current network configuration. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance provided for + initialization. If ``None``, a new subtensor instance is created using the current network configuration. Returns: - subtensor (bittensor.core.subtensor.Subtensor): The initialized subtensor instance, ready to be used for syncing the metagraph. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The initialized subtensor instance, ready to be + used for syncing the metagraph. Internal Usage: Used internally during the sync process to ensure a valid subtensor instance is available:: @@ -615,22 +672,28 @@ def _initialize_subtensor(self, subtensor: "Subtensor"): if not subtensor: # TODO: Check and test the initialization of the new subtensor # Lazy import due to circular import (subtensor -> metagraph, metagraph -> subtensor) - from bittensor.core.subtensor import Subtensor + from bittensor.core.subtensor import AsyncSubtensor - subtensor = Subtensor(network=self.chain_endpoint) + subtensor = AsyncSubtensor(network=self.chain_endpoint) self.subtensor = subtensor return subtensor - def _assign_neurons(self, block: int, lite: bool, subtensor: "Subtensor"): + async def _assign_neurons( + self, block: int, lite: bool, subtensor: "AsyncSubtensor" + ): """ Assigns neurons to the metagraph based on the provided block number and the lite flag. - This method is responsible for fetching and setting the neuron data in the metagraph, which includes neuron attributes like UID, stake, trust, and other relevant information. + This method is responsible for fetching and setting the neuron data in the metagraph, which includes neuron + attributes like UID, stake, trust, and other relevant information. Args: - block (int): The block number for which the neuron data needs to be fetched. If ``None``, the latest block data is used. - lite (bool): A boolean flag indicating whether to use a lite version of the neuron data. The lite version typically includes essential information and is quicker to fetch and process. - subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for fetching neuron data from the network. + block (int): The block number for which the neuron data needs to be fetched. If ``None``, the latest block + data is used. + lite (bool): A boolean flag indicating whether to use a lite version of the neuron data. The lite version + typically includes essential information and is quicker to fetch and process. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for fetching neuron + data from the network. Internal Usage: Used internally during the sync process to fetch and set neuron data:: @@ -643,15 +706,17 @@ def _assign_neurons(self, block: int, lite: bool, subtensor: "Subtensor"): self._assign_neurons(block, lite, subtensor) """ if lite: - self.neurons = subtensor.neurons_lite(block=block, netuid=self.netuid) + self.neurons = await subtensor.neurons_lite(block=block, netuid=self.netuid) + else: - self.neurons = subtensor.neurons(block=block, netuid=self.netuid) + self.neurons = await subtensor.neurons(block=block, netuid=self.netuid) self.lite = lite @staticmethod def _create_tensor(data, dtype) -> Union[NDArray, "torch.nn.Parameter"]: """ - Creates a numpy array with the given data and data type. This method is a utility function used internally to encapsulate data into a np.array, making it compatible with the metagraph's numpy model structure. + Creates a numpy array with the given data and data type. This method is a utility function used internally to + encapsulate data into a np.array, making it compatible with the metagraph's numpy model structure. Args: data: The data to be included in the tensor. This could be any numeric data, like stakes, ranks, etc. @@ -672,12 +737,17 @@ def _create_tensor(data, dtype) -> Union[NDArray, "torch.nn.Parameter"]: else np.array(data, dtype=dtype) ) - def _set_weights_and_bonds(self, subtensor: "Optional[Subtensor]" = None): + async def _set_weights_and_bonds( + self, subtensor: Optional["AsyncSubtensor"] = None + ): """ - Computes and sets the weights and bonds for each neuron in the metagraph. This method is responsible for processing the raw weight and bond data obtained from the network and converting it into a structured format suitable for the metagraph model. + Computes and sets the weights and bonds for each neuron in the metagraph. This method is responsible for + processing the raw weight and bond data obtained from the network and converting it into a structured format + suitable for the metagraph model. Args: - subtensor: The subtensor instance used for fetching weights and bonds data. If ``None``, the weights and bonds are not updated. + subtensor: The subtensor instance used for fetching weights and bonds data. If ``None``, the weights and + bonds are not updated. Internal Usage: Used internally during the sync process to update the weights and bonds of the neurons:: @@ -686,7 +756,7 @@ def _set_weights_and_bonds(self, subtensor: "Optional[Subtensor]" = None): """ # TODO: Check and test the computation of weights and bonds if self.netuid == 0: - self.weights = self._process_root_weights( + self.weights = await self._process_root_weights( [neuron.weights for neuron in self.neurons], "weights", subtensor, @@ -703,11 +773,14 @@ def _process_weights_or_bonds( self, data, attribute: str ) -> Union[NDArray, "torch.nn.Parameter"]: """ - Processes the raw weights or bonds data and converts it into a structured tensor format. This method handles the transformation of neuron connection data (``weights`` or ``bonds``) from a list or other unstructured format into a tensor that can be utilized within the metagraph model. + Processes the raw weights or bonds data and converts it into a structured tensor format. This method handles the + transformation of neuron connection data (``weights`` or ``bonds``) from a list or other unstructured format + into a tensor that can be utilized within the metagraph model. Args: data: The raw weights or bonds data to be processed. This data typically comes from the subtensor. - attribute: A string indicating whether the data is ``weights`` or ``bonds``, which determines the specific processing steps to be applied. + attribute: A string indicating whether the data is ``weights`` or ``bonds``, which determines the specific + processing steps to be applied. Returns: A tensor parameter encapsulating the processed weights or bonds data. @@ -761,19 +834,21 @@ def _process_weights_or_bonds( return tensor_param @abstractmethod - def _set_metagraph_attributes(self, block, subtensor): + async def _set_metagraph_attributes(self, block, subtensor): pass - def _process_root_weights( - self, data: list, attribute: str, subtensor: "Subtensor" + async def _process_root_weights( + self, data: list, attribute: str, subtensor: "AsyncSubtensor" ) -> Union[NDArray, "torch.nn.Parameter"]: """ - Specifically processes the root weights data for the metagraph. This method is similar to :func:`_process_weights_or_bonds` but is tailored for processing root weights, which have a different structure and significance in the network. + Specifically processes the root weights data for the metagraph. This method is similar to :func:`_process_weights_or_bonds` + but is tailored for processing root weights, which have a different structure and significance in the network. Args: data (list): The raw root weights data to be processed. attribute (str): A string indicating the attribute type, here it's typically ``weights``. - subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for additional data and context needed in processing. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for additional data + and context needed in processing. Returns: A tensor parameter encapsulating the processed root weights data. @@ -784,8 +859,8 @@ def _process_root_weights( self.root_weights = self._process_root_weights(raw_root_weights_data, "weights", subtensor) """ data_array = [] - n_subnets = subtensor.get_total_subnets() or 0 - subnets = subtensor.get_subnets() + n_subnets = await subtensor.get_total_subnets() or 0 + subnets = await subtensor.get_subnets() for item in data: if len(item) == 0: if use_torch(): @@ -820,9 +895,15 @@ def _process_root_weights( ) return tensor_param - def save(self) -> "Metagraph": + def save(self, root_dir: Optional[list[str]] = None) -> "AsyncMetagraph": """ - Saves the current state of the metagraph to a file on disk. This function is crucial for persisting the current state of the network's metagraph, which can later be reloaded or analyzed. The save operation includes all neuron attributes and parameters, ensuring a complete snapshot of the metagraph's state. + Saves the current state of the metagraph to a file on disk. This function is crucial for persisting the current + state of the network's metagraph, which can later be reloaded or analyzed. The save operation includes all + neuron attributes and parameters, ensuring a complete snapshot of the metagraph's state. + + Args: + root_dir: list to the file path for the root directory of your metagraph saves + (i.e. ['/', 'tmp', 'metagraphs'], defaults to ["~", ".bittensor", "metagraphs"] Returns: metagraph (bittensor.core.metagraph.Metagraph): The metagraph instance after saving its state. @@ -842,7 +923,7 @@ def save(self) -> "Metagraph": metagraph.load_from_path(dir_path) """ - save_directory = get_save_dir(self.network, self.netuid) + save_directory = get_save_dir(self.network, self.netuid, root_dir=root_dir) os.makedirs(save_directory, exist_ok=True) if use_torch(): graph_filename = f"{save_directory}/block-{self.block.item()}.pt" @@ -858,7 +939,7 @@ def save(self) -> "Metagraph": pickle.dump(state_dict, graph_file) return self - def load(self): + def load(self, root_dir: Optional[list[str]] = None) -> None: """ Loads the state of the metagraph from the default save directory. This method is instrumental for restoring the metagraph to its last saved state. It automatically identifies the save directory based on the ``network`` and ``netuid`` properties of the metagraph, locates the latest block file in that directory, and loads all metagraph parameters from it. @@ -868,6 +949,10 @@ def load(self): The method delegates to ``load_from_path``, supplying it with the directory path constructed from the metagraph's current ``network`` and ``netuid`` properties. This abstraction simplifies the process of loading the metagraph's state for the user, requiring no direct path specifications. + Args: + root_dir: list to the file path for the root directory of your metagraph saves + (i.e. ['/', 'tmp', 'metagraphs'], defaults to ["~", ".bittensor", "metagraphs"] + Returns: metagraph (bittensor.core.metagraph.Metagraph): The metagraph instance after loading its state from the default directory. @@ -881,22 +966,27 @@ def load(self): Note: The default save directory is determined based on the metagraph's ``network`` and ``netuid`` attributes. It is important to ensure that these attributes are set correctly and that the default save directory contains the appropriate state files for the metagraph. """ - self.load_from_path(get_save_dir(self.network, self.netuid)) + self.load_from_path(get_save_dir(self.network, self.netuid, root_dir=root_dir)) @abstractmethod - def load_from_path(self, dir_path: str) -> "Metagraph": + def load_from_path(self, dir_path: str) -> "AsyncMetagraph": """ - Loads the state of the metagraph from a specified directory path. This method is crucial for restoring the metagraph to a specific state based on saved data. It locates the latest block file in the given - directory and loads all metagraph parameters from it. This is particularly useful for analyses that require historical states of the network or for restoring previous states of the metagraph in different - execution environments. + Loads the state of the metagraph from a specified directory path. This method is crucial for restoring the + metagraph to a specific state based on saved data. It locates the latest block file in the given directory and + loads all metagraph parameters from it. This is particularly useful for analyses that require historical states + of the network or for restoring previous states of the metagraph in different execution environments. - The method first identifies the latest block file in the specified directory, then loads the metagraph state including neuron attributes and parameters from this file. This ensures that the metagraph is accurately reconstituted to reflect the network state at the time of the saved block. + The method first identifies the latest block file in the specified directory, then loads the metagraph state + including neuron attributes and parameters from this file. This ensures that the metagraph is accurately + reconstituted to reflect the network state at the time of the saved block. Args: - dir_path (str): The directory path where the metagraph's state files are stored. This path should contain one or more saved state files, typically named in a format that includes the block number. + dir_path (str): The directory path where the metagraph's state files are stored. This path should contain + one or more saved state files, typically named in a format that includes the block number. Returns: - metagraph (bittensor.core.metagraph.Metagraph): The metagraph instance after loading its state from the specified directory path. + metagraph (bittensor.core.metagraph.AsyncMetagraph): The metagraph instance after loading its state from the + specified directory path. Example: Load the metagraph state from a specific directory:: @@ -904,7 +994,8 @@ def load_from_path(self, dir_path: str) -> "Metagraph": dir_path = "/path/to/saved/metagraph/states" metagraph.load_from_path(dir_path) - The metagraph is now restored to the state it was in at the time of the latest saved block in the specified directory. + The metagraph is now restored to the state it was in at the time of the latest saved block in the specified + directory. Note: This method assumes that the state files in the specified directory are correctly formatted and @@ -937,31 +1028,38 @@ def __copy__(self): return new_instance -BaseClass: Union["torch.nn.Module", object] = torch.nn.Module if use_torch() else object +if use_torch(): + BaseClass = torch.nn.Module +else: + BaseClass = object """ Base class that extends :class:`torch.nn.Module` if PyTorch is used; otherwise, it defaults to object. """ -class TorchMetaGraph(MetagraphMixin, BaseClass): +class AsyncTorchMetaGraph(AsyncMetagraphMixin, BaseClass): def __init__( self, netuid: int, network: str = settings.DEFAULT_NETWORK, lite: bool = True, sync: bool = True, - subtensor: "Subtensor" = None, + subtensor: "AsyncSubtensor" = None, ): """ - 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. + 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. Synchronization involves updating the metagraph's parameters to reflect the current state of the network. + 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. + 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:: @@ -971,7 +1069,7 @@ def __init__( metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) """ torch.nn.Module.__init__(self) - MetagraphMixin.__init__(self, netuid, network, lite, sync, subtensor) + AsyncMetagraphMixin.__init__(self, netuid, network, lite, sync, subtensor) self.netuid = netuid self.network, self.chain_endpoint = determine_chain_endpoint_and_network( network @@ -1032,19 +1130,32 @@ def __init__( torch.tensor([], dtype=torch.int64), requires_grad=False ) self.axons: list[AxonInfo] = [] + self.neurons = [] self.subtensor = subtensor - if sync: - self.sync(block=None, lite=lite, subtensor=subtensor) + self.should_sync = sync + + if self.should_sync: + execute_coroutine(self.sync(block=None, lite=lite, subtensor=subtensor)) - def _set_metagraph_attributes(self, block: int, subtensor: "Subtensor"): + async def __aenter__(self): + if self.should_sync: + await self.sync(block=None, lite=self.lite, subtensor=self.subtensor) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + pass + + async def _set_metagraph_attributes(self, block: int, subtensor: "AsyncSubtensor"): """ Sets various attributes of the metagraph based on the latest network data fetched from the subtensor. - - This method updates parameters like the number of neurons, block number, stakes, trusts, ranks, and other neuron-specific information. + This method updates parameters like the number of neurons, block number, stakes, trusts, ranks, and other + neuron-specific information. Args: - block (int): The block number for which the metagraph attributes need to be set. If ``None``, the latest block data is used. - subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for fetching the latest network data. + block (int): The block number for which the metagraph attributes need to be set. If ``None``, the latest + block data is used. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for fetching the + latest network data. Internal Usage: Used internally during the sync process to update the metagraph's attributes:: @@ -1059,7 +1170,7 @@ def _set_metagraph_attributes(self, block: int, subtensor: "Subtensor"): self.n = self._create_tensor(len(self.neurons), dtype=torch.int64) self.version = self._create_tensor([settings.version_as_int], dtype=torch.int64) self.block = self._create_tensor( - block if block else subtensor.block, dtype=torch.int64 + block if block else await subtensor.block, dtype=torch.int64 ) self.uids = self._create_tensor( [neuron.uid for neuron in self.neurons], dtype=torch.int64 @@ -1102,7 +1213,7 @@ def _set_metagraph_attributes(self, block: int, subtensor: "Subtensor"): ) self.axons = [n.axon_info for n in self.neurons] - def load_from_path(self, dir_path: str) -> "Metagraph": + def load_from_path(self, dir_path: str) -> "AsyncMetagraph": """ Loads the metagraph state from a specified directory path. @@ -1110,7 +1221,7 @@ def load_from_path(self, dir_path: str) -> "Metagraph": dir_path (str): The directory path where the state file is located. Returns: - metagraph (bittensor.core.metagraph.Metagraph): The current metagraph instance with the loaded state. + metagraph (bittensor.core.metagraph.AsyncMetagraph): The current metagraph instance with the loaded state. Example:: @@ -1166,25 +1277,29 @@ def load_from_path(self, dir_path: str) -> "Metagraph": return self -class NonTorchMetagraph(MetagraphMixin): +class AsyncNonTorchMetagraph(AsyncMetagraphMixin): def __init__( self, netuid: int, network: str = settings.DEFAULT_NETWORK, lite: bool = True, sync: bool = True, - subtensor: "Subtensor" = None, + subtensor: "AsyncSubtensor" = None, ): """ - 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. + 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. Synchronization involves updating the metagraph's parameters to reflect the current state of the network. + 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. + 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:: @@ -1194,7 +1309,7 @@ def __init__( metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) """ # super(metagraph, self).__init__() - MetagraphMixin.__init__(self, netuid, network, lite, sync, subtensor) + AsyncMetagraphMixin.__init__(self, netuid, network, lite, sync, subtensor) self.netuid = netuid self.network, self.chain_endpoint = determine_chain_endpoint_and_network( @@ -1219,19 +1334,32 @@ def __init__( self.bonds = np.array([], dtype=np.int64) self.uids = np.array([], dtype=np.int64) self.axons: list[AxonInfo] = [] + self.neurons = [] self.subtensor = subtensor - if sync: - self.sync(block=None, lite=lite, subtensor=subtensor) + self.should_sync = sync - def _set_metagraph_attributes(self, block: int, subtensor: "Subtensor"): - """ - Sets various attributes of the metagraph based on the latest network data fetched from the subtensor. + if self.should_sync: + execute_coroutine(self.sync(block=None, lite=lite, subtensor=subtensor)) - This method updates parameters like the number of neurons, block number, stakes, trusts, ranks, and other neuron-specific information. + async def __aenter__(self): + if self.should_sync: + await self.sync(block=None, lite=self.lite, subtensor=self.subtensor) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + pass + + async def _set_metagraph_attributes(self, block: int, subtensor: "AsyncSubtensor"): + """ + Sets various attributes of the metagraph based on the latest network data fetched from the subtensor. This + method updates parameters like the number of neurons, block number, stakes, trusts, ranks, and other + neuron-specific information. Args: - block (int): The block number for which the metagraph attributes need to be set. If ``None``, the latest block data is used. - subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for fetching the latest network data. + block (int): The block number for which the metagraph attributes need to be set. If ``None``, the latest + block data is used. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for fetching the + latest network data. Internal Usage: Used internally during the sync process to update the metagraph's attributes:: @@ -1242,7 +1370,7 @@ def _set_metagraph_attributes(self, block: int, subtensor: "Subtensor"): self.n = self._create_tensor(len(self.neurons), dtype=np.int64) self.version = self._create_tensor([settings.version_as_int], dtype=np.int64) self.block = self._create_tensor( - block if block else subtensor.block, dtype=np.int64 + block if block else await subtensor.block, dtype=np.int64 ) self.uids = self._create_tensor( [neuron.uid for neuron in self.neurons], dtype=np.int64 @@ -1285,7 +1413,7 @@ def _set_metagraph_attributes(self, block: int, subtensor: "Subtensor"): ) self.axons = [n.axon_info for n in self.neurons] - def load_from_path(self, dir_path: str) -> "Metagraph": + def load_from_path(self, dir_path: str) -> "AsyncMetagraph": """ Loads the state of the Metagraph from a specified directory path. @@ -1293,7 +1421,8 @@ def load_from_path(self, dir_path: str) -> "Metagraph": dir_path (str): The directory path where the metagraph's state file is located. Returns: - metagraph (:func:`bittensor.core.metagraph.Metagraph`): An instance of the Metagraph with the state loaded from the file. + metagraph (:func:`bittensor.core.metagraph.AsyncMetagraph`): An instance of the Metagraph with the state loaded + from the file. Raises: pickle.UnpicklingError: If there is an error unpickling the state file. @@ -1346,9 +1475,77 @@ def load_from_path(self, dir_path: str) -> "Metagraph": return self -Metagraph = TorchMetaGraph if use_torch() else NonTorchMetagraph +if use_torch(): + AsyncMetagraph = AsyncTorchMetaGraph +else: + AsyncMetagraph = AsyncNonTorchMetagraph """Metagraph class that uses :class:`TorchMetaGraph` if PyTorch is available; otherwise, it falls back to :class:`NonTorchMetagraph`. - **With PyTorch**: When `use_torch()` returns `True`, `Metagraph` is set to :class:`TorchMetaGraph`, which utilizes PyTorch functionalities. - **Without PyTorch**: When `use_torch()` returns `False`, `Metagraph` is set to :class:`NonTorchMetagraph`, which does not rely on PyTorch. """ + + +class Metagraph(AsyncMetagraph): + """ + Represents a wrapper for the asynchronous metagraph functionality. + + This class provides a synchronous interface to interact with an asynchronous metagraph. It is initialized with + configuration related to the network and provides methods for synchronizing and accessing asynchronous metagraph + attributes. + """ + + def __init__( + self, + netuid: int, + network: str = settings.DEFAULT_NETWORK, + lite: bool = True, + sync: bool = True, + subtensor: "Subtensor" = None, + ): + self.subtensor: Optional["Subtensor"] = subtensor + self._async_metagraph = AsyncMetagraph( + netuid=netuid, + network=network, + lite=lite, + sync=sync, + subtensor=subtensor.async_subtensor if subtensor else None, + ) + + def sync( + self, + block: Optional[int] = None, + lite: bool = True, + subtensor: Optional["Subtensor"] = None, + ): + """Synchronizes the metagraph to the specified block, lite, and subtensor instance if available.""" + if subtensor: + event_loop = subtensor.event_loop + elif self.subtensor: + event_loop = self.subtensor.event_loop + else: + event_loop = None + execute_coroutine( + self._async_metagraph.sync( + block=block, + lite=lite, + subtensor=subtensor.async_subtensor if subtensor else None, + ), + event_loop=event_loop, + ) + + def __getattr__(self, name): + attr = getattr(self._async_metagraph, name) + if callable(attr): + if asyncio.iscoroutinefunction(attr): + + def wrapper(*args, **kwargs): + return execute_coroutine( + attr(*args, **kwargs), + event_loop=self.subtensor.event_loop + if self.subtensor + else None, + ) + + return wrapper + return attr diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 1e19c266d9..87760952ce 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1,139 +1,42 @@ -""" -The ``bittensor.core.subtensor.Subtensor`` module in Bittensor serves as a crucial interface for interacting with the -Bittensor blockchain, facilitating a range of operations essential for the decentralized machine learning network. -""" - -import argparse -import copy -import ssl -from typing import Union, Optional, TypedDict, Any +from functools import lru_cache +from typing import TYPE_CHECKING, Any, Iterable, Optional, Union import numpy as np -import scalecodec -from bittensor_wallet import Wallet from numpy.typing import NDArray -from scalecodec.base import RuntimeConfiguration -from scalecodec.exceptions import RemainingScaleBytesNotEmptyException -from scalecodec.type_registry import load_type_registry_preset -from scalecodec.types import ScaleType -from substrateinterface.base import QueryMapResult, SubstrateInterface -from websockets.exceptions import InvalidStatus -from websockets.sync import client as ws_client - -from bittensor.core import settings -from bittensor.core.axon import Axon -from bittensor.core.chain_data import ( - custom_rpc_type_registry, - DelegateInfo, - NeuronInfo, - NeuronInfoLite, - PrometheusInfo, - SubnetHyperparameters, - SubnetInfo, -) -from bittensor.core.config import Config -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.registration import ( - burned_register_extrinsic, - register_extrinsic, -) -from bittensor.core.extrinsics.root import ( - root_register_extrinsic, - set_root_weights_extrinsic, -) -from bittensor.core.extrinsics.serving import ( - do_serve_axon, - serve_axon_extrinsic, - publish_metadata, - get_metadata, -) -from bittensor.core.extrinsics.set_weights import set_weights_extrinsic -from bittensor.core.extrinsics.staking import ( - add_stake_extrinsic, - add_stake_multiple_extrinsic, -) -from bittensor.core.extrinsics.transfer import ( - transfer_extrinsic, -) -from bittensor.core.extrinsics.unstaking import ( - unstake_extrinsic, - unstake_multiple_extrinsic, -) -from bittensor.core.metagraph import Metagraph -from bittensor.utils import ( - networking, - torch, - ss58_to_vec_u8, - u16_normalized_float, - hex_to_bytes, - Certificate, -) -from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging -from bittensor.utils.registration import legacy_torch_api_compat -from bittensor.utils.weight_utils import generate_weight_hash - -KEY_NONCE: dict[str, int] = {} - -class ParamWithTypes(TypedDict): - name: str # Name of the parameter. - type: str # ScaleType string of the parameter. +from bittensor.core.async_subtensor import AsyncSubtensor +from bittensor.core.metagraph import Metagraph +from bittensor.core.settings import version_as_int +from bittensor.utils.substrate_interface import SubstrateInterface +from bittensor.utils import execute_coroutine, torch, get_event_loop + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import ProposalVoteData + from bittensor.core.axon import Axon + from bittensor.core.config import Config + from bittensor.core.chain_data.delegate_info import DelegateInfo + from bittensor.core.chain_data.neuron_info import NeuronInfo + from bittensor.core.chain_data.neuron_info_lite import NeuronInfoLite + from bittensor.core.chain_data.stake_info import StakeInfo + from bittensor.core.chain_data.subnet_hyperparameters import SubnetHyperparameters + from bittensor.core.chain_data.subnet_info import SubnetInfo + from bittensor.utils.balance import Balance + from bittensor.utils import Certificate + from bittensor.utils.substrate_interface import QueryMapResult + from bittensor.utils.delegates_details import DelegatesDetails + from scalecodec.types import ScaleType class Subtensor: - """ - The Subtensor class in Bittensor serves as a crucial interface for interacting with the Bittensor blockchain, - facilitating a range of operations essential for the decentralized machine learning network. - - This class enables neurons (network participants) to engage in activities such as registering on the network, - managing staked weights, setting inter-neuronal weights, and participating in consensus mechanisms. - - The Bittensor network operates on a digital ledger where each neuron holds stakes (S) and learns a set - of inter-peer weights (W). These weights, set by the neurons themselves, play a critical role in determining - the ranking and incentive mechanisms within the network. Higher-ranked neurons, as determined by their - contributions and trust within the network, receive more incentives. - - The Subtensor class connects to various Bittensor networks like the main ``finney`` network or local test - networks, providing a gateway to the blockchain layer of Bittensor. It leverages a staked weighted trust - system and consensus to ensure fair and distributed incentive mechanisms, where incentives (I) are - primarily allocated to neurons that are trusted by the majority of the network. - - Additionally, Bittensor introduces a speculation-based reward mechanism in the form of bonds (B), allowing - neurons to accumulate bonds in other neurons, speculating on their future value. This mechanism aligns - with market-based speculation, incentivizing neurons to make judicious decisions in their inter-neuronal - investments. - - Example Usage:: - - from bittensor.core.subtensor import Subtensor - - # Connect to the main Bittensor network (Finney). - finney_subtensor = Subtensor(network='finney') - - # Close websocket connection with the Bittensor network. - finney_subtensor.close() - - # Register a new neuron on the network. - wallet = bittensor_wallet.Wallet(...) # Assuming a wallet instance is created. - netuid = 1 - success = finney_subtensor.register(wallet=wallet, netuid=netuid) - - # Set inter-neuronal weights for collaborative learning. - success = finney_subtensor.set_weights(wallet=wallet, netuid=netuid, uids=[...], weights=[...]) - - # Get the metagraph for a specific subnet using given subtensor connection - metagraph = finney_subtensor.metagraph(netuid=netuid) - - By facilitating these operations, the Subtensor class is instrumental in maintaining the decentralized - intelligence and dynamic learning environment of the Bittensor network, as envisioned in its foundational - principles and mechanisms described in the `NeurIPS paper - `_. paper. - """ + # get static methods from AsyncSubtensor + config = AsyncSubtensor.config + setup_config = AsyncSubtensor.setup_config + help = AsyncSubtensor.help + add_args = AsyncSubtensor.add_args + determine_chain_endpoint_and_network = ( + AsyncSubtensor.determine_chain_endpoint_and_network + ) def __init__( self, @@ -141,444 +44,47 @@ def __init__( config: Optional["Config"] = None, _mock: bool = False, log_verbose: bool = False, - connection_timeout: int = 600, - websocket: Optional[ws_client.ClientConnection] = None, - ) -> None: - """ - Initializes a Subtensor interface for interacting with the Bittensor blockchain. - - NOTE: - Currently subtensor defaults to the ``finney`` network. This will change in a future release. - - We strongly encourage users to run their own local subtensor node whenever possible. This increases decentralization and resilience of the network. In a future release, local subtensor will become the default and the fallback to ``finney`` removed. Please plan ahead for this change. We will provide detailed instructions on how to run a local subtensor node in the documentation in a subsequent release. - - Args: - network (Optional[str]): The network name to connect to (e.g., ``finney``, ``local``). This can also be the chain endpoint (e.g., ``wss://entrypoint-finney.opentensor.ai:443``) and will be correctly parsed into the network and chain endpoint. If not specified, defaults to the main Bittensor network. - config (Optional[bittensor.core.config.Config]): Configuration object for the subtensor. If not provided, a default configuration is used. - _mock (bool): If set to ``True``, uses a mocked connection for testing purposes. Default is ``False``. - log_verbose (bool): Whether to enable verbose logging. If set to ``True``, detailed log information about the connection and network operations will be provided. Default is ``True``. - connection_timeout (int): The maximum time in seconds to keep the connection alive. Default is ``600``. - websocket (websockets.sync.client.ClientConnection): websockets sync (threading) client object connected to the network. - - This initialization sets up the connection to the specified Bittensor network, allowing for various blockchain operations such as neuron registration, stake management, and setting weights. - """ - # Determine config.subtensor.chain_endpoint and config.subtensor.network config. - # If chain_endpoint is set, we override the network flag, otherwise, the chain_endpoint is assigned by the - # network. - # Argument importance: network > chain_endpoint > config.subtensor.chain_endpoint > config.subtensor.network - - if config is None: - config = Subtensor.config() - self._config = copy.deepcopy(config) - - # Setup config.subtensor.network and config.subtensor.chain_endpoint - self.chain_endpoint, self.network = Subtensor.setup_config( - network, self._config - ) - - if ( - self.network == "finney" - or self.chain_endpoint == settings.FINNEY_ENTRYPOINT - ) and log_verbose: - logging.info( - f"You are connecting to {self.network} network with endpoint {self.chain_endpoint}." - ) - logging.debug( - "We strongly encourage running a local subtensor node whenever possible. " - "This increases decentralization and resilience of the network." - ) - logging.debug( - "In a future release, local subtensor will become the default endpoint. " - "To get ahead of this change, please run a local subtensor node and point to it." - ) - + ): + self.event_loop = get_event_loop() + self.network = network + self._config = config self.log_verbose = log_verbose - self._connection_timeout = connection_timeout - self.substrate: "SubstrateInterface" = None - self.websocket = websocket - self._get_substrate() - - def __str__(self) -> str: - if self.network == self.chain_endpoint: - # Connecting to chain endpoint without network known. - return f"subtensor({self.chain_endpoint})" - else: - # Connecting to network with endpoint known. - return f"subtensor({self.network}, {self.chain_endpoint})" - - def __repr__(self) -> str: - return self.__str__() - - def close(self): - """Cleans up resources for this subtensor instance like active websocket connection and active extensions.""" - if self.substrate: - self.substrate.close() - - def _get_substrate(self, force: bool = False): - """ - Establishes a connection to the Substrate node using configured parameters. - - Args: - force: forces a reconnection if this flag is set - - """ - try: - # Set up params. - if force and self.websocket: - logging.debug("Closing websocket connection") - self.websocket.close() - - if force or self.websocket is None or self.websocket.close_code is not None: - self.websocket = ws_client.connect( - self.chain_endpoint, - open_timeout=self._connection_timeout, - max_size=2**32, - ) - - self.substrate = SubstrateInterface( - ss58_format=settings.SS58_FORMAT, - use_remote_preset=True, - type_registry=settings.TYPE_REGISTRY, - websocket=self.websocket, - ) - if self.log_verbose: - logging.info( - f"Connected to {self.network} network and {self.chain_endpoint}." - ) - - except ConnectionRefusedError as error: - logging.critical( - f"[red]Could not connect to[/red] [blue]{self.network}[/blue] [red]network with[/red] [blue]{self.chain_endpoint}[/blue] [red]chain endpoint.[/red]", - ) - raise ConnectionRefusedError(error.args) - - except ssl.SSLError as error: - logging.critical( - "SSL error occurred. To resolve this issue, run the following command in your terminal:" - ) - logging.critical("[blue]sudo python -m bittensor certifi[/blue]") - raise RuntimeError( - "SSL configuration issue, please follow the instructions above." - ) from error - - except InvalidStatus as error: - logging.critical( - f"Error [red]'{error.response.reason_phrase}'[/red] with status code [red]{error.response.status_code}[/red]." - ) - logging.debug(f"Server response is '{error.response}'.") - raise - - @staticmethod - def config() -> "Config": - """ - Creates and returns a Bittensor configuration object. - - Returns: - config (bittensor.core.config.Config): A Bittensor configuration object configured with arguments added by the `subtensor.add_args` method. - """ - parser = argparse.ArgumentParser() - Subtensor.add_args(parser) - return Config(parser) - - @staticmethod - def setup_config(network: Optional[str], config: "Config"): - """ - Sets up and returns the configuration for the Subtensor network and endpoint. - - This method determines the appropriate network and chain endpoint based on the provided network string or - configuration object. It evaluates the network and endpoint in the following order of precedence: - 1. Provided network string. - 2. Configured chain endpoint in the `config` object. - 3. Configured network in the `config` object. - 4. Default chain endpoint. - 5. Default network. - - Args: - network (Optional[str]): The name of the Subtensor network. If None, the network and endpoint will be determined from the `config` object. - config (bittensor.core.config.Config): The configuration object containing the network and chain endpoint settings. - - Returns: - tuple: A tuple containing the formatted WebSocket endpoint URL and the evaluated network name. - """ - if network is not None: - ( - evaluated_network, - evaluated_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network(network) - else: - if config.is_set("subtensor.chain_endpoint"): - ( - evaluated_network, - evaluated_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network( - config.subtensor.chain_endpoint - ) - - elif config.is_set("subtensor.network"): - ( - evaluated_network, - evaluated_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network( - config.subtensor.network - ) - - elif config.subtensor.get("chain_endpoint"): - ( - evaluated_network, - evaluated_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network( - config.subtensor.chain_endpoint - ) - - elif config.subtensor.get("network"): - ( - evaluated_network, - evaluated_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network( - config.subtensor.network - ) - - else: - ( - evaluated_network, - evaluated_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network( - settings.DEFAULTS.subtensor.network - ) - - return ( - networking.get_formatted_ws_endpoint_url(evaluated_endpoint), - evaluated_network, - ) - - @classmethod - def help(cls): - """Print help to stdout.""" - parser = argparse.ArgumentParser() - cls.add_args(parser) - print(cls.__new__.__doc__) - parser.print_help() - - @classmethod - def add_args(cls, parser: "argparse.ArgumentParser", prefix: Optional[str] = None): - """ - Adds command-line arguments to the provided ArgumentParser for configuring the Subtensor settings. - - Args: - parser (argparse.ArgumentParser): The ArgumentParser object to which the Subtensor arguments will be added. - prefix (Optional[str]): An optional prefix for the argument names. If provided, the prefix is prepended to each argument name. - - Arguments added: - --subtensor.network: The Subtensor network flag. Possible values are 'finney', 'test', 'archive', and 'local'. Overrides the chain endpoint if set. - --subtensor.chain_endpoint: The Subtensor chain endpoint flag. If set, it overrides the network flag. - --subtensor._mock: If true, uses a mocked connection to the chain. - - Example: - parser = argparse.ArgumentParser() - Subtensor.add_args(parser) - """ - prefix_str = "" if prefix is None else f"{prefix}." - try: - default_network = settings.DEFAULT_NETWORK - default_chain_endpoint = settings.FINNEY_ENTRYPOINT - - parser.add_argument( - f"--{prefix_str}subtensor.network", - default=default_network, - type=str, - help="""The subtensor network flag. The likely choices are: - -- finney (main network) - -- test (test network) - -- archive (archive network +300 blocks) - -- local (local running network) - If this option is set it overloads subtensor.chain_endpoint with - an entry point node from that network. - """, - ) - parser.add_argument( - f"--{prefix_str}subtensor.chain_endpoint", - default=default_chain_endpoint, - type=str, - help="""The subtensor endpoint flag. If set, overrides the --network flag.""", - ) - parser.add_argument( - f"--{prefix_str}subtensor._mock", - default=False, - type=bool, - help="""If true, uses a mocked connection to the chain.""", - ) - - except argparse.ArgumentError: - # re-parsing arguments. - pass - - # Inner private functions - def _encode_params( - self, - call_definition: dict[str, list["ParamWithTypes"]], - params: Union[list[Any], dict[str, Any]], - ) -> str: - """Returns a hex encoded string of the params using their types.""" - param_data = scalecodec.ScaleBytes(b"") - - for i, param in enumerate(call_definition["params"]): - scale_obj = self.substrate.create_scale_object(param["type"]) - if isinstance(params, list): - param_data += scale_obj.encode(params[i]) - else: - if param["name"] not in params: - raise ValueError(f"Missing param {param['name']} in params dict.") - - param_data += scale_obj.encode(params[param["name"]]) - - return param_data.to_hex() - - def _get_hyperparameter( - self, param_name: str, netuid: int, block: Optional[int] = None - ) -> Optional[Any]: - """ - Retrieves a specified hyperparameter for a specific subnet. - - Args: - param_name (str): The name of the hyperparameter to retrieve. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[Union[int, float]]: The value of the specified hyperparameter if the subnet exists, ``None`` otherwise. - """ - if not self.subnet_exists(netuid, block): - return None - - result = self.query_subtensor(param_name, block, [netuid]) - if result is None or not hasattr(result, "value"): - return None - - return result.value - - # Chain calls methods ============================================================================================== - - def query_subtensor( - self, name: str, block: Optional[int] = None, params: Optional[list] = None - ) -> "ScaleType": - """ - Queries named storage from the Subtensor module on the Bittensor blockchain. This function is used to retrieve specific data or parameters from the blockchain, such as stake, rank, or other neuron-specific attributes. - - Args: - name (str): The name of the storage function to query. - block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[list[object]]): A list of parameters to pass to the query function. - - Returns: - query_response (scalecodec.ScaleType): An object containing the requested data. - - This query function is essential for accessing detailed information about the network and its neurons, providing valuable insights into the state and dynamics of the Bittensor ecosystem. - """ - - return self.substrate.query( - module="SubtensorModule", - storage_function=name, - params=params, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - def query_map_subtensor( - self, name: str, block: Optional[int] = None, params: Optional[list] = None - ) -> "QueryMapResult": - """ - Queries map storage from the Subtensor module on the Bittensor blockchain. This function is designed to retrieve a map-like data structure, which can include various neuron-specific details or network-wide attributes. - - Args: - name (str): The name of the map storage function to query. - block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[list[object]]): A list of parameters to pass to the query function. - - Returns: - QueryMapResult (substrateinterface.base.QueryMapResult): An object containing the map-like data structure, or ``None`` if not found. - - This function is particularly useful for analyzing and understanding complex network structures and relationships within the Bittensor ecosystem, such as inter-neuronal connections and stake distributions. - """ - return self.substrate.query_map( - module="SubtensorModule", - storage_function=name, - params=params, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), + self.async_subtensor = AsyncSubtensor( + network=network, + config=config, + log_verbose=log_verbose, + event_loop=self.event_loop, + _mock=_mock, ) - def query_runtime_api( - self, - runtime_api: str, - method: str, - params: Optional[Union[list[int], dict[str, int]]] = None, - block: Optional[int] = None, - ) -> Optional[str]: - """ - Queries the runtime API of the Bittensor blockchain, providing a way to interact with the underlying runtime and retrieve data encoded in Scale Bytes format. This function is essential for advanced users who need to interact with specific runtime methods and decode complex data types. - - Args: - runtime_api (str): The name of the runtime API to query. - method (str): The specific method within the runtime API to call. - params (Optional[list[ParamWithTypes]]): The parameters to pass to the method call. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - Optional[str]: The Scale Bytes encoded result from the runtime API call, or ``None`` if the call fails. - - This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed and specific interactions with the network's runtime environment. - """ - call_definition = settings.TYPE_REGISTRY["runtime_api"][runtime_api]["methods"][ - method - ] - - json_result = self.state_call( - method=f"{runtime_api}_{method}", - data=( - "0x" - if params is None - else self._encode_params(call_definition=call_definition, params=params) - ), - block=block, + self.substrate = SubstrateInterface( + url=self.async_subtensor.chain_endpoint, + _mock=_mock, + substrate=self.async_subtensor.substrate, ) + self.chain_endpoint = self.async_subtensor.chain_endpoint - if json_result is None: - return None + def __str__(self): + return self.async_subtensor.__str__() - return_type = call_definition["type"] - as_scale_bytes = scalecodec.ScaleBytes(json_result["result"]) + def __repr__(self): + return self.async_subtensor.__repr__() - rpc_runtime_config = RuntimeConfiguration() - rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) - rpc_runtime_config.update_type_registry(custom_rpc_type_registry) - obj = rpc_runtime_config.create_scale_object(return_type, as_scale_bytes) - if obj.data.to_hex() == "0x0400": # RPC returned None result - return None - - return obj.decode() - - def state_call( - self, method: str, data: str, block: Optional[int] = None - ) -> dict[Any, Any]: - """ - Makes a state call to the Bittensor blockchain, allowing for direct queries of the blockchain's state. This function is typically used for advanced queries that require specific method calls and data inputs. + def execute_coroutine(self, coroutine) -> Any: + return execute_coroutine(coroutine, self.event_loop) - Args: - method (str): The method name for the state call. - data (str): The data to be passed to the method. - block (Optional[int]): The blockchain block number at which to perform the state call. + def close(self): + execute_coroutine(self.async_subtensor.close()) - Returns: - result (dict[Any, Any]): The result of the rpc call. + # Subtensor queries =========================================================================================== - The state call function provides a more direct and flexible way of querying blockchain data, useful for specific use cases where standard queries are insufficient. - """ - block_hash = None if block is None else self.substrate.get_block_hash(block) - return self.substrate.rpc_request( - method="state_call", - params=[method, data, block_hash] if block_hash else [method, data], + def query_constant( + self, module_name: str, constant_name: str, block: Optional[int] = None + ) -> Optional["ScaleType"]: + return self.execute_coroutine( + self.async_subtensor.query_constant( + module_name=module_name, constant_name=constant_name, block=block + ) ) def query_map( @@ -588,51 +94,19 @@ def query_map( block: Optional[int] = None, params: Optional[list] = None, ) -> "QueryMapResult": - """ - Queries map storage from any module on the Bittensor blockchain. This function retrieves data structures that represent key-value mappings, essential for accessing complex and structured data within the blockchain modules. - - Args: - module (str): The name of the module from which to query the map storage. - name (str): The specific storage function within the module to query. - block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[list[object]]): Parameters to be passed to the query. - - Returns: - result (substrateinterface.base.QueryMapResult): A data structure representing the map storage if found, ``None`` otherwise. - - This function is particularly useful for retrieving detailed and structured data from various blockchain modules, offering insights into the network's state and the relationships between its different components. - """ - return self.substrate.query_map( - module=module, - storage_function=name, - params=params, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), + return self.execute_coroutine( + self.async_subtensor.query_map( + module=module, name=name, block=block, params=params + ) ) - def query_constant( - self, module_name: str, constant_name: str, block: Optional[int] = None - ) -> Optional["ScaleType"]: - """ - Retrieves a constant from the specified module on the Bittensor blockchain. This function is used to access fixed parameters or values defined within the blockchain's modules, which are essential for understanding the network's configuration and rules. - - Args: - module_name (str): The name of the module containing the constant. - constant_name (str): The name of the constant to retrieve. - block (Optional[int]): The blockchain block number at which to query the constant. - - Returns: - Optional[scalecodec.ScaleType]: The value of the constant if found, ``None`` otherwise. - - Constants queried through this function can include critical network parameters such as inflation rates, consensus rules, or validation thresholds, providing a deeper understanding of the Bittensor network's operational parameters. - """ - return self.substrate.get_constant( - module_name=module_name, - constant_name=constant_name, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), + def query_map_subtensor( + self, name: str, block: Optional[int] = None, params: Optional[list] = None + ) -> "QueryMapResult": + return self.execute_coroutine( + self.async_subtensor.query_map_subtensor( + name=name, block=block, params=params + ) ) def query_module( @@ -642,1279 +116,544 @@ def query_module( block: Optional[int] = None, params: Optional[list] = None, ) -> "ScaleType": - """ - Queries any module storage on the Bittensor blockchain with the specified parameters and block number. This function is a generic query interface that allows for flexible and diverse data retrieval from various blockchain modules. - - Args: - module (str): The name of the module from which to query data. - name (str): The name of the storage function within the module. - block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[list[object]]): A list of parameters to pass to the query function. - - Returns: - Optional[scalecodec.ScaleType]: An object containing the requested data if found, ``None`` otherwise. - - This versatile query function is key to accessing a wide range of data and insights from different parts of the Bittensor blockchain, enhancing the understanding and analysis of the network's state and dynamics. - """ - return self.substrate.query( - module=module, - storage_function=name, - params=params, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - def get_account_next_index(self, address: str) -> int: - """ - Returns the next nonce for an account, taking into account the transaction pool. - """ - if not self.substrate.supports_rpc_method("account_nextIndex"): - raise Exception("account_nextIndex not supported") - - return self.substrate.rpc_request("account_nextIndex", [address])["result"] - - # Common subtensor methods ========================================================================================= - def metagraph( - self, netuid: int, lite: bool = True, block: Optional[int] = None - ) -> "Metagraph": # type: ignore - """ - Returns a synced metagraph for a specified subnet within the Bittensor network. The metagraph represents the network's structure, including neuron connections and interactions. - - Args: - netuid (int): The network UID of the subnet to query. - lite (bool): If true, returns a metagraph using a lightweight sync (no weights, no bonds). Default is ``True``. - block (Optional[int]): Block number for synchronization, or ``None`` for the latest block. - - Returns: - bittensor.core.metagraph.Metagraph: 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, - ) - metagraph.sync(block=block, lite=lite, subtensor=self) - - return metagraph - - @staticmethod - def determine_chain_endpoint_and_network( - network: str, - ) -> tuple[Optional[str], Optional[str]]: - """Determines the chain endpoint and network from the passed network or chain_endpoint. - - Args: - network (str): The network flag. The choices are: ``finney`` (main network), ``archive`` (archive network +300 blocks), ``local`` (local running network), ``test`` (test network). - - Returns: - tuple[Optional[str], Optional[str]]: The network and chain endpoint flag. If passed, overrides the ``network`` argument. - """ - - if network is None: - return None, None - if network in settings.NETWORKS: - return network, settings.NETWORK_MAP[network] - else: - if ( - network == settings.FINNEY_ENTRYPOINT - or "entrypoint-finney.opentensor.ai" in network - ): - return "finney", settings.FINNEY_ENTRYPOINT - elif ( - network == settings.FINNEY_TEST_ENTRYPOINT - or "test.finney.opentensor.ai" in network - ): - return "test", settings.FINNEY_TEST_ENTRYPOINT - elif ( - network == settings.ARCHIVE_ENTRYPOINT - or "archive.chain.opentensor.ai" in network - ): - return "archive", settings.ARCHIVE_ENTRYPOINT - elif "127.0.0.1" in network or "localhost" in network: - return "local", network - else: - return "unknown", network - - def get_netuids_for_hotkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> list[int]: - """ - Retrieves a list of subnet UIDs (netuids) for which a given hotkey is a member. This function identifies the specific subnets within the Bittensor network where the neuron associated with the hotkey is active. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - list[int]: A list of netuids where the neuron is a member. - """ - result = self.query_map_subtensor("IsNetworkMember", block, [hotkey_ss58]) - return ( - [record[0].value for record in result if record[1]] - if result and hasattr(result, "records") - else [] + return self.execute_coroutine( + self.async_subtensor.query_module( + module=module, + name=name, + block=block, + params=params, + ) ) - def get_current_block(self) -> int: - """ - Returns the current block number on the Bittensor blockchain. This function provides the latest block number, indicating the most recent state of the blockchain. - - Returns: - int: The current chain block number. - - Knowing the current block number is essential for querying real-time data and performing time-sensitive operations on the blockchain. It serves as a reference point for network activities and data synchronization. - """ - return self.substrate.get_block_number(None) # type: ignore - - def is_hotkey_registered_any( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> bool: - """ - Checks if a neuron's hotkey is registered on any subnet within the Bittensor network. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int]): The blockchain block number at which to perform the check. - - Returns: - bool: ``True`` if the hotkey is registered on any subnet, False otherwise. - - This function is essential for determining the network-wide presence and participation of a neuron. - """ - return len(self.get_netuids_for_hotkey(hotkey_ss58, block)) > 0 - - def is_hotkey_registered_on_subnet( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> bool: - """ - Checks if a neuron's hotkey is registered on a specific subnet within the Bittensor network. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number at which to perform the check. - - Returns: - bool: ``True`` if the hotkey is registered on the specified subnet, False otherwise. - - This function helps in assessing the participation of a neuron in a particular subnet, indicating its specific area of operation or influence within the network. - """ - return self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block) is not None - - def is_hotkey_registered( + def query_runtime_api( self, - hotkey_ss58: str, - netuid: Optional[int] = None, + runtime_api: str, + method: str, + params: Optional[Union[list[int], dict[str, int]]] = None, block: Optional[int] = None, - ) -> bool: - """ - Determines whether a given hotkey (public key) is registered in the Bittensor network, either globally across any subnet or specifically on a specified subnet. This function checks the registration status of a neuron identified by its hotkey, which is crucial for validating its participation and activities within the network. + ) -> Optional[str]: + return self.execute_coroutine( + coroutine=self.async_subtensor.query_runtime_api( + runtime_api=runtime_api, + method=method, + params=params, + block=block, + ) + ) - Args: - hotkey_ss58 (str): The SS58 address of the neuron's hotkey. - netuid (Optional[int]): The unique identifier of the subnet to check the registration. If ``None``, the registration is checked across all subnets. - block (Optional[int]): The blockchain block number at which to perform the query. + def query_subtensor( + self, name: str, block: Optional[int] = None, params: Optional[list] = None + ) -> "ScaleType": + return self.execute_coroutine( + self.async_subtensor.query_subtensor(name=name, block=block, params=params) + ) - Returns: - bool: ``True`` if the hotkey is registered in the specified context (either any subnet or a specific subnet), ``False`` otherwise. + def state_call( + self, method: str, data: str, block: Optional[int] = None + ) -> dict[Any, Any]: + return self.execute_coroutine( + self.async_subtensor.state_call(method=method, data=data, block=block) + ) - This function is important for verifying the active status of neurons in the Bittensor network. It aids in understanding whether a neuron is eligible to participate in network processes such as consensus, validation, and incentive distribution based on its registration status. - """ - if netuid is None: - return self.is_hotkey_registered_any(hotkey_ss58, block) - else: - return self.is_hotkey_registered_on_subnet(hotkey_ss58, netuid, block) + # Common subtensor calls =========================================================================================== - # metagraph @property def block(self) -> int: - """Returns current chain block. - - Returns: - block (int): Current chain block. - """ return self.get_current_block() def blocks_since_last_update(self, netuid: int, uid: int) -> Optional[int]: - """ - Returns the number of blocks since the last update for a specific UID in the subnetwork. - - Args: - netuid (int): The unique identifier of the subnetwork. - uid (int): The unique identifier of the neuron. - - Returns: - Optional[int]: The number of blocks since the last update, or ``None`` if the subnetwork or UID does not exist. - """ - call = self._get_hyperparameter(param_name="LastUpdate", netuid=netuid) - return None if call is None else self.get_current_block() - int(call[uid]) - - def get_block_hash(self, block_id: int) -> str: - """ - Retrieves the hash of a specific block on the Bittensor blockchain. The block hash is a unique identifier representing the cryptographic hash of the block's content, ensuring its integrity and immutability. - - Args: - block_id (int): The block number for which the hash is to be retrieved. - - Returns: - str: The cryptographic hash of the specified block. - - The block hash is a fundamental aspect of blockchain technology, providing a secure reference to each block's data. It is crucial for verifying transactions, ensuring data consistency, and maintaining the trustworthiness of the blockchain. - """ - return self.substrate.get_block_hash(block_id=block_id) - - def weights_rate_limit(self, netuid: int) -> Optional[int]: - """ - Returns network WeightsSetRateLimit hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - - Returns: - Optional[int]: The value of the WeightsSetRateLimit hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. - """ - call = self._get_hyperparameter(param_name="WeightsSetRateLimit", netuid=netuid) - return None if call is None else int(call) - - def commit(self, wallet, netuid: int, data: str): - """ - Commits arbitrary data to the Bittensor network by publishing metadata. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the data. - netuid (int): The unique identifier of the subnetwork. - data (str): The data to be committed to the network. - """ - publish_metadata(self, wallet, netuid, f"Raw{len(data)}", data.encode()) - - def subnetwork_n(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """ - Returns network SubnetworkN hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int]): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the SubnetworkN hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="SubnetworkN", netuid=netuid, block=block + return self.execute_coroutine( + self.async_subtensor.blocks_since_last_update(netuid=netuid, uid=uid) ) - return None if call is None else int(call) - def get_neuron_for_pubkey_and_subnet( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> Optional["NeuronInfo"]: - """ - Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific subnet UID (netuid). This function provides detailed neuron information for a particular subnet within the Bittensor network. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - Optional[bittensor.core.chain_data.neuron_info.NeuronInfo]: Detailed information about the neuron if found, ``None`` otherwise. - - This function is crucial for accessing specific neuron data and understanding its status, stake, and other attributes within a particular subnet of the Bittensor ecosystem. - """ - return self.neuron_for_uid( - self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block=block), - netuid, - block=block, + def bonds( + self, netuid: int, block: Optional[int] = None + ) -> list[tuple[int, list[tuple[int, int]]]]: + return self.execute_coroutine( + self.async_subtensor.bonds(netuid=netuid, block=block), ) - def get_neuron_certificate( - self, hotkey: str, netuid: int, block: Optional[int] = None - ) -> Optional["Certificate"]: - """ - Retrieves the TLS certificate for a specific neuron identified by its unique identifier (UID) - within a specified subnet (netuid) of the Bittensor network. - - Args: - hotkey (str): The hotkey to query. - netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - Optional[Certificate]: the certificate of the neuron if found, ``None`` otherwise. - - This function is used for certificate discovery for setting up mutual tls communication between neurons - """ - - certificate = self.query_module( - module="SubtensorModule", - name="NeuronCertificates", - block=block, - params=[netuid, hotkey], - ) - try: - serialized_certificate = certificate.serialize() - if serialized_certificate: - return ( - chr(serialized_certificate["algorithm"]) - + serialized_certificate["public_key"] - ) - except AttributeError: - return None - return None - - def neuron_for_uid( - self, uid: int, netuid: int, block: Optional[int] = None - ) -> "NeuronInfo": - """ - Retrieves detailed information about a specific neuron identified by its unique identifier (UID) within a specified subnet (netuid) of the Bittensor network. This function provides a comprehensive view of a neuron's attributes, including its stake, rank, and operational status. - - Args: - uid (int): The unique identifier of the neuron. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - bittensor.core.chain_data.neuron_info.NeuronInfo: Detailed information about the neuron if found, ``None`` otherwise. - - This function is crucial for analyzing individual neurons' contributions and status within a specific subnet, offering insights into their roles in the network's consensus and validation mechanisms. - """ - if uid is None: - return NeuronInfo.get_null_neuron() - - block_hash = None if block is None else self.substrate.get_block_hash(block) - params = [netuid, uid] - if block_hash: - params = params + [block_hash] - - json_body = self.substrate.rpc_request( - method="neuronInfo_getNeuron", - params=params, # custom rpc method + def commit(self, wallet, netuid: int, data: str) -> bool: + return self.execute_coroutine( + self.async_subtensor.commit(wallet=wallet, netuid=netuid, data=data) ) - if not (result := json_body.get("result", None)): - return NeuronInfo.get_null_neuron() - - return NeuronInfo.from_vec_u8(result) - - def get_subnet_hyperparameters( + def commit_reveal_enabled( self, netuid: int, block: Optional[int] = None - ) -> Optional[Union[list, "SubnetHyperparameters"]]: - """ - Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters define the operational settings and rules governing the subnet's behavior. - - Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[bittensor.core.chain_data.subnet_hyperparameters.SubnetHyperparameters]: The subnet's hyperparameters, or ``None`` if not available. - - Understanding the hyperparameters is crucial for comprehending how subnets are configured and managed, and how they interact with the network's consensus and incentive mechanisms. - """ - hex_bytes_result = self.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], - block=block, + ) -> Optional[bool]: + return self.execute_coroutine( + self.async_subtensor.commit_reveal_enabled(netuid=netuid, block=block) ) - if hex_bytes_result is None: - return [] - - return SubnetHyperparameters.from_vec_u8(hex_to_bytes(hex_bytes_result)) - - def immunity_period( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Retrieves the 'ImmunityPeriod' hyperparameter for a specific subnet. This parameter defines the duration during which new neurons are protected from certain network penalties or restrictions. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[int]: The value of the 'ImmunityPeriod' hyperparameter if the subnet exists, ``None`` otherwise. - - The 'ImmunityPeriod' is a critical aspect of the network's governance system, ensuring that new participants have a grace period to establish themselves and contribute to the network without facing immediate punitive actions. - """ - call = self._get_hyperparameter( - param_name="ImmunityPeriod", netuid=netuid, block=block + def difficulty(self, netuid: int, block: Optional[int] = None) -> Optional[int]: + return self.execute_coroutine( + self.async_subtensor.difficulty(netuid=netuid, block=block), ) - return None if call is None else int(call) - - def get_uid_for_hotkey_on_subnet( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Retrieves the unique identifier (UID) for a neuron's hotkey on a specific subnet. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[int]: The UID of the neuron if it is registered on the subnet, ``None`` otherwise. - - The UID is a critical identifier within the network, linking the neuron's hotkey to its operational and governance activities on a particular subnet. - """ - _result = self.query_subtensor("Uids", block, [netuid, hotkey_ss58]) - return getattr(_result, "value", None) - - def tempo(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """ - Returns network Tempo hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int]): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the Tempo hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. - """ - call = self._get_hyperparameter(param_name="Tempo", netuid=netuid, block=block) - return None if call is None else int(call) - def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> str: - """ - Retrieves the on-chain commitment for a specific neuron in the Bittensor network. - - Args: - netuid (int): The unique identifier of the subnetwork. - uid (int): The unique identifier of the neuron. - block (Optional[int]): The block number to retrieve the commitment from. If None, the latest block is used. Default is ``None``. - - Returns: - str: The commitment data as a string. - """ - metagraph = self.metagraph(netuid) - hotkey = metagraph.hotkeys[uid] # type: ignore - - metadata = get_metadata(self, netuid, hotkey, block) - try: - commitment = metadata["info"]["fields"][0] # type: ignore - hex_data = commitment[list(commitment.keys())[0]][2:] # type: ignore - return bytes.fromhex(hex_data).decode() - - except TypeError: - return "" - - def min_allowed_weights( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network MinAllowedWeights hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int]): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the MinAllowedWeights hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="MinAllowedWeights", block=block, netuid=netuid + def does_hotkey_exist(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: + return self.execute_coroutine( + self.async_subtensor.does_hotkey_exist(hotkey_ss58=hotkey_ss58, block=block) ) - return None if call is None else int(call) - def max_weight_limit( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Returns network MaxWeightsLimit hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int]): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. - - Returns: - Optional[float]: The value of the MaxWeightsLimit hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="MaxWeightsLimit", block=block, netuid=netuid + def get_all_subnets_info(self, block: Optional[int] = None) -> list["SubnetInfo"]: + return self.execute_coroutine( + self.async_subtensor.get_all_subnets_info(block=block), ) - return None if call is None else u16_normalized_float(int(call)) - - def commit_reveal_enabled( - self, netuid: int, block: Optional[int] = None - ) -> Optional[bool]: - """ - Check if commit-reveal mechanism is enabled for a given network at a specific block. - Arguments: - netuid (int): The network identifier for which to check the commit-reveal mechanism. - block (Optional[int]): The block number at which to check the parameter (default is None, which implies the current block). - - Returns: - (Optional[bool]): Returns the integer value of the hyperparameter if available; otherwise, returns None. - """ - call = self._get_hyperparameter( - param_name="CommitRevealWeightsEnabled", block=block, netuid=netuid + def get_balance(self, address: str, block: Optional[int] = None) -> "Balance": + return self.execute_coroutine( + self.async_subtensor.get_balance(address, block=block), ) - return True if call is True else False - def get_subnet_reveal_period_epochs( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """Retrieve the SubnetRevealPeriodEpochs hyperparameter.""" - return self._get_hyperparameter( - param_name="RevealPeriodEpochs", block=block, netuid=netuid - ) - - def get_prometheus_info( - self, netuid: int, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional["PrometheusInfo"]: - """ - Returns the prometheus information for this hotkey account. - - Args: - netuid (int): The unique identifier of the subnetwork. - hotkey_ss58 (str): The SS58 address of the hotkey. - block (Optional[int]): The block number to retrieve the prometheus information from. If ``None``, the latest block is used. Default is ``None``. - - Returns: - Optional[bittensor.core.chain_data.prometheus_info.PrometheusInfo]: A PrometheusInfo object containing the prometheus information, or ``None`` if the prometheus information is not found. - """ - result = self.query_subtensor("Prometheus", block, [netuid, hotkey_ss58]) - if result is not None and getattr(result, "value", None) is not None: - return PrometheusInfo( - ip=networking.int_to_ip(result.value["ip"]), - ip_type=result.value["ip_type"], - port=result.value["port"], - version=result.value["version"], - block=result.value["block"], - ) - return None - - def subnet_exists(self, netuid: int, block: Optional[int] = None) -> bool: - """ - Checks if a subnet with the specified unique identifier (netuid) exists within the Bittensor network. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number at which to check the subnet's existence. - - Returns: - bool: ``True`` if the subnet exists, False otherwise. - - This function is critical for verifying the presence of specific subnets in the network, enabling a deeper understanding of the network's structure and composition. - """ - _result = self.query_subtensor("NetworksAdded", block, [netuid]) - return getattr(_result, "value", False) - - 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 comprehensive data on each subnet, including its characteristics and operational parameters. - - Args: - block (Optional[int]): The blockchain block number for the query. - - Returns: - list[SubnetInfo]: A list of SubnetInfo objects, each containing detailed information about a subnet. - - Gaining insights into the subnets' details assists in understanding the network's composition, the roles of different subnets, and their unique features. - """ - hex_bytes_result = self.query_runtime_api( - "SubnetInfoRuntimeApi", "get_subnets_info", params=[], block=block + def get_balances( + self, + *addresses: str, + block: Optional[int] = None, + ) -> dict[str, "Balance"]: + return self.execute_coroutine( + self.async_subtensor.get_balances(*addresses, block=block), ) - if not hex_bytes_result: - return [] - else: - return SubnetInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) - - def bonds( - self, netuid: int, block: Optional[int] = None - ) -> list[tuple[int, list[tuple[int, int]]]]: - """ - Retrieves the bond distribution set by neurons within a specific subnet of the Bittensor network. Bonds represent the investments or commitments made by neurons in one another, indicating a level of trust and perceived value. This bonding mechanism is integral to the network's market-based approach to measuring and rewarding machine intelligence. - Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int]): The blockchain block number for the query. - - Returns: - list[tuple[int, list[tuple[int, int]]]]: A list of tuples mapping each neuron's UID to its bonds with other neurons. - - Understanding bond distributions is crucial for analyzing the trust dynamics and market behavior within the subnet. It reflects how neurons recognize and invest in each other's intelligence and contributions, supporting diverse and niche systems within the Bittensor ecosystem. - """ - b_map = [] - b_map_encoded = self.query_map_subtensor( - name="Bonds", block=block, params=[netuid] + def get_current_block(self) -> int: + return self.execute_coroutine( + coroutine=self.async_subtensor.get_current_block(), ) - if b_map_encoded.records: - for uid, b in b_map_encoded: - b_map.append((uid.serialize(), b.serialize())) - - return b_map - - def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[str]: - """ - Retrieves the burn cost for registering a new subnet within the Bittensor network. This cost represents the amount of Tao that needs to be locked or burned to establish a new subnet. - Args: - block (Optional[int]): The blockchain block number for the query. - - Returns: - int: The burn cost for subnet registration. - - The subnet burn cost is an important economic parameter, reflecting the network's mechanisms for controlling the proliferation of subnets and ensuring their commitment to the network's long-term viability. - """ - lock_cost = self.query_runtime_api( - runtime_api="SubnetRegistrationRuntimeApi", - method="get_network_registration_cost", - params=[], - block=block, + @lru_cache(maxsize=128) + def get_block_hash(self, block: Optional[int] = None) -> str: + return self.execute_coroutine( + coroutine=self.async_subtensor.get_block_hash(block=block), ) - if lock_cost is None: - return None - - return lock_cost - - def neurons(self, netuid: int, block: Optional[int] = None) -> list["NeuronInfo"]: - """ - Retrieves a list of all neurons within a specified subnet of the Bittensor network. This function provides a snapshot of the subnet's neuron population, including each neuron's attributes and network interactions. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - list[bittensor.core.chain_data.neuron_info.NeuronInfo]: A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. - - Understanding the distribution and status of neurons within a subnet is key to comprehending the network's decentralized structure and the dynamics of its consensus and governance processes. - """ - neurons_lite = self.neurons_lite(netuid=netuid, block=block) - weights = self.weights(block=block, netuid=netuid) - bonds = self.bonds(block=block, netuid=netuid) - - weights_as_dict = {uid: w for uid, w in weights} - bonds_as_dict = {uid: b for uid, b in bonds} - - neurons = [ - NeuronInfo.from_weights_bonds_and_neuron_lite( - neuron_lite, weights_as_dict, bonds_as_dict - ) - for neuron_lite in neurons_lite - ] - - return neurons - - def last_drand_round( - self, - ) -> Optional[int]: - """ - Retrieves the last drand round emitted in bittensor. This corresponds when committed weights will be revealed. + def get_children( + self, hotkey: str, netuid: int, block: Optional[int] = None + ) -> tuple[bool, list, str]: + return self.execute_coroutine( + self.async_subtensor.get_children( + hotkey=hotkey, netuid=netuid, block=block + ), + ) - Returns: - int: The latest Drand round emitted in bittensor. - """ - result = self.substrate.query( - module="Drand", storage_function="LastStoredRound" + def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> str: + return self.execute_coroutine( + self.async_subtensor.get_commitment(netuid=netuid, uid=uid, block=block), ) - return getattr(result, "value", None) def get_current_weight_commit_info( self, netuid: int, block: Optional[int] = None ) -> list: - """ - Retrieves CRV3 weight commit information for a specific subnet. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - list: A list of commit details, where each entry is a dictionary with keys 'who', - 'serialized_commit', and 'reveal_round', or an empty list if no data is found. - """ - result = self.query_map( - module="SubtensorModule", - name="CRV3WeightCommits", - params=[netuid], - block=block, + return self.execute_coroutine( + self.async_subtensor.get_current_weight_commit_info( + netuid=netuid, block=block + ), ) - return result.records[0][1].value if result and result.records else [] - - def get_total_stake_for_coldkey( - self, ss58_address: str, block: Optional[int] = None - ) -> Optional["Balance"]: - """Retrieves the total stake held by a coldkey across all associated hotkeys, including delegated stakes. - - Args: - ss58_address (str): The SS58 address of the coldkey account. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - Optional[Balance]: The total stake amount held by the coldkey, or None if the query fails. - """ - result = self.query_subtensor("TotalColdkeyStake", block, [ss58_address]) - if getattr(result, "value", None) is None: - return None - return Balance.from_rao(result.value) - - def get_total_stake_for_hotkey( - self, ss58_address: str, block: Optional[int] = None - ) -> Optional["Balance"]: - """Retrieves the total stake associated with a hotkey. - - Args: - ss58_address (str): The SS58 address of the hotkey account. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - Optional[Balance]: The total stake amount held by the hotkey, or None if the query fails. - """ - result = self.query_subtensor("TotalHotkeyStake", block, [ss58_address]) - if getattr(result, "value", None) is None: - return None - return Balance.from_rao(result.value) - - def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: - """ - Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. - - Args: - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[int]: The total number of subnets in the network. - - Understanding the total number of subnets is essential for assessing the network's growth and the extent of its decentralized infrastructure. - """ - _result = self.query_subtensor("TotalNetworks", block) - return getattr(_result, "value", None) - - def get_subnets(self, block: Optional[int] = None) -> list[int]: - """ - Retrieves a list of all subnets currently active within the Bittensor network. This function provides an overview of the various subnets and their identifiers. - Args: - block (Optional[int]): The blockchain block number for the query. - - Returns: - list[int]: A list of network UIDs representing each active subnet. - - This function is valuable for understanding the network's structure and the diversity of subnets available for neuron participation and collaboration. - """ - result = self.query_map_subtensor("NetworksAdded", block) - return ( - [network[0].value for network in result.records if network[1]] - if result and hasattr(result, "records") - else [] + def get_delegate_by_hotkey( + self, hotkey_ss58: str, block: Optional[int] = None + ) -> Optional["DelegateInfo"]: + return self.execute_coroutine( + self.async_subtensor.get_delegate_by_hotkey( + hotkey_ss58=hotkey_ss58, block=block + ), ) - def neurons_lite( - self, netuid: int, block: Optional[int] = None - ) -> list["NeuronInfoLite"]: - """ - Retrieves a list of neurons in a 'lite' format from a specific subnet of the Bittensor network. This function provides a streamlined view of the neurons, focusing on key attributes such as stake and network participation. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - list[bittensor.core.chain_data.neuron_info_lite.NeuronInfoLite]: A list of simplified neuron information for the subnet. - - This function offers a quick overview of the neuron population within a subnet, facilitating efficient analysis of the network's decentralized structure and neuron dynamics. - """ - hex_bytes_result = self.query_runtime_api( - runtime_api="NeuronInfoRuntimeApi", - method="get_neurons_lite", - params=[netuid], - block=block, + def get_delegate_identities( + self, block: Optional[int] = None + ) -> dict[str, "DelegatesDetails"]: + return self.execute_coroutine( + self.async_subtensor.get_delegate_identities(block=block), ) - if hex_bytes_result is None: - return [] - - return NeuronInfoLite.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) # type: ignore - - def weights( - self, netuid: int, block: Optional[int] = None - ) -> list[tuple[int, list[tuple[int, int]]]]: - """ - Retrieves the weight distribution set by neurons within a specific subnet of the Bittensor network. This function maps each neuron's UID to the weights it assigns to other neurons, reflecting the network's trust and value assignment mechanisms. - - Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int]): The blockchain block number for the query. - - Returns: - list[tuple[int, list[tuple[int, int]]]]: A list of tuples mapping each neuron's UID to its assigned 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. - """ - w_map = [] - w_map_encoded = self.query_map_subtensor( - name="Weights", block=block, params=[netuid] + def get_delegate_take( + self, hotkey_ss58: str, block: Optional[int] = None + ) -> Optional[float]: + return self.execute_coroutine( + self.async_subtensor.get_delegate_take( + hotkey_ss58=hotkey_ss58, block=block + ), ) - if w_map_encoded.records: - for uid, w in w_map_encoded: - w_map.append((uid.serialize(), w.serialize())) - - return w_map - - def get_balance(self, address: str, block: Optional[int] = None) -> "Balance": - """ - Retrieves the token balance of a specific address within the Bittensor network. This function queries the blockchain to determine the amount of Tao held by a given account. - - Args: - address (str): The Substrate address in ``ss58`` format. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - bittensor.utils.balance.Balance: The account balance at the specified block, represented as a Balance object. - - This function is important for monitoring account holdings and managing financial transactions within the Bittensor ecosystem. It helps in assessing the economic status and capacity of network participants. - """ - try: - result = self.substrate.query( - module="System", - storage_function="Account", - params=[address], - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - except RemainingScaleBytesNotEmptyException: - logging.error( - "Received a corrupted message. This likely points to an error with the network or subnet." - ) - return Balance(1000) - return Balance(result.value["data"]["free"]) - - def get_transfer_fee( - self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] - ) -> "Balance": - """ - Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. This function simulates the transfer to estimate the associated cost, taking into account the current network conditions and transaction complexity. - - Args: - wallet (bittensor_wallet.Wallet): The wallet from which the transfer is initiated. - dest (str): The ``SS58`` address of the destination account. - value (Union[bittensor.utils.balance.Balance, float, int]): The amount of tokens to be transferred, specified as a Balance object, or in Tao (float) or Rao (int) units. - - Returns: - bittensor.utils.balance.Balance: The estimated transaction fee for the transfer, represented as a Balance object. - - Estimating the transfer fee is essential for planning and executing token transactions, ensuring that the wallet has sufficient funds to cover both the transfer amount and the associated costs. This function provides a crucial tool for managing financial operations within the Bittensor network. - """ - if isinstance(value, float): - value = Balance.from_tao(value) - elif isinstance(value, int): - value = Balance.from_rao(value) - - if isinstance(value, Balance): - call = self.substrate.compose_call( - call_module="Balances", - call_function="transfer_allow_death", - call_params={"dest": dest, "value": value.rao}, - ) + def get_delegated( + self, coldkey_ss58: str, block: Optional[int] = None + ) -> list[tuple["DelegateInfo", "Balance"]]: + return self.execute_coroutine( + self.async_subtensor.get_delegated(coldkey_ss58=coldkey_ss58, block=block), + ) - try: - payment_info = self.substrate.get_payment_info( - call=call, keypair=wallet.coldkeypub - ) - except Exception as e: - logging.error(f"[red]Failed to get payment info.[/red] {e}") - payment_info = {"partialFee": int(2e7)} # assume 0.02 Tao - - fee = Balance.from_rao(payment_info["partialFee"]) - return fee - else: - fee = Balance.from_rao(int(2e7)) - logging.error( - "To calculate the transaction fee, the value must be Balance, float, or int. Received type: %s. Fee " - "is %s", - type(value), - 2e7, - ) - return fee + def get_delegates(self, block: Optional[int] = None) -> list["DelegateInfo"]: + return self.execute_coroutine( + self.async_subtensor.get_delegates(block=block), + ) def get_existential_deposit( self, block: Optional[int] = None ) -> Optional["Balance"]: - """ - Retrieves the existential deposit amount for the Bittensor blockchain. The existential deposit is the minimum amount of TAO required for an account to exist on the blockchain. Accounts with balances below this threshold can be reaped to conserve network resources. - - Args: - block (Optional[int]): Block number at which to query the deposit amount. If ``None``, the current block is used. - - Returns: - Optional[bittensor.utils.balance.Balance]: The existential deposit amount, or ``None`` if the query fails. - - The existential deposit is a fundamental economic parameter in the Bittensor network, ensuring efficient use of storage and preventing the proliferation of dust accounts. - """ - result = self.query_constant( - module_name="Balances", constant_name="ExistentialDeposit", block=block - ) - if result is None or not hasattr(result, "value"): - return None - return Balance.from_rao(result.value) - - def difficulty(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """ - Retrieves the 'Difficulty' hyperparameter for a specified subnet in the Bittensor network. - - This parameter is instrumental in determining the computational challenge required for neurons to participate in consensus and validation processes. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[int]: The value of the 'Difficulty' hyperparameter if the subnet exists, ``None`` otherwise. - - The 'Difficulty' parameter directly impacts the network's security and integrity by setting the computational effort required for validating transactions and participating in the network's consensus mechanism. - """ - call = self._get_hyperparameter( - param_name="Difficulty", netuid=netuid, block=block + return self.execute_coroutine( + self.async_subtensor.get_existential_deposit(block=block), ) - if call is None: - return None - return int(call) - def recycle(self, netuid: int, block: Optional[int] = None) -> Optional["Balance"]: - """ - Retrieves the 'Burn' hyperparameter for a specified subnet. The 'Burn' parameter represents the amount of Tao that is effectively recycled within the Bittensor network. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[Balance]: The value of the 'Burn' hyperparameter if the subnet exists, None otherwise. - - Understanding the 'Burn' rate is essential for analyzing the network registration usage, particularly how it is correlated with user activity and the overall cost of participation in a given subnet. - """ - call = self._get_hyperparameter(param_name="Burn", netuid=netuid, block=block) - return None if call is None else Balance.from_rao(int(call)) - - def get_delegate_take( + def get_hotkey_owner( self, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[float]: - """ - Retrieves the delegate 'take' percentage for a neuron identified by its hotkey. The 'take' represents the percentage of rewards that the delegate claims from its nominators' stakes. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int]): The blockchain block number for the query. + ) -> Optional[str]: + return self.execute_coroutine( + self.async_subtensor.get_hotkey_owner(hotkey_ss58=hotkey_ss58, block=block), + ) - Returns: - Optional[float]: The delegate take percentage, None if not available. + def get_minimum_required_stake(self) -> "Balance": + return self.execute_coroutine( + self.async_subtensor.get_minimum_required_stake(), + ) - The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of rewards among neurons and their nominators. - """ - _result = self.query_subtensor("Delegates", block, [hotkey_ss58]) - return ( - None - if getattr(_result, "value", None) is None - else u16_normalized_float(_result.value) + def get_netuids_for_hotkey( + self, hotkey_ss58: str, block: Optional[int] = None, reuse_block: bool = False + ) -> list[int]: + return self.execute_coroutine( + self.async_subtensor.get_netuids_for_hotkey( + hotkey_ss58=hotkey_ss58, block=block, reuse_block=reuse_block + ), ) - def get_delegate_by_hotkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[DelegateInfo]: - """ - Retrieves detailed information about a delegate neuron based on its hotkey. This function provides a comprehensive view of the delegate's status, including its stakes, nominators, and reward distribution. + def get_neuron_certificate( + self, hotkey: str, netuid: int, block: Optional[int] = None + ) -> Optional["Certificate"]: + return self.execute_coroutine( + self.async_subtensor.get_neuron_certificate(hotkey, netuid, block=block), + ) - Args: - hotkey_ss58 (str): The ``SS58`` address of the delegate's hotkey. - block (Optional[int]): The blockchain block number for the query. Default is ``None``. + def get_neuron_for_pubkey_and_subnet( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> Optional["NeuronInfo"]: + return self.execute_coroutine( + self.async_subtensor.get_neuron_for_pubkey_and_subnet( + hotkey_ss58, netuid, block=block + ), + ) - Returns: - Optional[DelegateInfo]: Detailed information about the delegate neuron, ``None`` if not found. + def get_stake_for_coldkey_and_hotkey( + self, hotkey_ss58: str, coldkey_ss58: str, block: Optional[int] = None + ) -> Optional["Balance"]: + return self.execute_coroutine( + self.async_subtensor.get_stake_for_coldkey_and_hotkey( + hotkey_ss58=hotkey_ss58, coldkey_ss58=coldkey_ss58, block=block + ), + ) - This function is essential for understanding the roles and influence of delegate neurons within the Bittensor network's consensus and governance structures. - """ - encoded_hotkey = ss58_to_vec_u8(hotkey_ss58) + def get_stake_info_for_coldkey( + self, coldkey_ss58: str, block: Optional[int] = None + ) -> list["StakeInfo"]: + return self.execute_coroutine( + self.async_subtensor.get_stake_info_for_coldkey( + coldkey_ss58=coldkey_ss58, block=block + ), + ) - block_hash = None if block is None else self.substrate.get_block_hash(block) + def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[str]: + return self.execute_coroutine( + self.async_subtensor.get_subnet_burn_cost(block=block), + ) - json_body = self.substrate.rpc_request( - method="delegateInfo_getDelegate", # custom rpc method - params=([encoded_hotkey, block_hash] if block_hash else [encoded_hotkey]), + def get_subnet_hyperparameters( + self, netuid: int, block: Optional[int] = None + ) -> Optional[Union[list, "SubnetHyperparameters"]]: + return self.execute_coroutine( + self.async_subtensor.get_subnet_hyperparameters(netuid=netuid, block=block), ) - if not (result := json_body.get("result", None)): - return None + def get_subnet_reveal_period_epochs( + self, netuid: int, block: Optional[int] = None + ) -> int: + return self.execute_coroutine( + self.async_subtensor.get_subnet_reveal_period_epochs( + netuid=netuid, block=block + ), + ) - return DelegateInfo.from_vec_u8(bytes(result)) + def get_subnets(self, block: Optional[int] = None) -> list[int]: + return self.execute_coroutine( + self.async_subtensor.get_subnets(block=block), + ) - def get_stake_for_coldkey_and_hotkey( - self, hotkey_ss58: str, coldkey_ss58: str, block: Optional[int] = None - ) -> Optional["Balance"]: - """ - Returns the stake under a coldkey - hotkey pairing. + def get_total_stake_for_coldkey( + self, ss58_address: str, block: Optional[int] = None + ) -> "Balance": + result = self.execute_coroutine( + self.async_subtensor.get_total_stake_for_coldkey(ss58_address, block=block), + ) + return result - Args: - hotkey_ss58 (str): The SS58 address of the hotkey. - coldkey_ss58 (str): The SS58 address of the coldkey. - block (Optional[int]): The block number to retrieve the stake from. If ``None``, the latest block is used. Default is ``None``. + def get_total_stake_for_hotkey( + self, ss58_address: str, block: Optional[int] = None + ) -> "Balance": + result = self.execute_coroutine( + self.async_subtensor.get_total_stake_for_hotkey(ss58_address, block=block), + ) + return result - Returns: - Optional[Balance]: The stake under the coldkey - hotkey pairing, or ``None`` if the pairing does not exist or the stake is not found. - """ - result = self.query_subtensor("Stake", block, [hotkey_ss58, coldkey_ss58]) - return ( - None - if getattr(result, "value", None) is None - else Balance.from_rao(result.value) + def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: + return self.execute_coroutine( + self.async_subtensor.get_total_subnets(block=block), ) - def does_hotkey_exist(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: - """ - Returns true if the hotkey is known by the chain and there are accounts. + def get_transfer_fee( + self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] + ) -> "Balance": + return self.execute_coroutine( + self.async_subtensor.get_transfer_fee( + wallet=wallet, dest=dest, value=value + ), + ) - Args: - hotkey_ss58 (str): The SS58 address of the hotkey. - block (Optional[int]): The block number to check the hotkey against. If ``None``, the latest block is used. Default is ``None``. + def get_vote_data( + self, proposal_hash: str, block: Optional[int] = None + ) -> Optional["ProposalVoteData"]: + return self.execute_coroutine( + self.async_subtensor.get_vote_data( + proposal_hash=proposal_hash, block=block + ), + ) - Returns: - bool: ``True`` if the hotkey is known by the chain and there are accounts, ``False`` otherwise. - """ - result = self.query_subtensor("Owner", block, [hotkey_ss58]) - return ( - False - if getattr(result, "value", None) is None - else result.value != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" + def get_uid_for_hotkey_on_subnet( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + return self.execute_coroutine( + self.async_subtensor.get_uid_for_hotkey_on_subnet( + hotkey_ss58=hotkey_ss58, netuid=netuid, block=block + ), ) - def get_hotkey_owner( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[str]: - """ - Returns the coldkey owner of the passed hotkey. + def filter_netuids_by_registered_hotkeys( + self, + all_netuids: Iterable[int], + filter_for_netuids: Iterable[int], + all_hotkeys: Iterable["Wallet"], + block: Optional[int], + ) -> list[int]: + return self.execute_coroutine( + self.async_subtensor.filter_netuids_by_registered_hotkeys( + all_netuids=all_netuids, + filter_for_netuids=filter_for_netuids, + all_hotkeys=all_hotkeys, + block=block, + ), + ) - Args: - hotkey_ss58 (str): The SS58 address of the hotkey. - block (Optional[int]): The block number to check the hotkey owner against. If ``None``, the latest block is used. Default is ``None``. + def immunity_period( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + return self.execute_coroutine( + self.async_subtensor.immunity_period(netuid=netuid, block=block), + ) - Returns: - Optional[str]: The SS58 address of the coldkey owner, or ``None`` if the hotkey does not exist or the owner is not found. - """ - result = self.query_subtensor("Owner", block, [hotkey_ss58]) - return ( - None - if getattr(result, "value", None) is None - or not self.does_hotkey_exist(hotkey_ss58, block) - else result.value + def is_hotkey_delegate(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: + return self.execute_coroutine( + self.async_subtensor.is_hotkey_delegate( + hotkey_ss58=hotkey_ss58, block=block + ), ) - def get_minimum_required_stake( + def is_hotkey_registered( self, - ) -> Balance: - """ - Returns the minimum required stake for nominators in the Subtensor network. + hotkey_ss58: str, + netuid: Optional[int] = None, + block: Optional[int] = None, + ) -> bool: + return self.execute_coroutine( + self.async_subtensor.is_hotkey_registered( + hotkey_ss58=hotkey_ss58, netuid=netuid, block=block + ), + ) - This method retries the substrate call up to three times with exponential backoff in case of failures. + def is_hotkey_registered_any( + self, + hotkey_ss58: str, + block: Optional[int] = None, + ) -> bool: + return self.execute_coroutine( + self.async_subtensor.is_hotkey_registered_any( + hotkey_ss58=hotkey_ss58, + block=block, + ), + ) - Returns: - Balance: The minimum required stake as a Balance object. + def is_hotkey_registered_on_subnet( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> bool: + return self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block) is not None - Raises: - Exception: If the substrate call fails after the maximum number of retries. - """ + def last_drand_round(self) -> Optional[int]: + return self.execute_coroutine( + self.async_subtensor.last_drand_round(), + ) - result = self.substrate.query( - module="SubtensorModule", storage_function="NominatorMinRequiredStake" + def max_weight_limit( + self, netuid: int, block: Optional[int] = None + ) -> Optional[float]: + return self.execute_coroutine( + self.async_subtensor.max_weight_limit(netuid=netuid, block=block), ) - return Balance.from_rao(result.decode()) - def tx_rate_limit(self, block: Optional[int] = None) -> Optional[int]: - """ - Retrieves the transaction rate limit for the Bittensor network as of a specific blockchain block. - This rate limit sets the maximum number of transactions that can be processed within a given time frame. + def metagraph( + self, netuid: int, lite: bool = True, block: Optional[int] = None + ) -> "Metagraph": + metagraph = Metagraph( + network=self.chain_endpoint, + netuid=netuid, + lite=lite, + sync=False, + subtensor=self, + ) + metagraph.sync(block=block, lite=lite, subtensor=self) - Args: - block (Optional[int]): The blockchain block number at which to perform the query. + return metagraph - Returns: - Optional[int]: The transaction rate limit of the network, None if not available. + def min_allowed_weights( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + return self.execute_coroutine( + self.async_subtensor.min_allowed_weights(netuid=netuid, block=block), + ) - The transaction rate limit is an essential parameter for ensuring the stability and scalability of the Bittensor network. It helps in managing network load and preventing congestion, thereby maintaining efficient and timely transaction processing. - """ - result = self.query_subtensor("TxRateLimit", block) - return getattr(result, "value", None) + def neuron_for_uid( + self, uid: int, netuid: int, block: Optional[int] = None + ) -> "NeuronInfo": + return self.execute_coroutine( + self.async_subtensor.neuron_for_uid(uid=uid, netuid=netuid, block=block), + ) - def get_delegates(self, block: Optional[int] = None) -> list[DelegateInfo]: - """ - Retrieves a list of all delegate neurons within the Bittensor network. This function provides an overview of the neurons that are actively involved in the network's delegation system. + def neurons(self, netuid: int, block: Optional[int] = None) -> list["NeuronInfo"]: + return self.execute_coroutine( + self.async_subtensor.neurons(netuid=netuid, block=block), + ) - Analyzing the delegate population offers insights into the network's governance dynamics and the distribution of trust and responsibility among participating neurons. + def neurons_lite( + self, netuid: int, block: Optional[int] = None + ) -> list["NeuronInfoLite"]: + return self.execute_coroutine( + self.async_subtensor.neurons_lite(netuid=netuid, block=block), + ) - Args: - block (Optional[int]): The blockchain block number for the query. + def query_identity(self, key: str, block: Optional[int] = None) -> Optional[str]: + return self.execute_coroutine( + self.async_subtensor.query_identity(key=key, block=block), + ) - Returns: - list[DelegateInfo]: A list of DelegateInfo objects detailing each delegate's characteristics. + def recycle(self, netuid: int, block: Optional[int] = None) -> Optional["Balance"]: + return self.execute_coroutine( + self.async_subtensor.recycle(netuid=netuid, block=block), + ) - """ - block_hash = None if block is None else self.substrate.get_block_hash(block) + def subnet_exists(self, netuid: int, block: Optional[int] = None) -> bool: + return self.execute_coroutine( + self.async_subtensor.subnet_exists(netuid=netuid, block=block), + ) - json_body = self.substrate.rpc_request( - method="delegateInfo_getDelegates", - params=[block_hash] if block_hash else [], + def subnetwork_n(self, netuid: int, block: Optional[int] = None) -> Optional[int]: + return self.execute_coroutine( + self.async_subtensor.subnetwork_n(netuid=netuid, block=block), ) - if not (result := json_body.get("result", None)): - return [] + def tempo(self, netuid: int, block: Optional[int] = None) -> Optional[int]: + return self.execute_coroutine( + self.async_subtensor.tempo(netuid=netuid, block=block), + ) - return DelegateInfo.list_from_vec_u8(bytes(result)) + def tx_rate_limit(self, block: Optional[int] = None) -> Optional[int]: + return self.execute_coroutine( + self.async_subtensor.tx_rate_limit(block=block), + ) - def is_hotkey_delegate(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: - """ - Determines whether a given hotkey (public key) is a delegate on the Bittensor network. This function checks if the neuron associated with the hotkey is part of the network's delegation system. + def weights( + self, netuid: int, block: Optional[int] = None + ) -> list[tuple[int, list[tuple[int, int]]]]: + return self.execute_coroutine( + self.async_subtensor.weights(netuid=netuid, block=block), + ) - Args: - hotkey_ss58 (str): The SS58 address of the neuron's hotkey. - block (Optional[int]): The blockchain block number for the query. + def weights_rate_limit( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + return self.execute_coroutine( + self.async_subtensor.weights_rate_limit(netuid=netuid, block=block), + ) - Returns: - bool: ``True`` if the hotkey is a delegate, ``False`` otherwise. + # Extrinsics ======================================================================================================= - Being a delegate is a significant status within the Bittensor network, indicating a neuron's involvement in consensus and governance processes. - """ - return hotkey_ss58 in [ - info.hotkey_ss58 for info in self.get_delegates(block=block) - ] + def add_stake( + self, + wallet: "Wallet", + hotkey_ss58: Optional[str] = None, + amount: Optional[Union["Balance", float]] = None, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + return self.execute_coroutine( + self.async_subtensor.add_stake( + wallet=wallet, + hotkey_ss58=hotkey_ss58, + amount=amount, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), + ) - # Extrinsics ======================================================================================================= + def add_stake_multiple( + self, + wallet: "Wallet", + hotkey_ss58s: list[str], + amounts: Optional[list[Union["Balance", float]]] = None, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + return self.execute_coroutine( + self.async_subtensor.add_stake_multiple( + wallet=wallet, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), + ) - def set_weights( + def burned_register( self, wallet: "Wallet", netuid: int, - uids: Union[NDArray[np.int64], "torch.LongTensor", list], - weights: Union[NDArray[np.float32], "torch.FloatTensor", list], - version_key: int = settings.version_as_int, wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - max_retries: int = 5, - ) -> tuple[bool, str]: - """ - Sets the inter-neuronal 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. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. - netuid (int): The unique identifier of the subnet. - uids (Union[NDArray[np.int64], torch.LongTensor, list]): The list of neuron UIDs that the weights are being set for. - weights (Union[NDArray[np.float32], torch.FloatTensor, list]): The corresponding weights to be set for each UID. - version_key (int): Version key for compatibility with the network. Default is ``int representation of 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 set weights. Default is ``5``. - - 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. - - 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【81†source】. - """ - retries = 0 - success = False - if ( - uid := self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) - ) is None: - return ( - False, - f"Hotkey {wallet.hotkey.ss58_address} not registered in subnet {netuid}", - ) + wait_for_finalization: bool = True, + ) -> bool: + return self.execute_coroutine( + self.async_subtensor.burned_register( + wallet=wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), + ) - if self.commit_reveal_enabled(netuid=netuid) is True: - # go with `commit reveal v3` extrinsic - message = "No attempt made. Perhaps it is too soon to commit weights!" - while ( - self.blocks_since_last_update(netuid, uid) # type: ignore - > self.weights_rate_limit(netuid) # type: ignore - and retries < max_retries - and success is False - ): - logging.info( - f"Committing weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." - ) - success, message = commit_reveal_v3_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - retries += 1 - return success, message - else: - # go with classic `set weights` logic - message = "No attempt made. Perhaps it is too soon to set weights!" - while ( - self.blocks_since_last_update(netuid, uid) # type: ignore - > self.weights_rate_limit(netuid) # type: ignore - and retries < max_retries - and success is False - ): - try: - logging.info( - f"Setting weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." - ) - success, message = set_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - except Exception as e: - logging.error(f"Error setting weights: {e}") - finally: - retries += 1 - return success, message - - @legacy_torch_api_compat - def root_set_weights( + def commit_weights( self, wallet: "Wallet", - netuids: Union[NDArray[np.int64], "torch.LongTensor", list], - weights: Union[NDArray[np.float32], "torch.FloatTensor", list], - version_key: int = 0, + netuid: int, + salt: list[int], + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.int64], list], + version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, - ) -> bool: - """ - Sets the weights for neurons on the root network. This action is crucial for defining the influence and interactions of neurons at the root level of the Bittensor network. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. - netuids (Union[NDArray[np.int64], torch.LongTensor, list]): The list of neuron UIDs for which weights are being set. - weights (Union[NDArray[np.float32], torch.FloatTensor, list]): The corresponding weights to be set for each UID. - version_key (int, optional): Version key for compatibility with the network. Default is ``0``. - wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Defaults to ``False``. - wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. Defaults to ``False``. - - Returns: - bool: ``True`` if the setting of root-level weights is successful, False otherwise. - - This function plays a pivotal role in shaping the root network's collective intelligence and decision-making processes, reflecting the principles of decentralized governance and collaborative learning in Bittensor. - """ - return set_root_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuids=netuids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + max_retries: int = 5, + ) -> tuple[bool, str]: + return self.execute_coroutine( + self.async_subtensor.commit_weights( + wallet=wallet, + netuid=netuid, + salt=salt, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + max_retries=max_retries, + ), ) def register( @@ -1932,354 +671,145 @@ def register( update_interval: Optional[int] = None, log_verbose: bool = False, ) -> bool: - """ - Registers a neuron on the Bittensor network using the provided wallet. - - Registration is a critical step for a neuron to become an active participant in the network, enabling it to stake, set weights, and receive incentives. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron to be registered. - netuid (int): The unique identifier of the subnet. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. Defaults to `False`. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Defaults to `True`. - max_allowed_attempts (int): Maximum number of attempts to register the wallet. - output_in_place (bool): If true, prints the progress of the proof of work to the console in-place. Meaning the progress is printed on the same lines. Defaults to `True`. - cuda (bool): If ``true``, the wallet should be registered using CUDA device(s). Defaults to `False`. - dev_id (Union[List[int], int]): The CUDA device id to use, or a list of device ids. Defaults to `0` (zero). - tpb (int): The number of threads per block (CUDA). Default to `256`. - num_processes (Optional[int]): The number of processes to use to register. Default to `None`. - update_interval (Optional[int]): The number of nonces to solve between updates. Default to `None`. - log_verbose (bool): If ``true``, the registration process will log more information. Default to `False`. - - Returns: - bool: ``True`` if the registration is successful, False otherwise. - - This function facilitates the entry of new neurons into the network, supporting the decentralized - growth and scalability of the Bittensor ecosystem. - """ - return register_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - max_allowed_attempts=max_allowed_attempts, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - def root_register( - self, - wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> bool: - """ - Registers the neuron associated with the wallet on the root network. This process is integral for participating in the highest layer of decision-making and governance within the Bittensor network. - - Args: - wallet (bittensor_wallet.wallet): The wallet associated with the neuron to be registered on the root network. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. Defaults to `False`. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Defaults to `True`. - - Returns: - bool: ``True`` if the registration on the root network is successful, False otherwise. - - This function enables neurons to engage in the most critical and influential aspects of the network's governance, signifying a high level of commitment and responsibility in the Bittensor ecosystem. - """ - return root_register_extrinsic( - subtensor=self, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + return self.execute_coroutine( + self.async_subtensor.register( + wallet=wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + max_allowed_attempts=max_allowed_attempts, + output_in_place=output_in_place, + cuda=cuda, + dev_id=dev_id, + tpb=tpb, + num_processes=num_processes, + update_interval=update_interval, + log_verbose=log_verbose, + ), ) - def burned_register( + 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], + version_key: int = version_as_int, wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> bool: - """ - Registers a neuron on the Bittensor network by recycling TAO. This method of registration involves recycling TAO tokens, allowing them to be re-mined by performing work on the network. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron to be registered. - netuid (int): The unique identifier of the subnet. - wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Defaults to `False`. - wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. Defaults to `True`. - - Returns: - bool: ``True`` if the registration is successful, False otherwise. - """ - return burned_register_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + wait_for_finalization: bool = False, + max_retries: int = 5, + ) -> tuple[bool, str]: + return self.execute_coroutine( + self.async_subtensor.reveal_weights( + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + salt=salt, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + max_retries=max_retries, + ), ) - def serve_axon( + def root_register( self, - netuid: int, - axon: "Axon", + wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, - certificate: Optional[Certificate] = None, ) -> bool: - """ - Registers an ``Axon`` serving endpoint on the Bittensor network for a specific neuron. This function is used to set up the Axon, a key component of a neuron that handles incoming queries and data processing tasks. - - Args: - netuid (int): The unique identifier of the subnetwork. - axon (bittensor.core.axon.Axon): The Axon instance to be registered for serving. - 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 ``True``. - - Returns: - bool: ``True`` if the Axon serve registration is successful, False otherwise. - - By registering an Axon, the neuron becomes an active part of the network's distributed computing infrastructure, contributing to the collective intelligence of Bittensor. - """ - return serve_axon_extrinsic( - self, netuid, axon, wait_for_inclusion, wait_for_finalization, certificate + return execute_coroutine( + self.async_subtensor.root_register( + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), ) - _do_serve_axon = do_serve_axon - - def transfer( + def root_set_weights( self, wallet: "Wallet", - dest: str, - amount: Union["Balance", float], - wait_for_inclusion: bool = True, + netuids: list[int], + weights: list[float], + version_key: int = 0, + wait_for_inclusion: bool = False, wait_for_finalization: bool = False, ) -> bool: - """ - Executes a transfer of funds from the provided wallet to the specified destination address. This function is used to move TAO tokens within the Bittensor network, facilitating transactions between neurons. - - Args: - wallet (bittensor_wallet.Wallet): The wallet from which funds are being transferred. - dest (str): The destination public key address. - amount (Union[bittensor.utils.balance.Balance, float]): The amount of TAO to be transferred. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``True``. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is ``False``. - - Returns: - transfer_extrinsic (bool): ``True`` if the transfer is successful, False otherwise. - - This function is essential for the fluid movement of tokens in the network, supporting various economic activities such as staking, delegation, and reward distribution. - """ - return transfer_extrinsic( - subtensor=self, - wallet=wallet, - dest=dest, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + return self.execute_coroutine( + self.async_subtensor.root_set_weights( + wallet=wallet, + netuids=netuids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), ) - def commit_weights( + def set_weights( self, wallet: "Wallet", netuid: int, - salt: list[int], - uids: Union[NDArray[np.int64], list], - weights: Union[NDArray[np.int64], list], - version_key: int = settings.version_as_int, + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, max_retries: int = 5, ) -> 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. - - Args: - 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 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``. - - Returns: - tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string value describing the success or potential error. - - This function allows neurons to create a tamper-proof record of their weight distribution at a specific point in time, enhancing transparency and accountability within the Bittensor network. - """ - retries = 0 - success = False - message = "No attempt made. Perhaps it is too soon to commit weights!" - - logging.info( - f"Committing weights with params: netuid={netuid}, uids={uids}, weights={weights}, version_key={version_key}" - ) - - # 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, - ) - - logging.info(f"Commit Hash: {commit_hash}") - - while retries < max_retries: - try: - success, message = commit_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - commit_hash=commit_hash, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - if success: - break - except Exception as e: - logging.error(f"Error committing weights: {e}") - finally: - retries += 1 - - return success, message + return self.execute_coroutine( + self.async_subtensor.set_weights( + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + max_retries=max_retries, + ) + ) - def reveal_weights( + def serve_axon( self, - wallet: "Wallet", netuid: int, - uids: Union[NDArray[np.int64], list], - weights: Union[NDArray[np.int64], list], - salt: Union[NDArray[np.int64], list], - version_key: int = settings.version_as_int, + axon: "Axon", wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - max_retries: int = 5, - ) -> 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 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``. - - Returns: - tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, 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. - """ - - retries = 0 - success = False - message = "No attempt made. Perhaps it is too soon to reveal weights!" - - while retries < max_retries: - try: - success, message = reveal_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - uids=list(uids), - weights=list(weights), - salt=list(salt), - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - if success: - break - except Exception as e: - logging.error(f"Error revealing weights: {e}") - finally: - retries += 1 - - return success, message - - def add_stake( - self, - wallet: "Wallet", - hotkey_ss58: Optional[str] = None, - amount: Optional[Union["Balance", float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, + wait_for_finalization: bool = True, + certificate: Optional["Certificate"] = None, ) -> bool: - """ - Adds the specified amount of stake to a neuron identified by the hotkey ``SS58`` address. - Staking is a fundamental process in the Bittensor network that enables neurons to participate actively and earn incentives. - - Args: - wallet (bittensor_wallet.Wallet): The wallet to be used for staking. - hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey associated with the neuron. - amount (Union[Balance, float]): The amount of TAO to stake. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - - Returns: - bool: ``True`` if the staking is successful, False otherwise. - - This function enables neurons to increase their stake in the network, enhancing their influence and potential rewards in line with Bittensor's consensus and reward mechanisms. - """ - return add_stake_extrinsic( - subtensor=self, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + return self.execute_coroutine( + self.async_subtensor.serve_axon( + netuid=netuid, + axon=axon, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + certificate=certificate, + ), ) - def add_stake_multiple( + def transfer( self, wallet: "Wallet", - hotkey_ss58s: list[str], - amounts: Optional[list[Union["Balance", float]]] = None, + dest: str, + amount: Union["Balance", float], wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + transfer_all: bool = False, + keep_alive: bool = True, ) -> bool: - """ - Adds stakes to multiple neurons identified by their hotkey SS58 addresses. - This bulk operation allows for efficient staking across different neurons from a single wallet. - - Args: - wallet (bittensor_wallet.Wallet): The wallet used for staking. - hotkey_ss58s (list[str]): List of ``SS58`` addresses of hotkeys to stake to. - amounts (list[Union[Balance, float]]): Corresponding amounts of TAO to stake for each hotkey. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - - Returns: - bool: ``True`` if the staking is successful for all specified neurons, False otherwise. - - This function is essential for managing stakes across multiple neurons, reflecting the dynamic and collaborative nature of the Bittensor network. - """ - return add_stake_multiple_extrinsic( - subtensor=self, - wallet=wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + return self.execute_coroutine( + self.async_subtensor.transfer( + wallet=wallet, + destination=dest, + amount=amount, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + transfer_all=transfer_all, + keep_alive=keep_alive, + ), ) def unstake( @@ -2290,28 +820,14 @@ def unstake( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - """ - Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting individual neuron stakes within the Bittensor network. - - Args: - wallet (bittensor_wallet.wallet): The wallet associated with the neuron from which the stake is being removed. - hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey account to unstake from. - amount (Union[Balance, float]): The amount of TAO to unstake. If not specified, unstakes all. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - - Returns: - bool: ``True`` if the unstaking process is successful, False otherwise. - - This function supports flexible stake management, allowing neurons to adjust their network participation and potential reward accruals. - """ - return unstake_extrinsic( - subtensor=self, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + return self.execute_coroutine( + self.async_subtensor.unstake( + wallet=wallet, + hotkey_ss58=hotkey_ss58, + amount=amount, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), ) def unstake_multiple( @@ -2322,26 +838,12 @@ def unstake_multiple( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - """ - Performs batch unstaking from multiple hotkey accounts, allowing a neuron to reduce its staked amounts efficiently. This function is useful for managing the distribution of stakes across multiple neurons. - - Args: - wallet (bittensor_wallet.Wallet): The wallet linked to the coldkey from which the stakes are being withdrawn. - hotkey_ss58s (List[str]): A list of hotkey ``SS58`` addresses to unstake from. - amounts (List[Union[Balance, float]]): The amounts of TAO to unstake from each hotkey. If not provided, unstakes all available stakes. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - - Returns: - bool: ``True`` if the batch unstaking is successful, False otherwise. - - This function allows for strategic reallocation or withdrawal of stakes, aligning with the dynamic stake management aspect of the Bittensor network. - """ - return unstake_multiple_extrinsic( - subtensor=self, - wallet=wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + return self.execute_coroutine( + self.async_subtensor.unstake_multiple( + wallet=wallet, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), ) diff --git a/bittensor/core/types.py b/bittensor/core/types.py index 577df5b6ba..908e384015 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -38,3 +38,8 @@ class PrometheusServeCallParams(TypedDict): port: int ip_type: int netuid: int + + +class ParamWithTypes(TypedDict): + name: str # Name of the parameter. + type: str # ScaleType string of the parameter. diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 563b969b82..886825a6a5 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -15,10 +15,11 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +import asyncio import ast from collections import namedtuple import hashlib -from typing import Any, Literal, Union, Optional, TYPE_CHECKING +from typing import Any, Literal, Union, Optional, TYPE_CHECKING, Coroutine from urllib.parse import urlparse import scalecodec @@ -32,18 +33,34 @@ from .version import version_checking, check_version, VersionCheckError if TYPE_CHECKING: - from bittensor.utils.substrate_interface import AsyncSubstrateInterface - from substrateinterface import SubstrateInterface from bittensor_wallet import Wallet + +# redundant aliases +logging = logging +torch = torch +use_torch = use_torch +version_checking = version_checking +check_version = check_version +VersionCheckError = VersionCheckError + + RAOPERTAO = 1e9 U16_MAX = 65535 U64_MAX = 18446744073709551615 Certificate = str +UnlockStatus = namedtuple("UnlockStatus", ["success", "message"]) -UnlockStatus = namedtuple("UnlockStatus", ["success", "message"]) +def event_loop_is_running() -> Optional[asyncio.AbstractEventLoop]: + """ + Simple function to check if event loop is running. Returns the loop if it is, otherwise None. + """ + try: + return asyncio.get_running_loop() + except RuntimeError: + return None def ss58_to_vec_u8(ss58_address: str) -> list[int]: @@ -398,3 +415,37 @@ def hex_to_bytes(hex_str: str) -> bytes: else: bytes_result = bytes.fromhex(hex_str) return bytes_result + + +def get_event_loop() -> asyncio.AbstractEventLoop: + """ + If an event loop is already running, returns that. Otherwise, creates a new event loop, + and sets it as the main event loop for this thread, returning the newly-created event loop. + """ + if loop := event_loop_is_running(): + event_loop = loop + else: + event_loop = asyncio.get_event_loop() + asyncio.set_event_loop(event_loop) + return event_loop + + +def execute_coroutine( + coroutine: "Coroutine", event_loop: asyncio.AbstractEventLoop = None +): + """ + Helper function to run an asyncio coroutine synchronously. + + Args: + coroutine (Coroutine): The coroutine to run. + event_loop (AbstractEventLoop): The event loop to use. If `None`, attempts to fetch the already-running + loop. If one if not running, a new loop is created. + + Returns: + The result of the coroutine execution. + """ + if event_loop: + event_loop = event_loop + else: + event_loop = get_event_loop() + return event_loop.run_until_complete(asyncio.wait_for(coroutine, timeout=None)) diff --git a/bittensor/utils/delegates_details.py b/bittensor/utils/delegates_details.py index 88a5633e76..eeb8d24c77 100644 --- a/bittensor/utils/delegates_details.py +++ b/bittensor/utils/delegates_details.py @@ -2,6 +2,7 @@ from typing import Any, Optional +# TODO: consider move it to `bittensor.core.chain_data` @dataclass class DelegatesDetails: display: str diff --git a/bittensor/utils/mock/subtensor_mock.py b/bittensor/utils/mock/subtensor_mock.py index 0e35cd2478..e19fd82471 100644 --- a/bittensor/utils/mock/subtensor_mock.py +++ b/bittensor/utils/mock/subtensor_mock.py @@ -20,7 +20,7 @@ from hashlib import sha256 from types import SimpleNamespace from typing import Any, Optional, Union, TypedDict -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, patch, AsyncMock from bittensor_wallet import Wallet @@ -34,8 +34,9 @@ from bittensor.core.types import AxonServeCallParams, PrometheusServeCallParams from bittensor.core.errors import ChainQueryError from bittensor.core.subtensor import Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor import bittensor.core.subtensor as subtensor_module -from bittensor.utils import RAOPERTAO, u16_normalized_float +from bittensor.utils import RAOPERTAO, u16_normalized_float, get_event_loop from bittensor.utils.balance import Balance # Mock Testing Constant @@ -167,6 +168,21 @@ class MockChainState(TypedDict): SubtensorModule: MockSubtensorState +class ReusableCoroutine: + def __init__(self, coroutine): + self.coroutine = coroutine + + def __await__(self): + return self.reset().__await__() + + def reset(self): + return self.coroutine() + + +async def _async_block(): + return 1 + + class MockSubtensor(Subtensor): """ A Mock Subtensor class for running tests. @@ -251,6 +267,9 @@ def setup(self) -> None: self.network = "mock" self.chain_endpoint = "ws://mock_endpoint.bt" self.substrate = MagicMock(autospec=SubstrateInterface) + self.async_subtensor = AsyncMock(autospec=AsyncSubtensor) + self.async_subtensor.block = ReusableCoroutine(_async_block) + self.event_loop = get_event_loop() def __init__(self, *args, **kwargs) -> None: mock_substrate_interface = MagicMock(autospec=SubstrateInterface) diff --git a/bittensor/utils/substrate_interface.py b/bittensor/utils/substrate_interface.py index 6622da8fb2..a07e64e6f4 100644 --- a/bittensor/utils/substrate_interface.py +++ b/bittensor/utils/substrate_interface.py @@ -14,7 +14,15 @@ from dataclasses import dataclass from datetime import datetime from hashlib import blake2b -from typing import Optional, Any, Union, Callable, Awaitable, cast, TYPE_CHECKING +from typing import ( + Optional, + Any, + Union, + Callable, + Awaitable, + cast, + TYPE_CHECKING, +) import asyncstdlib as a from bittensor_wallet import Keypair @@ -32,6 +40,7 @@ ExtrinsicNotFound, BlockNotFound, ) +from bittensor.utils import execute_coroutine from bittensor.utils import hex_to_bytes from bittensor.utils.btlogging import logging @@ -42,9 +51,30 @@ ExtrinsicReceiptLike = Union["AsyncExtrinsicReceipt", "ExtrinsicReceipt"] -@dataclass class ScaleObj: - value: Any + def __new__(cls, value): + if isinstance(value, (dict, str, int)): + return value + return super().__new__(cls) + + def __init__(self, value): + self.value = list(value) if isinstance(value, tuple) else value + + def __str__(self): + return f"BittensorScaleType(value={self.value})>" + + def __repr__(self): + return repr(self.value) + + def __eq__(self, other): + return self.value == other + + def __iter__(self): + for item in self.value: + yield item + + def __getitem__(self, item): + return self.value[item] class AsyncExtrinsicReceipt: @@ -908,6 +938,8 @@ def __init__( sync_calls: bool = False, max_retries: int = 5, retry_timeout: float = 60.0, + event_loop: Optional[asyncio.BaseEventLoop] = None, + _mock: bool = False, ): """ The asyncio-compatible version of the subtensor interface commands we use in bittensor. It is important to @@ -924,6 +956,8 @@ def __init__( sync_calls: whether this instance is going to be called through a sync wrapper or plain max_retries: number of times to retry RPC requests before giving up retry_timeout: how to long wait since the last ping to retry the RPC request + event_loop: the event loop to use + _mock: whether to use mock version of the subtensor interface """ self.max_retries = max_retries @@ -955,11 +989,18 @@ def __init__( ) self.__metadata_cache = {} self.metadata_version_hex = "0x0f000000" # v15 - self.event_loop = asyncio.get_event_loop() + self.event_loop = event_loop or asyncio.get_event_loop() self.sync_calls = sync_calls self.extrinsic_receipt_cls = ( AsyncExtrinsicReceipt if self.sync_calls is False else ExtrinsicReceipt ) + if not _mock: + execute_coroutine( + coroutine=self.initialize(), + event_loop=event_loop, + ) + else: + self.reload_type_registry() async def __aenter__(self): await self.initialize() @@ -2585,7 +2626,7 @@ async def supports_rpc_method(self, name: str) -> bool: ------- bool """ - result = await self.rpc_request("rpc_methods", []).get("result") + result = (await self.rpc_request("rpc_methods", [])).get("result") if result: self.config["rpc_methods"] = result.get("methods", []) @@ -3206,7 +3247,7 @@ async def get_account_nonce(self, account_address: str) -> int: nonce_obj = await self.runtime_call( "AccountNonceApi", "account_nonce", [account_address] ) - return nonce_obj + return getattr(nonce_obj, "value", nonce_obj) else: response = await self.query( module="System", storage_function="Account", params=[account_address] @@ -3463,7 +3504,10 @@ async def query( runtime, result_handler=subscription_handler, ) - return responses[preprocessed.queryable][0] + result = responses[preprocessed.queryable][0] + if isinstance(result, (list, tuple, int, float)): + return ScaleObj(result) + return result async def query_map( self, @@ -3866,6 +3910,15 @@ async def _handler(block_data: dict[str, Any]): return await co +class SyncWebsocket: + def __init__(self, websocket: "Websocket", event_loop: asyncio.AbstractEventLoop): + self._ws = websocket + self._event_loop = event_loop + + def close(self): + execute_coroutine(self._ws.shutdown(), event_loop=self._event_loop) + + class SubstrateInterface: """ A wrapper around AsyncSubstrateInterface that allows for using all the calls from it in a synchronous context @@ -3880,25 +3933,42 @@ def __init__( type_registry: Optional[dict] = None, chain_name: Optional[str] = None, event_loop: Optional[asyncio.AbstractEventLoop] = None, - mock: bool = False, + _mock: bool = False, + substrate: Optional["AsyncSubstrateInterface"] = None, ): - self._async_instance = AsyncSubstrateInterface( - url=url, - use_remote_preset=use_remote_preset, - auto_discover=auto_discover, - ss58_format=ss58_format, - type_registry=type_registry, - chain_name=chain_name, - sync_calls=True, + event_loop = substrate.event_loop if substrate else event_loop + self.url = url + self._async_instance = ( + AsyncSubstrateInterface( + url=url, + use_remote_preset=use_remote_preset, + auto_discover=auto_discover, + ss58_format=ss58_format, + type_registry=type_registry, + chain_name=chain_name, + sync_calls=True, + event_loop=event_loop, + _mock=_mock, + ) + if not substrate + else substrate ) self.event_loop = event_loop or asyncio.get_event_loop() - if not mock: - self.event_loop.run_until_complete(self._async_instance.initialize()) - else: - self._async_instance.reload_type_registry() + self.websocket = SyncWebsocket(self._async_instance.ws, self.event_loop) + + @property + def last_block_hash(self): + return self._async_instance.last_block_hash + + @property + def metadata(self): + return self._async_instance.metadata def __del__(self): - self.event_loop.run_until_complete(self._async_instance.close()) + execute_coroutine(self._async_instance.close()) + + def _run(self, coroutine): + return execute_coroutine(coroutine, self.event_loop) def __getattr__(self, name): attr = getattr(self._async_instance, name) @@ -3906,12 +3976,12 @@ def __getattr__(self, name): if asyncio.iscoroutinefunction(attr): def sync_method(*args, **kwargs): - return self.event_loop.run_until_complete(attr(*args, **kwargs)) + return self._run(attr(*args, **kwargs)) return sync_method elif asyncio.iscoroutine(attr): # indicates this is an async_property - return self.event_loop.run_until_complete(attr) + return self._run(attr) else: return attr @@ -3925,7 +3995,7 @@ def query( subscription_handler=None, reuse_block_hash: bool = False, ) -> "ScaleType": - return self.event_loop.run_until_complete( + return self._run( self._async_instance.query( module, storage_function, @@ -3944,8 +4014,155 @@ def get_constant( block_hash: Optional[str] = None, reuse_block_hash: bool = False, ) -> Optional["ScaleType"]: - return self.event_loop.run_until_complete( + return self._run( self._async_instance.get_constant( module_name, constant_name, block_hash, reuse_block_hash ) ) + + def submit_extrinsic( + self, + extrinsic: GenericExtrinsic, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + ) -> "ExtrinsicReceipt": + return self._run( + self._async_instance.submit_extrinsic( + extrinsic, wait_for_inclusion, wait_for_finalization + ) + ) + + def close(self): + return self._run(self._async_instance.close()) + + def create_scale_object( + self, + type_string: str, + data: Optional[ScaleBytes] = None, + block_hash: Optional[str] = None, + **kwargs, + ) -> "ScaleType": + return self._run( + self._async_instance.create_scale_object( + type_string, data, block_hash, **kwargs + ) + ) + + def rpc_request( + self, + method: str, + params: Optional[list], + block_hash: Optional[str] = None, + reuse_block_hash: bool = False, + ) -> Any: + return self._run( + self._async_instance.rpc_request( + method, params, block_hash, reuse_block_hash + ) + ) + + def get_block_number(self, block_hash: Optional[str] = None) -> int: + return self._run(self._async_instance.get_block_number(block_hash)) + + def create_signed_extrinsic( + self, + call: GenericCall, + keypair: Keypair, + era: Optional[dict] = None, + nonce: Optional[int] = None, + tip: int = 0, + tip_asset_id: Optional[int] = None, + signature: Optional[Union[bytes, str]] = None, + ) -> "GenericExtrinsic": + return self._run( + self._async_instance.create_signed_extrinsic( + call, keypair, era, nonce, tip, tip_asset_id, signature + ) + ) + + def compose_call( + self, + call_module: str, + call_function: str, + call_params: Optional[dict] = None, + block_hash: Optional[str] = None, + ) -> GenericCall: + return self._run( + self._async_instance.compose_call( + call_module, call_function, call_params, block_hash + ) + ) + + def get_block_hash(self, block_id: int) -> str: + return self._run(self._async_instance.get_block_hash(block_id)) + + def get_payment_info(self, call: GenericCall, keypair: Keypair) -> dict[str, Any]: + return self._run(self._async_instance.get_payment_info(call, keypair)) + + def get_chain_head(self) -> str: + return self._run(self._async_instance.get_chain_head()) + + def get_events(self, block_hash: Optional[str] = None) -> list: + return self._run(self._async_instance.get_events(block_hash)) + + def query_map( + self, + module: str, + storage_function: str, + params: Optional[list] = None, + block_hash: Optional[str] = None, + max_results: Optional[int] = None, + start_key: Optional[str] = None, + page_size: int = 100, + ignore_decoding_errors: bool = False, + reuse_block_hash: bool = False, + ) -> "QueryMapResult": + return self._run( + self._async_instance.query_map( + module, + storage_function, + params, + block_hash, + max_results, + start_key, + page_size, + ignore_decoding_errors, + reuse_block_hash, + ) + ) + + def query_multi( + self, storage_keys: list[StorageKey], block_hash: Optional[str] = None + ) -> list: + return self._run(self._async_instance.query_multi(storage_keys, block_hash)) + + def get_block( + self, + block_hash: Optional[str] = None, + block_number: Optional[int] = None, + ignore_decoding_errors: bool = False, + include_author: bool = False, + finalized_only: bool = False, + ) -> Optional[dict]: + return self._run( + self._async_instance.get_block( + block_hash, + block_number, + ignore_decoding_errors, + include_author, + finalized_only, + ) + ) + + def create_storage_key( + self, + pallet: str, + storage_function: str, + params: Optional[list] = None, + block_hash: str = None, + ) -> StorageKey: + return self._run( + self._async_instance.create_storage_key( + pallet, storage_function, params, block_hash + ) + ) diff --git a/tests/e2e_tests/test_commit_reveal_v3.py b/tests/e2e_tests/test_commit_reveal_v3.py index 4c038c3764..b5c9267ff5 100644 --- a/tests/e2e_tests/test_commit_reveal_v3.py +++ b/tests/e2e_tests/test_commit_reveal_v3.py @@ -1,4 +1,5 @@ import re +import time import numpy as np import pytest @@ -192,8 +193,11 @@ async def test_commit_and_reveal_weights_cr3(local_chain): ) # Fetch weights on the chain as they should be revealed now - revealed_weights = subtensor.weights(netuid=netuid)[0][1] + revealed_weights_ = subtensor.weights(netuid=netuid) + time.sleep(10) + print("revealed weights", revealed_weights_) + revealed_weights = revealed_weights_[0][1] # Assert correct weights were revealed assert weight_uids[0] == revealed_weights[0][0] assert weight_vals[0] == revealed_weights[0][1] diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index e5510038b7..90ab7a9303 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -190,7 +190,6 @@ async def test_commit_weights_uses_next_nonce(local_chain): AssertionError: If any of the checks or verifications fail """ netuid = 1 - utils.EXTRINSIC_SUBMISSION_TIMEOUT = 12 # handle fast blocks print("Testing test_commit_and_reveal_weights") # Register root as Alice keypair, alice_wallet = setup_wallet("//Alice") diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index cfafef42b5..d7a063cffa 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -15,8 +15,7 @@ templates_repo, ) from bittensor.utils.balance import Balance -from bittensor.core.extrinsics import utils -from bittensor.core.extrinsics.set_weights import do_set_weights +from bittensor.core.extrinsics.asyncex.weights import _do_set_weights from bittensor.core.metagraph import Metagraph @@ -41,8 +40,6 @@ async def test_incentive(local_chain): print("Testing test_incentive") netuid = 1 - utils.EXTRINSIC_SUBMISSION_TIMEOUT = 12 # handle fast blocks - # Register root as Alice - the subnet owner and validator alice_keypair, alice_wallet = setup_wallet("//Alice") register_subnet(local_chain, alice_wallet) @@ -159,8 +156,8 @@ async def test_incentive(local_chain): await wait_epoch(subtensor) # Set weights by Alice on the subnet - do_set_weights( - self=subtensor, + await _do_set_weights( + subtensor=subtensor.async_subtensor, wallet=alice_wallet, uids=[1], vals=[65535], diff --git a/tests/e2e_tests/test_root_set_weights.py b/tests/e2e_tests/test_root_set_weights.py index e23ccfc05d..46fe6bbfe3 100644 --- a/tests/e2e_tests/test_root_set_weights.py +++ b/tests/e2e_tests/test_root_set_weights.py @@ -8,7 +8,7 @@ wait_epoch, sudo_set_hyperparameter_values, ) -from bittensor.core.extrinsics.root import _do_set_root_weights +from bittensor.core.extrinsics.asyncex.root import _do_set_root_weights from tests.e2e_tests.utils.e2e_test_utils import ( setup_wallet, template_path, @@ -154,11 +154,11 @@ async def test_root_reg_hyperparams(local_chain): await wait_epoch(subtensor) # Set root weights to root network (0) and sn 1 - assert _do_set_root_weights( - subtensor, + assert await _do_set_root_weights( + subtensor=subtensor.async_subtensor, wallet=alice_wallet, - uids=[0, 1], - vals=weights, + netuids=[0, 1], + weights=weights, netuid=0, version_key=0, wait_for_inclusion=True, diff --git a/tests/e2e_tests/test_set_weights.py b/tests/e2e_tests/test_set_weights.py index edf208ba8d..75163ff55b 100644 --- a/tests/e2e_tests/test_set_weights.py +++ b/tests/e2e_tests/test_set_weights.py @@ -33,7 +33,6 @@ async def test_set_weights_uses_next_nonce(local_chain): AssertionError: If any of the checks or verifications fail """ netuids = [1, 2] - utils.EXTRINSIC_SUBMISSION_TIMEOUT = 12 # handle fast blocks print("Testing test_set_weights_uses_next_nonce") # Register root as Alice keypair, alice_wallet = setup_wallet("//Alice") diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 4f6805f6d1..3255debc30 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -37,7 +37,6 @@ def sudo_set_hyperparameter_bool( wait_for_inclusion=True, wait_for_finalization=True, ) - response.process_events() return response.is_success @@ -62,7 +61,6 @@ def sudo_set_hyperparameter_values( wait_for_inclusion=True, wait_for_finalization=True, ) - response.process_events() if return_error_message: return response.is_success, response.error_message @@ -87,7 +85,6 @@ def add_stake( response = substrate.submit_extrinsic( extrinsic, wait_for_finalization=True, wait_for_inclusion=True ) - response.process_events() return response.is_success @@ -106,7 +103,6 @@ def register_subnet(substrate: "SubstrateInterface", wallet: "Wallet") -> bool: response = substrate.submit_extrinsic( extrinsic, wait_for_finalization=True, wait_for_inclusion=True ) - response.process_events() return response.is_success @@ -216,7 +212,6 @@ def sudo_set_admin_utils( wait_for_inclusion=True, wait_for_finalization=True, ) - response.process_events() if return_error_message: return response.is_success, response.error_message @@ -246,7 +241,6 @@ async def root_set_subtensor_hyperparameter_values( wait_for_inclusion=True, wait_for_finalization=True, ) - response.process_events() if return_error_message: return response.is_success, response.error_message diff --git a/tests/integration_tests/test_metagraph_integration.py b/tests/integration_tests/test_metagraph_integration.py index 34bf4f590e..4ec58285ee 100644 --- a/tests/integration_tests/test_metagraph_integration.py +++ b/tests/integration_tests/test_metagraph_integration.py @@ -14,6 +14,7 @@ # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +from unittest import mock import bittensor import torch @@ -48,13 +49,19 @@ def test_sync_block_0(self): self.metagraph.sync(lite=True, block=0, subtensor=self.sub) def test_load_sync_save(self): - self.metagraph.sync(lite=True, subtensor=self.sub) - self.metagraph.save() - self.metagraph.load() - self.metagraph.save() + with mock.patch.object( + self.sub.async_subtensor, "neurons_lite", return_value=[] + ): + self.metagraph.sync(lite=True, subtensor=self.sub) + self.metagraph.save() + self.metagraph.load() + self.metagraph.save() def test_load_sync_save_from_torch(self): - self.metagraph.sync(lite=True, subtensor=self.sub) + with mock.patch.object( + self.sub.async_subtensor, "neurons_lite", return_value=[] + ): + self.metagraph.sync(lite=True, subtensor=self.sub) def deprecated_save_torch(metagraph): save_directory = get_save_dir(metagraph.network, metagraph.netuid) diff --git a/tests/unit_tests/extrinsics/__init__.py b/tests/unit_tests/extrinsics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/extrinsics/asyncex/__init__.py b/tests/unit_tests/extrinsics/asyncex/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/extrinsics/test_async_commit_reveal.py b/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py similarity index 94% rename from tests/unit_tests/extrinsics/test_async_commit_reveal.py rename to tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py index ce4a5ccbfa..1dd7e6aab9 100644 --- a/tests/unit_tests/extrinsics/test_async_commit_reveal.py +++ b/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py @@ -1,10 +1,11 @@ from bittensor.core import async_subtensor as subtensor_module from bittensor.core.chain_data import SubnetHyperparameters from bittensor.core.async_subtensor import AsyncSubtensor -from bittensor.core.extrinsics import async_commit_reveal +from bittensor.core.extrinsics.asyncex import commit_reveal as async_commit_reveal import pytest import torch import numpy as np +from bittensor_wallet import Wallet @pytest.fixture @@ -54,7 +55,7 @@ def hyperparams(): async def test_do_commit_reveal_v3_success(mocker, subtensor): """Test successful commit-reveal with wait for finalization.""" # Preps - fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_commit = b"fake_commit" fake_reveal_round = 1 @@ -90,7 +91,6 @@ async def test_do_commit_reveal_v3_success(mocker, subtensor): call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey ) mocked_submit_extrinsic.assert_awaited_once_with( - subtensor=subtensor, extrinsic=mocked_create_signed_extrinsic.return_value, wait_for_inclusion=False, wait_for_finalization=False, @@ -102,7 +102,7 @@ async def test_do_commit_reveal_v3_success(mocker, subtensor): async def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor): """Test commit-reveal fails due to an error in submission.""" # Preps - fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_commit = b"fake_commit" fake_reveal_round = 1 @@ -149,7 +149,6 @@ async def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor): call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey ) mocked_submit_extrinsic.assert_awaited_once_with( - subtensor=subtensor, extrinsic=mocked_create_signed_extrinsic.return_value, wait_for_inclusion=True, wait_for_finalization=True, @@ -164,7 +163,7 @@ async def test_commit_reveal_v3_extrinsic_success_with_torch( ): """Test successful commit-reveal with torch tensors.""" # Preps - fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = torch.tensor([1, 2, 3], dtype=torch.int64) fake_weights = torch.tensor([0.1, 0.2, 0.7], dtype=torch.float32) @@ -192,7 +191,9 @@ async def test_commit_reveal_v3_extrinsic_success_with_torch( async_commit_reveal, "_do_commit_reveal_v3", return_value=(True, "Success") ) mock_block = mocker.patch.object( - subtensor.substrate, "get_block_number", return_value=1 + subtensor.substrate, + "get_block", + return_value={"header": {"number": 1, "hash": "fakehash"}}, ) mock_hyperparams = mocker.patch.object( subtensor, @@ -224,7 +225,7 @@ async def test_commit_reveal_v3_extrinsic_success_with_torch( version_key=async_commit_reveal.version_as_int, tempo=mock_hyperparams.return_value.tempo, netuid=fake_netuid, - current_block=mock_block.return_value, + current_block=mock_block.return_value["header"]["number"], ) mock_do_commit_reveal_v3.assert_awaited_once_with( subtensor=subtensor, @@ -243,7 +244,7 @@ async def test_commit_reveal_v3_extrinsic_success_with_numpy( ): """Test successful commit-reveal with numpy arrays.""" # Preps - fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = np.array([1, 2, 3], dtype=np.int64) fake_weights = np.array([0.1, 0.2, 0.7], dtype=np.float32) @@ -291,7 +292,7 @@ async def test_commit_reveal_v3_extrinsic_response_false( ): """Test unsuccessful commit-reveal with torch.""" # Preps - fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = torch.tensor([1, 2, 3], dtype=torch.int64) fake_weights = torch.tensor([0.1, 0.2, 0.7], dtype=torch.float32) @@ -348,7 +349,7 @@ async def test_commit_reveal_v3_extrinsic_response_false( async def test_commit_reveal_v3_extrinsic_exception(mocker, subtensor): """Test exception handling in commit-reveal.""" # Preps - fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) + fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = [1, 2, 3] fake_weights = [0.1, 0.2, 0.7] diff --git a/tests/unit_tests/extrinsics/test_async_registration.py b/tests/unit_tests/extrinsics/asyncex/test_registration.py similarity index 99% rename from tests/unit_tests/extrinsics/test_async_registration.py rename to tests/unit_tests/extrinsics/asyncex/test_registration.py index c7788bbc81..6baffe166c 100644 --- a/tests/unit_tests/extrinsics/test_async_registration.py +++ b/tests/unit_tests/extrinsics/asyncex/test_registration.py @@ -70,7 +70,7 @@ async def test_do_pow_register_success(subtensor, mocker): call=fake_call, keypair=fake_wallet.hotkey ) subtensor.substrate.submit_extrinsic.assert_awaited_once_with( - fake_extrinsic, wait_for_inclusion=True, wait_for_finalization=True + extrinsic=fake_extrinsic, wait_for_inclusion=True, wait_for_finalization=True ) assert result is True assert error_message is None diff --git a/tests/unit_tests/extrinsics/test_async_root.py b/tests/unit_tests/extrinsics/asyncex/test_root.py similarity index 99% rename from tests/unit_tests/extrinsics/test_async_root.py rename to tests/unit_tests/extrinsics/asyncex/test_root.py index 26a86f2341..4e3a3f04a9 100644 --- a/tests/unit_tests/extrinsics/test_async_root.py +++ b/tests/unit_tests/extrinsics/asyncex/test_root.py @@ -311,7 +311,7 @@ async def test_do_set_root_weights_success(subtensor, mocker): era={"period": 5}, ) subtensor.substrate.submit_extrinsic.assert_called_once_with( - fake_extrinsic, wait_for_inclusion=True, wait_for_finalization=True + extrinsic=fake_extrinsic, wait_for_inclusion=True, wait_for_finalization=True ) assert result is True assert message == "Successfully set weights." @@ -403,7 +403,7 @@ async def test_do_set_root_weights_no_waiting(subtensor, mocker): subtensor.substrate.compose_call.assert_called_once() subtensor.substrate.create_signed_extrinsic.assert_called_once() subtensor.substrate.submit_extrinsic.assert_called_once_with( - fake_extrinsic, wait_for_inclusion=False, wait_for_finalization=False + extrinsic=fake_extrinsic, wait_for_inclusion=False, wait_for_finalization=False ) assert result is True assert message == "Not waiting for finalization or inclusion." diff --git a/tests/unit_tests/extrinsics/test_async_transfer.py b/tests/unit_tests/extrinsics/asyncex/test_transfer.py similarity index 96% rename from tests/unit_tests/extrinsics/test_async_transfer.py rename to tests/unit_tests/extrinsics/asyncex/test_transfer.py index 45e90ff751..df0e788734 100644 --- a/tests/unit_tests/extrinsics/test_async_transfer.py +++ b/tests/unit_tests/extrinsics/asyncex/test_transfer.py @@ -60,7 +60,7 @@ async def test_do_transfer_success(subtensor, mocker): call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey ) subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, + extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -119,7 +119,7 @@ async def test_do_transfer_failure(subtensor, mocker): call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey ) subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, + extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -170,7 +170,7 @@ async def test_do_transfer_no_waiting(subtensor, mocker): call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey ) subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, + extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=False, wait_for_finalization=False, ) @@ -204,7 +204,7 @@ async def test_transfer_extrinsic_success(subtensor, mocker): mocked_get_balance = mocker.patch.object( subtensor, "get_balance", - return_value={fake_wallet.coldkeypub.ss58_address: 10000}, + return_value=10000, ) mocked_get_existential_deposit = mocker.patch.object( subtensor, "get_existential_deposit", return_value=1 @@ -269,7 +269,7 @@ async def test_transfer_extrinsic_call_successful_with_failed_response( mocked_get_balance = mocker.patch.object( subtensor, "get_balance", - return_value={fake_wallet.coldkeypub.ss58_address: 10000}, + return_value=10000, ) mocked_get_existential_deposit = mocker.patch.object( subtensor, "get_existential_deposit", return_value=1 @@ -333,9 +333,7 @@ async def test_transfer_extrinsic_insufficient_balance(subtensor, mocker): mocked_get_balance = mocker.patch.object( subtensor, "get_balance", - return_value={ - fake_wallet.coldkeypub.ss58_address: 1000 - }, # Insufficient balance + return_value=1000, # Insufficient balance ) mocked_get_existential_deposit = mocker.patch.object( subtensor, "get_existential_deposit", return_value=1 @@ -465,7 +463,7 @@ async def test_transfer_extrinsic_keep_alive_false_and_transfer_all_true( mocked_get_balance = mocker.patch.object( subtensor, "get_balance", - return_value={fake_wallet.coldkeypub.ss58_address: 1}, + return_value=1, ) mocked_get_existential_deposit = mocker.patch.object( subtensor, "get_existential_deposit", return_value=1 diff --git a/tests/unit_tests/extrinsics/test_async_weights.py b/tests/unit_tests/extrinsics/asyncex/test_weights.py similarity index 100% rename from tests/unit_tests/extrinsics/test_async_weights.py rename to tests/unit_tests/extrinsics/asyncex/test_weights.py diff --git a/tests/unit_tests/extrinsics/test_commit_reveal.py b/tests/unit_tests/extrinsics/test_commit_reveal.py index e1f7c9f877..30bd7c0e63 100644 --- a/tests/unit_tests/extrinsics/test_commit_reveal.py +++ b/tests/unit_tests/extrinsics/test_commit_reveal.py @@ -1,353 +1,48 @@ -from bittensor.core import subtensor as subtensor_module -from bittensor.core.chain_data import SubnetHyperparameters -from bittensor.core.subtensor import Subtensor from bittensor.core.extrinsics import commit_reveal -import pytest -import torch -import numpy as np -@pytest.fixture -def subtensor(mocker): - fake_substrate = mocker.MagicMock() - fake_substrate.websocket.sock.getsockopt.return_value = 0 - mocker.patch.object( - subtensor_module, "SubstrateInterface", return_value=fake_substrate - ) - yield Subtensor() - - -@pytest.fixture -def hyperparams(): - yield SubnetHyperparameters( - rho=0, - kappa=0, - immunity_period=0, - min_allowed_weights=0, - max_weight_limit=0.0, - tempo=0, - min_difficulty=0, - max_difficulty=0, - weights_version=0, - weights_rate_limit=0, - adjustment_interval=0, - activity_cutoff=0, - registration_allowed=False, - target_regs_per_interval=0, - min_burn=0, - max_burn=0, - bonds_moving_avg=0, - max_regs_per_block=0, - serving_rate_limit=0, - max_validators=0, - adjustment_alpha=0, - difficulty=0, - commit_reveal_weights_interval=0, - commit_reveal_weights_enabled=True, - alpha_high=0, - alpha_low=0, - liquid_alpha_enabled=False, - ) - - -def test_do_commit_reveal_v3_success(mocker, subtensor): - """Test successful commit-reveal with wait for finalization.""" - # Preps - fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) - fake_netuid = 1 - fake_commit = b"fake_commit" - fake_reveal_round = 1 - - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") - mocked_create_signed_extrinsic = mocker.patch.object( - subtensor.substrate, "create_signed_extrinsic" - ) - mocked_submit_extrinsic = mocker.patch.object(commit_reveal, "submit_extrinsic") - - # Call - result = commit_reveal._do_commit_reveal_v3( - self=subtensor, - wallet=fake_wallet, - netuid=fake_netuid, - commit=fake_commit, - reveal_round=fake_reveal_round, - ) - - # Asserts - mocked_compose_call.assert_called_once_with( - call_module="SubtensorModule", - call_function="commit_crv3_weights", - call_params={ - "netuid": fake_netuid, - "commit": fake_commit, - "reveal_round": fake_reveal_round, - }, - ) - mocked_create_signed_extrinsic.assert_called_once_with( - call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey - ) - mocked_submit_extrinsic.assert_called_once_with( - subtensor=subtensor, - extrinsic=mocked_create_signed_extrinsic.return_value, - wait_for_inclusion=False, - wait_for_finalization=False, - ) - assert result == (True, "Not waiting for finalization or inclusion.") - - -def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor): - """Test commit-reveal fails due to an error in submission.""" - # Preps - fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) - fake_netuid = 1 - fake_commit = b"fake_commit" - fake_reveal_round = 1 - - mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") - mocked_create_signed_extrinsic = mocker.patch.object( - subtensor.substrate, "create_signed_extrinsic" - ) - mocked_submit_extrinsic = mocker.patch.object( - commit_reveal, - "submit_extrinsic", - return_value=mocker.Mock(is_success=False, error_message="Mocked error"), - ) - mocked_format_error_message = mocker.patch.object( - commit_reveal, "format_error_message", return_value="Formatted error" - ) - - # Call - result = commit_reveal._do_commit_reveal_v3( - self=subtensor, - wallet=fake_wallet, - netuid=fake_netuid, - commit=fake_commit, - reveal_round=fake_reveal_round, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # Asserts - mocked_compose_call.assert_called_once_with( - call_module="SubtensorModule", - call_function="commit_crv3_weights", - call_params={ - "netuid": fake_netuid, - "commit": fake_commit, - "reveal_round": fake_reveal_round, - }, - ) - mocked_create_signed_extrinsic.assert_called_once_with( - call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey - ) - mocked_submit_extrinsic.assert_called_once_with( - subtensor=subtensor, - extrinsic=mocked_create_signed_extrinsic.return_value, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - mocked_format_error_message.assert_called_once_with("Mocked error") - assert result == (False, "Formatted error") - - -def test_commit_reveal_v3_extrinsic_success_with_torch(mocker, subtensor, hyperparams): - """Test successful commit-reveal with torch tensors.""" - # Preps - fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) - fake_netuid = 1 - fake_uids = torch.tensor([1, 2, 3], dtype=torch.int64) - fake_weights = torch.tensor([0.1, 0.2, 0.7], dtype=torch.float32) - fake_commit_for_reveal = b"mock_commit_for_reveal" - fake_reveal_round = 1 - - # Mocks - - mocked_uids = mocker.Mock() - mocked_weights = mocker.Mock() - mocked_convert_weights_and_uids_for_emit = mocker.patch.object( - commit_reveal, - "convert_weights_and_uids_for_emit", - return_value=(mocked_uids, mocked_weights), - ) - mocked_get_subnet_reveal_period_epochs = mocker.patch.object( - subtensor, "get_subnet_reveal_period_epochs" - ) - mocked_get_encrypted_commit = mocker.patch.object( - commit_reveal, - "get_encrypted_commit", - return_value=(fake_commit_for_reveal, fake_reveal_round), - ) - mock_do_commit_reveal_v3 = mocker.patch.object( - commit_reveal, "_do_commit_reveal_v3", return_value=(True, "Success") - ) - mock_block = mocker.patch.object(subtensor, "get_current_block", return_value=1) - mock_hyperparams = mocker.patch.object( - subtensor, - "get_subnet_hyperparameters", - return_value=hyperparams, - ) - - # Call - success, message = commit_reveal.commit_reveal_v3_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=fake_netuid, - uids=fake_uids, - weights=fake_weights, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # Asserts - assert success is True - assert message == "reveal_round:1" - mocked_convert_weights_and_uids_for_emit.assert_called_once_with( - fake_uids, fake_weights - ) - mocked_get_encrypted_commit.assert_called_once_with( - uids=mocked_uids, - weights=mocked_weights, - subnet_reveal_period_epochs=mock_hyperparams.return_value.commit_reveal_weights_interval, - version_key=commit_reveal.version_as_int, - tempo=mock_hyperparams.return_value.tempo, - netuid=fake_netuid, - current_block=mock_block.return_value, - ) - mock_do_commit_reveal_v3.assert_called_once_with( - self=subtensor, - wallet=fake_wallet, - netuid=fake_netuid, - commit=fake_commit_for_reveal, - reveal_round=fake_reveal_round, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - -def test_commit_reveal_v3_extrinsic_success_with_numpy(mocker, subtensor, hyperparams): - """Test successful commit-reveal with numpy arrays.""" - # Preps - fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) - fake_netuid = 1 - fake_uids = np.array([1, 2, 3], dtype=np.int64) - fake_weights = np.array([0.1, 0.2, 0.7], dtype=np.float32) - - mock_convert = mocker.patch.object( - commit_reveal, - "convert_weights_and_uids_for_emit", - return_value=(fake_uids, fake_weights), - ) - mock_encode_drand = mocker.patch.object( - commit_reveal, "get_encrypted_commit", return_value=(b"commit", 0) - ) - mock_do_commit = mocker.patch.object( - commit_reveal, "_do_commit_reveal_v3", return_value=(True, "Committed!") - ) - mocker.patch.object(subtensor, "get_current_block", return_value=1) - mocker.patch.object( - subtensor, - "get_subnet_hyperparameters", - return_value=hyperparams, - ) - - # Call - success, message = commit_reveal.commit_reveal_v3_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=fake_netuid, - uids=fake_uids, - weights=fake_weights, - wait_for_inclusion=False, - wait_for_finalization=False, - ) - - # Asserts - assert success is True - assert message == "reveal_round:0" - mock_convert.assert_called_once_with(fake_uids, fake_weights) - mock_encode_drand.assert_called_once() - mock_do_commit.assert_called_once() - - -def test_commit_reveal_v3_extrinsic_response_false(mocker, subtensor, hyperparams): - """Test unsuccessful commit-reveal with torch.""" +def test_commit_reveal_v3_extrinsic(mocker): + """ "Verify that sync `commit_reveal_v3_extrinsic` method calls proper async method.""" # Preps - fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) - fake_netuid = 1 - fake_uids = torch.tensor([1, 2, 3], dtype=torch.int64) - fake_weights = torch.tensor([0.1, 0.2, 0.7], dtype=torch.float32) - fake_commit_for_reveal = b"mock_commit_for_reveal" - fake_reveal_round = 1 - - # Mocks - mocker.patch.object( - commit_reveal, - "convert_weights_and_uids_for_emit", - return_value=(fake_uids, fake_weights), - ) - mocker.patch.object( - commit_reveal, - "get_encrypted_commit", - return_value=(fake_commit_for_reveal, fake_reveal_round), - ) - mock_do_commit_reveal_v3 = mocker.patch.object( - commit_reveal, "_do_commit_reveal_v3", return_value=(False, "Failed") - ) - mocker.patch.object(subtensor, "get_current_block", return_value=1) - mocker.patch.object( - subtensor, - "get_subnet_hyperparameters", - return_value=hyperparams, - ) + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() + netuid = 1 + uids = [1, 2, 3, 4] + weights = [0.1, 0.2, 0.3, 0.4] + version_key = 2 + wait_for_inclusion = True + wait_for_finalization = True + + mocked_execute_coroutine = mocker.patch.object(commit_reveal, "execute_coroutine") + mocked_commit_reveal_v3_extrinsic = mocker.Mock() + commit_reveal.async_commit_reveal_v3_extrinsic = mocked_commit_reveal_v3_extrinsic # Call - success, message = commit_reveal.commit_reveal_v3_extrinsic( - subtensor=subtensor, + result = commit_reveal.commit_reveal_v3_extrinsic( + subtensor=fake_subtensor, wallet=fake_wallet, - netuid=fake_netuid, - uids=fake_uids, - weights=fake_weights, - wait_for_inclusion=True, - wait_for_finalization=True, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) # Asserts - assert success is False - assert message == "Failed" - mock_do_commit_reveal_v3.assert_called_once_with( - self=subtensor, - wallet=fake_wallet, - netuid=fake_netuid, - commit=fake_commit_for_reveal, - reveal_round=fake_reveal_round, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - -def test_commit_reveal_v3_extrinsic_exception(mocker, subtensor): - """Test exception handling in commit-reveal.""" - # Preps - fake_wallet = mocker.Mock(autospec=subtensor_module.Wallet) - fake_netuid = 1 - fake_uids = [1, 2, 3] - fake_weights = [0.1, 0.2, 0.7] - mocker.patch.object( - commit_reveal, - "convert_weights_and_uids_for_emit", - side_effect=Exception("Test Error"), + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_commit_reveal_v3_extrinsic.return_value, + event_loop=fake_subtensor.event_loop, ) - - # Call - success, message = commit_reveal.commit_reveal_v3_extrinsic( - subtensor=subtensor, + mocked_commit_reveal_v3_extrinsic.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, wallet=fake_wallet, - netuid=fake_netuid, - uids=fake_uids, - weights=fake_weights, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) - - # Asserts - assert success is False - assert "Test Error" in message + assert result == mocked_execute_coroutine.return_value diff --git a/tests/unit_tests/extrinsics/test_commit_weights.py b/tests/unit_tests/extrinsics/test_commit_weights.py index 57d78a8013..54a0b80b74 100644 --- a/tests/unit_tests/extrinsics/test_commit_weights.py +++ b/tests/unit_tests/extrinsics/test_commit_weights.py @@ -1,41 +1,23 @@ -import pytest +from bittensor.core.extrinsics import commit_weights -from bittensor.core import subtensor as subtensor_module -from bittensor.core.settings import version_as_int -from bittensor.core.subtensor import Subtensor -from bittensor.core.extrinsics.commit_weights import ( - do_commit_weights, - do_reveal_weights, -) - -@pytest.fixture -def subtensor(mocker): - fake_substrate = mocker.MagicMock() - fake_substrate.websocket.sock.getsockopt.return_value = 0 - mocker.patch.object( - subtensor_module, "SubstrateInterface", return_value=fake_substrate - ) - return Subtensor() - - -def test_do_commit_weights(subtensor, mocker): - """Successful _do_commit_weights call.""" +def test_commit_weights_extrinsic(mocker): + """ "Verify that sync `commit_weights_extrinsic` method calls proper async method.""" # Preps - fake_wallet = mocker.MagicMock() + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() netuid = 1 - commit_hash = "fake_commit_hash" + commit_hash = "0x1234567890abcdef" wait_for_inclusion = True wait_for_finalization = True - subtensor.substrate.submit_extrinsic.return_value.is_success = None - - mocked_format_error_message = mocker.MagicMock() - subtensor_module.format_error_message = mocked_format_error_message + mocked_execute_coroutine = mocker.patch.object(commit_weights, "execute_coroutine") + mocked_commit_weights_extrinsic = mocker.Mock() + commit_weights.async_commit_weights_extrinsic = mocked_commit_weights_extrinsic # Call - result = do_commit_weights( - self=subtensor, + result = commit_weights.commit_weights_extrinsic( + subtensor=fake_subtensor, wallet=fake_wallet, netuid=netuid, commit_hash=commit_hash, @@ -43,92 +25,68 @@ def test_do_commit_weights(subtensor, mocker): wait_for_finalization=wait_for_finalization, ) - # Assertions - subtensor.substrate.compose_call.assert_called_once_with( - call_module="SubtensorModule", - call_function="commit_weights", - call_params={ - "netuid": netuid, - "commit_hash": commit_hash, - }, - ) - - subtensor.substrate.create_signed_extrinsic.assert_called_once() - _, kwargs = subtensor.substrate.create_signed_extrinsic.call_args - assert kwargs["call"] == subtensor.substrate.compose_call.return_value - assert kwargs["keypair"] == fake_wallet.hotkey + # Asserts - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_commit_weights_extrinsic.return_value, + event_loop=fake_subtensor.event_loop, + ) + mocked_commit_weights_extrinsic.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + wallet=fake_wallet, + netuid=netuid, + commit_hash=commit_hash, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - - subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - - assert result == ( - False, - subtensor.substrate.submit_extrinsic.return_value.error_message, - ) + assert result == mocked_execute_coroutine.return_value -def test_do_reveal_weights(subtensor, mocker): - """Verifies that the `_do_reveal_weights` method interacts with the right substrate methods.""" +def test_reveal_weights_extrinsic(mocker): + """Verify that sync `reveal_weights_extrinsic` method calls proper async method.""" # Preps - fake_wallet = mocker.MagicMock() - fake_wallet.hotkey = "hotkey" - + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() netuid = 1 uids = [1, 2, 3, 4] - values = [1, 2, 3, 4] - salt = [4, 2, 2, 1] + weights = [5, 6, 7, 8] + salt = [1, 2, 3, 4] + version_key = 2 wait_for_inclusion = True wait_for_finalization = True - subtensor.substrate.submit_extrinsic.return_value.is_success = None - - mocked_format_error_message = mocker.MagicMock() - subtensor_module.format_error_message = mocked_format_error_message + mocked_execute_coroutine = mocker.patch.object(commit_weights, "execute_coroutine") + mocked_reveal_weights_extrinsic = mocker.Mock() + commit_weights.async_reveal_weights_extrinsic = mocked_reveal_weights_extrinsic # Call - result = do_reveal_weights( - self=subtensor, + result = commit_weights.reveal_weights_extrinsic( + subtensor=fake_subtensor, wallet=fake_wallet, netuid=netuid, uids=uids, - values=values, + weights=weights, salt=salt, - version_key=version_as_int, + version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) # Asserts - subtensor.substrate.compose_call.assert_called_once_with( - call_module="SubtensorModule", - call_function="reveal_weights", - call_params={ - "netuid": netuid, - "uids": uids, - "values": values, - "salt": salt, - "version_key": version_as_int, - }, - ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_reveal_weights_extrinsic.return_value, + event_loop=fake_subtensor.event_loop, ) - - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, + mocked_reveal_weights_extrinsic.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + wallet=fake_wallet, + netuid=netuid, + uids=uids, + weights=weights, + salt=salt, + version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - - subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - - assert result == ( - False, - subtensor.substrate.submit_extrinsic.return_value.error_message, - ) + assert result == mocked_execute_coroutine.return_value diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py index ec21084436..99b2c3cd1d 100644 --- a/tests/unit_tests/extrinsics/test_registration.py +++ b/tests/unit_tests/extrinsics/test_registration.py @@ -1,224 +1,101 @@ -# The MIT License (MIT) -# Copyright © 2024 Opentensor Foundation -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import pytest -from bittensor_wallet import Wallet - from bittensor.core.extrinsics import registration -from bittensor.core.subtensor import Subtensor -from bittensor.utils.registration import POWSolution - - -# Mocking external dependencies -@pytest.fixture -def mock_subtensor(mocker): - mock = mocker.MagicMock(spec=Subtensor) - mock.network = "mock_network" - mock.substrate = mocker.AsyncMock() - return mock - - -@pytest.fixture -def mock_wallet(mocker): - mock = mocker.MagicMock(spec=Wallet) - mock.coldkeypub.ss58_address = "mock_address" - mock.coldkey = mocker.MagicMock() - mock.hotkey = mocker.MagicMock() - mock.hotkey.ss58_address = "fake_ss58_address" - return mock - - -@pytest.fixture -def mock_pow_solution(mocker): - mock = mocker.MagicMock(spec=POWSolution) - mock.block_number = 123 - mock.nonce = 456 - mock.seal = [0, 1, 2, 3] - mock.is_stale.return_value = False - return mock - - -@pytest.fixture -def mock_new_wallet(mocker): - mock = mocker.MagicMock(spec=Wallet) - mock.coldkeypub.ss58_address = "mock_address" - mock.coldkey = mocker.MagicMock() - mock.hotkey = mocker.MagicMock() - return mock - - -@pytest.mark.parametrize( - "subnet_exists, neuron_is_null, cuda_available, expected_result, test_id", - [ - (False, True, True, False, "subnet-does-not-exist"), - (True, False, True, True, "neuron-already-registered"), - (True, True, False, False, "cuda-unavailable"), - ], -) -def test_register_extrinsic_without_pow( - mock_subtensor, - mock_wallet, - subnet_exists, - neuron_is_null, - cuda_available, - expected_result, - test_id, - mocker, -): - # Arrange - with ( - mocker.patch.object( - mock_subtensor, "subnet_exists", return_value=subnet_exists - ), - mocker.patch.object( - mock_subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.MagicMock(is_null=neuron_is_null), - ), - mocker.patch("torch.cuda.is_available", return_value=cuda_available), - mocker.patch( - "bittensor.utils.registration.pow._get_block_with_retry", - return_value=(0, 0, "00ff11ee"), - ), - ): - # Act - result = registration.register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - wait_for_inclusion=True, - wait_for_finalization=True, - max_allowed_attempts=3, - output_in_place=True, - cuda=True, - dev_id=0, - tpb=256, - num_processes=None, - update_interval=None, - log_verbose=False, - ) - - # Assert - assert result == expected_result, f"Test failed for test_id: {test_id}" - - -@pytest.mark.parametrize( - "pow_success, pow_stale, registration_success, cuda, hotkey_registered, expected_result, test_id", - [ - (True, False, True, False, False, True, "successful-with-valid-pow"), - (True, False, True, True, False, True, "successful-with-valid-cuda-pow"), - # Pow failed but key was registered already - (False, False, False, False, True, True, "hotkey-registered"), - # Pow was a success but registration failed with error 'key already registered' - (True, False, False, False, False, True, "registration-fail-key-registered"), - ], -) -def test_register_extrinsic_with_pow( - mock_subtensor, - mock_wallet, - mock_pow_solution, - pow_success, - pow_stale, - registration_success, - cuda, - hotkey_registered, - expected_result, - test_id, - mocker, -): - # Arrange - with mocker.patch( - "bittensor.utils.registration.pow._solve_for_difficulty_fast", - return_value=mock_pow_solution if pow_success else None, - ), mocker.patch( - "bittensor.utils.registration.pow._solve_for_difficulty_fast_cuda", - return_value=mock_pow_solution if pow_success else None, - ), mocker.patch( - "bittensor.core.extrinsics.registration._do_pow_register", - return_value=(registration_success, "HotKeyAlreadyRegisteredInSubNet"), - ), mocker.patch("torch.cuda.is_available", return_value=cuda): - # Act - if pow_success: - mock_pow_solution.is_stale.return_value = pow_stale - - if not pow_success and hotkey_registered: - mock_subtensor.is_hotkey_registered = mocker.MagicMock( - return_value=hotkey_registered - ) - - result = registration.register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - wait_for_inclusion=True, - wait_for_finalization=True, - max_allowed_attempts=3, - output_in_place=True, - cuda=cuda, - dev_id=0, - tpb=256, - num_processes=None, - update_interval=None, - log_verbose=False, - ) - - # Assert - assert result == expected_result, f"Test failed for test_id: {test_id}." -@pytest.mark.parametrize( - "subnet_exists, neuron_is_null, recycle_success, is_registered, expected_result, test_id", - [ - # Happy paths - (True, False, None, None, True, "neuron-not-null"), - (True, True, True, True, True, "happy-path-wallet-registered"), - # Error paths - (False, True, False, None, False, "subnet-non-existence"), - (True, True, False, False, False, "error-path-recycling-failed"), - (True, True, True, False, False, "error-path-not-registered"), - ], -) -def test_burned_register_extrinsic( - mock_subtensor, - mock_wallet, - subnet_exists, - neuron_is_null, - recycle_success, - is_registered, - expected_result, - test_id, - mocker, -): - # Arrange - with mocker.patch.object( - mock_subtensor, "subnet_exists", return_value=subnet_exists - ), mocker.patch.object( - mock_subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.MagicMock(is_null=neuron_is_null), - ), mocker.patch( - "bittensor.core.extrinsics.registration._do_burned_register", - return_value=(recycle_success, "Mock error message"), - ), mocker.patch.object( - mock_subtensor, "is_hotkey_registered", return_value=is_registered - ): - # Act - result = registration.burned_register_extrinsic( - subtensor=mock_subtensor, wallet=mock_wallet, netuid=123 - ) - # Assert - assert result == expected_result, f"Test failed for test_id: {test_id}" +def test_burned_register_extrinsic(mocker): + """ "Verify that sync `burned_register_extrinsic` method calls proper async method.""" + # Preps + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() + netuid = 1 + wait_for_inclusion = True + wait_for_finalization = True + + mocked_execute_coroutine = mocker.patch.object(registration, "execute_coroutine") + mocked_burned_register_extrinsic = mocker.Mock() + registration.async_burned_register_extrinsic = mocked_burned_register_extrinsic + + # Call + result = registration.burned_register_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Asserts + + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_burned_register_extrinsic.return_value, + event_loop=fake_subtensor.event_loop, + ) + mocked_burned_register_extrinsic.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + wallet=fake_wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + assert result == mocked_execute_coroutine.return_value + + +def test_register_extrinsic(mocker): + """ "Verify that sync `register_extrinsic` method calls proper async method.""" + # Preps + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() + netuid = 1 + wait_for_inclusion = True + wait_for_finalization = True + max_allowed_attempts = 7 + output_in_place = True + cuda = True + dev_id = 5 + tpb = 12 + num_processes = 8 + update_interval = 2 + log_verbose = True + + mocked_execute_coroutine = mocker.patch.object(registration, "execute_coroutine") + mocked_register_extrinsic = mocker.Mock() + registration.async_register_extrinsic = mocked_register_extrinsic + + # Call + result = registration.register_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + max_allowed_attempts=max_allowed_attempts, + output_in_place=output_in_place, + cuda=cuda, + dev_id=dev_id, + tpb=tpb, + num_processes=num_processes, + update_interval=update_interval, + log_verbose=log_verbose, + ) + + # Asserts + + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_register_extrinsic.return_value, + event_loop=fake_subtensor.event_loop, + ) + mocked_register_extrinsic.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + wallet=fake_wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + max_allowed_attempts=max_allowed_attempts, + output_in_place=output_in_place, + cuda=cuda, + dev_id=dev_id, + tpb=tpb, + num_processes=num_processes, + update_interval=update_interval, + log_verbose=log_verbose, + ) + assert result == mocked_execute_coroutine.return_value diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py index 96d90fe09a..7fae887011 100644 --- a/tests/unit_tests/extrinsics/test_root.py +++ b/tests/unit_tests/extrinsics/test_root.py @@ -3,240 +3,81 @@ from bittensor.core.extrinsics import root -@pytest.fixture -def mock_subtensor(mocker): - mock = mocker.MagicMock(spec=Subtensor) - mock.network = "magic_mock" - return mock - +def test_root_register_extrinsic(mocker): + """Verify that sync `root_register_extrinsic` method calls proper async method.""" + # Preps + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() + wait_for_inclusion = True + wait_for_finalization = True -@pytest.fixture -def mock_wallet(mocker): - mock = mocker.MagicMock() - mock.hotkey.ss58_address = "fake_hotkey_address" - return mock + mocked_execute_coroutine = mocker.patch.object(root, "execute_coroutine") + mocked_root_register_extrinsic = mocker.Mock() + root.async_root_register_extrinsic = mocked_root_register_extrinsic + # Call + result = root.root_register_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, hotkey_registered, registration_success, expected_result", - [ - ( - False, - True, - [True, None], - True, - True, - ), # Already registered after attempt - ( - False, - True, - [False, True], - True, - True, - ), # Registration succeeds with user confirmation - (False, True, [False, False], False, None), # Registration fails - ( - False, - True, - [False, False], - True, - None, - ), # Registration succeeds but neuron not found - ], - ids=[ - "success-already-registered", - "success-registration-succeeds", - "failure-registration-failed", - "failure-neuron-not-found", - ], -) -def test_root_register_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - hotkey_registered, - registration_success, - expected_result, - mocker, -): - # Arrange - mock_subtensor.is_hotkey_registered.side_effect = hotkey_registered + # Asserts - # Preps - mock_register = mocker.Mock( - return_value=(registration_success, "Error registering") + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_root_register_extrinsic.return_value, + event_loop=fake_subtensor.event_loop, ) - root._do_root_register = mock_register - - # Act - result = root.root_register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, + mocked_root_register_extrinsic.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + wallet=fake_wallet, + netuid=0, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - # Assert - assert result == expected_result - - if not hotkey_registered[0]: - mock_register.assert_called_once() + assert result == mocked_execute_coroutine.return_value -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, netuids, weights, expected_success", - [ - (True, False, [1, 2], [0.5, 0.5], True), # Success - weights set - ( - False, - False, - [1, 2], - [0.5, 0.5], - True, - ), # Success - weights set no wait - ( - True, - False, - [1, 2], - [2000, 20], - True, - ), # Success - large value to be normalized - ( - True, - False, - [1, 2], - [2000, 0], - True, - ), # Success - single large value - ( - True, - False, - [1, 2], - [0.5, 0.5], - False, - ), # Failure - setting weights failed - ( - True, - False, - [], - [], - False, - ), # Exception catched - ValueError 'min() arg is an empty sequence' - ], - ids=[ - "success-weights-set", - "success-not-wait", - "success-large-value", - "success-single-value", - "failure-setting-weights", - "failure-value-error-exception", - ], -) -def test_set_root_weights_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - netuids, - weights, - expected_success, - mocker, -): +def test_set_root_weights_extrinsic(mocker): + """Verify that sync `set_root_weights_extrinsic` method calls proper async method.""" # Preps - root._do_set_root_weights = mocker.Mock( - return_value=(expected_success, "Mock error") - ) - mock_subtensor.min_allowed_weights = mocker.Mock(return_value=0) - mock_subtensor.max_weight_limit = mocker.Mock(return_value=1) + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() + netuids = [1, 2, 3, 4] + weights = [0.1, 0.2, 0.3, 0.4] + version_key = 2 + wait_for_inclusion = True + wait_for_finalization = True + + mocked_execute_coroutine = mocker.patch.object(root, "execute_coroutine") + mocked_set_root_weights_extrinsic = mocker.Mock() + root.async_set_root_weights_extrinsic = mocked_set_root_weights_extrinsic # Call result = root.set_root_weights_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, + subtensor=fake_subtensor, + wallet=fake_wallet, netuids=netuids, weights=weights, - version_key=0, + version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) # Asserts - assert result == expected_success - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, netuids, weights, user_response, expected_success", - [ - (True, False, [1, 2], [0.5, 0.5], True, True), # Success - weights set - ( - False, - False, - [1, 2], - [0.5, 0.5], - None, - True, - ), # Success - weights set no wait - ( - True, - False, - [1, 2], - [2000, 20], - True, - True, - ), # Success - large value to be normalized - ( - True, - False, - [1, 2], - [2000, 0], - True, - True, - ), # Success - single large value - ( - True, - False, - [1, 2], - [0.5, 0.5], - None, - False, - ), # Failure - setting weights failed - ( - True, - False, - [], - [], - False, - False, - ), # Exception catched - ValueError 'min() arg is an empty sequence' - ], - ids=[ - "success-weights-set", - "success-not-wait", - "success-large-value", - "success-single-value", - "failure-setting-weights", - "failure-value-error-exception", - ], -) -def test_set_root_weights_extrinsic_torch( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - netuids, - weights, - user_response, - expected_success, - force_legacy_torch_compatible_api, - mocker, -): - test_set_root_weights_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - netuids, - weights, - expected_success, - mocker, + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_set_root_weights_extrinsic.return_value, + event_loop=fake_subtensor.event_loop, + ) + mocked_set_root_weights_extrinsic.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + wallet=fake_wallet, + netuids=netuids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) + assert result == mocked_execute_coroutine.return_value diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index 46eef17888..27b11ede1f 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -1,376 +1,158 @@ -# The MIT License (MIT) -# Copyright © 2024 Opentensor Foundation -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -from unittest.mock import MagicMock, patch - -import pytest -from bittensor_wallet import Wallet - -from bittensor.core.axon import Axon -from bittensor.core.subtensor import Subtensor from bittensor.core.extrinsics import serving -@pytest.fixture -def mock_subtensor(mocker): - mock_subtensor = mocker.MagicMock(spec=Subtensor) - mock_subtensor.network = "test_network" - mock_subtensor.substrate = mocker.MagicMock() - return mock_subtensor - - -@pytest.fixture -def mock_wallet(mocker): - wallet = mocker.MagicMock(spec=Wallet) - wallet.hotkey.ss58_address = "hotkey_address" - wallet.coldkeypub.ss58_address = "coldkey_address" - return wallet - - -@pytest.fixture -def mock_axon(mock_wallet, mocker): - axon = mocker.MagicMock(spec=Axon) - axon.wallet = mock_wallet() - axon.external_port = 9221 - return axon - - -@pytest.mark.parametrize( - "ip,port,protocol,netuid,placeholder1,placeholder2,wait_for_inclusion,wait_for_finalization,expected,test_id,", - [ - ( - "192.168.1.1", - 9221, - 1, - 0, - 0, - 0, - False, - True, - True, - "happy-path-no-wait", - ), - ( - "192.168.1.2", - 9222, - 2, - 1, - 1, - 1, - True, - False, - True, - "happy-path-wait-for-inclusion", - ), - ( - "192.168.1.3", - 9223, - 3, - 2, - 2, - 2, - False, - True, - True, - "happy-path-wait-for-finalization", - ), - ], - ids=[ - "happy-path-no-wait", - "happy-path-wait-for-inclusion", - "happy-path-wait-for-finalization", - ], -) -def test_serve_extrinsic_happy_path( - mock_subtensor, - mock_wallet, - ip, - port, - protocol, - netuid, - placeholder1, - placeholder2, - wait_for_inclusion, - wait_for_finalization, - expected, - test_id, - mocker, -): - # Arrange - serving.do_serve_axon = mocker.MagicMock(return_value=(True, "")) - # Act - result = serving.serve_extrinsic( - mock_subtensor, - mock_wallet, - ip, - port, - protocol, - netuid, - placeholder1, - placeholder2, - wait_for_inclusion, - wait_for_finalization, +def test_do_serve_axon(mocker): + """Verify that sync `do_serve_axon` method calls proper async method.""" + # Preps + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() + call_params = mocker.Mock() + wait_for_inclusion = True + wait_for_finalization = True + + mocked_execute_coroutine = mocker.patch.object(serving, "execute_coroutine") + mocked_do_serve_axon = mocker.Mock() + serving.async_do_serve_axon = mocked_do_serve_axon + + # Call + result = serving.do_serve_axon( + self=fake_subtensor, + wallet=fake_wallet, + call_params=call_params, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) - # Assert - assert result == expected, f"Test ID: {test_id}" + # Asserts - -# Various edge cases -@pytest.mark.parametrize( - "ip,port,protocol,netuid,placeholder1,placeholder2,wait_for_inclusion,wait_for_finalization,expected,test_id,", - [ - ( - "192.168.1.4", - 9224, - 4, - 3, - 3, - 3, - True, - True, - True, - "edge_case_max_values", - ), - ], - ids=["edge-case-max-values"], -) -def test_serve_extrinsic_edge_cases( - mock_subtensor, - mock_wallet, - ip, - port, - protocol, - netuid, - placeholder1, - placeholder2, - wait_for_inclusion, - wait_for_finalization, - expected, - test_id, - mocker, -): - # Arrange - serving.do_serve_axon = mocker.MagicMock(return_value=(True, "")) - # Act - result = serving.serve_extrinsic( - mock_subtensor, - mock_wallet, - ip, - port, - protocol, - netuid, - placeholder1, - placeholder2, - wait_for_inclusion, - wait_for_finalization, + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_do_serve_axon.return_value, + event_loop=fake_subtensor.event_loop, ) - - # Assert - assert result == expected, f"Test ID: {test_id}" - - -# Various error cases -@pytest.mark.parametrize( - "ip,port,protocol,netuid,placeholder1,placeholder2,wait_for_inclusion,wait_for_finalization,expected_error_message,test_id,", - [ - ( - "192.168.1.5", - 9225, - 5, - 4, - 4, - 4, - True, - True, - False, - "error-case-failed-serve", - ), - ], - ids=["error-case-failed-serve"], -) -def test_serve_extrinsic_error_cases( - mock_subtensor, - mock_wallet, - ip, - port, - protocol, - netuid, - placeholder1, - placeholder2, - wait_for_inclusion, - wait_for_finalization, - expected_error_message, - test_id, - mocker, -): - # Arrange - serving.do_serve_axon = mocker.MagicMock(return_value=(False, "Error serving axon")) - # Act - result = serving.serve_extrinsic( - mock_subtensor, - mock_wallet, - ip, - port, - protocol, - netuid, - placeholder1, - placeholder2, - wait_for_inclusion, - wait_for_finalization, + mocked_do_serve_axon.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + wallet=fake_wallet, + call_params=call_params, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + assert result == mocked_execute_coroutine.return_value + + +def test_serve_axon_extrinsic(mocker): + """Verify that sync `serve_axon_extrinsic` method calls proper async method.""" + # Preps + fake_subtensor = mocker.Mock() + netuid = 2 + axon = mocker.Mock() + wait_for_inclusion = True + wait_for_finalization = True + certificate = mocker.Mock() + + mocked_execute_coroutine = mocker.patch.object(serving, "execute_coroutine") + mocked_serve_axon_extrinsic = mocker.Mock() + serving.async_serve_axon_extrinsic = mocked_serve_axon_extrinsic + + # Call + result = serving.serve_axon_extrinsic( + subtensor=fake_subtensor, + netuid=netuid, + axon=axon, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + certificate=certificate, ) - # Assert - assert result == expected_error_message, f"Test ID: {test_id}" - + # Asserts -@pytest.mark.parametrize( - "netuid, wait_for_inclusion, wait_for_finalization, external_ip, external_ip_success, serve_success, expected_result, test_id", - [ - # Happy path test - (1, False, True, "192.168.1.1", True, True, True, "happy-ext-ip"), - (1, False, True, None, True, True, True, "happy-net-external-ip"), - # Edge cases - (1, True, True, "192.168.1.1", True, True, True, "edge-case-wait"), - # Error cases - (1, False, True, None, False, True, False, "error-fetching-external-ip"), - ( - 1, - False, - True, - "192.168.1.1", - True, - False, - False, - "error-serving-axon", - ), - ], - ids=[ - "happy-axon-external-ip", - "happy-net-external-ip", - "edge-case-wait", - "error-fetching-external-ip", - "error-serving-axon", - ], -) -def test_serve_axon_extrinsic( - mock_subtensor, - mock_axon, - netuid, - wait_for_inclusion, - wait_for_finalization, - external_ip, - external_ip_success, - serve_success, - expected_result, - test_id, - mocker, -): - mock_axon.external_ip = external_ip - # Arrange - with patch( - "bittensor.utils.networking.get_external_ip", - side_effect=Exception("Failed to fetch IP") - if not external_ip_success - else MagicMock(return_value="192.168.1.1"), - ): - serving.do_serve_axon = mocker.MagicMock(return_value=(serve_success, "")) - # Act - if not external_ip_success: - with pytest.raises(RuntimeError): - serving.serve_axon_extrinsic( - mock_subtensor, - netuid, - mock_axon, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - else: - result = serving.serve_axon_extrinsic( - mock_subtensor, - netuid, - mock_axon, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_serve_axon_extrinsic.return_value, + event_loop=fake_subtensor.event_loop, + ) + mocked_serve_axon_extrinsic.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + netuid=netuid, + axon=axon, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + certificate=certificate, + ) + assert result == mocked_execute_coroutine.return_value + + +def test_publish_metadata(mocker): + """Verify that `publish_metadata` calls proper async method.""" + # Preps + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() + netuid = 2 + data_type = "data_type" + data = b"data" + wait_for_inclusion = True + wait_for_finalization = True + + mocked_execute_coroutine = mocker.patch.object(serving, "execute_coroutine") + mocked_publish_metadata = mocker.Mock() + serving.async_publish_metadata = mocked_publish_metadata + + # Call + result = serving.publish_metadata( + self=fake_subtensor, + wallet=fake_wallet, + netuid=netuid, + data_type=data_type, + data=data, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) - # Assert - assert result == expected_result, f"Test ID: {test_id}" + # Asserts + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_publish_metadata.return_value, + event_loop=fake_subtensor.event_loop, + ) + mocked_publish_metadata.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + wallet=fake_wallet, + netuid=netuid, + data_type=data_type, + data=data, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + assert result == mocked_execute_coroutine.return_value + + +def test_get_metadata(mocker): + """Verify that `get_metadata` calls proper async method.""" + # Preps + fake_subtensor = mocker.Mock() + netuid = 2 + hotkey = "hotkey" + block = 123 + + mocked_execute_coroutine = mocker.patch.object(serving, "execute_coroutine") + mocked_get_metadata = mocker.Mock() + serving.async_get_metadata = mocked_get_metadata + + # Call + result = serving.get_metadata( + self=fake_subtensor, + netuid=netuid, + hotkey=hotkey, + block=block, + ) -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, net_uid, type_u, data, response_success, expected_result, test_id", - [ - ( - True, - True, - 1, - "Sha256", - b"mock_bytes_data", - True, - True, - "happy-path-wait", - ), - ( - False, - False, - 1, - "Sha256", - b"mock_bytes_data", - True, - True, - "happy-path-no-wait", - ), - ], - ids=["happy-path-wait", "happy-path-no-wait"], -) -def test_publish_metadata( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - net_uid, - type_u, - data, - response_success, - expected_result, - test_id, -): - # Arrange - with patch.object(mock_subtensor.substrate, "compose_call"), patch.object( - mock_subtensor.substrate, "create_signed_extrinsic" - ), patch.object( - mock_subtensor.substrate, - "submit_extrinsic", - return_value=MagicMock( - is_success=response_success, - process_events=MagicMock(), - error_message="error", - ), - ): - # Act - result = serving.publish_metadata( - self=mock_subtensor, - wallet=mock_wallet, - netuid=net_uid, - data_type=type_u, - data=data, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # Assert - assert result == expected_result, f"Test ID: {test_id}" + # Asserts + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_get_metadata.return_value, + event_loop=fake_subtensor.event_loop, + ) + mocked_get_metadata.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + netuid=netuid, + hotkey=hotkey, + block=block, + ) + assert result == mocked_execute_coroutine.return_value diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index 6c070bf5c4..116065463f 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -1,250 +1,47 @@ -from unittest.mock import MagicMock, patch +from bittensor.core.extrinsics import set_weights -import pytest -import torch -from bittensor_wallet import Wallet -from bittensor.core import subtensor as subtensor_module -from bittensor.core.extrinsics.set_weights import ( - do_set_weights, - set_weights_extrinsic, -) -from bittensor.core.settings import version_as_int -from bittensor.core.subtensor import Subtensor +def test_set_weights_extrinsic(mocker): + """ "Verify that sync `set_weights_extrinsic` method calls proper async method.""" + # Preps + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() + netuid = 2 + uids = [1, 2, 3, 4] + weights = [0.1, 0.2, 0.3, 0.4] + version_key = 2 + wait_for_inclusion = True + wait_for_finalization = True - -@pytest.fixture -def mock_subtensor(): - mock = MagicMock(spec=Subtensor) - mock.network = "mock_network" - mock.substrate = MagicMock() - return mock - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock(spec=Wallet) - return mock - - -@pytest.mark.parametrize( - "uids, weights, version_key, wait_for_inclusion, wait_for_finalization, expected_success, expected_message", - [ - ( - [1, 2], - [0.5, 0.5], - 0, - True, - False, - True, - "Successfully set weights and Finalized.", - ), - ( - [1, 2], - [0.5, 0.4], - 0, - False, - False, - True, - "Not waiting for finalization or inclusion.", - ), - ( - [1, 2], - [0.5, 0.5], - 0, - True, - False, - False, - "Mock error message", - ), - ], - ids=[ - "happy-flow", - "not-waiting-finalization-inclusion", - "error-flow", - ], -) -def test_set_weights_extrinsic( - mock_subtensor, - mock_wallet, - uids, - weights, - version_key, - wait_for_inclusion, - wait_for_finalization, - expected_success, - expected_message, -): - uids_tensor = torch.tensor(uids, dtype=torch.int64) - weights_tensor = torch.tensor(weights, dtype=torch.float32) - with patch( - "bittensor.utils.weight_utils.convert_weights_and_uids_for_emit", - return_value=(uids_tensor, weights_tensor), - ), patch( - "bittensor.core.extrinsics.set_weights.do_set_weights", - return_value=(expected_success, "Mock error message"), - ) as mock_do_set_weights: - result, message = set_weights_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - assert result == expected_success, f"Test {expected_message} failed." - assert message == expected_message, f"Test {expected_message} failed." - - -def test_do_set_weights_is_success(mock_subtensor, mocker): - """Successful _do_set_weights call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_uids = [1, 2, 3] - fake_vals = [4, 5, 6] - fake_netuid = 1 - fake_wait_for_inclusion = True - fake_wait_for_finalization = True - - mock_subtensor.substrate.submit_extrinsic.return_value.is_success = True + mocked_execute_coroutine = mocker.patch.object(set_weights, "execute_coroutine") + mocked_set_weights_extrinsic = mocker.Mock() + set_weights.async_set_weights_extrinsic = mocked_set_weights_extrinsic # Call - result = do_set_weights( - self=mock_subtensor, + result = set_weights.set_weights_extrinsic( + subtensor=fake_subtensor, wallet=fake_wallet, - uids=fake_uids, - vals=fake_vals, - netuid=fake_netuid, - version_key=version_as_int, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) # Asserts - mock_subtensor.substrate.compose_call.assert_called_once_with( - call_module="SubtensorModule", - call_function="set_weights", - call_params={ - "dests": fake_uids, - "weights": fake_vals, - "netuid": fake_netuid, - "version_key": version_as_int, - }, + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_set_weights_extrinsic.return_value, + event_loop=fake_subtensor.event_loop, ) - - mock_subtensor.substrate.create_signed_extrinsic.assert_called_once() - _, kwargs = mock_subtensor.substrate.create_signed_extrinsic.call_args - assert kwargs["call"] == mock_subtensor.substrate.compose_call.return_value - assert kwargs["keypair"] == fake_wallet.hotkey - assert kwargs["era"] == {"period": 5} - - mock_subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - assert result == (True, "Successfully set weights.") - - -def test_do_set_weights_is_not_success(mock_subtensor, mocker): - """Unsuccessful _do_set_weights call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_uids = [1, 2, 3] - fake_vals = [4, 5, 6] - fake_netuid = 1 - fake_wait_for_inclusion = True - fake_wait_for_finalization = True - - mock_subtensor.substrate.submit_extrinsic.return_value.is_success = False - mocked_format_error_message = mocker.MagicMock() - subtensor_module.format_error_message = mocked_format_error_message - - # Call - result = do_set_weights( - self=mock_subtensor, + mocked_set_weights_extrinsic.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, wallet=fake_wallet, - uids=fake_uids, - vals=fake_vals, - netuid=fake_netuid, - version_key=version_as_int, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - # Asserts - mock_subtensor.substrate.compose_call.assert_called_once_with( - call_module="SubtensorModule", - call_function="set_weights", - call_params={ - "dests": fake_uids, - "weights": fake_vals, - "netuid": fake_netuid, - "version_key": version_as_int, - }, - ) - - mock_subtensor.substrate.create_signed_extrinsic.assert_called_once() - _, kwargs = mock_subtensor.substrate.create_signed_extrinsic.call_args - assert kwargs["call"] == mock_subtensor.substrate.compose_call.return_value - assert kwargs["keypair"] == fake_wallet.hotkey - assert kwargs["era"] == {"period": 5} - - mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( - mock_subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - mock_subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - assert result == ( - False, - "Subtensor returned `UnknownError(UnknownType)` error. This means: `Unknown Description`.", - ) - - -def test_do_set_weights_no_waits(mock_subtensor, mocker): - """Successful _do_set_weights call without wait flags for fake_wait_for_inclusion and fake_wait_for_finalization.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_uids = [1, 2, 3] - fake_vals = [4, 5, 6] - fake_netuid = 1 - fake_wait_for_inclusion = False - fake_wait_for_finalization = False - - # Call - result = do_set_weights( - self=mock_subtensor, - wallet=fake_wallet, - uids=fake_uids, - vals=fake_vals, - netuid=fake_netuid, - version_key=version_as_int, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - # Asserts - mock_subtensor.substrate.compose_call.assert_called_once_with( - call_module="SubtensorModule", - call_function="set_weights", - call_params={ - "dests": fake_uids, - "weights": fake_vals, - "netuid": fake_netuid, - "version_key": version_as_int, - }, - ) - - mock_subtensor.substrate.create_signed_extrinsic.assert_called_once() - _, kwargs = mock_subtensor.substrate.create_signed_extrinsic.call_args - assert kwargs["call"] == mock_subtensor.substrate.compose_call.return_value - assert kwargs["keypair"] == fake_wallet.hotkey - assert kwargs["era"] == {"period": 5} - - mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( - mock_subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - assert result == (True, "Not waiting for finalization or inclusion.") + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + assert result == mocked_execute_coroutine.return_value diff --git a/tests/unit_tests/extrinsics/test_staking.py b/tests/unit_tests/extrinsics/test_staking.py new file mode 100644 index 0000000000..d30d225ebd --- /dev/null +++ b/tests/unit_tests/extrinsics/test_staking.py @@ -0,0 +1,81 @@ +from bittensor.core.extrinsics import staking + + +def test_add_stake_extrinsic(mocker): + """Verify that sync `add_stake_extrinsic` method calls proper async method.""" + # Preps + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() + hotkey_ss58 = "hotkey" + amount = 1.1 + wait_for_inclusion = True + wait_for_finalization = True + + mocked_execute_coroutine = mocker.patch.object(staking, "execute_coroutine") + mocked_add_stake_extrinsic = mocker.Mock() + staking.async_add_stake_extrinsic = mocked_add_stake_extrinsic + + # Call + result = staking.add_stake_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + hotkey_ss58=hotkey_ss58, + amount=amount, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Asserts + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_add_stake_extrinsic.return_value, + event_loop=fake_subtensor.event_loop, + ) + mocked_add_stake_extrinsic.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + wallet=fake_wallet, + hotkey_ss58=hotkey_ss58, + amount=amount, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + assert result == mocked_execute_coroutine.return_value + + +def test_add_stake_multiple_extrinsic(mocker): + """Verify that sync `add_stake_multiple_extrinsic` method calls proper async method.""" + # Preps + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() + hotkey_ss58s = ["hotkey1", "hotkey2"] + amounts = [1.1, 2.2] + wait_for_inclusion = True + wait_for_finalization = True + + mocked_execute_coroutine = mocker.patch.object(staking, "execute_coroutine") + mocked_add_stake_multiple_extrinsic = mocker.Mock() + staking.async_add_stake_multiple_extrinsic = mocked_add_stake_multiple_extrinsic + + # Call + result = staking.add_stake_multiple_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Asserts + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_add_stake_multiple_extrinsic.return_value, + event_loop=fake_subtensor.event_loop, + ) + mocked_add_stake_multiple_extrinsic.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + wallet=fake_wallet, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + assert result == mocked_execute_coroutine.return_value diff --git a/tests/unit_tests/extrinsics/test_transfer.py b/tests/unit_tests/extrinsics/test_transfer.py index af59d5769b..f85e3f267e 100644 --- a/tests/unit_tests/extrinsics/test_transfer.py +++ b/tests/unit_tests/extrinsics/test_transfer.py @@ -1,142 +1,47 @@ -import pytest +from bittensor.core.extrinsics import transfer -from bittensor.core import subtensor as subtensor_module -from bittensor.core.extrinsics.transfer import do_transfer -from bittensor.core.subtensor import Subtensor -from bittensor.utils.balance import Balance +def test_transfer_extrinsic(mocker): + """Verify that sync `transfer_extrinsic` method calls proper async method.""" + # Preps + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() + dest = "hotkey" + amount = 1.1 + transfer_all = True + wait_for_inclusion = True + wait_for_finalization = True + keep_alive = False -@pytest.fixture -def subtensor(mocker): - fake_substrate = mocker.MagicMock() - fake_substrate.websocket.sock.getsockopt.return_value = 0 - mocker.patch.object( - subtensor_module, "SubstrateInterface", return_value=fake_substrate - ) - return Subtensor() - - -def test_do_transfer_is_success_true(subtensor, mocker): - """Successful do_transfer call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_dest = "SS58PUBLICKEY" - fake_transfer_balance = Balance(1) - fake_wait_for_inclusion = True - fake_wait_for_finalization = True - - subtensor.substrate.submit_extrinsic.return_value.is_success = True - - # Call - result = do_transfer( - subtensor, - fake_wallet, - fake_dest, - fake_transfer_balance, - fake_wait_for_inclusion, - fake_wait_for_finalization, - ) - - # Asserts - subtensor.substrate.compose_call.assert_called_once_with( - call_module="Balances", - call_function="transfer_allow_death", - call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, - ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey - ) - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - assert result == ( - True, - subtensor.substrate.submit_extrinsic.return_value.block_hash, - None, - ) - - -def test_do_transfer_is_success_false(subtensor, mocker): - """Successful do_transfer call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_dest = "SS58PUBLICKEY" - fake_transfer_balance = Balance(1) - fake_wait_for_inclusion = True - fake_wait_for_finalization = True - - subtensor.substrate.submit_extrinsic.return_value.is_success = False - - mocked_format_error_message = mocker.MagicMock() - subtensor_module.format_error_message = mocked_format_error_message - - # Call - result = do_transfer( - subtensor, - fake_wallet, - fake_dest, - fake_transfer_balance, - fake_wait_for_inclusion, - fake_wait_for_finalization, - ) - - # Asserts - subtensor.substrate.compose_call.assert_called_once_with( - call_module="Balances", - call_function="transfer_allow_death", - call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, - ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey - ) - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - - assert result == ( - False, - None, - subtensor.substrate.submit_extrinsic.return_value.error_message, - ) - - -def test_do_transfer_no_waits(subtensor, mocker): - """Successful do_transfer call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_dest = "SS58PUBLICKEY" - fake_transfer_balance = Balance(1) - fake_wait_for_inclusion = False - fake_wait_for_finalization = False + mocked_execute_coroutine = mocker.patch.object(transfer, "execute_coroutine") + mocked_transfer_extrinsic = mocker.Mock() + transfer.async_transfer_extrinsic = mocked_transfer_extrinsic # Call - result = do_transfer( - subtensor, - fake_wallet, - fake_dest, - fake_transfer_balance, - fake_wait_for_inclusion, - fake_wait_for_finalization, + result = transfer.transfer_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + dest=dest, + amount=amount, + transfer_all=transfer_all, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + keep_alive=keep_alive, ) # Asserts - subtensor.substrate.compose_call.assert_called_once_with( - call_module="Balances", - call_function="transfer_allow_death", - call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, - ) - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey - ) - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - assert result == (True, None, None) + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_transfer_extrinsic.return_value, + event_loop=fake_subtensor.event_loop, + ) + mocked_transfer_extrinsic.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + wallet=fake_wallet, + destination=dest, + amount=amount, + transfer_all=transfer_all, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + keep_alive=keep_alive, + ) + assert result == mocked_execute_coroutine.return_value diff --git a/tests/unit_tests/extrinsics/test_unstaking.py b/tests/unit_tests/extrinsics/test_unstaking.py new file mode 100644 index 0000000000..afd3c23e76 --- /dev/null +++ b/tests/unit_tests/extrinsics/test_unstaking.py @@ -0,0 +1,81 @@ +from bittensor.core.extrinsics import unstaking + + +def test_unstake_extrinsic(mocker): + """Verify that sync `unstake_extrinsic` method calls proper async method.""" + # Preps + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() + hotkey_ss58 = "hotkey" + amount = 1.1 + wait_for_inclusion = True + wait_for_finalization = True + + mocked_execute_coroutine = mocker.patch.object(unstaking, "execute_coroutine") + mocked_unstake_extrinsic = mocker.Mock() + unstaking.async_unstake_extrinsic = mocked_unstake_extrinsic + + # Call + result = unstaking.unstake_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + hotkey_ss58=hotkey_ss58, + amount=amount, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Asserts + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_unstake_extrinsic.return_value, + event_loop=fake_subtensor.event_loop, + ) + mocked_unstake_extrinsic.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + wallet=fake_wallet, + hotkey_ss58=hotkey_ss58, + amount=amount, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + assert result == mocked_execute_coroutine.return_value + + +def test_unstake_multiple_extrinsic(mocker): + """Verify that sync `unstake_multiple_extrinsic` method calls proper async method.""" + # Preps + fake_subtensor = mocker.Mock() + fake_wallet = mocker.Mock() + hotkey_ss58s = ["hotkey1", "hotkey2"] + amounts = [1.1, 1.2] + wait_for_inclusion = True + wait_for_finalization = True + + mocked_execute_coroutine = mocker.patch.object(unstaking, "execute_coroutine") + mocked_unstake_multiple_extrinsic = mocker.Mock() + unstaking.async_unstake_multiple_extrinsic = mocked_unstake_multiple_extrinsic + + # Call + result = unstaking.unstake_multiple_extrinsic( + subtensor=fake_subtensor, + wallet=fake_wallet, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Asserts + mocked_execute_coroutine.assert_called_once_with( + coroutine=mocked_unstake_multiple_extrinsic.return_value, + event_loop=fake_subtensor.event_loop, + ) + mocked_unstake_multiple_extrinsic.assert_called_once_with( + subtensor=fake_subtensor.async_subtensor, + wallet=fake_wallet, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + assert result == mocked_execute_coroutine.return_value diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index b524b10d2e..52ac0de2ec 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -1,7 +1,9 @@ import pytest +from bittensor_wallet import Wallet from bittensor import AsyncSubtensor from bittensor.core import async_subtensor +from bittensor.core.chain_data import proposal_vote_data @pytest.fixture(autouse=True) @@ -18,7 +20,9 @@ def subtensor(mocker): def test_decode_ss58_tuples_in_proposal_vote_data(mocker): """Tests that ProposalVoteData instance instantiation works properly,""" # Preps - mocked_decode_account_id = mocker.patch.object(async_subtensor, "decode_account_id") + mocked_decode_account_id = mocker.patch.object( + proposal_vote_data, "decode_account_id" + ) fake_proposal_dict = { "index": "0", "threshold": 1, @@ -82,7 +86,7 @@ async def test_init_if_unknown_network_is_valid(mocker): # Asserts assert subtensor.chain_endpoint == fake_valid_endpoint - assert subtensor.network == "custom" + assert subtensor.network == "unknown" @pytest.mark.asyncio @@ -110,11 +114,8 @@ async def test_init_if_unknown_network_is_not_valid(mocker): subtensor = AsyncSubtensor("blabla-net") # Asserts - assert ( - subtensor.chain_endpoint - == async_subtensor.NETWORK_MAP[async_subtensor.DEFAULTS.subtensor.network] - ) - assert subtensor.network == async_subtensor.DEFAULTS.subtensor.network + assert subtensor.chain_endpoint == "ws://blabla-net" + assert subtensor.network == "unknown" def test__str__return(subtensor): @@ -150,7 +151,7 @@ async def test_async_subtensor_magic_methods(mocker): @pytest.mark.parametrize( "error", - [ConnectionRefusedError, async_subtensor.ssl.SSLError], + [ConnectionRefusedError, async_subtensor.ssl.SSLError, TimeoutError], ) @pytest.mark.asyncio async def test_async_subtensor_aenter_connection_refused_error( @@ -260,7 +261,7 @@ async def test_get_block_hash_without_block_id_aka_none(subtensor): async def test_get_block_hash_with_block_id(subtensor): """Tests get_block_hash method with passed block_id.""" # Call - result = await subtensor.get_block_hash(block_id=1) + result = await subtensor.get_block_hash(block=1) # Asserts assert result == subtensor.substrate.get_block_hash.return_value @@ -408,7 +409,7 @@ async def test_get_delegates(subtensor, mocker, fake_hex_bytes_result, response) ) # Call - result = await subtensor.get_delegates(block_hash=None, reuse_block=True) + result = await subtensor.get_delegates(block_hash=None, reuse_block=False) # Asserts if fake_hex_bytes_result: @@ -424,7 +425,7 @@ async def test_get_delegates(subtensor, mocker, fake_hex_bytes_result, response) method="get_delegates", params=[], block_hash=None, - reuse_block=True, + reuse_block=False, ) @@ -573,32 +574,41 @@ async def test_query_runtime_api(subtensor, mocker): async def test_get_balance(subtensor, mocker): """Tests get_balance method.""" # Preps - fake_addresses = ("a1", "a2") + fake_address = "a1" + fake_block = 123 fake_block_hash = None + reuse_block = True - mocked_substrate_create_storage_key = mocker.AsyncMock() - subtensor.substrate.create_storage_key = mocked_substrate_create_storage_key + expected_balance = async_subtensor.Balance(1000) - mocked_batch_0_call = mocker.Mock( - params=[ - 0, - ] - ) - mocked_batch_1_call = {"data": {"free": 1000}} - mocked_substrate_query_multi = mocker.AsyncMock( - return_value=[ - (mocked_batch_0_call, mocked_batch_1_call), - ] + mocked_determine_block_hash = mocker.AsyncMock() + mocker.patch.object( + async_subtensor.AsyncSubtensor, + "determine_block_hash", + mocked_determine_block_hash, ) - subtensor.substrate.query_multi = mocked_substrate_query_multi + mocked_get_balances = mocker.AsyncMock( + return_value={fake_address: expected_balance} + ) + mocker.patch.object( + async_subtensor.AsyncSubtensor, "get_balances", mocked_get_balances + ) # Call - result = await subtensor.get_balance(*fake_addresses, block_hash=fake_block_hash) + result = await subtensor.get_balance( + fake_address, fake_block, fake_block_hash, reuse_block + ) - assert mocked_substrate_create_storage_key.call_count == len(fake_addresses) - mocked_substrate_query_multi.assert_called_once() - assert result == {0: async_subtensor.Balance(1000)} + mocked_determine_block_hash.assert_awaited_once_with( + fake_block, fake_block_hash, reuse_block + ) + mocked_get_balances.assert_awaited_once_with( + *[fake_address], + block_hash=mocked_determine_block_hash.return_value, + reuse_block=reuse_block, + ) + assert result == expected_balance @pytest.mark.parametrize("balance", [100, 100.1]) @@ -606,7 +616,7 @@ async def test_get_balance(subtensor, mocker): async def test_get_transfer_fee(subtensor, mocker, balance): """Tests get_transfer_fee method.""" # Preps - fake_wallet = mocker.Mock(coldkeypub="coldkeypub", autospec=async_subtensor.Wallet) + fake_wallet = mocker.Mock(coldkeypub="coldkeypub", autospec=Wallet) fake_dest = "fake_dest" fake_value = balance @@ -643,7 +653,7 @@ async def test_get_transfer_fee(subtensor, mocker, balance): async def test_get_transfer_fee_with_non_balance_accepted_value_type(subtensor, mocker): """Tests get_transfer_fee method with non balance accepted value type.""" # Preps - fake_wallet = mocker.Mock(coldkeypub="coldkeypub", autospec=async_subtensor.Wallet) + fake_wallet = mocker.Mock(coldkeypub="coldkeypub", autospec=Wallet) fake_dest = "fake_dest" fake_value = "1000" @@ -782,7 +792,7 @@ async def test_subnet_exists(subtensor, mocker): # Preps fake_netuid = 1 fake_block_hash = "block_hash" - fake_reuse_block_hash = True + fake_reuse_block_hash = False mocked_substrate_query = mocker.AsyncMock( autospec=async_subtensor.AsyncSubstrateInterface.query @@ -814,7 +824,7 @@ async def test_get_hyperparameter_happy_path(subtensor, mocker): fake_param_name = "param_name" fake_netuid = 1 fake_block_hash = "block_hash" - fake_reuse_block_hash = True + fake_reuse_block_hash = False # kind of fake subnet exists mocked_subtensor_subnet_exists = mocker.AsyncMock(return_value=True) @@ -842,7 +852,7 @@ async def test_get_hyperparameter_happy_path(subtensor, mocker): block_hash=fake_block_hash, reuse_block_hash=fake_reuse_block_hash, ) - assert result == mocked_substrate_query.return_value + assert result == mocked_substrate_query.return_value.value @pytest.mark.asyncio @@ -882,16 +892,16 @@ async def test_filter_netuids_by_registered_hotkeys( ): """Tests filter_netuids_by_registered_hotkeys method.""" # Preps - fake_wallet_1 = mocker.Mock(autospec=async_subtensor.Wallet) + fake_wallet_1 = mocker.Mock(autospec=Wallet) fake_wallet_1.hotkey.ss58_address = "ss58_address_1" - fake_wallet_2 = mocker.Mock(autospec=async_subtensor.Wallet) + fake_wallet_2 = mocker.Mock(autospec=Wallet) fake_wallet_2.hotkey.ss58_address = "ss58_address_2" fake_all_netuids = all_netuids fake_filter_for_netuids = filter_for_netuids fake_all_hotkeys = [fake_wallet_1, fake_wallet_2] fake_block_hash = "fake_block_hash" - fake_reuse_block = True + fake_reuse_block = False mocked_get_netuids_for_hotkey = mocker.AsyncMock( # returned subnets list @@ -927,7 +937,7 @@ async def test_get_existential_deposit_happy_path(subtensor, mocker): """Tests get_existential_deposit method.""" # Preps fake_block_hash = "block_hash" - fake_reuse_block_hash = True + fake_reuse_block_hash = False mocked_substrate_get_constant = mocker.AsyncMock(return_value=1) subtensor.substrate.get_constant = mocked_substrate_get_constant @@ -958,7 +968,7 @@ async def test_get_existential_deposit_raise_exception(subtensor, mocker): """Tests get_existential_deposit method raise Exception.""" # Preps fake_block_hash = "block_hash" - fake_reuse_block_hash = True + fake_reuse_block_hash = False mocked_substrate_get_constant = mocker.AsyncMock(return_value=None) subtensor.substrate.get_constant = mocked_substrate_get_constant @@ -988,7 +998,7 @@ async def test_neurons(subtensor, mocker): # Preps fake_netuid = 1 fake_block_hash = "block_hash" - fake_reuse_block_hash = True + fake_reuse_block_hash = False mocked_query_runtime_api = mocker.patch.object( subtensor, "query_runtime_api", return_value="NOT NONE" @@ -1027,7 +1037,7 @@ async def test_neurons_lite(subtensor, mocker, fake_hex_bytes_result, response): # Preps fake_netuid = 1 fake_block_hash = "block_hash" - fake_reuse_block_hash = True + fake_reuse_block_hash = False mocked_query_runtime_api = mocker.AsyncMock(return_value=fake_hex_bytes_result) subtensor.query_runtime_api = mocked_query_runtime_api @@ -1102,7 +1112,9 @@ async def test_get_neuron_for_pubkey_and_subnet_success(subtensor, mocker): ) subtensor.substrate.rpc_request.assert_awaited_once() subtensor.substrate.rpc_request.assert_called_once_with( - method="neuronInfo_getNeuron", params=[fake_netuid, fake_uid] + method="neuronInfo_getNeuron", + params=[fake_netuid, fake_uid], + reuse_block_hash=False, ) mocked_neuron_info.assert_called_once_with(fake_result) assert result == "fake_neuron_info" @@ -1177,7 +1189,9 @@ async def test_get_neuron_for_pubkey_and_subnet_rpc_result_empty(subtensor, mock reuse_block_hash=False, ) subtensor.substrate.rpc_request.assert_called_once_with( - method="neuronInfo_getNeuron", params=[fake_netuid, fake_uid] + method="neuronInfo_getNeuron", + params=[fake_netuid, fake_uid], + reuse_block_hash=False, ) mocked_get_null_neuron.assert_called_once() assert result == "null_neuron" @@ -1289,7 +1303,9 @@ async def test_get_delegated_no_block_hash_no_reuse(subtensor, mocker): # Asserts mocked_ss58_to_vec_u8.assert_called_once_with(fake_coldkey_ss58) mocked_rpc_request.assert_called_once_with( - method="delegateInfo_getDelegated", params=[b"encoded_coldkey"] + method="delegateInfo_getDelegated", + params=[b"encoded_coldkey"], + reuse_block_hash=False, ) mocked_delegated_list_from_vec_u8.assert_called_once_with(b"mocked_result") assert result == mocked_delegated_list_from_vec_u8.return_value @@ -1321,7 +1337,9 @@ async def test_get_delegated_with_block_hash(subtensor, mocker): # Asserts mocked_ss58_to_vec_u8.assert_called_once_with(fake_coldkey_ss58) mocked_rpc_request.assert_called_once_with( - method="delegateInfo_getDelegated", params=[fake_block_hash, b"encoded_coldkey"] + method="delegateInfo_getDelegated", + params=[fake_block_hash, b"encoded_coldkey"], + reuse_block_hash=False, ) mocked_delegated_list_from_vec_u8.assert_called_once_with(b"mocked_result") assert result == mocked_delegated_list_from_vec_u8.return_value @@ -1333,6 +1351,7 @@ async def test_get_delegated_with_reuse_block(subtensor, mocker): # Preps fake_coldkey_ss58 = "fake_ss58_address" subtensor.substrate.last_block_hash = "last_block_hash" + reuse_block = True mocked_ss58_to_vec_u8 = mocker.Mock(return_value=b"encoded_coldkey") mocker.patch.object(async_subtensor, "ss58_to_vec_u8", mocked_ss58_to_vec_u8) @@ -1347,14 +1366,15 @@ async def test_get_delegated_with_reuse_block(subtensor, mocker): # Call result = await subtensor.get_delegated( - coldkey_ss58=fake_coldkey_ss58, reuse_block=True + coldkey_ss58=fake_coldkey_ss58, reuse_block=reuse_block ) # Asserts mocked_ss58_to_vec_u8.assert_called_once_with(fake_coldkey_ss58) mocked_rpc_request.assert_called_once_with( method="delegateInfo_getDelegated", - params=["last_block_hash", b"encoded_coldkey"], + params=[b"encoded_coldkey"], + reuse_block_hash=reuse_block, ) mocked_delegated_list_from_vec_u8.assert_called_once_with(b"mocked_result") assert result == mocked_delegated_list_from_vec_u8.return_value @@ -1378,7 +1398,9 @@ async def test_get_delegated_with_empty_result(subtensor, mocker): # Asserts mocked_ss58_to_vec_u8.assert_called_once_with(fake_coldkey_ss58) mocked_rpc_request.assert_called_once_with( - method="delegateInfo_getDelegated", params=[b"encoded_coldkey"] + method="delegateInfo_getDelegated", + params=[b"encoded_coldkey"], + reuse_block_hash=False, ) assert result == [] @@ -1734,7 +1756,6 @@ async def fake_is_success(): wait_for_inclusion=True, wait_for_finalization=True, ) - fake_response.process_events.assert_awaited_once() assert result == (True, "") @@ -1785,7 +1806,6 @@ async def fake_error_message(): wait_for_inclusion=True, wait_for_finalization=True, ) - fake_response.process_events.assert_awaited_once() assert result == (False, mocked_format_error_message.return_value) @@ -1889,9 +1909,11 @@ async def test_get_children_success(subtensor, mocker): # Asserts mocked_query.assert_called_once_with( + block_hash=None, module="SubtensorModule", storage_function="ChildKeys", params=[fake_hotkey, fake_netuid], + reuse_block_hash=False, ) mocked_decode_account_id.assert_has_calls( [mocker.call("child_key_1"), mocker.call("child_key_2")] @@ -1915,9 +1937,11 @@ async def test_get_children_no_children(subtensor, mocker): # Asserts mocked_query.assert_called_once_with( + block_hash=None, module="SubtensorModule", storage_function="ChildKeys", params=[fake_hotkey, fake_netuid], + reuse_block_hash=False, ) assert result == (True, [], "") @@ -1943,9 +1967,11 @@ async def test_get_children_substrate_request_exception(subtensor, mocker): # Asserts mocked_query.assert_called_once_with( + block_hash=None, module="SubtensorModule", storage_function="ChildKeys", params=[fake_hotkey, fake_netuid], + reuse_block_hash=False, ) mocked_format_error_message.assert_called_once_with(fake_exception) assert result == (False, [], "Formatted error message") @@ -2372,11 +2398,14 @@ async def test_commit_reveal_enabled(subtensor, mocker): ) # Call - result = await subtensor.commit_reveal_enabled(netuid, block_hash) + result = await subtensor.commit_reveal_enabled(netuid, block_hash=block_hash) # Assertions mocked_get_hyperparameter.assert_awaited_once_with( - param_name="CommitRevealWeightsEnabled", block_hash=block_hash, netuid=netuid + param_name="CommitRevealWeightsEnabled", + block_hash=block_hash, + netuid=netuid, + reuse_block=False, ) assert result is False @@ -2392,7 +2421,9 @@ async def test_get_subnet_reveal_period_epochs(subtensor, mocker): ) # Call - result = await subtensor.get_subnet_reveal_period_epochs(netuid, block_hash) + result = await subtensor.get_subnet_reveal_period_epochs( + netuid, block_hash=block_hash + ) # Assertions mocked_get_hyperparameter.assert_awaited_once_with( @@ -2429,13 +2460,15 @@ async def test_transfer_success(subtensor, mocker): ) # Asserts - mocked_transfer_extrinsic.assert_awaited_once() - mocked_transfer_extrinsic.assert_called_once_with( - subtensor, - fake_wallet, - fake_destination, - mocked_balance_from_tao, - fake_transfer_all, + mocked_transfer_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + destination=fake_destination, + amount=mocked_balance_from_tao, + transfer_all=fake_transfer_all, + wait_for_inclusion=True, + wait_for_finalization=False, + keep_alive=True, ) assert result == mocked_transfer_extrinsic.return_value @@ -2444,158 +2477,32 @@ async def test_transfer_success(subtensor, mocker): async def test_register_success(subtensor, mocker): """Tests register when there is enough balance and registration succeeds.""" # Preps - fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) - fake_wallet.coldkeypub.ss58_address = "wallet_address" - fake_netuid = 1 - fake_block_hash = "block_hash" - fake_recycle_amount = 100 - fake_balance = 200 - - mocked_get_block_hash = mocker.AsyncMock(return_value=fake_block_hash) - subtensor.get_block_hash = mocked_get_block_hash - - mocked_get_hyperparameter = mocker.AsyncMock(return_value=str(fake_recycle_amount)) - subtensor.get_hyperparameter = mocked_get_hyperparameter - - mocked_get_balance = mocker.AsyncMock( - return_value={fake_wallet.coldkeypub.ss58_address: fake_balance} - ) - subtensor.get_balance = mocked_get_balance - - mocked_balance_from_rao = mocker.Mock(return_value=fake_recycle_amount) - mocker.patch.object(async_subtensor.Balance, "from_rao", mocked_balance_from_rao) - - # Call - result = await subtensor.root_register(wallet=fake_wallet, netuid=fake_netuid) - - # Asserts - mocked_get_block_hash.assert_called_once() - mocked_get_hyperparameter.assert_called_once_with( - param_name="Burn", netuid=fake_netuid, reuse_block=True - ) - mocked_get_balance.assert_called_once_with( - fake_wallet.coldkeypub.ss58_address, block_hash=fake_block_hash - ) - assert result is True - - -@pytest.mark.asyncio -async def test_register_insufficient_balance(subtensor, mocker): - """Tests register when the wallet balance is insufficient.""" - # Preps - fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) - fake_wallet.coldkeypub.ss58_address = "wallet_address" - fake_netuid = 1 - fake_block_hash = "block_hash" - fake_recycle_amount = 200 - fake_balance = 100 - - mocked_get_block_hash = mocker.AsyncMock(return_value=fake_block_hash) - subtensor.get_block_hash = mocked_get_block_hash - - mocked_get_hyperparameter = mocker.AsyncMock(return_value=str(fake_recycle_amount)) - subtensor.get_hyperparameter = mocked_get_hyperparameter - - mocked_get_balance = mocker.AsyncMock( - return_value={fake_wallet.coldkeypub.ss58_address: fake_balance} - ) - subtensor.get_balance = mocked_get_balance - - mocked_balance_from_rao = mocker.Mock(return_value=fake_recycle_amount) - mocker.patch.object(async_subtensor.Balance, "from_rao", mocked_balance_from_rao) - - # Call - result = await subtensor.root_register(wallet=fake_wallet, netuid=fake_netuid) - - # Asserts - mocked_get_block_hash.assert_called_once() - mocked_get_hyperparameter.assert_called_once_with( - param_name="Burn", netuid=fake_netuid, reuse_block=True - ) - mocked_get_balance.assert_called_once_with( - fake_wallet.coldkeypub.ss58_address, block_hash=fake_block_hash - ) - assert result is False - - -@pytest.mark.asyncio -async def test_register_balance_retrieval_error(subtensor, mocker): - """Tests register when there is an error retrieving the balance.""" - # Preps - fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) - fake_wallet.coldkeypub.ss58_address = "wallet_address" + fake_wallet = mocker.Mock() fake_netuid = 1 - fake_block_hash = "block_hash" - fake_recycle_amount = 100 - - mocked_get_block_hash = mocker.AsyncMock(return_value=fake_block_hash) - subtensor.get_block_hash = mocked_get_block_hash - mocked_get_hyperparameter = mocker.AsyncMock(return_value=str(fake_recycle_amount)) - subtensor.get_hyperparameter = mocked_get_hyperparameter - - mocked_get_balance = mocker.AsyncMock(return_value={}) - subtensor.get_balance = mocked_get_balance - - # Call - result = await subtensor.root_register(wallet=fake_wallet, netuid=fake_netuid) - - # Asserts - mocked_get_block_hash.assert_called_once() - mocked_get_hyperparameter.assert_called_once_with( - param_name="Burn", netuid=fake_netuid, reuse_block=True - ) - mocked_get_balance.assert_called_once_with( - fake_wallet.coldkeypub.ss58_address, block_hash=fake_block_hash - ) - assert result is False - - -@pytest.mark.asyncio -async def test_pow_register_success(subtensor, mocker): - """Tests pow_register when the registration is successful.""" - # Preps - fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) - fake_netuid = 1 - fake_processors = 4 - fake_update_interval = 10 - fake_output_in_place = True - fake_verbose = True - fake_use_cuda = False - fake_dev_id = 0 - fake_threads_per_block = 128 - - mocked_register_extrinsic = mocker.AsyncMock(return_value=True) + mocked_register_extrinsic = mocker.AsyncMock() mocker.patch.object( async_subtensor, "register_extrinsic", mocked_register_extrinsic ) # Call - result = await subtensor.register( - wallet=fake_wallet, - netuid=fake_netuid, - processors=fake_processors, - update_interval=fake_update_interval, - output_in_place=fake_output_in_place, - verbose=fake_verbose, - use_cuda=fake_use_cuda, - dev_id=fake_dev_id, - threads_per_block=fake_threads_per_block, - ) + result = await subtensor.register(wallet=fake_wallet, netuid=fake_netuid) # Asserts - mocked_register_extrinsic.assert_awaited_once() - mocked_register_extrinsic.assert_called_once_with( + mocked_register_extrinsic.assert_awaited_once_with( + cuda=False, + dev_id=0, + log_verbose=False, + max_allowed_attempts=3, + netuid=1, + num_processes=None, + output_in_place=False, subtensor=subtensor, + tpb=256, + update_interval=None, + wait_for_finalization=True, + wait_for_inclusion=False, wallet=fake_wallet, - netuid=fake_netuid, - tpb=fake_threads_per_block, - update_interval=fake_update_interval, - num_processes=fake_processors, - cuda=fake_use_cuda, - dev_id=fake_dev_id, - output_in_place=fake_output_in_place, - log_verbose=fake_verbose, ) assert result == mocked_register_extrinsic.return_value @@ -2604,7 +2511,7 @@ async def test_pow_register_success(subtensor, mocker): async def test_set_weights_success(subtensor, mocker): """Tests set_weights with successful weight setting on the first try.""" # Preps - fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) + fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = [1, 2, 3] fake_weights = [0.3, 0.5, 0.2] @@ -2662,7 +2569,7 @@ async def test_set_weights_success(subtensor, mocker): async def test_set_weights_with_exception(subtensor, mocker): """Tests set_weights when set_weights_extrinsic raises an exception.""" # Preps - fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) + fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_uids = [1, 2, 3] fake_weights = [0.3, 0.5, 0.2] @@ -2707,7 +2614,7 @@ async def test_set_weights_with_exception(subtensor, mocker): async def test_root_set_weights_success(subtensor, mocker): """Tests root_set_weights when the setting of weights is successful.""" # Preps - fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) + fake_wallet = mocker.Mock(autospec=Wallet) fake_netuids = [1, 2, 3] fake_weights = [0.3, 0.5, 0.2] @@ -2749,7 +2656,7 @@ async def test_root_set_weights_success(subtensor, mocker): async def test_commit_weights_success(subtensor, mocker): """Tests commit_weights when the weights are committed successfully.""" # Preps - fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) + fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_salt = [12345, 67890] fake_uids = [1, 2, 3] @@ -2801,7 +2708,7 @@ async def test_commit_weights_success(subtensor, mocker): async def test_commit_weights_with_exception(subtensor, mocker): """Tests commit_weights when an exception is raised during weight commitment.""" # Preps - fake_wallet = mocker.Mock(autospec=async_subtensor.Wallet) + fake_wallet = mocker.Mock(autospec=Wallet) fake_netuid = 1 fake_salt = [12345, 67890] fake_uids = [1, 2, 3] diff --git a/tests/unit_tests/test_metagraph.py b/tests/unit_tests/test_metagraph.py index 98c0a86ae5..0a95bbbbc7 100644 --- a/tests/unit_tests/test_metagraph.py +++ b/tests/unit_tests/test_metagraph.py @@ -1,35 +1,21 @@ -# The MIT License (MIT) -# Copyright © 2024 Opentensor Foundation -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -from unittest.mock import MagicMock +import asyncio +import copy +from functools import partial from unittest.mock import Mock import numpy as np import pytest -import copy from bittensor.core import settings from bittensor.core.metagraph import Metagraph +from bittensor.core.subtensor import Subtensor +from bittensor.utils import execute_coroutine @pytest.fixture -def mock_environment(): +def mock_environment(mocker): # Create a Mock for subtensor - subtensor = Mock() + subtensor = mocker.AsyncMock() # Create a list of Mock Neurons neurons = [ @@ -57,11 +43,12 @@ def mock_environment(): return subtensor, neurons -def test_set_metagraph_attributes(mock_environment): +@pytest.mark.asyncio +async def test_set_metagraph_attributes(mock_environment): subtensor, neurons = mock_environment metagraph = Metagraph(1, sync=False) metagraph.neurons = neurons - metagraph._set_metagraph_attributes(block=5, subtensor=subtensor) + await metagraph._set_metagraph_attributes(block=5, subtensor=subtensor) # Check the attributes are set as expected assert metagraph.n.item() == len(neurons) @@ -128,21 +115,27 @@ def test_process_weights_or_bonds(mock_environment): # Mocking the bittensor.Subtensor class for testing purposes @pytest.fixture -def mock_subtensor(): - subtensor = MagicMock() +def mock_subtensor(mocker): + subtensor = mocker.Mock(spec=Subtensor) subtensor.chain_endpoint = settings.FINNEY_ENTRYPOINT subtensor.network = "finney" - subtensor.get_current_block.return_value = 601 + subtensor.async_subtensor = mocker.AsyncMock( + get_current_block=mocker.AsyncMock(return_value=601) + ) + subtensor.event_loop = asyncio.new_event_loop() + subtensor.execute_coroutine = partial( + execute_coroutine, event_loop=subtensor.event_loop + ) return subtensor # Mocking the metagraph instance for testing purposes @pytest.fixture -def metagraph_instance(): +def metagraph_instance(mocker): metagraph = Metagraph(netuid=1337, sync=False) - metagraph._assign_neurons = MagicMock() - metagraph._set_metagraph_attributes = MagicMock() - metagraph._set_weights_and_bonds = MagicMock() + metagraph._assign_neurons = mocker.AsyncMock() + metagraph._set_metagraph_attributes = mocker.AsyncMock() + metagraph._set_weights_and_bonds = mocker.AsyncMock() return metagraph diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 8194598219..259893f878 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1,2842 +1,43 @@ -# The MIT License (MIT) -# Copyright © 2024 Opentensor Foundation -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the “Software”), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. +from bittensor.core.subtensor import Subtensor -import argparse -import unittest.mock as mock -from unittest.mock import MagicMock -import pytest -from bittensor_wallet import Wallet +# TODO: It's probably worth adding a test for each corresponding method to check the correctness of the call with arguments -from bittensor.core import subtensor as subtensor_module, settings -from bittensor.core.axon import Axon -from bittensor.core.chain_data import SubnetHyperparameters -from bittensor.core.settings import version_as_int -from bittensor.core.subtensor import Subtensor, logging -from bittensor.utils import u16_normalized_float, u64_normalized_float, Certificate -from bittensor.utils.balance import Balance -U16_MAX = 65535 -U64_MAX = 18446744073709551615 - - -@pytest.fixture -def fake_call_params(): - return call_params() - - -def call_params(): - return { - "version": "1.0", - "ip": "0.0.0.0", - "port": 9090, - "ip_type": 4, - "netuid": 1, - "certificate": None, - } - - -def call_params_with_certificate(): - params = call_params() - params["certificate"] = Certificate("fake_cert") - return params - - -def test_serve_axon_with_external_ip_set(): - internal_ip: str = "192.0.2.146" - external_ip: str = "2001:0db8:85a3:0000:0000:8a2e:0370:7334" - - mock_serve_axon = MagicMock(return_value=True) - - mock_subtensor = MagicMock(spec=Subtensor, serve_axon=mock_serve_axon) - - mock_wallet = MagicMock( - spec=Wallet, - coldkey=MagicMock(), - coldkeypub=MagicMock( - # mock ss58 address - ss58_address="5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - ), - hotkey=MagicMock( - ss58_address="5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg" - ), - ) - - mock_config = Axon.config() - mock_axon_with_external_ip_set = Axon( - wallet=mock_wallet, - ip=internal_ip, - external_ip=external_ip, - config=mock_config, - ) - - mock_subtensor.serve_axon( - netuid=-1, - axon=mock_axon_with_external_ip_set, - ) - - mock_serve_axon.assert_called_once() - - # verify that the axon is served to the network with the external ip - _, kwargs = mock_serve_axon.call_args - axon_info = kwargs["axon"].info() - assert axon_info.ip == external_ip - - -def test_serve_axon_with_external_port_set(): - external_ip: str = "2001:0db8:85a3:0000:0000:8a2e:0370:7334" - - internal_port: int = 1234 - external_port: int = 5678 - - mock_serve = MagicMock(return_value=True) - - mock_serve_axon = MagicMock(return_value=True) - - mock_subtensor = MagicMock( - spec=Subtensor, - serve=mock_serve, - serve_axon=mock_serve_axon, - ) - - mock_wallet = MagicMock( - spec=Wallet, - coldkey=MagicMock(), - coldkeypub=MagicMock( - # mock ss58 address - ss58_address="5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - ), - hotkey=MagicMock( - ss58_address="5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg" - ), - ) - - mock_config = Axon.config() - - mock_axon_with_external_port_set = Axon( - wallet=mock_wallet, - port=internal_port, - external_port=external_port, - config=mock_config, - ) - - with mock.patch( - "bittensor.utils.networking.get_external_ip", return_value=external_ip - ): - # mock the get_external_ip function to return the external ip - mock_subtensor.serve_axon( - netuid=-1, - axon=mock_axon_with_external_port_set, - ) - - mock_serve_axon.assert_called_once() - # verify that the axon is served to the network with the external port - _, kwargs = mock_serve_axon.call_args - axon_info = kwargs["axon"].info() - assert axon_info.port == external_port - - -class ExitEarly(Exception): - """Mock exception to exit early from the called code""" - - pass - - -@pytest.mark.parametrize( - "test_id, expected_output", - [ - # Happy path test - ( - "happy_path_default", - "Create and return a new object. See help(type) for accurate signature.", - ), - ], -) -def test_help(test_id, expected_output, capsys): - # Act - Subtensor.help() - - # Assert - captured = capsys.readouterr() - assert expected_output in captured.out, f"Test case {test_id} failed" - - -@pytest.fixture -def parser(): - return argparse.ArgumentParser() - - -# Mocking argparse.ArgumentParser.add_argument method to simulate ArgumentError -def test_argument_error_handling(monkeypatch, parser): - def mock_add_argument(*args, **kwargs): - raise argparse.ArgumentError(None, "message") - - monkeypatch.setattr(argparse.ArgumentParser, "add_argument", mock_add_argument) - # No exception should be raised - Subtensor.add_args(parser) - - -@pytest.mark.parametrize( - "network, expected_network, expected_endpoint", - [ - # Happy path tests - ("finney", "finney", settings.FINNEY_ENTRYPOINT), - ("local", "local", settings.LOCAL_ENTRYPOINT), - ("test", "test", settings.FINNEY_TEST_ENTRYPOINT), - ("archive", "archive", settings.ARCHIVE_ENTRYPOINT), - # Endpoint override tests - ( - settings.FINNEY_ENTRYPOINT, - "finney", - settings.FINNEY_ENTRYPOINT, - ), - ( - "entrypoint-finney.opentensor.ai", - "finney", - settings.FINNEY_ENTRYPOINT, - ), - ( - settings.FINNEY_TEST_ENTRYPOINT, - "test", - settings.FINNEY_TEST_ENTRYPOINT, - ), - ( - "test.finney.opentensor.ai", - "test", - settings.FINNEY_TEST_ENTRYPOINT, - ), - ( - settings.ARCHIVE_ENTRYPOINT, - "archive", - settings.ARCHIVE_ENTRYPOINT, - ), - ( - "archive.chain.opentensor.ai", - "archive", - settings.ARCHIVE_ENTRYPOINT, - ), - ("127.0.0.1", "local", "127.0.0.1"), - ("localhost", "local", "localhost"), - # Edge cases - (None, None, None), - ("unknown", "unknown", "unknown"), - ], -) -def test_determine_chain_endpoint_and_network( - network, expected_network, expected_endpoint -): - # Act - result_network, result_endpoint = Subtensor.determine_chain_endpoint_and_network( - network - ) - - # Assert - assert result_network == expected_network - assert result_endpoint == expected_endpoint - - -@pytest.fixture -def subtensor(mocker): - fake_substrate = mocker.AsyncMock() - mocker.patch( - "bittensor.utils.substrate_interface.AsyncSubstrateInterface", - return_value=fake_substrate, - ) - return Subtensor() - - -@pytest.fixture -def mock_logger(): - with mock.patch.object(logging, "warning") as mock_warning: - yield mock_warning - - -def test_hyperparameter_subnet_does_not_exist(subtensor, mocker): - """Tests when the subnet does not exist.""" - subtensor.subnet_exists = mocker.MagicMock(return_value=False) - assert subtensor._get_hyperparameter("Difficulty", 1, None) is None - subtensor.subnet_exists.assert_called_once_with(1, None) - - -def test_hyperparameter_result_is_none(subtensor, mocker): - """Tests when query_subtensor returns None.""" - subtensor.subnet_exists = mocker.MagicMock(return_value=True) - subtensor.query_subtensor = mocker.MagicMock(return_value=None) - assert subtensor._get_hyperparameter("Difficulty", 1, None) is None - subtensor.subnet_exists.assert_called_once_with(1, None) - subtensor.query_subtensor.assert_called_once_with("Difficulty", None, [1]) - - -def test_hyperparameter_result_has_no_value(subtensor, mocker): - """Test when the result has no 'value' attribute.""" - subtensor.subnet_exists = mocker.MagicMock(return_value=True) - subtensor.query_subtensor = mocker.MagicMock(return_value=None) - assert subtensor._get_hyperparameter("Difficulty", 1, None) is None - subtensor.subnet_exists.assert_called_once_with(1, None) - subtensor.query_subtensor.assert_called_once_with("Difficulty", None, [1]) - - -def test_hyperparameter_success_int(subtensor, mocker): - """Test when query_subtensor returns an integer value.""" - subtensor.subnet_exists = mocker.MagicMock(return_value=True) - subtensor.query_subtensor = mocker.MagicMock(return_value=100) - assert subtensor._get_hyperparameter("Difficulty", 1, None) == 100 - subtensor.subnet_exists.assert_called_once_with(1, None) - subtensor.query_subtensor.assert_called_once_with("Difficulty", None, [1]) - - -def test_hyperparameter_success_float(subtensor, mocker): - """Test when query_subtensor returns a float value.""" - subtensor.subnet_exists = mocker.MagicMock(return_value=True) - subtensor.query_subtensor = mocker.MagicMock(return_value=0.5) - assert subtensor._get_hyperparameter("Difficulty", 1, None) == 0.5 - subtensor.subnet_exists.assert_called_once_with(1, None) - subtensor.query_subtensor.assert_called_once_with("Difficulty", None, [1]) - - -def test_blocks_since_last_update_success_calls(subtensor, mocker): - """Tests the weights_rate_limit method to ensure it correctly fetches the LastUpdate hyperparameter.""" - # Prep - uid = 7 - mocked_current_block = 2 - mocked_result = {uid: 1} - subtensor._get_hyperparameter = mocker.MagicMock(return_value=mocked_result) - subtensor.get_current_block = mocker.MagicMock(return_value=mocked_current_block) - - # Call - result = subtensor.blocks_since_last_update(netuid=7, uid=uid) - - # Assertions - subtensor.get_current_block.assert_called_once() - subtensor._get_hyperparameter.assert_called_once_with( - param_name="LastUpdate", netuid=7 - ) - assert result == 1 - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(result, int) - - -def test_weights_rate_limit_success_calls(subtensor, mocker): - """Tests the weights_rate_limit method to ensure it correctly fetches the WeightsSetRateLimit hyperparameter.""" - # Prep - subtensor._get_hyperparameter = mocker.MagicMock(return_value=5) - - # Call - result = subtensor.weights_rate_limit(netuid=7) - - # Assertions - subtensor._get_hyperparameter.assert_called_once_with( - param_name="WeightsSetRateLimit", netuid=7 - ) - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(result, int) - - -@pytest.fixture -def sample_hyperparameters(): - return MagicMock(spec=SubnetHyperparameters) - - -def normalize_hyperparameters( - subnet: "SubnetHyperparameters", -) -> list[tuple[str, str, str]]: - """ - Normalizes the hyperparameters of a subnet. - - Args: - subnet: The subnet hyperparameters object. - - Returns: - A list of tuples containing the parameter name, value, and normalized value. - """ - param_mappings = { - "adjustment_alpha": u64_normalized_float, - "min_difficulty": u64_normalized_float, - "max_difficulty": u64_normalized_float, - "difficulty": u64_normalized_float, - "bonds_moving_avg": u64_normalized_float, - "max_weight_limit": u16_normalized_float, - "kappa": u16_normalized_float, - "alpha_high": u16_normalized_float, - "alpha_low": u16_normalized_float, - "min_burn": Balance.from_rao, - "max_burn": Balance.from_rao, - } - - normalized_values: list[tuple[str, str, str]] = [] - subnet_dict = subnet.__dict__ - - for param, value in subnet_dict.items(): - try: - if param in param_mappings: - norm_value = param_mappings[param](value) - if isinstance(norm_value, float): - norm_value = f"{norm_value:.{10}g}" - else: - norm_value = value - except Exception as e: - logging.console.error(f"❌ Error normalizing parameter '{param}': {e}") - norm_value = "-" - - normalized_values.append((param, str(value), str(norm_value))) - - return normalized_values - - -def get_normalized_value(normalized_data, param_name): - return next( - ( - norm_value - for p_name, _, norm_value in normalized_data - if p_name == param_name - ), - None, - ) - - -@pytest.mark.parametrize( - "param_name, max_value, mid_value, zero_value, is_balance", - [ - ("adjustment_alpha", U64_MAX, U64_MAX / 2, 0, False), - ("max_weight_limit", U16_MAX, U16_MAX / 2, 0, False), - ("difficulty", U64_MAX, U64_MAX / 2, 0, False), - ("min_difficulty", U64_MAX, U64_MAX / 2, 0, False), - ("max_difficulty", U64_MAX, U64_MAX / 2, 0, False), - ("bonds_moving_avg", U64_MAX, U64_MAX / 2, 0, False), - ("min_burn", 10000000000, 5000000000, 0, True), # These are in rao - ("max_burn", 20000000000, 10000000000, 0, True), - ], - ids=[ - "adjustment-alpha", - "max_weight_limit", - "difficulty", - "min_difficulty", - "max_difficulty", - "bonds_moving_avg", - "min_burn", - "max_burn", - ], -) -def test_hyperparameter_normalization( - sample_hyperparameters, param_name, max_value, mid_value, zero_value, is_balance -): - setattr(sample_hyperparameters, param_name, mid_value) - normalized = normalize_hyperparameters(sample_hyperparameters) - norm_value = get_normalized_value(normalized, param_name) - - # Mid-value test - if is_balance: - numeric_value = float(str(norm_value).lstrip(settings.TAO_SYMBOL)) - expected_tao = mid_value / 1e9 - assert ( - numeric_value == expected_tao - ), f"Mismatch in tao value for {param_name} at mid value" - else: - assert float(norm_value) == 0.5, f"Failed mid-point test for {param_name}" - - # Max-value test - setattr(sample_hyperparameters, param_name, max_value) - normalized = normalize_hyperparameters(sample_hyperparameters) - norm_value = get_normalized_value(normalized, param_name) - - if is_balance: - numeric_value = float(str(norm_value).lstrip(settings.TAO_SYMBOL)) - expected_tao = max_value / 1e9 - assert ( - numeric_value == expected_tao - ), f"Mismatch in tao value for {param_name} at max value" - else: - assert float(norm_value) == 1.0, f"Failed max value test for {param_name}" - - # Zero-value test - setattr(sample_hyperparameters, param_name, zero_value) - normalized = normalize_hyperparameters(sample_hyperparameters) - norm_value = get_normalized_value(normalized, param_name) - - if is_balance: - numeric_value = float(str(norm_value).lstrip(settings.TAO_SYMBOL)) - expected_tao = zero_value / 1e9 - assert ( - numeric_value == expected_tao - ), f"Mismatch in tao value for {param_name} at zero value" - else: - assert float(norm_value) == 0.0, f"Failed zero value test for {param_name}" - - -########################### -# Account functions tests # -########################### - - -def test_commit_reveal_enabled(subtensor, mocker): - """Test commit_reveal_enabled.""" - # Preps - netuid = 1 - block = 123 - mocked_get_hyperparameter = mocker.patch.object(subtensor, "_get_hyperparameter") - - # Call - result = subtensor.commit_reveal_enabled(netuid, block) - - # Assertions - mocked_get_hyperparameter.assert_called_once_with( - param_name="CommitRevealWeightsEnabled", block=block, netuid=netuid - ) - assert result is False - - -def test_get_subnet_reveal_period_epochs(subtensor, mocker): - """Test get_subnet_reveal_period_epochs.""" - # Preps - netuid = 1 - block = 123 - mocked_get_hyperparameter = mocker.patch.object(subtensor, "_get_hyperparameter") - - # Call - result = subtensor.get_subnet_reveal_period_epochs(netuid, block) - - # Assertions - mocked_get_hyperparameter.assert_called_once_with( - param_name="RevealPeriodEpochs", block=block, netuid=netuid - ) - assert result == mocked_get_hyperparameter.return_value - - -# get_prometheus_info tests -def test_get_prometheus_info_success(mocker, subtensor): - """Test get_prometheus_info returns correct data when information is found.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - block = 123 - mock_result = { - "ip": 3232235777, # 192.168.1.1 - "ip_type": 4, - "port": 9090, - "version": "1.0", - "block": 1000, - } - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_prometheus_info(netuid, hotkey_ss58, block) - - # Asserts - assert result is not None - assert result.ip == "192.168.1.1" - assert result.ip_type == 4 - assert result.port == 9090 - assert result.version == "1.0" - assert result.block == 1000 - subtensor.query_subtensor.assert_called_once_with( - "Prometheus", block, [netuid, hotkey_ss58] - ) - - -def test_get_prometheus_info_no_data(mocker, subtensor): - """Test get_prometheus_info returns None when no information is found.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.get_prometheus_info(netuid, hotkey_ss58, block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with( - "Prometheus", block, [netuid, hotkey_ss58] - ) - - -def test_get_prometheus_info_no_block(mocker, subtensor): - """Test get_prometheus_info with no block specified.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - mock_result = { - "ip": "192.168.1.1", - "ip_type": 4, - "port": 9090, - "version": "1.0", - "block": 1000, - } - with mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result): - # Call - result = subtensor.get_prometheus_info(netuid, hotkey_ss58) - - # Asserts - assert result is not None - assert result.ip == "192.168.1.1" - assert result.ip_type == 4 - assert result.port == 9090 - assert result.version == "1.0" - assert result.block == 1000 - subtensor.query_subtensor.assert_called_once_with( - "Prometheus", None, [netuid, hotkey_ss58] - ) - - -########################### -# Global Parameters tests # -########################### - - -# `block` property test -def test_block_property(mocker, subtensor): - """Test block property returns the correct block number.""" - expected_block = 123 - mocker.patch.object(subtensor, "get_current_block", return_value=expected_block) - - result = subtensor.block - - assert result == expected_block - subtensor.get_current_block.assert_called_once() - - -# `subnet_exists` tests -def test_subnet_exists_success(mocker, subtensor): - """Test subnet_exists returns True when subnet exists.""" - # Prep - netuid = 1 - block = 123 - with mocker.patch.object(subtensor, "query_subtensor", return_value=True): - # Call - result = subtensor.subnet_exists(netuid, block) - - # Asserts - assert result is True - subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) - - -def test_subnet_exists_no_data(mocker, subtensor): - """Test subnet_exists returns False when no subnet information is found.""" - # Prep - netuid = 1 - block = 123 - with mocker.patch.object(subtensor, "query_subtensor", return_value=None): - # Call - result = subtensor.subnet_exists(netuid, block) - - # Asserts - assert result is False - subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) - - -def test_subnet_exists_no_value_attribute(mocker, subtensor): - """Test subnet_exists returns False when result has no value attribute.""" - # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.subnet_exists(netuid, block) - - # Asserts - assert result is False - subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) - - -def test_subnet_exists_no_block(mocker, subtensor): - """Test subnet_exists with no block specified.""" - # Prep - netuid = 1 - mock_result = mocker.MagicMock(value=True) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.subnet_exists(netuid) - - # Asserts - assert result is True - subtensor.query_subtensor.assert_called_once_with("NetworksAdded", None, [netuid]) - - -# `get_total_subnets` tests -def test_get_total_subnets_success(mocker, subtensor): - """Test get_total_subnets returns correct data when total subnet information is found.""" - # Prep - block = 123 - total_subnets_value = 10 - mocker.patch.object(subtensor, "query_subtensor", return_value=total_subnets_value) - - # Call - result = subtensor.get_total_subnets(block) - - # Asserts - assert result is not None - assert result == total_subnets_value - subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) - - -def test_get_total_subnets_no_data(mocker, subtensor): - """Test get_total_subnets returns None when no total subnet information is found.""" - # Prep - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.get_total_subnets(block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) - - -def test_get_total_subnets_no_value_attribute(mocker, subtensor): - """Test get_total_subnets returns None when result has no value attribute.""" - # Prep - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.get_total_subnets(block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) - - -def test_get_total_subnets_no_block(mocker, subtensor): - """Test get_total_subnets with no block specified.""" - # Prep - total_subnets_value = 10 - mocker.patch.object(subtensor, "query_subtensor", return_value=total_subnets_value) - - # Call - result = subtensor.get_total_subnets() - - # Asserts - assert result is not None - assert result == total_subnets_value - subtensor.query_subtensor.assert_called_once_with("TotalNetworks", None) - - -# `get_subnets` tests -def test_get_subnets_success(mocker, subtensor): - """Test get_subnets returns correct list when subnet information is found.""" - # Prep - block = 123 - mock_netuid1 = 1 - mock_netuid2 = 2 - mock_result = mocker.MagicMock() - mock_result.records = [(mock_netuid1, True), (mock_netuid2, True)] - mock_result.__iter__.side_effect = lambda: iter(mock_result.records) - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_subnets(block) - - # Asserts - assert result == [1, 2] - subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", block) - - -def test_get_subnets_no_data(mocker, subtensor): - """Test get_subnets returns empty list when no subnet information is found.""" - # Prep - block = 123 - mock_result = mocker.MagicMock() - mock_result.records = [] - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_subnets(block) - - # Asserts - assert result == [] - subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", block) - - -def test_get_subnets_no_records_attribute(mocker, subtensor): - """Test get_subnets returns empty list when result has no records attribute.""" - # Prep - block = 123 - mock_result = mocker.MagicMock() - del mock_result.records # Simulating a missing records attribute - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_subnets(block) - - # Asserts - assert result == [] - subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", block) - - -def test_get_subnets_no_block_specified(mocker, subtensor): - """Test get_subnets with no block specified.""" - # Prep - mock_netuid1 = 1 - mock_netuid2 = 2 - mock_result = mocker.MagicMock() - mock_result.records = [(mock_netuid1, True), (mock_netuid2, True)] - mock_result.__iter__.side_effect = lambda: iter(mock_result.records) - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_subnets() - - # Asserts - assert result == [mock_netuid1, mock_netuid2] - subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", None) - - -# `get_subnet_hyperparameters` tests -def test_get_subnet_hyperparameters_success(mocker, subtensor): - """Test get_subnet_hyperparameters returns correct data when hyperparameters are found.""" - # Prep - netuid = 1 - block = 123 - hex_bytes_result = "0x010203" - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - mocker.patch.object(subtensor, "query_runtime_api", return_value=hex_bytes_result) - mocker.patch.object( - subtensor_module.SubnetHyperparameters, - "from_vec_u8", - return_value=["from_vec_u8"], - ) - - # Call - result = subtensor.get_subnet_hyperparameters(netuid, block) - - # Asserts - subtensor.query_runtime_api.assert_called_once_with( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], - block=block, - ) - subtensor_module.SubnetHyperparameters.from_vec_u8.assert_called_once_with( - bytes_result - ) - - -def test_get_subnet_hyperparameters_hex_without_prefix(subtensor, mocker): - """Test get_subnet_hyperparameters correctly processes hex string without '0x' prefix.""" - # Prep - netuid = 1 - block = 123 - hex_bytes_result = "010203" - bytes_result = bytes.fromhex(hex_bytes_result) - mocker.patch.object(subtensor, "query_runtime_api", return_value=hex_bytes_result) - mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_vec_u8") - - # Call - result = subtensor.get_subnet_hyperparameters(netuid, block) - - # Asserts - subtensor.query_runtime_api.assert_called_once_with( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], - block=block, - ) - subtensor_module.SubnetHyperparameters.from_vec_u8.assert_called_once_with( - bytes_result - ) - - -def test_get_subnet_hyperparameters_no_data(mocker, subtensor): - """Test get_subnet_hyperparameters returns empty list when no data is found.""" - # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "query_runtime_api", return_value=None) - mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_vec_u8") - - # Call - result = subtensor.get_subnet_hyperparameters(netuid, block) - - # Asserts - assert result == [] - subtensor.query_runtime_api.assert_called_once_with( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], - block=block, - ) - subtensor_module.SubnetHyperparameters.from_vec_u8.assert_not_called() - - -def test_query_subtensor(subtensor, mocker): - """Tests query_subtensor call.""" - # Prep - fake_name = "module_name" - - # Call - result = subtensor.query_subtensor(fake_name) - - # Asserts - subtensor.substrate.query.assert_called_once_with( - module="SubtensorModule", - storage_function=fake_name, - params=None, - block_hash=None, - ) - assert result == subtensor.substrate.query.return_value - - -def test_query_runtime_api(subtensor, mocker): - """Tests query_runtime_api call.""" - # Prep - fake_runtime_api = "NeuronInfoRuntimeApi" - fake_method = "get_neuron_lite" - - mocked_state_call = mocker.MagicMock() - subtensor.state_call = mocked_state_call - - mocked_runtime_configuration = mocker.patch.object( - subtensor_module, "RuntimeConfiguration" - ) - mocked_scalecodec = mocker.patch.object(subtensor_module.scalecodec, "ScaleBytes") - - # Call - result = subtensor.query_runtime_api(fake_runtime_api, fake_method, None) - - # Asserts - subtensor.state_call.assert_called_once_with( - method=f"{fake_runtime_api}_{fake_method}", data="0x", block=None - ) - mocked_scalecodec.assert_called_once_with( - subtensor.state_call.return_value.__getitem__.return_value - ) - mocked_runtime_configuration.assert_called_once() - mocked_runtime_configuration.return_value.update_type_registry.assert_called() - mocked_runtime_configuration.return_value.create_scale_object.assert_called() - assert ( - result - == mocked_runtime_configuration.return_value.create_scale_object.return_value.decode.return_value - ) - - -def test_query_map_subtensor(subtensor, mocker): - """Tests query_map_subtensor call.""" - # Prep - fake_name = "module_name" - - # Call - result = subtensor.query_map_subtensor(fake_name) - - # Asserts - subtensor.substrate.query_map.assert_called_once_with( - module="SubtensorModule", - storage_function=fake_name, - params=None, - block_hash=None, - ) - assert result == subtensor.substrate.query_map.return_value - - -def test_state_call(subtensor, mocker): - """Tests state_call call.""" - # Prep - fake_method = "method" - fake_data = "data" - - # Call - result = subtensor.state_call(fake_method, fake_data) - - # Asserts - subtensor.substrate.rpc_request.assert_called_once_with( - method="state_call", - params=[fake_method, fake_data], - ) - assert result == subtensor.substrate.rpc_request.return_value - - -def test_query_map(subtensor, mocker): - """Tests query_map call.""" - # Prep - fake_module_name = "module_name" - fake_name = "constant_name" - - # Call - result = subtensor.query_map(fake_module_name, fake_name) - - # Asserts - subtensor.substrate.query_map.assert_called_once_with( - module=fake_module_name, - storage_function=fake_name, - params=None, - block_hash=None, - ) - assert result == subtensor.substrate.query_map.return_value - - -def test_query_constant(subtensor, mocker): - """Tests query_constant call.""" - # Prep - fake_module_name = "module_name" - fake_constant_name = "constant_name" - - # Call - result = subtensor.query_constant(fake_module_name, fake_constant_name) - - # Asserts - subtensor.substrate.get_constant.assert_called_once_with( - module_name=fake_module_name, - constant_name=fake_constant_name, - block_hash=None, - ) - assert result == subtensor.substrate.get_constant.return_value - - -def test_query_module(subtensor): - # Prep - fake_module = "module" - fake_name = "function_name" - - # Call - result = subtensor.query_module(fake_module, fake_name) - - # Asserts - subtensor.substrate.query.assert_called_once_with( - module=fake_module, - storage_function=fake_name, - params=None, - block_hash=None, - ) - assert result == subtensor.substrate.query.return_value - - -def test_metagraph(subtensor, mocker): - """Tests subtensor.metagraph call.""" - # Prep - fake_netuid = 1 - fake_lite = True - mocked_metagraph = mocker.patch.object(subtensor_module, "Metagraph") - - # Call - result = subtensor.metagraph(fake_netuid, fake_lite) - - # Asserts - mocked_metagraph.assert_called_once_with( - network=subtensor.chain_endpoint, - netuid=fake_netuid, - lite=fake_lite, - sync=False, - subtensor=subtensor, - ) - mocked_metagraph.return_value.sync.assert_called_once_with( - block=None, lite=fake_lite, subtensor=subtensor - ) - assert result == mocked_metagraph.return_value - - -def test_get_netuids_for_hotkey(subtensor, mocker): - """Tests get_netuids_for_hotkey call.""" - # Prep - fake_hotkey_ss58 = "hotkey_ss58" - fake_block = 123 - - mocked_query_map_subtensor = mocker.MagicMock() - subtensor.query_map_subtensor = mocked_query_map_subtensor - - # Call - result = subtensor.get_netuids_for_hotkey(fake_hotkey_ss58, fake_block) - - # Asserts - mocked_query_map_subtensor.assert_called_once_with( - "IsNetworkMember", fake_block, [fake_hotkey_ss58] - ) - assert result == [] - - -def test_get_current_block(subtensor): - """Tests get_current_block call.""" - # Call - result = subtensor.get_current_block() - - # Asserts - subtensor.substrate.get_block_number.assert_called_once_with(None) - assert result == subtensor.substrate.get_block_number.return_value - - -def test_is_hotkey_registered_any(subtensor, mocker): - """Tests is_hotkey_registered_any call""" - # Prep - fake_hotkey_ss58 = "hotkey_ss58" - fake_block = 123 - return_value = [1, 2] - - mocked_get_netuids_for_hotkey = mocker.MagicMock(return_value=return_value) - subtensor.get_netuids_for_hotkey = mocked_get_netuids_for_hotkey - - # Call - result = subtensor.is_hotkey_registered_any(fake_hotkey_ss58, fake_block) - - # Asserts - mocked_get_netuids_for_hotkey.assert_called_once_with(fake_hotkey_ss58, fake_block) - assert result is (len(return_value) > 0) - - -def test_is_hotkey_registered_on_subnet(subtensor, mocker): - """Tests is_hotkey_registered_on_subnet call.""" - # Prep - fake_hotkey_ss58 = "hotkey_ss58" - fake_netuid = 1 - fake_block = 123 - - mocked_get_uid_for_hotkey_on_subnet = mocker.MagicMock() - subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet - - # Call - result = subtensor.is_hotkey_registered_on_subnet( - fake_hotkey_ss58, fake_netuid, fake_block - ) - - # Asserts - mocked_get_uid_for_hotkey_on_subnet.assert_called_once_with( - fake_hotkey_ss58, fake_netuid, fake_block - ) - assert result is (mocked_get_uid_for_hotkey_on_subnet.return_value is not None) - - -def test_is_hotkey_registered_without_netuid(subtensor, mocker): - """Tests is_hotkey_registered call with no netuid specified.""" - # Prep - fake_hotkey_ss58 = "hotkey_ss58" - - mocked_is_hotkey_registered_any = mocker.MagicMock() - subtensor.is_hotkey_registered_any = mocked_is_hotkey_registered_any - - # Call - - result = subtensor.is_hotkey_registered(fake_hotkey_ss58) - - # Asserts - mocked_is_hotkey_registered_any.assert_called_once_with(fake_hotkey_ss58, None) - assert result == mocked_is_hotkey_registered_any.return_value - - -def test_is_hotkey_registered_with_netuid(subtensor, mocker): - """Tests is_hotkey_registered call with netuid specified.""" - # Prep - fake_hotkey_ss58 = "hotkey_ss58" - fake_netuid = 123 - - mocked_is_hotkey_registered_on_subnet = mocker.MagicMock() - subtensor.is_hotkey_registered_on_subnet = mocked_is_hotkey_registered_on_subnet - - # Call - - result = subtensor.is_hotkey_registered(fake_hotkey_ss58, fake_netuid) - - # Asserts - mocked_is_hotkey_registered_on_subnet.assert_called_once_with( - fake_hotkey_ss58, fake_netuid, None - ) - assert result == mocked_is_hotkey_registered_on_subnet.return_value - - -def test_set_weights(subtensor, mocker): - """Successful set_weights call.""" - # Preps - fake_wallet = mocker.MagicMock() - fake_netuid = 1 - fake_uids = [2, 4] - fake_weights = [0.4, 0.6] - fake_wait_for_inclusion = False - fake_wait_for_finalization = False - fake_max_retries = 5 - - expected_result = (True, None) - - mocked_get_uid_for_hotkey_on_subnet = mocker.MagicMock() - subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet - - mocked_blocks_since_last_update = mocker.MagicMock(return_value=2) - subtensor.blocks_since_last_update = mocked_blocks_since_last_update - - mocked_weights_rate_limit = mocker.MagicMock(return_value=1) - subtensor.weights_rate_limit = mocked_weights_rate_limit - - mocked_set_weights_extrinsic = mocker.patch.object( - subtensor_module, "set_weights_extrinsic", return_value=expected_result - ) - - # Call - result = subtensor.set_weights( - wallet=fake_wallet, - netuid=fake_netuid, - uids=fake_uids, - weights=fake_weights, - version_key=settings.version_as_int, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - max_retries=fake_max_retries, - ) - - # Asserts - mocked_get_uid_for_hotkey_on_subnet.assert_called_once_with( - fake_wallet.hotkey.ss58_address, fake_netuid - ) - mocked_blocks_since_last_update.assert_called_with( - fake_netuid, mocked_get_uid_for_hotkey_on_subnet.return_value - ) - mocked_weights_rate_limit.assert_called_with(fake_netuid) - mocked_set_weights_extrinsic.assert_called_with( - subtensor=subtensor, - wallet=fake_wallet, - netuid=fake_netuid, - uids=fake_uids, - weights=fake_weights, - version_key=settings.version_as_int, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - assert result == expected_result - - -def test_serve_axon(subtensor, mocker): - """Tests successful serve_axon call.""" - # Prep - fake_netuid = 123 - fake_axon = mocker.MagicMock() - fake_wait_for_inclusion = False - fake_wait_for_finalization = True - fake_certificate = None - - mocked_serve_axon_extrinsic = mocker.patch.object( - subtensor_module, "serve_axon_extrinsic" - ) - - # Call - result = subtensor.serve_axon( - fake_netuid, fake_axon, fake_wait_for_inclusion, fake_wait_for_finalization - ) - - # Asserts - mocked_serve_axon_extrinsic.assert_called_once_with( - subtensor, - fake_netuid, - fake_axon, - fake_wait_for_inclusion, - fake_wait_for_finalization, - fake_certificate, - ) - assert result == mocked_serve_axon_extrinsic.return_value - - -def test_get_block_hash(subtensor, mocker): - """Tests successful get_block_hash call.""" - # Prep - fake_block_id = 123 - - # Call - result = subtensor.get_block_hash(fake_block_id) - - # Asserts - subtensor.substrate.get_block_hash.assert_called_once_with(block_id=fake_block_id) - assert result == subtensor.substrate.get_block_hash.return_value - - -def test_commit(subtensor, mocker): - """Test successful commit call.""" - # Preps - fake_wallet = mocker.MagicMock() - fake_netuid = 1 - fake_data = "some data to network" - mocked_publish_metadata = mocker.patch.object(subtensor_module, "publish_metadata") - - # Call - result = subtensor.commit(fake_wallet, fake_netuid, fake_data) - - # Asserts - mocked_publish_metadata.assert_called_once_with( - subtensor, fake_wallet, fake_netuid, f"Raw{len(fake_data)}", fake_data.encode() - ) - assert result is None - - -def test_subnetwork_n(subtensor, mocker): - """Test successful subnetwork_n call.""" - # Prep - fake_netuid = 1 - fake_block = 123 - fake_result = 2 - - mocked_get_hyperparameter = mocker.MagicMock() - mocked_get_hyperparameter.return_value = fake_result - subtensor._get_hyperparameter = mocked_get_hyperparameter - - # Call - result = subtensor.subnetwork_n(fake_netuid, fake_block) - - # Asserts - mocked_get_hyperparameter.assert_called_once_with( - param_name="SubnetworkN", netuid=fake_netuid, block=fake_block - ) - assert result == mocked_get_hyperparameter.return_value - - -def test_transfer(subtensor, mocker): - """Tests successful transfer call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_dest = "SS58PUBLICKEY" - fake_amount = 1.1 - fake_wait_for_inclusion = True - fake_wait_for_finalization = True - mocked_transfer_extrinsic = mocker.patch.object( - subtensor_module, "transfer_extrinsic" - ) - - # Call - result = subtensor.transfer( - fake_wallet, - fake_dest, - fake_amount, - fake_wait_for_inclusion, - fake_wait_for_finalization, - ) - - # Asserts - mocked_transfer_extrinsic.assert_called_once_with( - subtensor=subtensor, - wallet=fake_wallet, - dest=fake_dest, - amount=fake_amount, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - assert result == mocked_transfer_extrinsic.return_value - - -def test_get_neuron_for_pubkey_and_subnet(subtensor, mocker): - """Successful call to get_neuron_for_pubkey_and_subnet.""" - # Prep - fake_hotkey_ss58 = "fake_hotkey" - fake_netuid = 1 - fake_block = 123 - - mocked_neuron_for_uid = mocker.MagicMock() - subtensor.neuron_for_uid = mocked_neuron_for_uid - - mocked_get_uid_for_hotkey_on_subnet = mocker.MagicMock() - subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet - - # Call - result = subtensor.get_neuron_for_pubkey_and_subnet( - hotkey_ss58=fake_hotkey_ss58, - netuid=fake_netuid, - block=fake_block, - ) - - # Asserts - mocked_neuron_for_uid.assert_called_once_with( - mocked_get_uid_for_hotkey_on_subnet.return_value, - fake_netuid, - block=fake_block, - ) - assert result == mocked_neuron_for_uid.return_value - - -def test_neuron_for_uid_none(subtensor, mocker): - """Test neuron_for_uid successful call.""" - # Prep - fake_uid = None - fake_netuid = 2 - fake_block = 123 - mocked_neuron_info = mocker.patch.object( - subtensor_module.NeuronInfo, "get_null_neuron" - ) - - # Call - result = subtensor.neuron_for_uid( - uid=fake_uid, netuid=fake_netuid, block=fake_block - ) - - # Asserts - mocked_neuron_info.assert_called_once() - assert result == mocked_neuron_info.return_value - - -def test_neuron_for_uid_response_none(subtensor, mocker): - """Test neuron_for_uid successful call.""" - # Prep - fake_uid = 1 - fake_netuid = 2 - fake_block = 123 - mocked_neuron_info = mocker.patch.object( - subtensor_module.NeuronInfo, "get_null_neuron" - ) - - subtensor.substrate.rpc_request.return_value.get.return_value = None - - # Call - result = subtensor.neuron_for_uid( - uid=fake_uid, netuid=fake_netuid, block=fake_block - ) - - # Asserts - subtensor.substrate.rpc_request.assert_called_once_with( - method="neuronInfo_getNeuron", - params=[fake_netuid, fake_uid, subtensor.substrate.get_block_hash.return_value], - ) - - mocked_neuron_info.assert_called_once() - assert result == mocked_neuron_info.return_value - - -def test_neuron_for_uid_success(subtensor, mocker): - """Test neuron_for_uid successful call.""" - # Prep - fake_uid = 1 - fake_netuid = 2 - fake_block = 123 - mocked_neuron_from_vec_u8 = mocker.patch.object( - subtensor_module.NeuronInfo, "from_vec_u8" - ) - mock_get_block_hash = mocker.patch.object(subtensor, "get_block_hash") - - # Call - result = subtensor.neuron_for_uid( - uid=fake_uid, netuid=fake_netuid, block=fake_block - ) - - # Asserts - mock_get_block_hash.assert_called_once_with(fake_block) - subtensor.substrate.rpc_request.assert_called_once_with( - method="neuronInfo_getNeuron", - params=[fake_netuid, fake_uid, mock_get_block_hash.return_value], - ) - - mocked_neuron_from_vec_u8.assert_called_once_with( - subtensor.substrate.rpc_request.return_value.get.return_value - ) - assert result == mocked_neuron_from_vec_u8.return_value - - -@pytest.mark.parametrize( - ["fake_call_params", "expected_call_function"], - [ - (call_params(), "serve_axon"), - (call_params_with_certificate(), "serve_axon_tls"), - ], -) -def test_do_serve_axon_is_success( - subtensor, mocker, fake_call_params, expected_call_function -): - """Successful do_serve_axon call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_wait_for_inclusion = True - fake_wait_for_finalization = True - - subtensor.substrate.submit_extrinsic.return_value.is_success = True - - # Call - result = subtensor._do_serve_axon( - wallet=fake_wallet, - call_params=fake_call_params, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - # Asserts - subtensor.substrate.compose_call.assert_called_once_with( - call_module="SubtensorModule", - call_function=expected_call_function, - call_params=fake_call_params, - ) - - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.hotkey, - ) - - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - assert result[0] is True - assert result[1] is None - - -def test_do_serve_axon_is_not_success(subtensor, mocker, fake_call_params): - """Unsuccessful do_serve_axon call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_wait_for_inclusion = True - fake_wait_for_finalization = True - - subtensor.substrate.submit_extrinsic.return_value.is_success = None - - # Call - result = subtensor._do_serve_axon( - wallet=fake_wallet, - call_params=fake_call_params, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - # Asserts - subtensor.substrate.compose_call.assert_called_once_with( - call_module="SubtensorModule", - call_function="serve_axon", - call_params=fake_call_params, - ) - - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.hotkey, - ) - - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - assert result == ( - False, - subtensor.substrate.submit_extrinsic.return_value.error_message, - ) - - -def test_do_serve_axon_no_waits(subtensor, mocker, fake_call_params): - """Unsuccessful do_serve_axon call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_wait_for_inclusion = False - fake_wait_for_finalization = False - - # Call - result = subtensor._do_serve_axon( - wallet=fake_wallet, - call_params=fake_call_params, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - # Asserts - subtensor.substrate.compose_call.assert_called_once_with( - call_module="SubtensorModule", - call_function="serve_axon", - call_params=fake_call_params, - ) - - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.hotkey, - ) - - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - assert result == (True, None) - - -def test_immunity_period(subtensor, mocker): - """Successful immunity_period call.""" - # Preps - fake_netuid = 1 - fake_block = 123 - fare_result = 101 - - mocked_get_hyperparameter = mocker.MagicMock() - mocked_get_hyperparameter.return_value = fare_result - subtensor._get_hyperparameter = mocked_get_hyperparameter - - # Call - result = subtensor.immunity_period(netuid=fake_netuid, block=fake_block) - - # Assertions - mocked_get_hyperparameter.assert_called_once_with( - param_name="ImmunityPeriod", - netuid=fake_netuid, - block=fake_block, - ) - assert result == mocked_get_hyperparameter.return_value - - -def test_get_uid_for_hotkey_on_subnet(subtensor, mocker): - """Successful get_uid_for_hotkey_on_subnet call.""" - # Prep - fake_hotkey_ss58 = "fake_hotkey_ss58" - fake_netuid = 1 - fake_block = 123 - mocked_query_subtensor = mocker.MagicMock() - subtensor.query_subtensor = mocked_query_subtensor - - # Call - result = subtensor.get_uid_for_hotkey_on_subnet( - hotkey_ss58=fake_hotkey_ss58, netuid=fake_netuid, block=fake_block - ) - - # Assertions - mocked_query_subtensor.assert_called_once_with( - "Uids", fake_block, [fake_netuid, fake_hotkey_ss58] - ) - - assert result == mocked_query_subtensor.return_value - - -def test_tempo(subtensor, mocker): - """Successful tempo call.""" - # Preps - fake_netuid = 1 - fake_block = 123 - fare_result = 101 - - mocked_get_hyperparameter = mocker.MagicMock() - mocked_get_hyperparameter.return_value = fare_result - subtensor._get_hyperparameter = mocked_get_hyperparameter - - # Call - result = subtensor.tempo(netuid=fake_netuid, block=fake_block) - - # Assertions - mocked_get_hyperparameter.assert_called_once_with( - param_name="Tempo", - netuid=fake_netuid, - block=fake_block, - ) - assert result == mocked_get_hyperparameter.return_value - - -def test_get_commitment(subtensor, mocker): - """Successful get_commitment call.""" - # Preps - fake_netuid = 1 - fake_uid = 2 - fake_block = 3 - fake_hotkey = "hotkey" - fake_hex_data = "0x010203" - expected_result = bytes.fromhex(fake_hex_data[2:]).decode() - - mocked_metagraph = mocker.MagicMock() - subtensor.metagraph = mocked_metagraph - mocked_metagraph.return_value.hotkeys = {fake_uid: fake_hotkey} - - mocked_get_metadata = mocker.patch.object(subtensor_module, "get_metadata") - mocked_get_metadata.return_value = { - "info": {"fields": [{fake_hex_data: fake_hex_data}]} - } - - # Call - result = subtensor.get_commitment( - netuid=fake_netuid, uid=fake_uid, block=fake_block - ) - - # Assertions - mocked_metagraph.assert_called_once_with(fake_netuid) - assert result == expected_result - - -def test_min_allowed_weights(subtensor, mocker): - """Successful min_allowed_weights call.""" - fake_netuid = 1 - fake_block = 123 - return_value = 10 - - mocked_get_hyperparameter = mocker.MagicMock(return_value=return_value) - subtensor._get_hyperparameter = mocked_get_hyperparameter - - # Call - result = subtensor.min_allowed_weights(netuid=fake_netuid, block=fake_block) - - # Assertion - mocked_get_hyperparameter.assert_called_once_with( - param_name="MinAllowedWeights", block=fake_block, netuid=fake_netuid - ) - assert result == return_value - - -def test_max_weight_limit(subtensor, mocker): - """Successful max_weight_limit call.""" - fake_netuid = 1 - fake_block = 123 - return_value = 100 - - mocked_get_hyperparameter = mocker.MagicMock(return_value=return_value) - subtensor._get_hyperparameter = mocked_get_hyperparameter - - mocked_u16_normalized_float = mocker.MagicMock() - subtensor_module.u16_normalized_float = mocked_u16_normalized_float - - # Call - result = subtensor.max_weight_limit(netuid=fake_netuid, block=fake_block) - - # Assertion - mocked_get_hyperparameter.assert_called_once_with( - param_name="MaxWeightsLimit", block=fake_block, netuid=fake_netuid - ) - assert result == mocked_u16_normalized_float.return_value - - -def test_get_transfer_fee(subtensor, mocker): - """Successful get_transfer_fee call.""" - # Preps - fake_wallet = mocker.MagicMock() - fake_dest = "SS58ADDRESS" - value = 1 - - fake_payment_info = {"partialFee": int(2e10)} - subtensor.substrate.get_payment_info.return_value = fake_payment_info - - # Call - result = subtensor.get_transfer_fee(wallet=fake_wallet, dest=fake_dest, value=value) - - # Asserts - subtensor.substrate.compose_call.assert_called_once_with( - call_module="Balances", - call_function="transfer_allow_death", - call_params={"dest": fake_dest, "value": value}, - ) - - subtensor.substrate.get_payment_info.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.coldkeypub, - ) - - assert result == 2e10 - - -def test_get_transfer_fee_incorrect_value(subtensor, mocker): - """Successful get_transfer_fee call.""" - # Preps - fake_wallet = mocker.MagicMock() - fake_dest = mocker.MagicMock() - value = "no_int_no_float_no_Balance" - - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.get_transfer_fee(wallet=fake_wallet, dest=fake_dest, value=value) - - # Asserts - spy_balance_from_rao.assert_called_once_with(2e7) - - assert result == Balance.from_rao(int(2e7)) - - -def test_get_existential_deposit(subtensor, mocker): - """Successful get_existential_deposit call.""" - # Prep - block = 123 - - mocked_query_constant = mocker.MagicMock() - value = 10 - mocked_query_constant.return_value = value - subtensor.query_constant = mocked_query_constant - - # Call - result = subtensor.get_existential_deposit(block=block) - - # Assertions - mocked_query_constant.assert_called_once_with( - module_name="Balances", constant_name="ExistentialDeposit", block=block - ) - - assert isinstance(result, Balance) - assert result == Balance.from_rao(value) - - -def test_commit_weights(subtensor, mocker): - """Successful commit_weights call.""" - # Preps - fake_wallet = mocker.MagicMock() - netuid = 1 - salt = [1, 3] - uids = [2, 4] - weights = [0.4, 0.6] - wait_for_inclusion = False - wait_for_finalization = False - 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 - ) - - # Call - result = subtensor.commit_weights( - wallet=fake_wallet, - netuid=netuid, - salt=salt, - uids=uids, - weights=weights, - version_key=settings.version_as_int, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - max_retries=max_retries, - ) - - # 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, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - assert result == expected_result - - -def test_reveal_weights(subtensor, mocker): - """Successful test_reveal_weights call.""" - # Preps - fake_wallet = mocker.MagicMock() - netuid = 1 - uids = [1, 2, 3, 4] - weights = [0.1, 0.2, 0.3, 0.4] - salt = [4, 2, 2, 1] - expected_result = (True, None) - mocked_extrinsic = mocker.patch.object( - subtensor_module, "reveal_weights_extrinsic", return_value=expected_result - ) - - # Call - result = subtensor.reveal_weights( - wallet=fake_wallet, - netuid=netuid, - uids=uids, - weights=weights, - salt=salt, - wait_for_inclusion=False, - wait_for_finalization=False, - ) - - # Assertions - assert result == (True, None) - mocked_extrinsic.assert_called_once_with( - subtensor=subtensor, - wallet=fake_wallet, - netuid=netuid, - uids=uids, - version_key=version_as_int, - weights=weights, - salt=salt, - wait_for_inclusion=False, - wait_for_finalization=False, - ) - - -def test_reveal_weights_false(subtensor, mocker): - """Failed test_reveal_weights call.""" - # Preps - fake_wallet = mocker.MagicMock() - netuid = 1 - uids = [1, 2, 3, 4] - weights = [0.1, 0.2, 0.3, 0.4] - salt = [4, 2, 2, 1] - - expected_result = ( - False, - "No attempt made. Perhaps it is too soon to reveal weights!", - ) - mocked_extrinsic = mocker.patch.object(subtensor_module, "reveal_weights_extrinsic") - - # Call - result = subtensor.reveal_weights( - wallet=fake_wallet, - netuid=netuid, - uids=uids, - weights=weights, - salt=salt, - wait_for_inclusion=False, - wait_for_finalization=False, - ) - - # Assertion - assert result == expected_result - assert mocked_extrinsic.call_count == 5 - - -def test_get_subnet_burn_cost_success(subtensor, mocker): - """Tests get_subnet_burn_cost method with successfully result.""" - # Preps - mocked_query_runtime_api = mocker.patch.object(subtensor, "query_runtime_api") - fake_block = 123 - - # Call - result = subtensor.get_subnet_burn_cost(fake_block) - - # Asserts - mocked_query_runtime_api.assert_called_once_with( - runtime_api="SubnetRegistrationRuntimeApi", - method="get_network_registration_cost", - params=[], - block=fake_block, - ) - - assert result == mocked_query_runtime_api.return_value - - -def test_get_subnet_burn_cost_none(subtensor, mocker): - """Tests get_subnet_burn_cost method with None result.""" - # Preps - mocked_query_runtime_api = mocker.patch.object( - subtensor, "query_runtime_api", return_value=None - ) - fake_block = 123 - - # Call - result = subtensor.get_subnet_burn_cost(fake_block) - - # Asserts - mocked_query_runtime_api.assert_called_once_with( - runtime_api="SubnetRegistrationRuntimeApi", - method="get_network_registration_cost", - params=[], - block=fake_block, - ) - - assert result is None - - -def test_difficulty_success(subtensor, mocker): - """Tests difficulty method with successfully result.""" - # Preps - mocked_get_hyperparameter = mocker.patch.object(subtensor, "_get_hyperparameter") - fake_netuid = 1 - fake_block = 2 - - # Call - result = subtensor.difficulty(fake_netuid, fake_block) - - # Asserts - mocked_get_hyperparameter.assert_called_once_with( - param_name="Difficulty", - netuid=fake_netuid, - block=fake_block, - ) - - assert result == int(mocked_get_hyperparameter.return_value) - - -def test_difficulty_none(subtensor, mocker): - """Tests difficulty method with None result.""" - # Preps - mocked_get_hyperparameter = mocker.patch.object( - subtensor, "_get_hyperparameter", return_value=None - ) - fake_netuid = 1 - fake_block = 2 - - # Call - result = subtensor.difficulty(fake_netuid, fake_block) - - # Asserts - mocked_get_hyperparameter.assert_called_once_with( - param_name="Difficulty", - netuid=fake_netuid, - block=fake_block, - ) - - assert result is None - - -def test_recycle_success(subtensor, mocker): - """Tests recycle method with successfully result.""" - # Preps - mocked_get_hyperparameter = mocker.patch.object( - subtensor, "_get_hyperparameter", return_value=0.1 - ) - fake_netuid = 1 - fake_block = 2 - mocked_balance = mocker.patch("bittensor.utils.balance.Balance") - - # Call - result = subtensor.recycle(fake_netuid, fake_block) - - # Asserts - mocked_get_hyperparameter.assert_called_once_with( - param_name="Burn", - netuid=fake_netuid, - block=fake_block, - ) - - mocked_balance.assert_called_once_with(int(mocked_get_hyperparameter.return_value)) - assert result == mocked_balance.return_value - - -def test_recycle_none(subtensor, mocker): - """Tests recycle method with None result.""" - # Preps - mocked_get_hyperparameter = mocker.patch.object( - subtensor_module.AsyncSubtensor, "get_hyperparameter", return_value=None - ) - - fake_netuid = 1 - fake_block = 2 - fake_block_hash = "0x123456789" - fake_reuse_block = True - - mocked_determine_block_hash = mocker.patch.object( - subtensor_module.AsyncSubtensor, "_determine_block_hash" - ) - - # Call - result = subtensor.recycle( - netuid=fake_netuid, - block=fake_block, - block_hash=fake_block_hash, - reuse_block=fake_reuse_block, - ) - - # Asserts - mocked_determine_block_hash.asseert_awaited_once_with( - fake_block, fake_block_hash, fake_reuse_block - ) - - mocked_get_hyperparameter.assert_called_once_with( - param_name="Burn", - netuid=fake_netuid, - block_hash=mocked_determine_block_hash.return_value, - reuse_block=fake_reuse_block, - ) - - assert result is None - - -# `get_all_subnets_info` tests -def test_get_all_subnets_info_success(mocker, subtensor): - """Test get_all_subnets_info returns correct data when subnet information is found.""" - # Prep - block = 123 - mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="mock_block_hash" - ) - hex_bytes_result = "0x010203" - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - mocker.patch.object(subtensor, "query_runtime_api", return_value=hex_bytes_result) - mocker.patch.object( - subtensor_module.SubnetInfo, - "list_from_vec_u8", - return_value="list_from_vec_u80", - ) - - # Call - subtensor.get_all_subnets_info(block) - - # Asserts - subtensor.query_runtime_api.assert_called_once_with( - "SubnetInfoRuntimeApi", "get_subnets_info", params=[], block=block - ) - subtensor_module.SubnetInfo.list_from_vec_u8.assert_called_once_with(bytes_result) - - -@pytest.mark.parametrize("result_", [[], None]) -def test_get_all_subnets_info_no_data(mocker, subtensor, result_): - """Test get_all_subnets_info returns empty list when no subnet information is found.""" - # Prep - block = 123 - mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="mock_block_hash" - ) - mocker.patch.object(subtensor_module.SubnetInfo, "list_from_vec_u8") - - mocker.patch.object(subtensor, "query_runtime_api", return_value=result_) - - # Call - result = subtensor.get_all_subnets_info(block) - - # Asserts - assert result == [] - subtensor.query_runtime_api.assert_called_once_with( - "SubnetInfoRuntimeApi", "get_subnets_info", params=[], block=block - ) - subtensor_module.SubnetInfo.list_from_vec_u8.assert_not_called() - - -def test_get_delegate_take_success(subtensor, mocker): - """Verify `get_delegate_take` method successful path.""" - # Preps - fake_hotkey_ss58 = "FAKE_SS58" - fake_block = 123 - - subtensor_module.u16_normalized_float = mocker.Mock() - subtensor.query_subtensor = mocker.Mock(return_value=mocker.Mock()) - - # Call - result = subtensor.get_delegate_take(hotkey_ss58=fake_hotkey_ss58, block=fake_block) - - # Asserts - subtensor.query_subtensor.assert_called_once_with( - "Delegates", fake_block, [fake_hotkey_ss58] - ) - subtensor_module.u16_normalized_float.assert_called_once_with( - subtensor.query_subtensor.return_value - ) - assert result == subtensor_module.u16_normalized_float.return_value - - -def test_get_delegate_take_none(subtensor, mocker): - """Verify `get_delegate_take` method returns None.""" +def test_methods_comparable(mocker): + """Verifies that methods in sync and async Subtensors are comparable.""" # Preps - fake_hotkey_ss58 = "FAKE_SS58" - fake_block = 123 - - subtensor.query_subtensor = mocker.Mock(return_value=None) - subtensor_module.u16_normalized_float = mocker.Mock() - - # Call - result = subtensor.get_delegate_take(hotkey_ss58=fake_hotkey_ss58, block=fake_block) - - # Asserts - subtensor.query_subtensor.assert_called_once_with( - "Delegates", fake_block, [fake_hotkey_ss58] - ) - - subtensor_module.u16_normalized_float.assert_not_called() - assert result is None - - -def test_networks_during_connection(mocker): - """Test networks during_connection.""" - # Preps - subtensor_module.SubstrateInterface = mocker.Mock() - mocker.patch("websockets.sync.client.connect") - # Call - for network in list(settings.NETWORK_MAP.keys()) + ["undefined"]: - sub = Subtensor(network) - - # Assertions - sub.network = network - sub.chain_endpoint = settings.NETWORK_MAP.get(network) - - -@pytest.mark.parametrize( - "fake_value_result", - [1, None], - ids=["result has value attr", "result has not value attr"], -) -def test_get_stake_for_coldkey_and_hotkey(subtensor, mocker, fake_value_result): - """Test get_stake_for_coldkey_and_hotkey calls right method with correct arguments.""" - # Preps - fake_hotkey_ss58 = "FAKE_H_SS58" - fake_coldkey_ss58 = "FAKE_C_SS58" - fake_block = 123 - - subtensor.query_subtensor = mocker.patch.object( - subtensor, "query_subtensor", return_value=fake_value_result - ) - spy_balance_from_rao = mocker.spy(subtensor_module.Balance, "from_rao") - - # Call - result = subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=fake_hotkey_ss58, - coldkey_ss58=fake_coldkey_ss58, - block=fake_block, - ) - - # Asserts - subtensor.query_subtensor.assert_called_once_with( - "Stake", fake_block, [fake_hotkey_ss58, fake_coldkey_ss58] - ) - if fake_value_result is not None: - spy_balance_from_rao.assert_called_once_with(fake_value_result) - else: - spy_balance_from_rao.assert_not_called() - assert result == fake_value_result - - -def test_does_hotkey_exist_true(mocker, subtensor): - """Test when the hotkey exists.""" - # Mock data - fake_hotkey_ss58 = "fake_hotkey" - fake_owner = "valid_owner" - fake_block = 123 - - # Mocks - mock_query_subtensor = mocker.patch.object( - subtensor, - "query_subtensor", - return_value=mocker.Mock(value=fake_owner), - ) - - # Call - result = subtensor.does_hotkey_exist(fake_hotkey_ss58, block=fake_block) - - # Assertions - mock_query_subtensor.assert_called_once_with( - "Owner", fake_block, [fake_hotkey_ss58] - ) - assert result is True - - -def test_does_hotkey_exist_no_value(mocker, subtensor): - """Test when query_subtensor returns no value.""" - # Mock data - fake_hotkey_ss58 = "fake_hotkey" - fake_block = 123 - - # Mocks - mock_query_subtensor = mocker.patch.object( - subtensor, - "query_subtensor", - return_value=None, - ) - - # Call - result = subtensor.does_hotkey_exist(fake_hotkey_ss58, block=fake_block) - - # Assertions - mock_query_subtensor.assert_called_once_with( - "Owner", fake_block, [fake_hotkey_ss58] - ) - assert result is False - - -def test_does_hotkey_exist_special_id(mocker, subtensor): - """Test when query_subtensor returns the special invalid owner identifier.""" - # Mock data - fake_hotkey_ss58 = "fake_hotkey" - fake_owner = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" - fake_block = 123 - - # Mocks - mock_query_subtensor = mocker.patch.object( - subtensor, - "query_subtensor", - return_value=fake_owner, - ) - - # Call - result = subtensor.does_hotkey_exist(fake_hotkey_ss58, block=fake_block) - - # Assertions - mock_query_subtensor.assert_called_once_with( - "Owner", fake_block, [fake_hotkey_ss58] - ) - assert result is False + mocker.patch("bittensor.utils.substrate_interface.AsyncSubstrateInterface") + subtensor = Subtensor() - -def test_does_hotkey_exist_latest_block(mocker, subtensor): - """Test when no block is provided (latest block).""" - # Mock data - fake_hotkey_ss58 = "fake_hotkey" - fake_owner = "valid_owner" - - # Mocks - mock_query_subtensor = mocker.patch.object( - subtensor, - "query_subtensor", - return_value=mocker.Mock(value=fake_owner), - ) - - # Call - result = subtensor.does_hotkey_exist(fake_hotkey_ss58) - - # Assertions - mock_query_subtensor.assert_called_once_with("Owner", None, [fake_hotkey_ss58]) - assert result is True - - -def test_get_hotkey_owner_success(mocker, subtensor): - """Test when hotkey exists and owner is found.""" - # Mock data - fake_hotkey_ss58 = "fake_hotkey" - fake_coldkey_ss58 = "fake_coldkey" - fake_block = 123 - - # Mocks - mock_query_subtensor = mocker.patch.object( - subtensor, - "query_subtensor", - return_value=fake_coldkey_ss58, - ) - mock_does_hotkey_exist = mocker.patch.object( - subtensor, "does_hotkey_exist", return_value=True - ) - - # Call - result = subtensor.get_hotkey_owner(fake_hotkey_ss58, block=fake_block) - - # Assertions - mock_query_subtensor.assert_called_once_with( - "Owner", fake_block, [fake_hotkey_ss58] - ) - mock_does_hotkey_exist.assert_called_once_with(fake_hotkey_ss58, fake_block) - assert result == fake_coldkey_ss58 - - -def test_get_hotkey_owner_no_value(mocker, subtensor): - """Test when query_subtensor returns no value.""" - # Mock data - fake_hotkey_ss58 = "fake_hotkey" - fake_block = 123 - - # Mocks - mock_query_subtensor = mocker.patch.object( - subtensor, - "query_subtensor", - return_value=None, - ) - mock_does_hotkey_exist = mocker.patch.object( - subtensor, "does_hotkey_exist", return_value=True - ) - - # Call - result = subtensor.get_hotkey_owner(fake_hotkey_ss58, block=fake_block) - - # Assertions - mock_query_subtensor.assert_called_once_with( - "Owner", fake_block, [fake_hotkey_ss58] - ) - mock_does_hotkey_exist.assert_not_called() - assert result is None - - -def test_get_hotkey_owner_does_not_exist(mocker, subtensor): - """Test when hotkey does not exist.""" - # Mock data - fake_hotkey_ss58 = "fake_hotkey" - fake_block = 123 - - # Mocks - mock_query_subtensor = mocker.patch.object( - subtensor, - "query_subtensor", - return_value=mocker.Mock(value="fake_coldkey"), - ) - mock_does_hotkey_exist = mocker.patch.object( - subtensor, "does_hotkey_exist", return_value=False - ) - - # Call - result = subtensor.get_hotkey_owner(fake_hotkey_ss58, block=fake_block) - - # Assertions - mock_query_subtensor.assert_called_once_with( - "Owner", fake_block, [fake_hotkey_ss58] - ) - mock_does_hotkey_exist.assert_called_once_with(fake_hotkey_ss58, fake_block) - assert result is None - - -def test_get_hotkey_owner_latest_block(mocker, subtensor): - """Test when no block is provided (latest block).""" - # Mock data - fake_hotkey_ss58 = "fake_hotkey" - fake_coldkey_ss58 = "fake_coldkey" - - # Mocks - mock_query_subtensor = mocker.patch.object( - subtensor, - "query_subtensor", - return_value=fake_coldkey_ss58, - ) - mock_does_hotkey_exist = mocker.patch.object( - subtensor, "does_hotkey_exist", return_value=True - ) - - # Call - result = subtensor.get_hotkey_owner(fake_hotkey_ss58) - - # Assertions - mock_query_subtensor.assert_called_once_with("Owner", None, [fake_hotkey_ss58]) - mock_does_hotkey_exist.assert_called_once_with(fake_hotkey_ss58, None) - assert result == fake_coldkey_ss58 - - -def test_get_minimum_required_stake_success(mocker, subtensor): - """Test successful call to get_minimum_required_stake.""" - # Mock data - fake_min_stake = "1000000000" # Example value in rao - - # Mocking - mock_query = mocker.patch.object( - subtensor.substrate, - "query", - return_value=mocker.Mock(decode=mocker.Mock(return_value=fake_min_stake)), - ) - mock_balance_from_rao = mocker.patch("bittensor.utils.balance.Balance.from_rao") - - # Call - result = subtensor.get_minimum_required_stake() - - # Assertions - mock_query.assert_called_once_with( - module="SubtensorModule", storage_function="NominatorMinRequiredStake" - ) - mock_balance_from_rao.assert_called_once_with(fake_min_stake) - assert result == mock_balance_from_rao.return_value - - -def test_get_minimum_required_stake_query_failure(mocker, subtensor): - """Test query failure in get_minimum_required_stake.""" - # Mocking - mock_query = mocker.patch.object( - subtensor.substrate, - "query", - side_effect=Exception("Query failed"), - ) - - # Call and Assertions - with pytest.raises(Exception, match="Query failed"): - subtensor.get_minimum_required_stake() - mock_query.assert_called_once_with( - module="SubtensorModule", storage_function="NominatorMinRequiredStake" - ) - - -def test_get_minimum_required_stake_invalid_result(mocker, subtensor): - """Test when the result cannot be decoded.""" - # Mock data - fake_invalid_stake = None # Simulate a failure in decoding - - # Mocking - mock_query = mocker.patch.object( - subtensor.substrate, - "query", - return_value=mocker.Mock(decode=mocker.Mock(return_value=fake_invalid_stake)), - ) - mock_balance_from_rao = mocker.patch("bittensor.utils.balance.Balance.from_rao") - - # Call - result = subtensor.get_minimum_required_stake() - - # Assertions - mock_query.assert_called_once_with( - module="SubtensorModule", storage_function="NominatorMinRequiredStake" - ) - mock_balance_from_rao.assert_called_once_with(fake_invalid_stake) - assert result == mock_balance_from_rao.return_value - - -def test_tx_rate_limit_success(mocker, subtensor): - """Test when tx_rate_limit is successfully retrieved.""" - # Mock data - fake_rate_limit = 100 - fake_block = 123 - - # Mocks - mock_query_subtensor = mocker.patch.object( - subtensor, - "query_subtensor", - return_value=fake_rate_limit, - ) - - # Call - result = subtensor.tx_rate_limit(block=fake_block) - - # Assertions - mock_query_subtensor.assert_called_once_with("TxRateLimit", fake_block) - assert result == fake_rate_limit - - -def test_tx_rate_limit_no_value(mocker, subtensor): - """Test when query_subtensor returns None.""" - # Mock data - fake_block = 123 - - # Mocks - mock_query_subtensor = mocker.patch.object( - subtensor, - "query_subtensor", - return_value=None, - ) - - # Call - result = subtensor.tx_rate_limit(block=fake_block) - - # Assertions - mock_query_subtensor.assert_called_once_with("TxRateLimit", fake_block) - assert result is None - - -def test_get_delegates_success(mocker, subtensor): - """Test when delegates are successfully retrieved.""" - # Mock data - fake_block = 123 - fake_block_hash = "0xabc123" - fake_json_body = { - "result": b"mock_encoded_delegates", - } - - # Mocks - mock_get_block_hash = mocker.patch.object( - subtensor, - "get_block_hash", - return_value=fake_block_hash, - ) - mock_rpc_request = mocker.patch.object( - subtensor.substrate, - "rpc_request", - return_value=fake_json_body, - ) - mock_list_from_vec_u8 = mocker.patch.object( - subtensor_module.DelegateInfo, - "list_from_vec_u8", - return_value=["delegate1", "delegate2"], - ) - - # Call - result = subtensor.get_delegates(block=fake_block) - - # Assertions - mock_get_block_hash.assert_called_once_with(fake_block) - mock_rpc_request.assert_called_once_with( - method="delegateInfo_getDelegates", - params=[fake_block_hash], - ) - mock_list_from_vec_u8.assert_called_once_with(fake_json_body["result"]) - assert result == ["delegate1", "delegate2"] - - -def test_get_delegates_no_result(mocker, subtensor): - """Test when rpc_request returns no result.""" - # Mock data - fake_block = 123 - fake_block_hash = "0xabc123" - fake_json_body = {} - - # Mocks - mock_get_block_hash = mocker.patch.object( - subtensor, - "get_block_hash", - return_value=fake_block_hash, - ) - mock_rpc_request = mocker.patch.object( - subtensor.substrate, - "rpc_request", - return_value=fake_json_body, - ) - - # Call - result = subtensor.get_delegates(block=fake_block) - - # Assertions - mock_get_block_hash.assert_called_once_with(fake_block) - mock_rpc_request.assert_called_once_with( - method="delegateInfo_getDelegates", - params=[fake_block_hash], - ) - assert result == [] - - -def test_get_delegates_latest_block(mocker, subtensor): - """Test when no block is provided (latest block).""" - # Mock data - fake_json_body = { - "result": b"mock_encoded_delegates", - } - - # Mocks - mock_rpc_request = mocker.patch.object( - subtensor.substrate, - "rpc_request", - return_value=fake_json_body, - ) - mock_list_from_vec_u8 = mocker.patch.object( - subtensor_module.DelegateInfo, - "list_from_vec_u8", - return_value=["delegate1", "delegate2"], - ) - - # Call - result = subtensor.get_delegates() - - # Assertions - mock_rpc_request.assert_called_once_with( - method="delegateInfo_getDelegates", - params=[], - ) - mock_list_from_vec_u8.assert_called_once_with(fake_json_body["result"]) - assert result == ["delegate1", "delegate2"] - - -def test_is_hotkey_delegate_true(mocker, subtensor): - """Test when hotkey is a delegate.""" - # Mock data - fake_hotkey_ss58 = "hotkey_1" - fake_block = 123 - fake_delegates = [ - mocker.Mock(hotkey_ss58="hotkey_1"), - mocker.Mock(hotkey_ss58="hotkey_2"), + # methods which lives in sync subtensor only + excluded_subtensor_methods = ["async_subtensor", "event_loop", "execute_coroutine"] + # methods which lives in async subtensor only + excluded_async_subtensor_methods = [ + "determine_block_hash", + "encode_params", + "get_hyperparameter", + "sign_and_send_extrinsic", ] - - # Mocks - mock_get_delegates = mocker.patch.object( - subtensor, "get_delegates", return_value=fake_delegates - ) - - # Call - result = subtensor.is_hotkey_delegate(fake_hotkey_ss58, block=fake_block) - - # Assertions - mock_get_delegates.assert_called_once_with(block=fake_block) - assert result is True - - -def test_is_hotkey_delegate_false(mocker, subtensor): - """Test when hotkey is not a delegate.""" - # Mock data - fake_hotkey_ss58 = "hotkey_3" - fake_block = 123 - fake_delegates = [ - mocker.Mock(hotkey_ss58="hotkey_1"), - mocker.Mock(hotkey_ss58="hotkey_2"), + subtensor_methods = [ + m + for m in dir(subtensor) + if not m.startswith("_") and m not in excluded_subtensor_methods ] - # Mocks - mock_get_delegates = mocker.patch.object( - subtensor, "get_delegates", return_value=fake_delegates - ) - - # Call - result = subtensor.is_hotkey_delegate(fake_hotkey_ss58, block=fake_block) - - # Assertions - mock_get_delegates.assert_called_once_with(block=fake_block) - assert result is False - - -def test_is_hotkey_delegate_empty_list(mocker, subtensor): - """Test when delegate list is empty.""" - # Mock data - fake_hotkey_ss58 = "hotkey_1" - fake_block = 123 - - # Mocks - mock_get_delegates = mocker.patch.object( - subtensor, "get_delegates", return_value=[] - ) - - # Call - result = subtensor.is_hotkey_delegate(fake_hotkey_ss58, block=fake_block) - - # Assertions - mock_get_delegates.assert_called_once_with(block=fake_block) - assert result is False - - -def test_add_stake_success(mocker, subtensor): - """Test add_stake returns True on successful staking.""" - # Prep - fake_wallet = mocker.Mock() - fake_hotkey_ss58 = "fake_hotkey" - fake_amount = 10.0 - - mock_add_stake_extrinsic = mocker.patch.object( - subtensor_module, "add_stake_extrinsic" - ) - - # Call - result = subtensor.add_stake( - wallet=fake_wallet, - hotkey_ss58=fake_hotkey_ss58, - amount=fake_amount, - wait_for_inclusion=True, - wait_for_finalization=False, - ) - - # Assertions - mock_add_stake_extrinsic.assert_called_once_with( - subtensor=subtensor, - wallet=fake_wallet, - hotkey_ss58=fake_hotkey_ss58, - amount=fake_amount, - wait_for_inclusion=True, - wait_for_finalization=False, - ) - assert result == mock_add_stake_extrinsic.return_value - - -def test_add_stake_multiple_success(mocker, subtensor): - """Test add_stake_multiple successfully stakes for all hotkeys.""" - # Prep - fake_wallet = mocker.Mock() - fake_hotkey_ss58 = ["fake_hotkey"] - fake_amount = [10.0] - - mock_add_stake_multiple_extrinsic = mocker.patch.object( - subtensor_module, "add_stake_multiple_extrinsic" - ) - - # Call - result = subtensor.add_stake_multiple( - wallet=fake_wallet, - hotkey_ss58s=fake_hotkey_ss58, - amounts=fake_amount, - wait_for_inclusion=True, - wait_for_finalization=False, - ) - - # Assertions - mock_add_stake_multiple_extrinsic.assert_called_once_with( - subtensor=subtensor, - wallet=fake_wallet, - hotkey_ss58s=fake_hotkey_ss58, - amounts=fake_amount, - wait_for_inclusion=True, - wait_for_finalization=False, - ) - assert result == mock_add_stake_multiple_extrinsic.return_value - - -def test_unstake_success(mocker, subtensor): - """Test unstake operation is successful.""" - # Preps - fake_wallet = mocker.Mock() - fake_hotkey_ss58 = "hotkey_1" - fake_amount = 10.0 - - mock_unstake_extrinsic = mocker.patch.object(subtensor_module, "unstake_extrinsic") - - # Call - result = subtensor.unstake( - wallet=fake_wallet, - hotkey_ss58=fake_hotkey_ss58, - amount=fake_amount, - wait_for_inclusion=True, - wait_for_finalization=False, - ) - - # Assertions - mock_unstake_extrinsic.assert_called_once_with( - subtensor=subtensor, - wallet=fake_wallet, - hotkey_ss58=fake_hotkey_ss58, - amount=fake_amount, - wait_for_inclusion=True, - wait_for_finalization=False, - ) - assert result == mock_unstake_extrinsic.return_value - - -def test_unstake_multiple_success(mocker, subtensor): - """Test unstake_multiple succeeds for all hotkeys.""" - # Preps - fake_wallet = mocker.Mock() - fake_hotkeys = ["hotkey_1", "hotkey_2"] - fake_amounts = [10.0, 20.0] - - mock_unstake_multiple_extrinsic = mocker.patch( - "bittensor.core.subtensor.unstake_multiple_extrinsic", return_value=True - ) - - # Call - result = subtensor.unstake_multiple( - wallet=fake_wallet, - hotkey_ss58s=fake_hotkeys, - amounts=fake_amounts, - wait_for_inclusion=True, - wait_for_finalization=False, - ) + async_subtensor_methods = [ + m + for m in dir(subtensor.async_subtensor) + if not m.startswith("_") and m not in excluded_async_subtensor_methods + ] # Assertions - mock_unstake_multiple_extrinsic.assert_called_once_with( - subtensor=subtensor, - wallet=fake_wallet, - hotkey_ss58s=fake_hotkeys, - amounts=fake_amounts, - wait_for_inclusion=True, - wait_for_finalization=False, - ) - assert result == mock_unstake_multiple_extrinsic.return_value - - -def test_set_weights_with_commit_reveal_enabled(subtensor, mocker): - """Test set_weights with commit_reveal_enabled is True.""" - # Preps - fake_wallet = mocker.Mock() - fake_netuid = 1 - fake_uids = [1, 5] - fake_weights = [0.1, 0.9] - fake_wait_for_inclusion = True - fake_wait_for_finalization = False - - 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_reveal_v3_extrinsic.return_value = ( - True, - "Weights committed successfully", - ) - mocker.patch.object(subtensor, "blocks_since_last_update", return_value=181) - mocker.patch.object(subtensor, "weights_rate_limit", return_value=180) - - # Call - result = subtensor.set_weights( - wallet=fake_wallet, - netuid=fake_netuid, - uids=fake_uids, - weights=fake_weights, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - # Asserts - mocked_commit_reveal_enabled.assert_called_once_with(netuid=fake_netuid) - mocked_commit_reveal_v3_extrinsic.assert_called_once_with( - subtensor=subtensor, - wallet=fake_wallet, - netuid=fake_netuid, - uids=fake_uids, - weights=fake_weights, - version_key=subtensor_module.settings.version_as_int, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - assert result == mocked_commit_reveal_v3_extrinsic.return_value - - -def test_connection_limit(mocker): - """Test connection limit is not exceeded.""" - # Technically speaking, this test should exist in integration tests. But to reduce server costs we will leave this - # test here. - - # Preps - mocker.patch.object( - subtensor_module.ws_client, - "connect", - side_effect=subtensor_module.InvalidStatus( - response=mocker.Mock( - response=mocker.Mock( - status_code=429, message="test connection limit error" - ) - ) - ), - ) - # Call with assertions + for method in subtensor_methods: + assert ( + method in async_subtensor_methods + ), f"`Subtensor.{method}` not in `AsyncSubtensor` class." - with pytest.raises(subtensor_module.InvalidStatus): - for i in range(2): - Subtensor("test") + for method in async_subtensor_methods: + assert ( + method in subtensor_methods + ), f"`AsyncSubtensor.{method}` not in `Subtensor` class." diff --git a/tests/unit_tests/utils/test_async_substrate_interface.py b/tests/unit_tests/utils/test_async_substrate_interface.py index 54afe1118f..3d9e696f04 100644 --- a/tests/unit_tests/utils/test_async_substrate_interface.py +++ b/tests/unit_tests/utils/test_async_substrate_interface.py @@ -1,34 +1,11 @@ import pytest -import asyncio -from bittensor.utils import substrate_interface -from typing import Any - - -@pytest.mark.asyncio -async def test_wait_for_block_invalid_result_handler(): - chain_interface = substrate_interface.AsyncSubstrateInterface("dummy_endpoint") - - with pytest.raises(ValueError): +from websockets.exceptions import InvalidURI - async def dummy_handler( - block_data: dict[str, Any], extra_arg - ): # extra argument - return block_data.get("header", {}).get("number", -1) == 2 - - await chain_interface.wait_for_block( - block=2, result_handler=dummy_handler, task_return=False - ) +from bittensor.utils import substrate_interface @pytest.mark.asyncio -async def test_wait_for_block_async_return(): - chain_interface = substrate_interface.AsyncSubstrateInterface("dummy_endpoint") - - async def dummy_handler(block_data: dict[str, Any]) -> bool: - return block_data.get("header", {}).get("number", -1) == 2 - - result = await chain_interface.wait_for_block( - block=2, result_handler=dummy_handler, task_return=True - ) - - assert isinstance(result, asyncio.Task) +async def test_invalid_url_raises_exception(): + """Test that invalid URI raises an InvalidURI exception.""" + with pytest.raises(InvalidURI): + substrate_interface.AsyncSubstrateInterface("non_existent_entry_point")