From 21b314df28cd35504c2c90374acf0c89451cb571 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 30 Jan 2025 14:29:38 -0800 Subject: [PATCH 01/12] improve `bittensor/utils/networking.py` logic --- bittensor/utils/networking.py | 81 +++++++++++++++-------------------- 1 file changed, 34 insertions(+), 47 deletions(-) diff --git a/bittensor/utils/networking.py b/bittensor/utils/networking.py index 84b2749e87..e01012d8b7 100644 --- a/bittensor/utils/networking.py +++ b/bittensor/utils/networking.py @@ -1,60 +1,50 @@ """Utils for handling local network with ip and ports.""" import os -import urllib from typing import Optional +from urllib import request as urllib_request -from async_substrate_interface.utils import json import netaddr import requests +from async_substrate_interface.utils import json + + +class ExternalIPNotFound(Exception): + """Raised if we cannot attain your external ip from CURL/URLLIB/IPIFY/AWS""" def int_to_ip(int_val: int) -> str: """Maps an integer to a unique ip-string - Args: - int_val (int): - The integer representation of an ip. Must be in the range (0, 3.4028237e+38). - Returns: - str_val (str): - The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 + Arguments: + int_val (int): The integer representation of an ip. Must be in the range (0, 3.4028237e+38). - Raises: - netaddr.core.AddrFormatError (Exception): Raised when the passed int_vals is not a valid ip int value. + Returns: + str_val (str): The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 """ return str(netaddr.IPAddress(int_val)) def ip_to_int(str_val: str) -> int: """Maps an ip-string to a unique integer. - arg: - str_val (:tyep:`str`, `required): - The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 - Returns: - int_val (:type:`int128`, `required`): - The integer representation of an ip. Must be in the range (0, 3.4028237e+38). + Arguments: + str_val (str): The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 - Raises: - netaddr.core.AddrFormatError (Exception): - Raised when the passed str_val is not a valid ip string value. + Returns: + int_val (int): The integer representation of an ip. Must be in the range (0, 3.4028237e+38). """ return int(netaddr.IPAddress(str_val)) def ip_version(str_val: str) -> int: """Returns the ip version (IPV4 or IPV6). - arg: - str_val (:tyep:`str`, `required): - The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 - Returns: - int_val (:type:`int128`, `required`): - The ip version (Either 4 or 6 for IPv4/IPv6) + Arguments: + str_val (str): The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 - Raises: - netaddr.core.AddrFormatError (Exception): - Raised when the passed str_val is not a valid ip string value. + Returns: + int_val (int): The ip version (Either 4 or 6 for IPv4/IPv6) """ return int(netaddr.IPAddress(str_val).version) @@ -64,26 +54,21 @@ def ip__str__(ip_type: int, ip_str: str, port: int): return "/ipv%i/%s:%i" % (ip_type, ip_str, port) -class ExternalIPNotFound(Exception): - """Raised if we cannot attain your external ip from CURL/URLLIB/IPIFY/AWS""" - - def get_external_ip() -> str: """Checks CURL/URLLIB/IPIFY/AWS for your external ip. + Returns: - external_ip (:obj:`str` `required`): - Your routers external facing ip as a string. + external_ip (str): Your routers external facing ip as a string. Raises: - ExternalIPNotFound (Exception): - Raised if all external ip attempts fail. + ExternalIPNotFound(Exception): Raised if all external ip attempts fail. """ # --- Try AWS try: external_ip = requests.get("https://checkip.amazonaws.com").text.strip() assert isinstance(ip_to_int(external_ip), int) return str(external_ip) - except Exception: + except ExternalIPNotFound: pass # --- Try ipconfig. @@ -93,7 +78,7 @@ def get_external_ip() -> str: process.close() assert isinstance(ip_to_int(external_ip), int) return str(external_ip) - except Exception: + except ExternalIPNotFound: pass # --- Try ipinfo. @@ -103,7 +88,7 @@ def get_external_ip() -> str: process.close() assert isinstance(ip_to_int(external_ip), int) return str(external_ip) - except Exception: + except ExternalIPNotFound: pass # --- Try myip.dnsomatic @@ -113,15 +98,15 @@ def get_external_ip() -> str: process.close() assert isinstance(ip_to_int(external_ip), int) return str(external_ip) - except Exception: + except ExternalIPNotFound: pass # --- Try urllib ipv6 try: - external_ip = urllib.request.urlopen("https://ident.me").read().decode("utf8") + external_ip = urllib_request.urlopen("https://ident.me").read().decode("utf8") assert isinstance(ip_to_int(external_ip), int) return str(external_ip) - except Exception: + except ExternalIPNotFound: pass # --- Try Wikipedia @@ -129,7 +114,7 @@ def get_external_ip() -> str: external_ip = requests.get("https://www.wikipedia.org").headers["X-Client-IP"] assert isinstance(ip_to_int(external_ip), int) return str(external_ip) - except Exception: + except ExternalIPNotFound: pass raise ExternalIPNotFound @@ -138,13 +123,15 @@ def get_external_ip() -> str: def get_formatted_ws_endpoint_url(endpoint_url: Optional[str]) -> Optional[str]: """ Returns a formatted websocket endpoint url. - Note: The port (or lack thereof) is left unchanged - Args: - endpoint_url (Optional[str]): - The endpoint url to format. + + Arguments: + endpoint_url (Optional[str]): The endpoint url to format. + Returns: formatted_endpoint_url (Optional[str]): The formatted endpoint url. In the form of ws:// or wss:// + + Note: The port (or lack thereof) is left unchanged. """ if endpoint_url is None: return None From 4b8a43ee8b4a3414b39715b6a2f79a6418fc3458 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 30 Jan 2025 14:44:32 -0800 Subject: [PATCH 02/12] improve `bittensor.utils.delegates_details.DelegatesDetails` logic --- bittensor/utils/delegates_details.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/utils/delegates_details.py b/bittensor/utils/delegates_details.py index eeb8d24c77..92f3872dd2 100644 --- a/bittensor/utils/delegates_details.py +++ b/bittensor/utils/delegates_details.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Any, Optional +from typing import Any, Optional, Union # TODO: consider move it to `bittensor.core.chain_data` @@ -17,7 +17,7 @@ class DelegatesDetails: @classmethod def from_chain_data(cls, data: dict[str, Any]) -> "DelegatesDetails": - def decode(key: str, default: Optional[str] = ""): + def decode(key: str, default: Union[Optional[str], list] = ""): try: if isinstance(data.get(key), dict): value = next(data.get(key).values()) From 3f5a691bfad93390f4a7b3d830740c14d445963b Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 30 Jan 2025 14:56:38 -0800 Subject: [PATCH 03/12] ruff --- bittensor/utils/balance.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index 680c765c2f..c49be249ed 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -1,5 +1,4 @@ import warnings - from typing import Union, TypedDict, Optional from scalecodec import ScaleType From 6d48826923452dd7504e27356113d724a3a46562 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 30 Jan 2025 15:02:25 -0800 Subject: [PATCH 04/12] remove comment --- bittensor/utils/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 07c3125879..75a3383c34 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -239,7 +239,6 @@ def format_error_message(error_message: Union[dict, Exception]) -> str: return f"Subtensor returned `{err_name}({err_type})` error. This means: `{err_description}`." -# Subnet 24 uses this function def is_valid_ss58_address(address: str) -> bool: """ Checks if the given address is a valid ss58 address. From f315a6c6d36828e00c26e7ed659a9b99f091bd36 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 30 Jan 2025 15:08:17 -0800 Subject: [PATCH 05/12] fix annotations --- bittensor/utils/substrate_utils/storage.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bittensor/utils/substrate_utils/storage.py b/bittensor/utils/substrate_utils/storage.py index b5e95296e5..315f7e44c8 100644 --- a/bittensor/utils/substrate_utils/storage.py +++ b/bittensor/utils/substrate_utils/storage.py @@ -25,11 +25,11 @@ class StorageKey: def __init__( self, - pallet: str, - storage_function: str, - params: list, - data: bytes, - value_scale_type: str, + pallet: Optional[str], + storage_function: Optional[str], + params: Optional[list], + data: Optional[bytes], + value_scale_type: Optional[str], metadata: GenericMetadataVersioned, runtime_config: RuntimeConfigurationObject, ): @@ -141,7 +141,7 @@ def convert_storage_parameter(self, scale_type: str, value: Any): return value - def to_hex(self) -> str: + def to_hex(self) -> Optional[str]: """ Returns a Hex-string representation of current StorageKey data From d4d37860000a9ed3a61e54685a356ae1bcd5b9cf Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 30 Jan 2025 15:08:56 -0800 Subject: [PATCH 06/12] fix annotations --- bittensor/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 75a3383c34..75993b543b 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -338,7 +338,7 @@ def decode_hex_identity_dict(info_dictionary) -> dict[str, Any]: {'name': 'john', 'additional': [('data', 'data')]} """ - def get_decoded(data: str) -> str: + def get_decoded(data: str) -> Optional[str]: """Decodes a hex-encoded string.""" try: return bytes.fromhex(data[2:]).decode() From d5a66a040f29e4e887ac2319c09bd0dc79cf3b9e Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 30 Jan 2025 15:17:30 -0800 Subject: [PATCH 07/12] remove unused class --- bittensor/core/types.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/bittensor/core/types.py b/bittensor/core/types.py index 2269ea3f9a..9563b00328 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -202,17 +202,6 @@ def determine_chain_endpoint_and_network( return "unknown", network -class AxonServeCallParams_(TypedDict): - """Axon serve chain call parameters.""" - - version: int - ip: int - port: int - ip_type: int - netuid: int - certificate: Optional[Certificate] - - class AxonServeCallParams: def __init__( self, From cef96d0e76e36bb72b0e4070898444e0fc4572e1 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 30 Jan 2025 15:30:52 -0800 Subject: [PATCH 08/12] fix annotation --- bittensor/core/chain_data/stake_info.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/core/chain_data/stake_info.py b/bittensor/core/chain_data/stake_info.py index 42588021c4..e4a439f56c 100644 --- a/bittensor/core/chain_data/stake_info.py +++ b/bittensor/core/chain_data/stake_info.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Optional, Any +from typing import Any, Optional, Union from scalecodec.utils.ss58 import ss58_encode @@ -81,7 +81,7 @@ def list_of_tuple_from_vec_u8( } @classmethod - def list_from_vec_u8(cls, vec_u8: list[int]) -> list["StakeInfo"]: + def list_from_vec_u8(cls, vec_u8: Union[list[int], bytes]) -> list["StakeInfo"]: """Returns a list of StakeInfo objects from a ``vec_u8``.""" decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo, is_vec=True) if decoded is None: From 4040a841984647949f23e91f0465515e9a541961 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 30 Jan 2025 15:32:35 -0800 Subject: [PATCH 09/12] fix annotation + docstrings --- bittensor/core/subtensor.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 036e40e5f1..a03e811f98 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1181,7 +1181,7 @@ def get_stake( block=block, params=[hotkey_ss58, coldkey_ss58, netuid], ) - hotkey_alpha_obj: ScaleType = self.query_module( + hotkey_alpha_obj: ScaleObj = self.query_module( module="SubtensorModule", name="TotalHotkeyAlpha", block=block, @@ -1541,7 +1541,7 @@ def get_vote_data( 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 = self.substrate.query( + vote_data: dict[str, Any] = self.substrate.query( module="Triumvirate", storage_function="Voting", params=[proposal_hash], @@ -2180,6 +2180,7 @@ def add_stake( 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. + netuid (Optional[int]): The unique identifier of the subnet to which the neuron belongs. amount (Balance): 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. @@ -2217,6 +2218,7 @@ def add_stake_multiple( Args: wallet (bittensor_wallet.Wallet): The wallet used for staking. hotkey_ss58s (list[str]): List of ``SS58`` addresses of hotkeys to stake to. + netuids (list[int]): List of network UIDs to stake to. amounts (list[Balance]): 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. @@ -2637,6 +2639,7 @@ def _blocks_weight_limit() -> bool: retries = 0 success = False + message = "No attempt made. Perhaps it is too soon to commit weights!" if ( uid := self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) ) is None: @@ -2647,7 +2650,7 @@ def _blocks_weight_limit() -> bool: 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 retries < max_retries and success is False and _blocks_weight_limit(): logging.info( f"Committing weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." @@ -2666,7 +2669,7 @@ def _blocks_weight_limit() -> bool: 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 _blocks_weight_limit(): try: logging.info( From ccb5154b78013d68985e42d80f3d203935b186a6 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 30 Jan 2025 15:51:01 -0800 Subject: [PATCH 10/12] use recommended import instead of dynamic --- bittensor/core/metagraph.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 9874d02a08..732e5d229d 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -1,5 +1,4 @@ import copy -import importlib import os import pickle import typing @@ -764,7 +763,7 @@ def _set_metagraph_attributes(self, block: int): ) self.axons = [n.axon_info for n in self.neurons] - def save(self, root_dir: Optional[list[str]] = None) -> "AsyncMetagraph": + def save(self, root_dir: Optional[list[str]] = None) -> "MetagraphMixin": """ 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 @@ -1022,7 +1021,7 @@ def __init__( self.axons: list["AxonInfo"] = [] self.total_stake: list["Balance"] = [] - def load_from_path(self, dir_path: str) -> "AsyncMetagraph": + def load_from_path(self, dir_path: str) -> "MetagraphMixin": """ Loads the metagraph state from a specified directory path. @@ -1152,7 +1151,7 @@ def __init__( self.axons: list["AxonInfo"] = [] self.total_stake: list["Balance"] = [] - def load_from_path(self, dir_path: str) -> "AsyncMetagraph": + def load_from_path(self, dir_path: str) -> "MetagraphMixin": """ Loads the state of the Metagraph from a specified directory path. @@ -1356,10 +1355,7 @@ async def _initialize_subtensor( subtensor = self.subtensor if not subtensor: # Lazy import due to circular import (subtensor -> metagraph, metagraph -> subtensor) - AsyncSubtensor = getattr( - importlib.import_module("bittensor.core.async_subtensor"), - "AsyncSubtensor", - ) + from bittensor.core.async_subtensor import AsyncSubtensor async with AsyncSubtensor(network=self.chain_endpoint) as subtensor: self.subtensor = subtensor @@ -1592,7 +1588,7 @@ def sync( """ # Initialize subtensor - subtensor = self._initialize_subtensor(subtensor) + subtensor = self._initialize_subtensor(subtensor=subtensor) if ( subtensor.chain_endpoint != settings.ARCHIVE_ENTRYPOINT @@ -1632,11 +1628,11 @@ def _initialize_subtensor(self, subtensor: "Subtensor") -> "Subtensor": according to the current network settings. Args: - subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance provided for + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance provided for initialization. If ``None``, a new subtensor instance is created using the current network configuration. Returns: - subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The initialized subtensor instance, ready to be + subtensor (bittensor.core.subtensor.Subtensor): The initialized subtensor instance, ready to be used for syncing the metagraph. Internal Usage: @@ -1650,9 +1646,8 @@ def _initialize_subtensor(self, subtensor: "Subtensor") -> "Subtensor": subtensor = self.subtensor if not subtensor: # Lazy import due to circular import (subtensor -> metagraph, metagraph -> subtensor) - Subtensor = getattr( - importlib.import_module("bittensor.core.subtensor"), "Subtensor" - ) + from bittensor.core.subtensor import Subtensor + subtensor = Subtensor(network=self.chain_endpoint) self.subtensor = subtensor From e7c15e50bd8377403ed1c6bbeeda91c855891e53 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 30 Jan 2025 15:55:50 -0800 Subject: [PATCH 11/12] fix annotation --- bittensor/core/chain_data/dynamic_info.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bittensor/core/chain_data/dynamic_info.py b/bittensor/core/chain_data/dynamic_info.py index eee481d3fd..7f3ccb90de 100644 --- a/bittensor/core/chain_data/dynamic_info.py +++ b/bittensor/core/chain_data/dynamic_info.py @@ -43,7 +43,7 @@ class DynamicInfo: subnet_identity: Optional[SubnetIdentity] @classmethod - def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DynamicInfo"]: + def from_vec_u8(cls, vec_u8: Union[list[int], bytes]) -> Optional["DynamicInfo"]: if len(vec_u8) == 0: return None decoded = from_scale_encoding(vec_u8, ChainDataType.DynamicInfo) @@ -154,8 +154,11 @@ def tao_to_alpha_with_slippage( ) -> Union[tuple[Balance, Balance], float]: """ Returns an estimate of how much Alpha would a staker receive if they stake their tao using the current pool state. - Args: + + Arguments: tao: Amount of TAO to stake. + percentage: percentage + Returns: If percentage is False, a tuple of balances where the first part is the amount of Alpha received, and the second part (slippage) is the difference between the estimated amount and ideal @@ -206,8 +209,11 @@ def alpha_to_tao_with_slippage( ) -> Union[tuple[Balance, Balance], float]: """ Returns an estimate of how much TAO would a staker receive if they unstake their alpha using the current pool state. + Args: alpha: Amount of Alpha to stake. + percentage: percentage + Returns: If percentage is False, a tuple of balances where the first part is the amount of TAO received, and the second part (slippage) is the difference between the estimated amount and ideal From 3c3f862acd89fdd232a2c7b42c1f5e0fa7f01743 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 30 Jan 2025 15:57:01 -0800 Subject: [PATCH 12/12] fixes --- bittensor/core/async_subtensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index e7f6629a2e..1c0ea4f27b 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -89,7 +89,7 @@ from bittensor_wallet import Wallet from bittensor.core.axon import Axon from bittensor.utils import Certificate - from async_substrate_interface import QueryMapResult + from async_substrate_interface import AsyncQueryMapResult class AsyncSubtensor(SubtensorMixin): @@ -316,7 +316,7 @@ async def query_map( block_hash: Optional[str] = None, reuse_block: bool = False, params: Optional[list] = None, - ) -> "QueryMapResult": + ) -> "AsyncQueryMapResult": """ 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 @@ -354,7 +354,7 @@ async def query_map_subtensor( block_hash: Optional[str] = None, reuse_block: bool = False, params: Optional[list] = None, - ) -> "QueryMapResult": + ) -> "AsyncQueryMapResult": """ 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. @@ -1380,7 +1380,7 @@ async def get_all_metagraphs_info( Returns: MetagraphInfo dataclass """ - block_hash = await self.determine_block_hash(block, block_hash.reuse_block) + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) if not block_hash and reuse_block: block_hash = self.substrate.last_block_hash query = await self.substrate.runtime_call( @@ -2019,7 +2019,7 @@ async def get_vote_data( 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( + vote_data: dict[str, Any] = await self.substrate.query( module="Triumvirate", storage_function="Voting", params=[proposal_hash], @@ -2598,7 +2598,7 @@ async def subnet( params=[netuid], block_hash=block_hash, ) - subnet = DynamicInfo.from_vec_u8(bytes.fromhex(query.decode()[2:])) + subnet = DynamicInfo.from_vec_u8(hex_to_bytes(query.decode())) return subnet async def subnet_exists(