diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index aa2b65fb30..508ae02439 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -7,7 +7,7 @@ import typer from bittensor_wallet import Wallet from bittensor_wallet.utils import SS58_FORMAT -from rich.prompt import Confirm +from numpy.typing import NDArray from scalecodec import GenericCall from scalecodec.base import RuntimeConfiguration from scalecodec.type_registry import load_type_registry_preset @@ -28,6 +28,10 @@ root_register_extrinsic, ) from bittensor.core.extrinsics.async_transfer import transfer_extrinsic +from bittensor.core.extrinsics.async_weights import ( + commit_weights_extrinsic, + set_weights_extrinsic, +) from bittensor.core.settings import ( TYPE_REGISTRY, DEFAULTS, @@ -35,7 +39,9 @@ DELEGATES_DETAILS_URL, DEFAULT_NETWORK, ) +from bittensor.core.settings import version_as_int from bittensor.utils import ( + torch, ss58_to_vec_u8, format_error_message, decode_hex_identity_dict, @@ -48,6 +54,7 @@ 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 class ParamWithTypes(TypedDict): @@ -152,14 +159,105 @@ async def encode_params( return param_data.to_hex() - async def get_all_subnet_netuids( + 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() + + 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.substrate.get_block_hash(block_id) + else: + return await self.substrate.get_chain_head() + + async def is_hotkey_registered_any( + self, hotkey_ss58: str, block_hash: Optional[str] = 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_hash (Optional[str]): The blockchain block_hash representation of block id. + + 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(await self.get_netuids_for_hotkey(hotkey_ss58, block_hash)) > 0 + + async def get_subnet_burn_cost( self, block_hash: Optional[str] = None - ) -> list[int]: + ) -> 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_hash (Optional[int]): The blockchain block_hash of the block id. + + 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 = await self.query_runtime_api( + runtime_api="SubnetRegistrationRuntimeApi", + method="get_network_registration_cost", + params=[], + block_hash=block_hash, + ) + + return lock_cost + + async def get_total_subnets( + self, block_hash: Optional[str] = None + ) -> Optional[int]: + """ + Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. + + Args: + block_hash (Optional[str]): The blockchain block_hash representation of block id. + + Returns: + 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. + """ + result = await self.substrate.query( + module="SubtensorModule", + storage_function="TotalNetworks", + params=[], + block_hash=block_hash + ) + return result + + async def get_subnets(self, block_hash: Optional[str] = None) -> list[int]: """ Retrieves the list of all subnet unique identifiers (netuids) currently present in the Bittensor network. - :param block_hash: The hash of the block to retrieve the subnet unique identifiers from. - :return: A list of subnet netuids. + Args: + block_hash (Optional[str]): The hash of the block to retrieve the subnet unique identifiers from. + + Returns: + 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. @@ -180,20 +278,20 @@ async def is_hotkey_delegate( self, hotkey_ss58: str, block_hash: Optional[str] = None, - reuse_block: Optional[bool] = False, + 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. + 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. - :param hotkey_ss58: The SS58 address of the neuron's hotkey. - :param block_hash: The hash of the blockchain block number for the query. - :param reuse_block: Whether to reuse the last-used block hash. + Args: + hotkey_ss58 (str): The SS58 address of the neuron's hotkey. + 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. - :return: `True` if the hotkey is a delegate, `False` otherwise. + 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. + Being a delegate is a significant status within the Bittensor network, indicating a neuron's involvement in consensus and governance processes. """ delegates = await self.get_delegates( block_hash=block_hash, reuse_block=reuse_block @@ -201,21 +299,24 @@ async def is_hotkey_delegate( return hotkey_ss58 in [info.hotkey_ss58 for info in delegates] async def get_delegates( - self, block_hash: Optional[str] = None, reuse_block: Optional[bool] = False + self, block_hash: Optional[str] = None, reuse_block: bool = False ) -> list[DelegateInfo]: """ Fetches all delegates on the chain - :param block_hash: hash of the blockchain block number for the query. - :param reuse_block: whether to reuse the last-used block hash. + Args: + 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. - :return: List of DelegateInfo objects, or an empty list if there are no delegates. + Returns: + List of DelegateInfo objects, or an empty list if there are no delegates. """ 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: try: @@ -234,17 +335,17 @@ async def get_stake_info_for_coldkey( reuse_block: bool = False, ) -> list[StakeInfo]: """ - 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. + 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. - :param coldkey_ss58: The ``SS58`` address of the account's coldkey. - :param block_hash: The hash of the blockchain block number for the query. - :param reuse_block: Whether to reuse the last-used block hash. + Args: + coldkey_ss58 (str): The ``SS58`` address of the account's coldkey. + 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. - :return: A list of StakeInfo objects detailing the stake allocations for the account. + 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. + 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) @@ -267,14 +368,18 @@ async def get_stake_info_for_coldkey( return StakeInfo.list_from_vec_u8(bytes_result) async def get_stake_for_coldkey_and_hotkey( - self, hotkey_ss58: str, coldkey_ss58: str, block_hash: Optional[str] + self, hotkey_ss58: str, coldkey_ss58: str, block_hash: Optional[str] = None ) -> Balance: """ Retrieves stake information associated with a specific coldkey and hotkey. - :param hotkey_ss58: the hotkey SS58 address to query - :param coldkey_ss58: the coldkey SS58 address to query - :param block_hash: the hash of the blockchain block number for the query. - :return: Stake Balance for the given coldkey and hotkey + + Args: + hotkey_ss58 (str): the hotkey SS58 address to query + coldkey_ss58 (str): the coldkey SS58 address to query + block_hash (Optional[str]): the hash of the blockchain block number for the query. + + Returns: + Stake Balance for the given coldkey and hotkey """ _result = await self.substrate.query( module="SubtensorModule", @@ -288,25 +393,24 @@ async def query_runtime_api( self, runtime_api: str, method: str, - params: Optional[Union[list[list[int]], dict[str, int]]], + params: Optional[Union[list[list[int]], dict[str, int], list[int]]], block_hash: Optional[str] = None, - reuse_block: Optional[bool] = False, + reuse_block: bool = False, ) -> 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. + 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. - :param runtime_api: The name of the runtime API to query. - :param method: The specific method within the runtime API to call. - :param params: The parameters to pass to the method call. - :param block_hash: The hash of the blockchain block number at which to perform the query. - :param reuse_block: Whether to reuse the last-used block hash. + 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[Union[list[list[int]], dict[str, int]]]): The parameters to pass to the method call. + block_hash (Optional[str]): The hash of the blockchain block number at which to perform the query. + reuse_block (bool): Whether to reuse the last-used block hash. - :return: The Scale Bytes encoded result from the runtime API call, or ``None`` if the call fails. + Returns: + 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. + 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 = TYPE_REGISTRY["runtime_api"][runtime_api]["methods"][method] @@ -322,6 +426,7 @@ async def query_runtime_api( json_result = await self.substrate.rpc_request( method="state_call", params=[api_method, data, block_hash] if block_hash else [api_method, data], + reuse_block_hash=reuse_block, ) if json_result is None: @@ -345,14 +450,16 @@ async def get_balance( self, *addresses: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> dict[str, Balance]: """ Retrieves the balance for given coldkey(s) - :param addresses: coldkey addresses(s) - :param block_hash: the block hash, optional - :param reuse_block: Whether to reuse the last-used block hash when retrieving info. - :return: dict of {address: Balance objects} + + Args: + addresses (str): coldkey addresses(s). + block_hash (Optional[str]): the block hash, optional. + + Returns: + Dict of {address: Balance objects}. """ calls = [ ( @@ -369,20 +476,70 @@ async def get_balance( results.update({item[0].params[0]: Balance(value["data"]["free"])}) return results + 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. + + 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 = 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: Failed to get payment info: {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 + async def get_total_stake_for_coldkey( self, *ss58_addresses, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> dict[str, Balance]: """ Returns the total stake held on a coldkey. - :param ss58_addresses: The SS58 address(es) of the coldkey(s) - :param block_hash: The hash of the block number to retrieve the stake from. - :param reuse_block: Whether to reuse the last-used block hash when retrieving info. + Args: + ss58_addresses (tuple[str]): The SS58 address(es) of the coldkey(s) + block_hash (str): The hash of the block number to retrieve the stake from. - :return: {address: Balance objects} + Returns: + Dict in view {address: Balance objects}. """ calls = [ ( @@ -410,11 +567,13 @@ async def get_total_stake_for_hotkey( """ Returns the total stake held on a hotkey. - :param ss58_addresses: The SS58 address(es) of the hotkey(s) - :param block_hash: The hash of the block number to retrieve the stake from. - :param reuse_block: Whether to reuse the last-used block hash when retrieving info. + Args: + ss58_addresses (tuple[str]): The SS58 address(es) of the hotkey(s) + 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. - :return: {address: Balance objects} + Returns: + Dict {address: Balance objects}. """ results = await self.substrate.query_multiple( params=[s for s in ss58_addresses], @@ -432,15 +591,15 @@ async def get_netuids_for_hotkey( reuse_block: bool = False, ) -> 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. + 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. - :param hotkey_ss58: The ``SS58`` address of the neuron's hotkey. - :param block_hash: The hash of the blockchain block number at which to perform the query. - :param reuse_block: Whether to reuse the last-used block hash when retrieving info. + Args: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + 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. - :return: A list of netuids where the neuron is a member. + Returns: + A list of netuids where the neuron is a member. """ result = await self.substrate.query_map( @@ -462,11 +621,13 @@ async def subnet_exists( """ Checks if a subnet with the specified unique identifier (netuid) exists within the Bittensor network. - :param netuid: The unique identifier of the subnet. - :param block_hash: The hash of the blockchain block number at which to check the subnet existence. - :param reuse_block: Whether to reuse the last-used block hash. + Args: + netuid (int): The unique identifier of the subnet. + 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. - :return: `True` if the subnet exists, `False` otherwise. + Returns: + `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. @@ -490,12 +651,14 @@ async def get_hyperparameter( """ Retrieves a specified hyperparameter for a specific subnet. - :param param_name: The name of the hyperparameter to retrieve. - :param netuid: The unique identifier of the subnet. - :param block_hash: The hash of blockchain block number for the query. - :param reuse_block: Whether to reuse the last-used block hash. + Args: + param_name (str): The name of the hyperparameter to retrieve. + netuid (int): The unique identifier of the subnet. + block_hash (Optional[str]): The hash of blockchain block number for the query. + reuse_block (bool): Whether to reuse the last-used block hash. - :return: The value of the specified hyperparameter if the subnet exists, or None + Returns: + The value of the specified hyperparameter if the subnet exists, or None """ if not await self.subnet_exists(netuid, block_hash): print("subnet does not exist") @@ -525,13 +688,15 @@ async def filter_netuids_by_registered_hotkeys( """ Filters a given list of all netuids for certain specified netuids and hotkeys - :param all_netuids: A list of netuids to filter. - :param filter_for_netuids: A subset of all_netuids to filter from the main list - :param all_hotkeys: Hotkeys to filter from the main list - :param block_hash: hash of the blockchain block number at which to perform the query. - :param reuse_block: whether to reuse the last-used blockchain hash when retrieving info. + Argumens: + 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_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. - :return: the filtered list of netuids. + Returns: + The filtered list of netuids. """ netuids_with_registered_hotkeys = [ item @@ -571,17 +736,18 @@ async def get_existential_deposit( self, block_hash: Optional[str] = None, reuse_block: bool = False ) -> 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. + 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. - :param block_hash: Block hash at which to query the deposit amount. If `None`, the current block is used. - :param reuse_block: Whether to reuse the last-used blockchain block hash. + Args: + 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. - :return: The existential deposit amount + Returns: + 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. + 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 = await self.substrate.get_constant( module_name="Balances", @@ -599,17 +765,17 @@ async def neurons( self, netuid: int, block_hash: Optional[str] = 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. + 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. - :param netuid: The unique identifier of the subnet. - :param block_hash: The hash of the blockchain block number for the query. + Args: + netuid (int): The unique identifier of the subnet. + block_hash (str): The hash of the blockchain block number for the query. - :return: A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. + 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. + 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, weights, bonds = await asyncio.gather( self.neurons_lite(netuid=netuid, block_hash=block_hash), @@ -634,17 +800,17 @@ async def neurons_lite( ) -> 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. + This function provides a streamlined view of the neurons, focusing on key attributes such as stake and network participation. - :param netuid: The unique identifier of the subnet. - :param block_hash: The hash of the blockchain block number for the query. - :param reuse_block: Whether to reuse the last-used blockchain block hash. + Args: + netuid (int): The unique identifier of the subnet. + 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. - :return: A list of simplified neuron information for the subnet. + Returns: + 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. + 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 = await self.query_runtime_api( runtime_api="NeuronInfoRuntimeApi", @@ -670,19 +836,17 @@ async def neuron_for_uid( self, uid: Optional[int], netuid: int, block_hash: Optional[str] = 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. - + 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. - :param uid: The unique identifier of the neuron. - :param netuid: The unique identifier of the subnet. - :param block_hash: The hash of the blockchain block number for the query. + Args: + uid (int): The unique identifier of the neuron. + netuid (int): The unique identifier of the subnet. + block_hash (str): The hash of the blockchain block number for the query. - :return: Detailed information about the neuron if found, a null neuron otherwise + 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. + 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() @@ -706,17 +870,17 @@ async def get_delegated( reuse_block: bool = False, ) -> list[tuple[DelegateInfo, Balance]]: """ - 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 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. - :param coldkey_ss58: The `SS58` address of the account's coldkey. - :param block_hash: The hash of the blockchain block number for the query. - :param reuse_block: Whether to reuse the last-used blockchain block hash. + Args: + coldkey_ss58 (str): The `SS58` address of the account's coldkey. + 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. - :return: A list of tuples, each containing a delegate's information and staked amount. + Returns: + A list of tuples, each containing a delegate's information and staked amount. - 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 important for account holders to understand their stake allocations and their involvement in the network's delegation and consensus mechanisms. """ block_hash = ( @@ -742,22 +906,20 @@ async def query_identity( reuse_block: bool = False, ) -> dict: """ - 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. + 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. - Note: - See the `Bittensor CLI documentation `_ for supported identity - parameters. + Args: + key (str): The key used to query the neuron's identity, typically the neuron's SS58 address. + 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. - :param key: The key used to query the neuron's identity, typically the neuron's SS58 address. - :param block_hash: The hash of the blockchain block number at which to perform the query. - :param reuse_block: Whether to reuse the last-used blockchain block hash. + Returns: + An object containing the identity information of the neuron if found, ``None`` otherwise. - :return: An object containing the identity information of the neuron if found, ``None`` otherwise. + 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. - 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. + Note: + See the `Bittensor CLI documentation `_ for supported identity parameters. """ def decode_hex_identity_dict_(info_dictionary): @@ -801,17 +963,16 @@ async def weights( ) -> 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. + 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: - :param netuid: The network UID of the subnet to query. - :param block_hash: The hash of the blockchain block for the query. + netuid (int): The network UID of the subnet to query. + block_hash (str): The hash of the blockchain block for the query. - :return: A list of tuples mapping each neuron's UID to its assigned weights. + Returns: + 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. + 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. """ # TODO look into seeing if we can speed this up with storage query w_map_encoded = await self.substrate.query_map( @@ -829,18 +990,16 @@ async def bonds( ) -> 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. + 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. - :param netuid: The network UID of the subnet to query. - :param block_hash: The hash of the blockchain block number for the query. + Args: + netuid (int): The network UID of the subnet to query. + block_hash (Optional[str]): The hash of the blockchain block number for the query. - :return: list of tuples mapping each neuron's UID to its bonds with other neurons. + 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. + 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_encoded = await self.substrate.query_map( module="SubtensorModule", @@ -861,11 +1020,13 @@ async def does_hotkey_exist( """ Returns true if the hotkey is known by the chain and there are accounts. - :param hotkey_ss58: The SS58 address of the hotkey. - :param block_hash: The hash of the block number to check the hotkey against. - :param reuse_block: Whether to reuse the last-used blockchain hash. + Args: + hotkey_ss58 (str): The SS58 address of the hotkey. + block_hash (Optional[str]): The hash of the block number to check the hotkey against. + reuse_block (bool): Whether to reuse the last-used blockchain hash. - :return: `True` if the hotkey is known by the chain and there are accounts, `False` otherwise. + Returns: + `True` if the hotkey is known by the chain and there are accounts, `False` otherwise. """ _result = await self.substrate.query( module="SubtensorModule", @@ -885,6 +1046,17 @@ async def does_hotkey_exist( async def get_hotkey_owner( self, hotkey_ss58: str, block_hash: str ) -> Optional[str]: + """ + Retrieves the owner of the given hotkey at a specific block hash. + 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 (str): The SS58 address of the hotkey. + block_hash (str): The hash of the block at which to check the hotkey ownership. + + Returns: + Optional[str]: The SS58 address of the owner if the hotkey exists, or None if it doesn't. + """ hk_owner_query = await self.substrate.query( module="SubtensorModule", storage_function="Owner", @@ -901,20 +1073,22 @@ async def get_hotkey_owner( async def sign_and_send_extrinsic( self, - call: GenericCall, - wallet: Wallet, + call: "GenericCall", + wallet: "Wallet", wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> tuple[bool, str]: """ Helper method to sign and submit an extrinsic call to chain. - :param call: a prepared Call object - :param wallet: the wallet whose coldkey will be used to sign the extrinsic - :param wait_for_inclusion: whether to wait until the extrinsic call is included on the chain - :param wait_for_finalization: whether to wait until the extrinsic call is finalized on the chain + 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 - :return: (success, error message) + Returns: + (success, error message) """ extrinsic = await self.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey @@ -938,16 +1112,16 @@ async def sign_and_send_extrinsic( except SubstrateRequestException as e: return False, format_error_message(e, substrate=self.substrate) - async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]: + async def get_children(self, hotkey: str, netuid: int) -> tuple[bool, list, str]: """ - 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. + 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. - :param hotkey: The hotkey value. - :param netuid: The netuid value. + Args: + hotkey (str): The hotkey value. + netuid (int): The netuid value. - :return: A tuple containing a boolean indicating success or failure, a list of formatted children, and an error - message (if applicable) + Returns: + A tuple containing a boolean indicating success or failure, a list of formatted children, and an error message (if applicable) """ try: children = await self.substrate.query( @@ -972,16 +1146,16 @@ async def get_subnet_hyperparameters( self, netuid: int, block_hash: Optional[str] = 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. + Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters define the operational settings and rules governing the subnet's behavior. - :param netuid: The network UID of the subnet to query. - :param block_hash: The hash of the blockchain block number for the query. + Args: + netuid (int): The network UID of the subnet to query. + block_hash (Optional[str]): The hash of the blockchain block number for the query. - :return: The subnet's hyperparameters, or `None` if not available. + Returns: + 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. + 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 = await self.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", @@ -1007,17 +1181,17 @@ async def get_vote_data( 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. + 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. - :param proposal_hash: The hash of the proposal for which voting data is requested. - :param block_hash: The hash of the blockchain block number to query the voting data. - :param reuse_block: Whether to reuse the last-used blockchain block hash. + Args: + proposal_hash (str): The hash of the proposal for which voting data is requested. + 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. - :return: An object containing the proposal's voting data, or `None` if not found. + 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. + 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. """ vote_data = await self.substrate.query( module="Triumvirate", @@ -1035,14 +1209,13 @@ async def get_delegate_identities( self, block_hash: Optional[str] = None ) -> dict[str, DelegatesDetails]: """ - 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. + 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: - block_hash: the hash of the blockchain block for the query + block_hash (str): the hash of the blockchain block for the query - Returns: {ss58: DelegatesDetails, ...} + Returns: + Dict {ss58: DelegatesDetails, ...} """ timeout = aiohttp.ClientTimeout(10.0) @@ -1105,17 +1278,52 @@ async def is_hotkey_registered(self, netuid: int, hotkey_ss58: str) -> bool: else: return False + async def get_uid_for_hotkey_on_subnet( + self, hotkey_ss58: str, netuid: int, block_hash: Optional[str] = None + ): + """ + 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_hash (Optional[str]): The blockchain block_hash representation of the block id. + + 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. + """ + return self.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, hotkey_ss58], + block_hash=block_hash + ) + # extrinsics async def transfer( self, - wallet: Wallet, + wallet: "Wallet", destination: str, amount: float, transfer_all: bool, prompt: bool, - ): - """Transfer token of amount to destination.""" + ) -> bool: + """ + Transfer token of amount to destination. + + 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. + prompt (bool): Flag to prompt user for confirmation before transferring. + + Returns: + `True` if the transferring was successful, otherwise `False`. + """ return await transfer_extrinsic( self, wallet, @@ -1125,17 +1333,37 @@ async def transfer( prompt=prompt, ) - async def register(self, wallet: Wallet, prompt: bool): - """Register neuron by recycling some TAO.""" + async def register( + self, + wallet: "Wallet", + netuid: int, + block_hash: Optional[str] = None, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Register neuron by recycling some TAO. + + Args: + wallet (bittensor_wallet.Wallet): Bittensor wallet instance. + netuid (int): Subnet uniq id. + 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 0 on network: {self.network}" ) # Check current recycle amount logging.info("Fetching recycle amount & balance.") + block_hash = block_hash if block_hash else await self.get_block_hash() recycle_call, balance_ = await asyncio.gather( - self.get_hyperparameter(param_name="Burn", netuid=0, reuse_block=True), - self.get_balance(wallet.coldkeypub.ss58_address, reuse_block=True), + self.get_hyperparameter(param_name="Burn", netuid=netuid, reuse_block=True), + self.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), ) current_recycle = Balance.from_rao(int(recycle_call)) try: @@ -1150,25 +1378,16 @@ async def register(self, wallet: Wallet, prompt: bool): # Check balance is sufficient if balance < current_recycle: logging.error( - f"Insufficient balance {balance} to register neuron. Current recycle is {current_recycle} TAO" + f"Insufficient balance {balance} to register neuron. Current recycle is {current_recycle} TAO." ) return False - if prompt: - if not Confirm.ask( - f"Your balance is: [bold green]{balance}[/bold green]\n" - f"The cost to register by recycle is [bold red]{current_recycle}[/bold red]\n" - f"Do you want to continue?", - default=False, - ): - return False - return await root_register_extrinsic( - self, - wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=prompt, + subtensor=self, + wallet=wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) async def pow_register( @@ -1199,13 +1418,80 @@ async def pow_register( ) async def set_weights( + self, + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + version_key: int = version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + max_retries: int = 5, + ): + """ + 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】. + """ + uid = self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to set weights!" + while ( + self.blocks_since_last_update(netuid, uid) > self.weights_rate_limit(netuid) # type: ignore + and retries < max_retries + ): + try: + logging.info( + f"Setting weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." + ) + 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 root_set_weights( self, wallet: "Wallet", netuids: list[int], weights: list[float], - prompt: bool, - ): - """Set weights for root network.""" + ) -> bool: + """ + Set weights for root network. + + 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. + + Returns: + `True` if the setting of weights is successful, `False` otherwise. + """ netuids_ = np.array(netuids, dtype=np.int64) weights_ = np.array(weights, dtype=np.float32) logging.info(f"Setting weights in network: {self.network}") @@ -1216,7 +1502,75 @@ async def set_weights( netuids=netuids_, weights=weights_, version_key=0, - prompt=prompt, wait_for_finalization=True, wait_for_inclusion=True, ) + + 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]: + """ + 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, + ) + + 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 diff --git a/bittensor/core/extrinsics/async_root.py b/bittensor/core/extrinsics/async_root.py index 9e73f98a30..dd44a55f2a 100644 --- a/bittensor/core/extrinsics/async_root.py +++ b/bittensor/core/extrinsics/async_root.py @@ -6,8 +6,6 @@ from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError from numpy.typing import NDArray -from rich.prompt import Confirm -from rich.table import Table, Column from substrateinterface.exceptions import SubstrateRequestException from bittensor.utils import u16_normalized_float, format_error_message @@ -22,6 +20,19 @@ async def get_limits(subtensor: "AsyncSubtensor") -> tuple[int, float]: + """ + Retrieves the minimum allowed weights and maximum weight limit for the given subnet. + + These values are fetched asynchronously using `asyncio.gather` to run both requests concurrently. + + Args: + subtensor (AsyncSubtensor): The AsyncSubtensor object used to interface with the network's substrate node. + + Returns: + tuple[int, float]: A tuple containing: + - `min_allowed_weights` (int): The minimum allowed weights. + - `max_weight_limit` (float): The maximum weight limit, normalized to a float value. + """ # Get weight restrictions. maw, mwl = await asyncio.gather( subtensor.get_hyperparameter("MinAllowedWeights", netuid=0), @@ -35,19 +46,21 @@ async def get_limits(subtensor: "AsyncSubtensor") -> tuple[int, float]: async def root_register_extrinsic( subtensor: "AsyncSubtensor", wallet: Wallet, + netuid: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, - prompt: bool = False, ) -> bool: """Registers the wallet to root network. - :param subtensor: The AsyncSubtensor object - :param wallet: Bittensor wallet object. - :param 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. - :param 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. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. + Arguments: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The AsyncSubtensor object + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + netuid (int): Subnet uid. + 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. - :return: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the response is `True`. + Returns: + `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the response is `True`. """ try: @@ -60,7 +73,7 @@ async def root_register_extrinsic( f"Checking if hotkey ({wallet.hotkey_str}) is registered on root." ) is_registered = await subtensor.is_hotkey_registered( - netuid=0, hotkey_ss58=wallet.hotkey.ss58_address + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address ) if is_registered: logging.error( @@ -82,7 +95,7 @@ async def root_register_extrinsic( ) if not success: - logging.error(f":cross_mark: Failed: {err_msg}") + logging.error(f":cross_mark: Failed error: {err_msg}") time.sleep(0.5) return False @@ -91,11 +104,11 @@ async def root_register_extrinsic( uid = await subtensor.substrate.query( module="SubtensorModule", storage_function="Uids", - params=[0, wallet.hotkey.ss58_address], + params=[netuid, wallet.hotkey.ss58_address], ) if uid is not None: logging.info( - f":white_heavy_check_mark: Registered with UID {uid}" + f":white_heavy_check_mark: Registered with UID {uid}." ) return True else: @@ -106,28 +119,26 @@ async def root_register_extrinsic( async def set_root_weights_extrinsic( subtensor: "AsyncSubtensor", - wallet: Wallet, + wallet: "Wallet", netuids: Union[NDArray[np.int64], list[int]], weights: Union[NDArray[np.float32], list[float]], version_key: int = 0, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, - prompt: bool = False, ) -> bool: """Sets the given weights and values on chain for wallet hotkey account. - :param subtensor: The AsyncSubtensor object - :param wallet: Bittensor wallet object. - :param netuids: The `netuid` of the subnet to set weights for. - :param weights: Weights to set. These must be `float` s and must correspond to the passed `netuid` s. - :param version_key: The version key of the validator. - :param 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. - :param 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. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - :return: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, - the response is `True`. + Arguments: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The AsyncSubtensor object + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + netuids (Union[NDArray[np.int64], list[int]]): The `netuid` of the subnet to set weights for. + weights (Union[NDArray[np.float32], 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. + 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: + `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the response is `True`. """ async def _do_set_weights(): @@ -168,13 +179,13 @@ async def _do_set_weights(): ) if my_uid is None: - logging.error("Your hotkey is not registered to the root network") + logging.error("Your hotkey is not registered to the root network.") return False try: wallet.unlock_coldkey() except KeyFileError: - logging.error("Error decrypting coldkey (possibly incorrect password)") + logging.error("Error decrypting coldkey (possibly incorrect password).") return False # First convert types. @@ -203,25 +214,6 @@ async def _do_set_weights(): f"Raw weights -> Normalized weights: {weights} -> {formatted_weights}" ) - # Ask before moving on. - if prompt: - table = Table( - Column("[dark_orange]Netuid", justify="center", style="bold green"), - Column( - "[dark_orange]Weight", justify="center", style="bold light_goldenrod2" - ), - expand=False, - show_edge=False, - ) - print("Netuid | Weight") - - for netuid, weight in zip(netuids, formatted_weights): - table.add_row(str(netuid), f"{weight:.8f}") - print(f"{netuid} | {weight}") - - if not Confirm.ask("\nDo you want to set these root weights?"): - return False - try: logging.info(":satellite: Setting root weights...") weight_uids, weight_vals = convert_weights_and_uids_for_emit(netuids, weights) @@ -236,10 +228,10 @@ async def _do_set_weights(): return True else: fmt_err = format_error_message(error_message, subtensor.substrate) - logging.error(f":cross_mark: Failed: {fmt_err}") + logging.error(f":cross_mark: Failed error: {fmt_err}") return False except SubstrateRequestException as e: fmt_err = format_error_message(e, subtensor.substrate) - logging.error(f":cross_mark: Failed: error:{fmt_err}") + logging.error(f":cross_mark: Failed error: {fmt_err}") return False diff --git a/bittensor/core/extrinsics/async_weights.py b/bittensor/core/extrinsics/async_weights.py new file mode 100644 index 0000000000..82f2dc6dc3 --- /dev/null +++ b/bittensor/core/extrinsics/async_weights.py @@ -0,0 +1,257 @@ +"""This module provides functionality for setting weights on the Bittensor network.""" + +from typing import Union, TYPE_CHECKING, Optional + +import numpy as np +from numpy.typing import NDArray + +import bittensor.utils.weight_utils as weight_utils +from bittensor.core.settings import version_as_int +from bittensor.utils import format_error_message +from bittensor.utils.btlogging import logging +from bittensor.utils.registration import torch, use_torch + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def _do_set_weights( + subtensor: "AsyncSubtensor", + 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, +) -> 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: + subtensor (subtensor.core.async_subtensor.AsyncSubtensor): Async Subtensor instance. + wallet (bittensor.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. + wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. + wait_for_finalization (bool, optional): 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 is vital for the dynamic weighting mechanism in Bittensor, where neurons adjust their + trust in other neurons based on observed performance and contributions. + """ + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_weights", + call_params={ + "dests": uids, + "weights": vals, + "netuid": netuid, + "version_key": version_key, + }, + ) + # Period dictates how long the extrinsic will stay as part of waiting pool + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + era={"period": 5}, + ) + response = await subtensor.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, "Not waiting for finalization or inclusion." + + await response.process_events() + if await response.is_success: + return True, "Successfully set weights." + else: + return False, format_error_message( + response.error_message, substrate=subtensor.substrate + ) + + +async def set_weights_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 = 0, + 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.subtensor): Bittensor subtensor object. + wallet (bittensor.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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + + Returns: + success (bool): Flag is ``true`` if extrinsic was finalized or included in the block. If we did not wait for finalization / inclusion, the response is ``true``. + """ + # 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( + ":satellite: Setting weights on {subtensor.network} ..." + ) + try: + success, error_message = await _do_set_weights( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + uids=weight_uids, + vals=weight_vals, + 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: + message = "Successfully set weights and Finalized." + logging.success(f":white_heavy_check_mark: {message}") + return True, message + else: + logging.error(f"Failed set weights. Error: {error_message}") + return False, error_message + + except Exception as error: + logging.error(f":cross_mark: Failed set weights. Error: {error}") + return False, str(error) + + +async def _do_commit_weights( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + commit_hash: str, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, Optional[str]]: + """ + 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: + 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, 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 = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_weights", + call_params={ + "netuid": netuid, + "commit_hash": commit_hash, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + ) + response = await subtensor.substrate.submit_extrinsic( + substrate=subtensor.substrate, + 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 + + await response.process_events() + if await response.is_success: + return True, None + else: + return False, format_error_message( + response.error_message, substrate=subtensor.substrate + ) + + +async def commit_weights_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + commit_hash: str, + 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, handling user prompts and error messages. + + 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 = await _do_commit_weights( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + commit_hash=commit_hash, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if success: + 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 diff --git a/bittensor/core/extrinsics/prometheus.py b/bittensor/core/extrinsics/prometheus.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 904b699926..ceab305b42 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -47,7 +47,7 @@ def do_set_weights( version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, -) -> tuple[bool, Optional[dict]]: # (success, error_message) +) -> 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. @@ -99,7 +99,9 @@ def make_substrate_call_with_retry(): if response.is_success: return True, "Successfully set weights." else: - return False, response.error_message + return False, format_error_message( + response.error_message, substrate=self.substrate + ) return make_substrate_call_with_retry() @@ -179,9 +181,6 @@ def set_weights_extrinsic( logging.success(f"Finalized! Set weights: {str(success)}") return True, "Successfully set weights and Finalized." else: - error_message = format_error_message( - error_message, substrate=subtensor.substrate - ) logging.error(error_message) return False, error_message diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 8eee9676ad..98d47104ae 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -54,6 +54,13 @@ NETWORKS[3]: LOCAL_ENTRYPOINT, } +NETWORK_MAP = { + NETWORKS[0]: FINNEY_ENTRYPOINT, + NETWORKS[1]: FINNEY_TEST_ENTRYPOINT, + NETWORKS[2]: ARCHIVE_ENTRYPOINT, + NETWORKS[3]: LOCAL_ENTRYPOINT, +} + # Currency Symbols Bittensor TAO_SYMBOL: str = chr(0x03C4) RAO_SYMBOL: str = chr(0x03C1) diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index 9c32fc9bdf..fbeee34dc0 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -61,7 +61,7 @@ def mock_wallet(): True, True, False, - "Subtensor returned `UnknownError(UnknownType)` error. This means: `Unknown Description`.", + "Mock error message", ), ([1, 2], [0.5, 0.5], 0, True, True, True, False, False, "Prompt refused."), ], @@ -226,7 +226,7 @@ def test_do_set_weights_is_not_success(mock_subtensor, mocker): mock_subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() assert result == ( False, - mock_subtensor.substrate.submit_extrinsic.return_value.error_message, + "Subtensor returned `UnknownError(UnknownType)` error. This means: `Unknown Description`.", )