diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 0e41597aa8..c3ab32e6e1 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1,4 +1,3 @@ -import argparse import asyncio import copy import ssl @@ -15,7 +14,7 @@ from scalecodec.base import RuntimeConfiguration from scalecodec.type_registry import load_type_registry_preset -from bittensor.core import settings +from bittensor.core.types import SubtensorMixin from bittensor.core.chain_data import ( DelegateInfo, StakeInfo, @@ -68,9 +67,8 @@ ss58_to_vec_u8, torch, u16_normalized_float, - execute_coroutine, + _decode_hex_identity_dict, ) -from bittensor.utils import networking from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.delegates_details import DelegatesDetails @@ -84,33 +82,7 @@ from async_substrate_interface import QueryMapResult -def _decode_hex_identity_dict(info_dictionary: dict[str, Any]) -> dict[str, Any]: - """Decodes a dictionary of hexadecimal identities.""" - for k, v in info_dictionary.items(): - if isinstance(v, dict): - item = next(iter(v.values())) - else: - item = v - if isinstance(item, tuple) and item: - if len(item) > 1: - try: - info_dictionary[k] = ( - bytes(item).hex(sep=" ", bytes_per_sep=2).upper() - ) - except UnicodeDecodeError: - logging.error(f"Could not decode: {k}: {item}.") - else: - try: - info_dictionary[k] = bytes(item[0]).decode("utf-8") - except UnicodeDecodeError: - logging.error(f"Could not decode: {k}: {item}.") - else: - info_dictionary[k] = item - - return info_dictionary - - -class AsyncSubtensor: +class AsyncSubtensor(SubtensorMixin): """Thin layer for interacting with Substrate Interface. Mostly a collection of frequently-used calls.""" def __init__( @@ -119,7 +91,6 @@ def __init__( config: Optional["Config"] = None, _mock: bool = False, log_verbose: bool = False, - event_loop: asyncio.AbstractEventLoop = None, ): """ Initializes an instance of the AsyncSubtensor class. @@ -129,7 +100,6 @@ def __init__( config (Optional[Config]): Configuration object for the AsyncSubtensor instance. _mock: Whether this is a mock instance. Mainly just for use in testing. log_verbose (bool): Enables or disables verbose logging. - event_loop (Optional[asyncio.AbstractEventLoop]): Custom asyncio event loop. Raises: Any exceptions raised during the setup, configuration, or connection process. @@ -155,7 +125,6 @@ def __init__( type_registry=TYPE_REGISTRY, use_remote_preset=True, chain_name="Bittensor", - event_loop=event_loop, _mock=_mock, ) if self.log_verbose: @@ -163,200 +132,30 @@ def __init__( f"Connected to {self.network} network and {self.chain_endpoint}." ) - def __str__(self): - return f"Network: {self.network}, Chain: {self.chain_endpoint}" - - def __repr__(self): - return self.__str__() - - def __del__(self): - execute_coroutine(self.close()) - - def _check_and_log_network_settings(self): - if self.network == settings.NETWORKS[3]: # local - logging.warning( - ":warning: Verify your local subtensor is running on port [blue]9944[/blue]." - ) - - if ( - self.network == "finney" - or self.chain_endpoint == settings.FINNEY_ENTRYPOINT - ) and self.log_verbose: - logging.info( - f"You are connecting to {self.network} network with endpoint {self.chain_endpoint}." - ) - logging.debug( - "We strongly encourage running a local subtensor node whenever possible. " - "This increases decentralization and resilience of the network." - ) - # TODO: remove or apply this warning as updated default endpoint? - logging.debug( - "In a future release, local subtensor will become the default endpoint. " - "To get ahead of this change, please run a local subtensor node and point to it." - ) - - @staticmethod - def config() -> "Config": - """ - Creates and returns a Bittensor configuration object. - - Returns: - config (bittensor.core.config.Config): A Bittensor configuration object configured with arguments added by - the `subtensor.add_args` method. - """ - parser = argparse.ArgumentParser() - AsyncSubtensor.add_args(parser) - return Config(parser) - - @staticmethod - def setup_config(network: Optional[str], config: "Config"): - """ - Sets up and returns the configuration for the Subtensor network and endpoint. - - This method determines the appropriate network and chain endpoint based on the provided network string or - configuration object. It evaluates the network and endpoint in the following order of precedence: - 1. Provided network string. - 2. Configured chain endpoint in the `config` object. - 3. Configured network in the `config` object. - 4. Default chain endpoint. - 5. Default network. - - Arguments: - network (Optional[str]): The name of the Subtensor network. If None, the network and endpoint will be - determined from the `config` object. - config (bittensor.core.config.Config): The configuration object containing the network and chain endpoint - settings. - - Returns: - tuple: A tuple containing the formatted WebSocket endpoint URL and the evaluated network name. - """ - if network is None: - candidates = [ - ( - config.is_set("subtensor.chain_endpoint"), - config.subtensor.chain_endpoint, - ), - (config.is_set("subtensor.network"), config.subtensor.network), - ( - config.subtensor.get("chain_endpoint"), - config.subtensor.chain_endpoint, - ), - (config.subtensor.get("network"), config.subtensor.network), - ] - for check, config_network in candidates: - if check: - network = config_network + async def close(self): + """Close the connection.""" + if self.substrate: + await self.substrate.close() - evaluated_network, evaluated_endpoint = ( - AsyncSubtensor.determine_chain_endpoint_and_network(network) + async def initialize(self): + logging.info( + f"[magenta]Connecting to Substrate:[/magenta] [blue]{self}[/blue][magenta]...[/magenta]" ) - - return networking.get_formatted_ws_endpoint_url( - evaluated_endpoint - ), evaluated_network - - @classmethod - def help(cls): - """Print help to stdout.""" - parser = argparse.ArgumentParser() - cls.add_args(parser) - print(cls.__new__.__doc__) - parser.print_help() - - @classmethod - def add_args(cls, parser: "argparse.ArgumentParser", prefix: Optional[str] = None): - """ - Adds command-line arguments to the provided ArgumentParser for configuring the Subtensor settings. - - Arguments: - parser (argparse.ArgumentParser): The ArgumentParser object to which the Subtensor arguments will be added. - prefix (Optional[str]): An optional prefix for the argument names. If provided, the prefix is prepended to - each argument name. - - Arguments added: - --subtensor.network: The Subtensor network flag. Possible values are 'finney', 'test', 'archive', and - 'local'. Overrides the chain endpoint if set. - --subtensor.chain_endpoint: The Subtensor chain endpoint flag. If set, it overrides the network flag. - --subtensor._mock: If true, uses a mocked connection to the chain. - - Example: - parser = argparse.ArgumentParser() - Subtensor.add_args(parser) - """ - prefix_str = "" if prefix is None else f"{prefix}." try: - default_network = settings.DEFAULT_NETWORK - default_chain_endpoint = settings.FINNEY_ENTRYPOINT - - parser.add_argument( - f"--{prefix_str}subtensor.network", - default=default_network, - type=str, - help="""The subtensor network flag. The likely choices are: - -- finney (main network) - -- test (test network) - -- archive (archive network +300 blocks) - -- local (local running network) - If this option is set it overloads subtensor.chain_endpoint with - an entry point node from that network. - """, - ) - parser.add_argument( - f"--{prefix_str}subtensor.chain_endpoint", - default=default_chain_endpoint, - type=str, - help="""The subtensor endpoint flag. If set, overrides the --network flag.""", + await self.substrate.initialize() + return self + except TimeoutError: + logging.error( + f"[red]Error[/red]: Timeout occurred connecting to substrate." + f" Verify your chain and network settings: {self}" ) - parser.add_argument( - f"--{prefix_str}subtensor._mock", - default=False, - type=bool, - help="""If true, uses a mocked connection to the chain.""", + raise ConnectionError + except (ConnectionRefusedError, ssl.SSLError) as error: + logging.error( + f"[red]Error[/red]: Connection refused when connecting to substrate. " + f"Verify your chain and network settings: {self}. Error: {error}" ) - - except argparse.ArgumentError: - # re-parsing arguments. - pass - - @staticmethod - def determine_chain_endpoint_and_network( - network: str, - ) -> tuple[Optional[str], Optional[str]]: - """Determines the chain endpoint and network from the passed network or chain_endpoint. - - Arguments: - network (str): The network flag. The choices are: ``finney`` (main network), ``archive`` (archive network - +300 blocks), ``local`` (local running network), ``test`` (test network). - - Returns: - tuple[Optional[str], Optional[str]]: The network and chain endpoint flag. If passed, overrides the - ``network`` argument. - """ - - if network is None: - return None, None - if network in settings.NETWORKS: - return network, settings.NETWORK_MAP[network] - - substrings_map = { - "entrypoint-finney.opentensor.ai": ("finney", settings.FINNEY_ENTRYPOINT), - "test.finney.opentensor.ai": ("test", settings.FINNEY_TEST_ENTRYPOINT), - "archive.chain.opentensor.ai": ("archive", settings.ARCHIVE_ENTRYPOINT), - "subvortex": ("subvortex", settings.SUBVORTEX_ENTRYPOINT), - "127.0.0.1": ("local", settings.LOCAL_ENTRYPOINT), - "localhost": ("local", settings.LOCAL_ENTRYPOINT), - } - - for substring, result in substrings_map.items(): - if substring in network: - return result - - return "unknown", network - - async def close(self): - """Close the connection.""" - if self.substrate: - await self.substrate.close() + raise ConnectionError async def __aenter__(self): logging.info( @@ -615,7 +414,7 @@ async def query_runtime_api( self, runtime_api: str, method: str, - params: Optional[Union[list[list[int]], dict[str, int], list[int]]], + params: Optional[Union[list[list[int]], dict[str, int], list[int]]] = None, block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, @@ -640,6 +439,7 @@ async def query_runtime_api( This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed and specific interactions with the network's runtime environment. """ + # TODO why doesn't this just use SubstrateInterface.runtime_call ? block_hash = await self.determine_block_hash(block, block_hash, reuse_block) call_definition = TYPE_REGISTRY["runtime_api"][runtime_api]["methods"][method] @@ -1552,9 +1352,9 @@ async def get_neuron_certificate( ) try: if certificate: - return ( - chr(certificate["algorithm"]) - + bytes(certificate["public_key"][0]).decode() + tuple_ascii = certificate["public_key"][0] + return chr(certificate["algorithm"]) + "".join( + chr(i) for i in tuple_ascii ) except AttributeError: @@ -1601,7 +1401,10 @@ async def get_neuron_for_pubkey_and_subnet( params = [netuid, uid.value] json_body = await self.substrate.rpc_request( - method="neuronInfo_getNeuron", params=params, reuse_block_hash=reuse_block + method="neuronInfo_getNeuron", + params=params, + block_hash=block_hash, + reuse_block_hash=reuse_block, ) if not (result := json_body.get("result", None)): @@ -2735,7 +2538,9 @@ async def weights( block_hash=block_hash, reuse_block_hash=reuse_block, ) - w_map = [(uid, w.value or []) async for uid, w in w_map_encoded] + w_map = [] + async for uid, w in w_map_encoded: + w_map.append((uid, w.value)) return w_map @@ -2960,7 +2765,8 @@ async def commit_weights( 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}" + f"Committing weights with params: netuid={netuid}, uids={uids}, weights={weights}, " + f"version_key={version_key}" ) # Generate the hash of the weights @@ -3116,7 +2922,6 @@ async def reveal_weights( async def root_register( self, wallet: "Wallet", - netuid: int = 0, block_hash: Optional[str] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, @@ -3126,7 +2931,6 @@ async def root_register( Arguments: wallet (bittensor_wallet.Wallet): Bittensor wallet instance. - netuid (int): Subnet uniq id. Root subnet uid is 0. block_hash (Optional[str]): The hash of the blockchain block for the query. wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is @@ -3135,6 +2939,7 @@ async def root_register( Returns: `True` if registration was successful, otherwise `False`. """ + netuid = 0 logging.info( f"Registering on netuid [blue]0[/blue] on network: [blue]{self.network}[/blue]" ) @@ -3153,9 +2958,6 @@ async def root_register( except TypeError as e: logging.error(f"Unable to retrieve current recycle. {e}") return False - except KeyError: - logging.error("Unable to retrieve current balance.") - return False current_recycle = Balance.from_rao(int(recycle_call)) @@ -3170,7 +2972,6 @@ async def root_register( return await root_register_extrinsic( subtensor=self, wallet=wallet, - netuid=netuid, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -3363,7 +3164,7 @@ async def serve_axon( async def transfer( self, wallet: "Wallet", - destination: str, + dest: str, amount: Union["Balance", float], transfer_all: bool = False, wait_for_inclusion: bool = True, @@ -3375,7 +3176,7 @@ async def transfer( Arguments: wallet (bittensor_wallet.Wallet): Source wallet for the transfer. - destination (str): Destination address for the transfer. + dest (str): Destination address for the transfer. amount (float): Amount of tokens to transfer. transfer_all (bool): Flag to transfer all tokens. Default is ``False``. wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``True``. @@ -3392,7 +3193,7 @@ async def transfer( return await transfer_extrinsic( subtensor=self, wallet=wallet, - destination=destination, + dest=dest, amount=amount, transfer_all=transfer_all, wait_for_inclusion=wait_for_inclusion, @@ -3470,3 +3271,20 @@ async def unstake_multiple( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) + + +async def get_async_subtensor( + network: Optional[str] = None, + config: Optional["Config"] = None, + _mock: bool = False, + log_verbose: bool = False, +) -> "AsyncSubtensor": + """ + Factory method to create an initialized AsyncSubtensor. Mainly useful for when you don't want to run + `await subtensor.initialize()` after instantiation. + """ + sub = AsyncSubtensor( + network=network, config=config, _mock=_mock, log_verbose=log_verbose + ) + await sub.initialize() + return sub diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index ce6923c2bd..392fc8dab8 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -5,7 +5,6 @@ import contextlib import copy import inspect -import json import threading import time import traceback @@ -15,6 +14,7 @@ from inspect import signature, Signature, Parameter from typing import Any, Awaitable, Callable, Optional, Tuple +from async_substrate_interface.utils import json import uvicorn from bittensor_wallet import Wallet, Keypair from fastapi import APIRouter, Depends, FastAPI diff --git a/bittensor/core/chain_data/axon_info.py b/bittensor/core/chain_data/axon_info.py index 9ee341df3d..8d7a920ed7 100644 --- a/bittensor/core/chain_data/axon_info.py +++ b/bittensor/core/chain_data/axon_info.py @@ -3,10 +3,10 @@ in the bittensor network. """ -import json from dataclasses import asdict, dataclass from typing import Any, Union +from async_substrate_interface.utils import json from bittensor.utils import networking from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index 8ab2e15acb..d314a7cede 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -14,7 +14,7 @@ from bittensor.core.settings import version_as_int from bittensor.core.stream import StreamingSynapse from bittensor.core.synapse import Synapse, TerminalInfo -from bittensor.utils import networking, event_loop_is_running +from bittensor.utils import networking from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch @@ -31,6 +31,14 @@ DENDRITE_DEFAULT_ERROR = ("422", "Failed to parse response") +def event_loop_is_running(): + try: + asyncio.get_running_loop() + return True + except RuntimeError: + return False + + class DendriteMixin: """ The Dendrite class represents the abstracted implementation of a network client module. diff --git a/bittensor/core/errors.py b/bittensor/core/errors.py index 7abe28e8b3..9f856d15e6 100644 --- a/bittensor/core/errors.py +++ b/bittensor/core/errors.py @@ -17,6 +17,14 @@ ExtrinsicNotFound = ExtrinsicNotFound +class MaxSuccessException(Exception): + """Raised when the POW Solver has reached the max number of successful solutions.""" + + +class MaxAttemptsException(Exception): + """Raised when the POW Solver has reached the max number of attempts.""" + + class ChainError(SubstrateRequestException): """Base error for any chain related errors.""" diff --git a/bittensor/core/extrinsics/asyncex/commit_reveal.py b/bittensor/core/extrinsics/asyncex/commit_reveal.py index 9d095b9e91..9399299a08 100644 --- a/bittensor/core/extrinsics/asyncex/commit_reveal.py +++ b/bittensor/core/extrinsics/asyncex/commit_reveal.py @@ -7,7 +7,6 @@ from numpy.typing import NDArray from bittensor.core.settings import version_as_int -from bittensor.utils import format_error_message from bittensor.utils.btlogging import logging from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit @@ -31,7 +30,7 @@ async def _do_commit_reveal_v3( finalization. Arguments: - subtensor: An instance of the Subtensor class. + subtensor: An instance of the AsyncSubtensor class. wallet: Wallet An instance of the Wallet class containing the user's keypair. netuid: int The network unique identifier. commit bytes The commit data in bytes format. @@ -57,26 +56,10 @@ async def _do_commit_reveal_v3( "reveal_round": reveal_round, }, ) - - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - ) - - response = await subtensor.substrate.submit_extrinsic( - extrinsic=extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + return await subtensor.sign_and_send_extrinsic( + call, wallet, wait_for_inclusion, wait_for_finalization, sign_with="hotkey" ) - if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." - - if await response.is_success: - return True, None - - return False, format_error_message(await response.error_message) - async def commit_reveal_v3_extrinsic( subtensor: "AsyncSubtensor", diff --git a/bittensor/core/extrinsics/asyncex/registration.py b/bittensor/core/extrinsics/asyncex/registration.py index aea5699b83..d4b5c6a271 100644 --- a/bittensor/core/extrinsics/asyncex/registration.py +++ b/bittensor/core/extrinsics/asyncex/registration.py @@ -10,28 +10,14 @@ import asyncio from typing import Optional, Union, TYPE_CHECKING -from bittensor.utils import format_error_message from bittensor.utils import unlock_key from bittensor.utils.btlogging import logging -from bittensor.utils.registration import log_no_torch_error, create_pow_async +from bittensor.utils.registration import log_no_torch_error, create_pow_async, torch if TYPE_CHECKING: - import torch from bittensor_wallet import Wallet from bittensor.core.async_subtensor import AsyncSubtensor from bittensor.utils.registration.pow import POWSolution -else: - from bittensor.utils.registration.pow import LazyLoadedTorch - - torch = LazyLoadedTorch() - - -class MaxSuccessException(Exception): - """Raised when the POW Solver has reached the max number of successful solutions.""" - - -class MaxAttemptsException(Exception): - """Raised when the POW Solver has reached the max number of attempts.""" async def _do_burned_register( @@ -40,21 +26,22 @@ async def _do_burned_register( wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, -) -> tuple[bool, Optional[str]]: +) -> tuple[bool, str]: """ Performs a burned register extrinsic call to the Subtensor chain. This method sends a registration transaction to the Subtensor blockchain using the burned register mechanism. Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): Subtensor instance. netuid (int): The network unique identifier to register on. wallet (bittensor_wallet.Wallet): The wallet to be registered. wait_for_inclusion (bool): Whether to wait for the transaction to be included in a block. Default is False. wait_for_finalization (bool): Whether to wait for the transaction to be finalized. Default is True. Returns: - Tuple[bool, Optional[str]]: A tuple containing a boolean indicating success or failure, and an optional error message. + Tuple[bool, Optional[str]]: A tuple containing a boolean indicating success or failure, and an optional error + message. """ # create extrinsic call @@ -66,26 +53,13 @@ async def _do_burned_register( "hotkey": wallet.hotkey.ss58_address, }, ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic=extrinsic, + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - # process if registration successful, try again if pow is still valid - if not await response.is_success: - return False, format_error_message(await response.error_message) - # Successful registration - - return True, None - async def burned_register_extrinsic( subtensor: "AsyncSubtensor", @@ -97,19 +71,20 @@ async def burned_register_extrinsic( """Registers the wallet to chain by recycling TAO. Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): Subtensor instance. wallet (bittensor.wallet): Bittensor wallet object. netuid (int): The ``netuid`` of the subnet to register on. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or - returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_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. + ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for - finalization / inclusion, the response is ``true``. + 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``. """ - if not await subtensor.subnet_exists(netuid): + block_hash = await subtensor.substrate.get_chain_head() + if not await subtensor.subnet_exists(netuid, block_hash=block_hash): logging.error( f":cross_mark: [red]Failed error:[/red] subnet [blue]{netuid}[/blue] does not exist." ) @@ -122,11 +97,17 @@ async def burned_register_extrinsic( logging.info( f":satellite: [magenta]Checking Account on subnet[/magenta] [blue]{netuid}[/blue][magenta] ...[/magenta]" ) - neuron = await subtensor.get_neuron_for_pubkey_and_subnet( - wallet.hotkey.ss58_address, netuid=netuid - ) - old_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + # We could do this as_completed because we don't actually need old_balance and recycle + # if neuron is null, but the complexity isn't worth it considering the small performance + # gains we'd hypothetically receive in this situation + neuron, old_balance, recycle_amount = await asyncio.gather( + subtensor.get_neuron_for_pubkey_and_subnet( + wallet.hotkey.ss58_address, netuid=netuid, block_hash=block_hash + ), + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), + subtensor.recycle(netuid=netuid, block_hash=block_hash), + ) if not neuron.is_null: logging.info(":white_heavy_check_mark: [green]Already Registered[/green]") @@ -136,9 +117,7 @@ async def burned_register_extrinsic( logging.info(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") return True - logging.info(":satellite: [magenta]Recycling TAO for Registration...[/magenta]") - - recycle_amount = await subtensor.recycle(netuid=netuid) + logging.debug(":satellite: [magenta]Recycling TAO for Registration...[/magenta]") logging.info(f"Recycling {recycle_amount} to register on subnet:{netuid}") success, err_msg = await _do_burned_register( @@ -196,7 +175,8 @@ async def _do_pow_register( Returns: success (bool): ``True`` if the extrinsic was included in a block. - error (Optional[str]): ``None`` on success or not waiting for inclusion/finalization, otherwise the error message. + error (Optional[str]): ``None`` on success or not waiting for inclusion/finalization, otherwise the error + message. """ # create extrinsic call call = await subtensor.substrate.compose_call( @@ -211,26 +191,13 @@ async def _do_pow_register( "coldkey": wallet.coldkeypub.ss58_address, }, ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic=extrinsic, + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - # process if registration successful, try again if pow is still valid - if not await response.is_success: - return False, format_error_message(error_message=await response.error_message) - # Successful registration - else: - return True, None - async def register_extrinsic( subtensor: "AsyncSubtensor", @@ -271,9 +238,9 @@ async def register_extrinsic( `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the response is `True`. """ - + block_hash = await subtensor.substrate.get_chain_head() logging.debug("[magenta]Checking subnet status... [/magenta]") - if not await subtensor.subnet_exists(netuid): + if not await subtensor.subnet_exists(netuid, block_hash=block_hash): logging.error( f":cross_mark: [red]Failed error:[/red] subnet [blue]{netuid}[/blue] does not exist." ) @@ -283,8 +250,7 @@ async def register_extrinsic( f":satellite: [magenta]Checking Account on subnet[/magenta] [blue]{netuid}[/blue] [magenta]...[/magenta]" ) neuron = await subtensor.get_neuron_for_pubkey_and_subnet( - hotkey_ss58=wallet.hotkey.ss58_address, - netuid=netuid, + hotkey_ss58=wallet.hotkey.ss58_address, netuid=netuid, block_hash=block_hash ) if not neuron.is_null: @@ -296,7 +262,8 @@ async def register_extrinsic( return True logging.debug( - f"Registration hotkey: {wallet.hotkey.ss58_address}, Public coldkey: {wallet.coldkey.ss58_address} in the network: {subtensor.network}." + f"Registration hotkey: {wallet.hotkey.ss58_address}, Public coldkey: " + f"{wallet.coldkey.ss58_address} in the network: {subtensor.network}." ) if not torch: @@ -372,7 +339,8 @@ async def register_extrinsic( if "HotKeyAlreadyRegisteredInSubNet" in err_msg: logging.info( - f":white_heavy_check_mark: [green]Already Registered on subnet:[/green] [blue]{netuid}[/blue]." + f":white_heavy_check_mark: [green]Already Registered on subnet:[/green] " + f"[blue]{netuid}[/blue]." ) return True logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}") @@ -399,13 +367,13 @@ async def register_extrinsic( # Exited loop because pow is no longer valid. logging.error("[red]POW is stale.[/red]") # Try again. - # continue if attempts < max_allowed_attempts: # Failed registration, retry pow attempts += 1 logging.error( - f":satellite: [magenta]Failed registration, retrying pow ...[/magenta] [blue]({attempts}/{max_allowed_attempts})[/blue]" + f":satellite: [magenta]Failed registration, retrying pow ...[/magenta] " + f"[blue]({attempts}/{max_allowed_attempts})[/blue]" ) else: # Failed to register after max attempts. diff --git a/bittensor/core/extrinsics/asyncex/root.py b/bittensor/core/extrinsics/asyncex/root.py index 0c6ada4215..61f9455988 100644 --- a/bittensor/core/extrinsics/asyncex/root.py +++ b/bittensor/core/extrinsics/asyncex/root.py @@ -1,9 +1,8 @@ import asyncio -import time from typing import Union, TYPE_CHECKING -import numpy as np from bittensor_wallet import Wallet +import numpy as np from numpy.typing import NDArray from bittensor.core.errors import SubstrateRequestException @@ -45,7 +44,6 @@ 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, ) -> bool: @@ -54,17 +52,16 @@ async def root_register_extrinsic( 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. Returns: - `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the - response is `True`. + `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, + the response is `True`. """ - + netuid = 0 if not (unlock := unlock_key(wallet)).success: logging.error(unlock.message) return False @@ -96,7 +93,7 @@ async def root_register_extrinsic( if not success: logging.error(f":cross_mark: [red]Failed error:[/red] {err_msg}") - time.sleep(0.5) + await asyncio.sleep(0.5) return False # Successful registration, final check for neuron and pubkey @@ -149,7 +146,8 @@ async def _do_set_root_weights( period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. Returns: - tuple: Returns a tuple containing a boolean indicating success and a message describing the result of the operation. + tuple: Returns a tuple containing a boolean indicating success and a message describing the result of the + operation. """ call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -209,8 +207,8 @@ async def set_root_weights_extrinsic( 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. + 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 diff --git a/bittensor/core/extrinsics/asyncex/serving.py b/bittensor/core/extrinsics/asyncex/serving.py index 1290e53205..4dd4417f99 100644 --- a/bittensor/core/extrinsics/asyncex/serving.py +++ b/bittensor/core/extrinsics/asyncex/serving.py @@ -10,11 +10,11 @@ Certificate, ) from bittensor.utils.btlogging import logging +from bittensor.core.types import AxonServeCallParams if TYPE_CHECKING: from bittensor.core.axon import Axon from bittensor.core.async_subtensor import AsyncSubtensor - from bittensor.core.types import AxonServeCallParams from bittensor_wallet import Wallet @@ -43,8 +43,7 @@ async def do_serve_axon( decentralized computation capabilities of Bittensor. """ - if call_params["certificate"] is None: - del call_params["certificate"] + if call_params.certificate is None: call_function = "serve_axon" else: call_function = "serve_axon_tls" @@ -52,7 +51,7 @@ async def do_serve_axon( call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function=call_function, - call_params=call_params, + call_params=call_params.dict(), ) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.hotkey @@ -95,54 +94,42 @@ async def serve_extrinsic( netuid (int): The network uid to serve on. placeholder1 (int): A placeholder for future use. placeholder2 (int): A placeholder for future use. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or - returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_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. + ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. Defaults to ``None``. Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for - finalization / inclusion, the response is ``true``. + 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``. """ # Decrypt hotkey if not (unlock := unlock_key(wallet, "hotkey")).success: logging.error(unlock.message) return False - params: "AxonServeCallParams" = { - "version": version_as_int, - "ip": net.ip_to_int(ip), - "port": port, - "ip_type": net.ip_version(ip), - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - "coldkey": wallet.coldkeypub.ss58_address, - "protocol": protocol, - "placeholder1": placeholder1, - "placeholder2": placeholder2, - "certificate": certificate, - } + params = AxonServeCallParams( + **{ + "version": version_as_int, + "ip": net.ip_to_int(ip), + "port": port, + "ip_type": net.ip_version(ip), + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + "coldkey": wallet.coldkeypub.ss58_address, + "protocol": protocol, + "placeholder1": placeholder1, + "placeholder2": placeholder2, + "certificate": certificate, + } + ) logging.debug("Checking axon ...") neuron = await subtensor.get_neuron_for_pubkey_and_subnet( wallet.hotkey.ss58_address, netuid=netuid ) - neuron_up_to_date = not neuron.is_null and params == { - "version": neuron.axon_info.version, - "ip": net.ip_to_int(neuron.axon_info.ip), - "port": neuron.axon_info.port, - "ip_type": neuron.axon_info.ip_type, - "netuid": neuron.netuid, - "hotkey": neuron.hotkey, - "coldkey": neuron.coldkey, - "protocol": neuron.axon_info.protocol, - "placeholder1": neuron.axon_info.placeholder1, - "placeholder2": neuron.axon_info.placeholder2, - } - output = params.copy() - output["coldkey"] = wallet.coldkeypub.ss58_address - output["hotkey"] = wallet.hotkey.ss58_address + neuron_up_to_date = not neuron.is_null and params == neuron if neuron_up_to_date: logging.debug( f"Axon already served on: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) " @@ -187,16 +174,16 @@ async def serve_axon_extrinsic( subtensor (bittensor.core.async_subtensor.AsyncSubtensor): Subtensor instance object. netuid (int): The ``netuid`` being served on. axon (bittensor.core.axon.Axon): Axon to serve. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or - returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_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. + ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. Defaults to ``None``. Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for - finalization / inclusion, the response is ``true``. + 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``. """ if not (unlock := unlock_key(axon.wallet, "hotkey")).success: logging.error(unlock.message) @@ -213,7 +200,7 @@ async def serve_axon_extrinsic( f":white_heavy_check_mark: [green]Found external ip:[/green] [blue]{external_ip}[/blue]" ) except Exception as e: - raise RuntimeError( + raise ConnectionError( f"Unable to attain your external ip. Check your internet connection. error: {e}" ) from e else: @@ -282,21 +269,21 @@ async def publish_metadata( }, ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = await subtensor.substrate.submit_extrinsic( - 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 + extrinsic = await substrate.create_signed_extrinsic( + call=call, keypair=wallet.hotkey + ) + response = await substrate.submit_extrinsic( + extrinsic=extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True - if await response.is_success: - return True - raise MetadataError(format_error_message(await response.error_message)) + if await response.is_success: + return True + raise MetadataError(format_error_message(await response.error_message)) async def get_metadata( diff --git a/bittensor/core/extrinsics/asyncex/staking.py b/bittensor/core/extrinsics/asyncex/staking.py index 2cba0cb92c..0396bf76a6 100644 --- a/bittensor/core/extrinsics/asyncex/staking.py +++ b/bittensor/core/extrinsics/asyncex/staking.py @@ -162,7 +162,8 @@ async def add_stake_extrinsic( logging.success(":white_heavy_check_mark: [green]Finalized[/green]") logging.info( - f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] " + "[magenta]...[/magenta]" ) new_block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( @@ -270,6 +271,10 @@ async def add_stake_multiple_extrinsic( total_staking_rao = sum( [amount.rao if amount is not None else 0 for amount in new_amounts] ) + if old_balance is None: + old_balance = await subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=block_hash + ) if total_staking_rao == 0: # Staking all to the first wallet. if old_balance.rao > 1000: @@ -305,7 +310,8 @@ async def add_stake_multiple_extrinsic( # Check enough to stake if staking_balance > old_balance: logging.error( - f":cross_mark: [red]Not enough balance[/red]: [green]{old_balance}[/green] to stake: [blue]{staking_balance}[/blue] from wallet: [white]{wallet.name}[/white]" + f":cross_mark: [red]Not enough balance[/red]: [green]{old_balance}[/green] to stake: " + f"[blue]{staking_balance}[/blue] from wallet: [white]{wallet.name}[/white]" ) continue @@ -328,9 +334,7 @@ async def add_stake_multiple_extrinsic( if idx < len(hotkey_ss58s) - 1: # Wait for tx rate limit. tx_query = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="TxRateLimit", - block_hash=block_hash, + module="SubtensorModule", storage_function="TxRateLimit" ) tx_rate_limit_blocks: int = tx_query if tx_rate_limit_blocks > 0: @@ -391,7 +395,8 @@ async def add_stake_multiple_extrinsic( if successful_stakes != 0: logging.info( - f":satellite: [magenta]Checking Balance on:[/magenta] ([blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + f":satellite: [magenta]Checking Balance on:[/magenta] ([blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" ) new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) logging.info( diff --git a/bittensor/core/extrinsics/asyncex/transfer.py b/bittensor/core/extrinsics/asyncex/transfer.py index 68b31a1c20..c86a29976e 100644 --- a/bittensor/core/extrinsics/asyncex/transfer.py +++ b/bittensor/core/extrinsics/asyncex/transfer.py @@ -68,7 +68,7 @@ async def _do_transfer( async def transfer_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", - destination: str, + dest: str, amount: "Balance", transfer_all: bool = False, wait_for_inclusion: bool = True, @@ -80,7 +80,7 @@ async def transfer_extrinsic( Args: subtensor (bittensor.core.async_subtensor.AsyncSubtensor): initialized AsyncSubtensor object used for transfer wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. - destination (str): Destination public key address (ss58_address or ed25519) of recipient. + dest (str): Destination public key address (ss58_address or ed25519) of recipient. amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. transfer_all (bool): Whether to transfer all funds from this wallet to the destination address. wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning `True`, or returns @@ -93,6 +93,7 @@ async def transfer_extrinsic( 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`, regardless of its inclusion. """ + destination = dest # Validate destination address. if not is_valid_bittensor_address_or_public_key(destination): logging.error( @@ -158,7 +159,7 @@ async def transfer_extrinsic( explorer_urls = get_explorer_url_for_network( subtensor.network, block_hash, NETWORK_EXPLORER_MAP ) - if explorer_urls != {} and explorer_urls: + if explorer_urls: logging.info( f"[green]Opentensor Explorer Link: {explorer_urls.get('opentensor')}[/green]" ) diff --git a/bittensor/core/extrinsics/asyncex/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index 381b54f1d3..fd57578bab 100644 --- a/bittensor/core/extrinsics/asyncex/unstaking.py +++ b/bittensor/core/extrinsics/asyncex/unstaking.py @@ -2,7 +2,7 @@ from typing import Union, Optional, TYPE_CHECKING from bittensor.core.errors import StakeError, NotRegisteredError -from bittensor.utils import format_error_message, unlock_key +from bittensor.utils import unlock_key from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -18,18 +18,19 @@ async def _check_threshold_amount( Checks if the remaining stake balance is above the minimum required stake threshold. Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): Subtensor instance. stake_balance (bittensor.utils.balance.Balance): the balance to check for threshold limits. Returns: - success (bool): ``true`` if the unstaking is above the threshold or 0, or ``false`` if the unstaking is below - the threshold, but not 0. + success (bool): `True` if the unstaking is above the threshold or 0, or `False` if the unstaking is below the + threshold, but not 0. """ min_req_stake: Balance = await subtensor.get_minimum_required_stake() if min_req_stake > stake_balance > 0: logging.warning( - f":cross_mark: [yellow]Remaining stake balance of {stake_balance} less than minimum of {min_req_stake} TAO[/yellow]" + f":cross_mark: [yellow]Remaining stake balance of {stake_balance} less than minimum of " + f"{min_req_stake} TAO[/yellow]" ) return False else: @@ -50,8 +51,8 @@ async def _do_unstake( wallet (bittensor_wallet.Wallet): Wallet object that can sign the extrinsic. hotkey_ss58 (str): Hotkey ``ss58`` address to unstake from. amount (bittensor.utils.balance.Balance): Amount to unstake. - wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. - wait_for_finalization (bool): If ``true``, waits for finalization before returning. + wait_for_inclusion (bool): If ``True``, waits for inclusion before returning. + wait_for_finalization (bool): If ``True``, waits for finalization before returning. Returns: success (bool): ``True`` if the extrinsic was successful. @@ -65,22 +66,13 @@ async def _do_unstake( call_function="remove_stake", call_params={"hotkey": hotkey_ss58, "amount_unstaked": amount.rao}, ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic=extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + success, err_msg = await subtensor.sign_and_send_extrinsic( + call, wallet, wait_for_inclusion, wait_for_finalization ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - if await response.is_success: - return True - - raise StakeError(format_error_message(await response.error_message)) + if success: + return success + else: + raise StakeError(err_msg) async def __do_remove_stake_single( @@ -98,14 +90,14 @@ async def __do_remove_stake_single( wallet (bittensor_wallet.Wallet): Bittensor wallet object. hotkey_ss58 (str): Hotkey address to unstake from. amount (bittensor.utils.balance.Balance): Amount to unstake as Bittensor balance object. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or - returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_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. + ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for - finalization / inclusion, the response is ``true``. + 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``. Raises: bittensor.core.errors.StakeError: If the extrinsic fails to be finalized or included in the block. @@ -140,19 +132,19 @@ async def unstake_extrinsic( """Removes stake into the wallet coldkey from the specified hotkey ``uid``. Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): AsyncSubtensor instance. wallet (bittensor_wallet.Wallet): Bittensor wallet object. hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey to unstake from. By default, the wallet hotkey is used. amount (Union[Balance, float]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or - returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_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. + ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for - finalization / inclusion, the response is ``true``. + 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``. """ # Decrypt keys, if not (unlock := unlock_key(wallet)).success: @@ -206,7 +198,8 @@ async def unstake_extrinsic( try: logging.info( - f":satellite: [magenta]Unstaking from chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + f":satellite: [magenta]Unstaking from chain:[/magenta] [blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" ) staking_response: bool = await __do_remove_stake_single( subtensor=subtensor, @@ -225,7 +218,8 @@ async def unstake_extrinsic( logging.success(":white_heavy_check_mark: [green]Finalized[/green]") logging.info( - f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" ) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( @@ -238,7 +232,7 @@ async def unstake_extrinsic( block_hash=block_hash, ), ) - logging.info(f"Balance:") + logging.info("Balance:") logging.info( f"\t\t[blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" ) @@ -257,7 +251,7 @@ async def unstake_extrinsic( ) return False except StakeError as e: - logging.error(":cross_mark: [red]Stake Error: {}[/red]".format(e)) + logging.error(f":cross_mark: [red]Stake Error: {e}[/red]") return False @@ -276,14 +270,14 @@ async def unstake_multiple_extrinsic( wallet (bittensor_wallet.Wallet): The wallet with the coldkey to unstake to. hotkey_ss58s (List[str]): List of hotkeys to unstake from. amounts (List[Union[Balance, float]]): List of amounts to unstake. If ``None``, unstake all. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or - returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_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. + ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or included in the block. Flag is ``true`` if any - wallet was unstaked. If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. Flag is ``True`` if any + wallet was unstaked. If we did not wait for finalization / inclusion, the response is ``True``. """ if not isinstance(hotkey_ss58s, list) or not all( isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s @@ -321,8 +315,6 @@ async def unstake_multiple_extrinsic( logging.error(unlock.message) return False - old_stakes = [] - own_hotkeys = [] logging.info( f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" ) diff --git a/bittensor/core/extrinsics/asyncex/weights.py b/bittensor/core/extrinsics/asyncex/weights.py index 4a0c9e14c3..cb9fe16798 100644 --- a/bittensor/core/extrinsics/asyncex/weights.py +++ b/bittensor/core/extrinsics/asyncex/weights.py @@ -14,6 +14,42 @@ from bittensor_wallet import Wallet from bittensor.core.async_subtensor import AsyncSubtensor from bittensor.utils.registration import torch + from scalecodec.types import GenericCall + + +async def sign_and_send_with_nonce( + subtensor: "AsyncSubtensor", + call: "GenericCall", + wallet: "Wallet", + wait_for_inclusion: bool, + wait_for_finalization: bool, + period: Optional[int] = None, +): + """ + Signs an extrinsic call with the wallet hotkey, adding an optional era for period + """ + next_nonce = await subtensor.substrate.get_account_next_index( + wallet.hotkey.ss58_address + ) + + extrinsic_data = {"call": call, "keypair": wallet.hotkey, "nonce": next_nonce} + if period is not None: + extrinsic_data["era"] = {"period": period} + + extrinsic = await subtensor.substrate.create_signed_extrinsic(**extrinsic_data) + response = await subtensor.substrate.submit_extrinsic( + 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 + + if await response.is_success: + return True, None + + return False, format_error_message(await response.error_message) async def _do_commit_weights( @@ -29,7 +65,8 @@ async def _do_commit_weights( This method constructs and submits the transaction, handling retries and blockchain communication. Args: - subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for blockchain interaction. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): 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. @@ -40,7 +77,7 @@ async def _do_commit_weights( 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. + verifiable record of the neuron's weight distribution at a specific point in time. """ call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -50,30 +87,10 @@ async def _do_commit_weights( "commit_hash": commit_hash, }, ) - - next_nonce = await subtensor.substrate.get_account_next_index( - wallet.hotkey.ss58_address + return await sign_and_send_with_nonce( + subtensor, call, wallet, wait_for_inclusion, wait_for_finalization ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - nonce=next_nonce, - ) - response = await subtensor.substrate.submit_extrinsic( - 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 - - if await response.is_success: - return True, None - - return False, format_error_message(response.error_message) - async def commit_weights_extrinsic( subtensor: "AsyncSubtensor", @@ -88,7 +105,8 @@ async def commit_weights_extrinsic( This function is a wrapper around the `do_commit_weights` method. Args: - subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for blockchain interaction. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): 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. @@ -100,7 +118,7 @@ async def commit_weights_extrinsic( 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. + error handling and user interaction when required. """ success, error_message = await _do_commit_weights( @@ -137,7 +155,8 @@ async def _do_reveal_weights( This method constructs and submits the transaction, handling retries and blockchain communication. Args: - subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for blockchain interaction. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for blockchain + interaction. wallet (bittensor_wallet.Wallet): The wallet associated with the neuron revealing the weights. netuid (int): The unique identifier of the subnet. uids (list[int]): List of neuron UIDs for which weights are being revealed. @@ -165,27 +184,9 @@ async def _do_reveal_weights( "version_key": version_key, }, ) - next_nonce = await subtensor.substrate.get_account_next_index( - wallet.hotkey.ss58_address + return await sign_and_send_with_nonce( + subtensor, call, wallet, wait_for_inclusion, wait_for_finalization ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - nonce=next_nonce, - ) - response = await subtensor.substrate.submit_extrinsic( - 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 - - if await response.is_success: - return True, None - - return False, await response.error_message async def reveal_weights_extrinsic( @@ -204,7 +205,8 @@ async def reveal_weights_extrinsic( This function is a wrapper around the `_do_reveal_weights` method. Args: - subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for blockchain interaction. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for blockchain + interaction. wallet (bittensor_wallet.Wallet): The wallet associated with the neuron revealing the weights. netuid (int): The unique identifier of the subnet. uids (list[int]): List of neuron UIDs for which weights are being revealed. @@ -219,7 +221,7 @@ async def reveal_weights_extrinsic( describing the success or potential error. This function provides a user-friendly interface for revealing weights on the Bittensor blockchain, ensuring proper - error handling and user interaction when required. + error handling and user interaction when required. """ success, error_message = await _do_reveal_weights( @@ -288,31 +290,9 @@ async def _do_set_weights( "version_key": version_key, }, ) - - next_nonce = await subtensor.substrate.get_account_next_index( - wallet.hotkey.ss58_address - ) - - # 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": period}, - nonce=next_nonce, + return await sign_and_send_with_nonce( + subtensor, call, wallet, wait_for_inclusion, wait_for_finalization, period ) - response = await subtensor.substrate.submit_extrinsic( - 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." - - if await response.is_success: - return True, "Successfully set weights." - - return False, format_error_message(response.error_message) async def set_weights_extrinsic( @@ -335,14 +315,14 @@ async def set_weights_extrinsic( 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_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. + ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or included in the block. If we did not wait for - finalization / inclusion, the response is ``true``. + success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for + finalization / inclusion, the response is ``True``. """ # First convert types. if isinstance(uids, list): diff --git a/bittensor/core/extrinsics/commit_reveal.py b/bittensor/core/extrinsics/commit_reveal.py index eac792f897..9f466a5e29 100644 --- a/bittensor/core/extrinsics/commit_reveal.py +++ b/bittensor/core/extrinsics/commit_reveal.py @@ -1,15 +1,14 @@ """This module provides sync functionality for commit reveal in the Bittensor network.""" -from typing import Union, TYPE_CHECKING +from typing import Union, TYPE_CHECKING, Optional +from bittensor_commit_reveal import get_encrypted_commit import numpy as np from numpy.typing import NDArray -from bittensor.core.extrinsics.asyncex.commit_reveal import ( - commit_reveal_v3_extrinsic as async_commit_reveal_v3_extrinsic, -) from bittensor.core.settings import version_as_int -from bittensor.utils import execute_coroutine +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -17,6 +16,51 @@ from bittensor.utils.registration import torch +def _do_commit_reveal_v3( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + commit: bytes, + reveal_round: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, Optional[str]]: + """ + Executes the commit-reveal phase 3 for a given netuid and commit, and optionally waits for extrinsic inclusion or + finalization. + + Arguments: + subtensor: An instance of the Subtensor class. + wallet: Wallet An instance of the Wallet class containing the user's keypair. + netuid: int The network unique identifier. + commit bytes The commit data in bytes format. + reveal_round: int The round number for the reveal phase. + wait_for_inclusion: bool, optional Flag indicating whether to wait for the extrinsic to be included in a block. + wait_for_finalization: bool, optional Flag indicating whether to wait for the extrinsic to be finalized. + + Returns: + A tuple where the first element is a boolean indicating success or failure, and the second element is an + optional string containing error message if any. + """ + logging.info( + f"Committing weights hash [blue]{commit.hex()}[/blue] for subnet #[blue]{netuid}[/blue] with " + f"reveal round [blue]{reveal_round}[/blue]..." + ) + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_crv3_weights", + call_params={ + "netuid": netuid, + "commit": commit, + "reveal_round": reveal_round, + }, + ) + return subtensor.sign_and_send_extrinsic( + call, wallet, wait_for_inclusion, wait_for_finalization, sign_with="hotkey" + ) + + def commit_reveal_v3_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -27,16 +71,72 @@ def commit_reveal_v3_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, ) -> tuple[bool, str]: - return execute_coroutine( - coroutine=async_commit_reveal_v3_extrinsic( - subtensor=subtensor.async_subtensor, - wallet=wallet, - netuid=netuid, + """ + Commits and reveals weights for given subtensor and wallet with provided uids and weights. + + Arguments: + subtensor: The Subtensor instance. + wallet: The wallet to use for committing and revealing. + netuid: The id of the network. + uids: The uids to commit. + weights: The weights associated with the uids. + version_key: The version key to use for committing and revealing. Default is version_as_int. + wait_for_inclusion: Whether to wait for the inclusion of the transaction. Default is False. + wait_for_finalization: Whether to wait for the finalization of the transaction. Default is False. + + Returns: + tuple[bool, str]: A tuple where the first element is a boolean indicating success or failure, and the second + element is a message associated with the result + """ + try: + # Convert uids and weights + if isinstance(uids, list): + uids = np.array(uids, dtype=np.int64) + if isinstance(weights, list): + weights = np.array(weights, dtype=np.float32) + + # Reformat and normalize. + uids, weights = convert_weights_and_uids_for_emit(uids, weights) + + current_block = subtensor.get_current_block() + subnet_hyperparameters = subtensor.get_subnet_hyperparameters( + netuid, block=current_block + ) + tempo = subnet_hyperparameters.tempo + subnet_reveal_period_epochs = ( + subnet_hyperparameters.commit_reveal_weights_interval + ) + + # Encrypt `commit_hash` with t-lock and `get reveal_round` + commit_for_reveal, reveal_round = get_encrypted_commit( uids=uids, weights=weights, version_key=version_key, + tempo=tempo, + current_block=current_block, + netuid=netuid, + subnet_reveal_period_epochs=subnet_reveal_period_epochs, + ) + + success, message = _do_commit_reveal_v3( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + commit=commit_for_reveal, + reveal_round=reveal_round, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - ), - event_loop=subtensor.event_loop, - ) + ) + + if success is not True: + logging.error(message) + return False, message + + logging.success( + f"[green]Finalized![/green] Weights commited with reveal round [blue]{reveal_round}[/blue]." + ) + return True, f"reveal_round:{reveal_round}" + + except Exception as e: + logging.error(f":cross_mark: [red]Failed. Error:[/red] {e}") + return False, str(e) diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index bd98f32ecc..c92d8b9529 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -1,18 +1,79 @@ """Module sync commit weights and reveal weights extrinsic.""" -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional -from bittensor.core.extrinsics.asyncex.weights import ( - reveal_weights_extrinsic as async_reveal_weights_extrinsic, - commit_weights_extrinsic as async_commit_weights_extrinsic, -) -from bittensor.utils import execute_coroutine +from bittensor.utils import format_error_message +from bittensor.utils.btlogging import logging if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.subtensor import Subtensor +def sign_and_send_with_nonce( + subtensor: "Subtensor", call, wallet, wait_for_inclusion, wait_for_finalization +): + next_nonce = subtensor.substrate.get_account_next_index(wallet.hotkey.ss58_address) + extrinsic = subtensor.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + nonce=next_nonce, + ) + response = subtensor.substrate.submit_extrinsic( + 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 + + if response.is_success: + return True, None + + return False, format_error_message(response.error_message) + + +def _do_commit_weights( + subtensor: "Subtensor", + 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 = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_weights", + call_params={ + "netuid": netuid, + "commit_hash": commit_hash, + }, + ) + + return sign_and_send_with_nonce( + subtensor, call, wallet, wait_for_inclusion, wait_for_finalization + ) + + def commit_weights_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -21,16 +82,90 @@ def commit_weights_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, ) -> tuple[bool, str]: - return execute_coroutine( - coroutine=async_commit_weights_extrinsic( - subtensor=subtensor.async_subtensor, - wallet=wallet, - netuid=netuid, - commit_hash=commit_hash, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), - event_loop=subtensor.event_loop, + """ + Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. + This function is a wrapper around the `do_commit_weights` method. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for blockchain interaction. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the weights. + netuid (int): The unique identifier of the subnet. + commit_hash (str): The hash of the neuron's weights to be committed. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + + Returns: + tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string + value describing the success or potential error. + + This function provides a user-friendly interface for committing weights to the Bittensor blockchain, ensuring proper + error handling and user interaction when required. + """ + + success, error_message = _do_commit_weights( + 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 + + logging.error(f"Failed to commit weights: {error_message}") + return False, error_message + + +def _do_reveal_weights( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + uids: list[int], + values: list[int], + salt: list[int], + version_key: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, Optional[dict]]: + """ + Internal method to send a transaction to the Bittensor blockchain, revealing the weights for a specific subnet. + This method constructs and submits the transaction, handling retries and blockchain communication. + + Args: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for blockchain interaction. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron revealing the weights. + netuid (int): The unique identifier of the subnet. + uids (list[int]): List of neuron UIDs for which weights are being revealed. + values (list[int]): List of weight values corresponding to each UID. + salt (list[int]): List of salt values corresponding to the hash function. + version_key (int): Version key for compatibility with the network. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + + Returns: + tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + + This method ensures that the weight revelation is securely recorded on the Bittensor blockchain, providing + transparency and accountability for the neuron's weight distribution. + """ + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="reveal_weights", + call_params={ + "netuid": netuid, + "uids": uids, + "values": values, + "salt": salt, + "version_key": version_key, + }, + ) + return sign_and_send_with_nonce( + subtensor, call, wallet, wait_for_inclusion, wait_for_finalization ) @@ -45,17 +180,46 @@ def reveal_weights_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, ) -> tuple[bool, str]: - return execute_coroutine( - coroutine=async_reveal_weights_extrinsic( - subtensor=subtensor.async_subtensor, - wallet=wallet, - netuid=netuid, - uids=uids, - weights=weights, - salt=salt, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), - event_loop=subtensor.event_loop, + """ + Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. + This function is a wrapper around the `_do_reveal_weights` method. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for blockchain interaction. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron revealing the weights. + netuid (int): The unique identifier of the subnet. + uids (list[int]): List of neuron UIDs for which weights are being revealed. + weights (list[int]): List of weight values corresponding to each UID. + salt (list[int]): List of salt values corresponding to the hash function. + version_key (int): Version key for compatibility with the network. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + + Returns: + tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string value + describing the success or potential error. + + This function provides a user-friendly interface for revealing weights on the Bittensor blockchain, ensuring proper + error handling and user interaction when required. + """ + + success, error_message = _do_reveal_weights( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + uids=uids, + values=weights, + salt=salt, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) + + if success: + success_message = "Successfully revealed weights." + logging.info(success_message) + return True, success_message + + error_message = format_error_message(error_message) + logging.error(f"Failed to reveal weights: {error_message}") + return False, error_message diff --git a/bittensor/core/extrinsics/registration.py b/bittensor/core/extrinsics/registration.py index 5fd231fa35..d5211ee199 100644 --- a/bittensor/core/extrinsics/registration.py +++ b/bittensor/core/extrinsics/registration.py @@ -6,18 +6,58 @@ - burned_register_extrinsic: Registers the wallet to chain by recycling TAO. """ -from typing import Union, Optional, TYPE_CHECKING +import time +from typing import Optional, Union, TYPE_CHECKING -from bittensor.core.extrinsics.asyncex.registration import ( - burned_register_extrinsic as async_burned_register_extrinsic, - register_extrinsic as async_register_extrinsic, -) -from bittensor.utils import execute_coroutine +from bittensor.utils import unlock_key +from bittensor.utils.btlogging import logging +from bittensor.utils.registration import create_pow, log_no_torch_error, torch -# For annotation and lazy import purposes if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.subtensor import Subtensor + from bittensor.utils.registration.pow import POWSolution + + +def _do_burned_register( + subtensor: "Subtensor", + netuid: int, + wallet: "Wallet", + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Performs a burned register extrinsic call to the Subtensor chain. + + This method sends a registration transaction to the Subtensor blockchain using the burned register mechanism. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. + netuid (int): The network unique identifier to register on. + wallet (bittensor_wallet.Wallet): The wallet to be registered. + wait_for_inclusion (bool): Whether to wait for the transaction to be included in a block. Default is False. + wait_for_finalization (bool): Whether to wait for the transaction to be finalized. Default is True. + + Returns: + Tuple[bool, Optional[str]]: A tuple containing a boolean indicating success or failure, and an optional error + message. + """ + + # create extrinsic call + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + }, + ) + return subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) def burned_register_extrinsic( @@ -27,15 +67,127 @@ def burned_register_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> bool: - return execute_coroutine( - coroutine=async_burned_register_extrinsic( - subtensor=subtensor.async_subtensor, - wallet=wallet, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), - event_loop=subtensor.event_loop, + """Registers the wallet to chain by recycling TAO. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. + wallet (bittensor.wallet): Bittensor wallet object. + netuid (int): The ``netuid`` of the subnet to register on. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``True``, or + returns ``False`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + + Returns: + success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for + finalization / inclusion, the response is ``True``. + """ + block = subtensor.get_current_block() + if not subtensor.subnet_exists(netuid, block=block): + logging.error( + f":cross_mark: [red]Failed error:[/red] subnet [blue]{netuid}[/blue] does not exist." + ) + return False + + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + logging.info( + f":satellite: [magenta]Checking Account on subnet[/magenta] [blue]{netuid}[/blue][magenta] ...[/magenta]" + ) + neuron = subtensor.get_neuron_for_pubkey_and_subnet( + wallet.hotkey.ss58_address, netuid=netuid, block=block + ) + + old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address, block=block) + + if not neuron.is_null: + logging.info(":white_heavy_check_mark: [green]Already Registered[/green]") + logging.info(f"\t\tuid: [blue]{neuron.uid}[/blue]") + logging.info(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") + logging.info(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") + logging.info(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") + return True + + recycle_amount = subtensor.recycle(netuid=netuid, block=block) + logging.debug(":satellite: [magenta]Recycling TAO for Registration...[/magenta]") + logging.info(f"Recycling {recycle_amount} to register on subnet:{netuid}") + + success, err_msg = _do_burned_register( + subtensor=subtensor, + netuid=netuid, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not success: + logging.error(f":cross_mark: [red]Failed error:[/red] {err_msg}") + time.sleep(0.5) + return False + # Successful registration, final check for neuron and pubkey + else: + logging.info(":satellite: [magenta]Checking Balance...[/magenta]") + block = subtensor.get_current_block() + new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address, block=block) + + logging.info( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + is_registered = subtensor.is_hotkey_registered( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address, block=block + ) + if is_registered: + logging.info(":white_heavy_check_mark: [green]Registered[/green]") + return True + else: + # neuron not found, try again + logging.error(":cross_mark: [red]Unknown error. Neuron not found.[/red]") + return False + + +def _do_pow_register( + subtensor: "Subtensor", + netuid: int, + wallet: "Wallet", + pow_result: "POWSolution", + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, +) -> tuple[bool, Optional[str]]: + """Sends a (POW) register extrinsic to the chain. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): The subtensor to send the extrinsic to. + netuid (int): The subnet to register on. + wallet (bittensor.wallet): The wallet to register. + pow_result (POWSolution): The PoW result to register. + wait_for_inclusion (bool): If ``True``, waits for the extrinsic to be included in a block. Default to `False`. + wait_for_finalization (bool): If ``True``, waits for the extrinsic to be finalized. Default to `True`. + + Returns: + success (bool): ``True`` if the extrinsic was included in a block. + error (Optional[str]): ``None`` on success or not waiting for inclusion/finalization, otherwise the error + message. + """ + # create extrinsic call + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="register", + call_params={ + "netuid": netuid, + "block_number": pow_result.block_number, + "nonce": pow_result.nonce, + "work": [int(byte_) for byte_ in pow_result.seal], + "hotkey": wallet.hotkey.ss58_address, + "coldkey": wallet.coldkeypub.ss58_address, + }, + ) + return subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) @@ -54,21 +206,168 @@ def register_extrinsic( update_interval: Optional[int] = None, log_verbose: bool = False, ) -> bool: - return execute_coroutine( - coroutine=async_register_extrinsic( - subtensor=subtensor.async_subtensor, - wallet=wallet, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - max_allowed_attempts=max_allowed_attempts, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ), - event_loop=subtensor.event_loop, + """Registers the wallet to the chain. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): Subtensor object to use for chain interactions + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + netuid (int): The ``netuid`` of the subnet to register on. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning `True`, or returns + `False` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + max_allowed_attempts (int): Maximum number of attempts to register the wallet. + output_in_place (bool): Whether the POW solving should be outputted to the console as it goes along. + cuda (bool): If `True`, the wallet should be registered using CUDA device(s). + dev_id: The CUDA device id to use, or a list of device ids. + tpb: The number of threads per block (CUDA). + num_processes: The number of processes to use to register. + update_interval: The number of nonces to solve between updates. + log_verbose: If `True`, the registration process will log more information. + + Returns: + `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the + response is `True`. + """ + + logging.debug("[magenta]Checking subnet status... [/magenta]") + block = subtensor.get_current_block() + if not subtensor.subnet_exists(netuid, block=block): + logging.error( + f":cross_mark: [red]Failed error:[/red] subnet [blue]{netuid}[/blue] does not exist." + ) + return False + + logging.info( + f":satellite: [magenta]Checking Account on subnet[/magenta] [blue]{netuid}[/blue] [magenta]...[/magenta]" + ) + neuron = subtensor.get_neuron_for_pubkey_and_subnet( + hotkey_ss58=wallet.hotkey.ss58_address, netuid=netuid, block=block ) + + if not neuron.is_null: + logging.info(":white_heavy_check_mark: [green]Already Registered[/green]") + logging.info(f"\t\tuid: [blue]{neuron.uid}[/blue]") + logging.info(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") + logging.info(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") + logging.info(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") + return True + + logging.debug( + f"Registration hotkey: {wallet.hotkey.ss58_address}, Public coldkey: " + f"{wallet.coldkey.ss58_address} in the network: {subtensor.network}." + ) + + if not torch: + log_no_torch_error() + return False + + # Attempt rolling registration. + attempts = 1 + + while True: + logging.info( + f":satellite: [magenta]Registering...[/magenta] [blue]({attempts}/{max_allowed_attempts})[/blue]" + ) + # Solve latest POW. + if cuda: + if not torch.cuda.is_available(): + return False + + pow_result = create_pow( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + output_in_place=output_in_place, + cuda=cuda, + dev_id=dev_id, + tpb=tpb, + num_processes=num_processes, + update_interval=update_interval, + log_verbose=log_verbose, + ) + else: + pow_result = create_pow( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + output_in_place=output_in_place, + cuda=cuda, + num_processes=num_processes, + update_interval=update_interval, + log_verbose=log_verbose, + ) + + # pow failed + if not pow_result: + # might be registered already on this subnet + is_registered = subtensor.is_hotkey_registered( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address + ) + if is_registered: + logging.error( + f":white_heavy_check_mark: [green]Already registered on netuid:[/green] [blue]{netuid}[/blue]" + ) + return True + + # pow successful, proceed to submit pow to chain for registration + else: + logging.info(":satellite: [magenta]Submitting POW...[/magenta]") + # check if pow result is still valid + while not pow_result.is_stale(subtensor=subtensor): + result: tuple[bool, Optional[str]] = _do_pow_register( + subtensor=subtensor, + netuid=netuid, + wallet=wallet, + pow_result=pow_result, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + success, err_msg = result + if not success: + # Look error here + # https://github.com/opentensor/subtensor/blob/development/pallets/subtensor/src/errors.rs + + if "HotKeyAlreadyRegisteredInSubNet" in err_msg: + logging.info( + f":white_heavy_check_mark: [green]Already Registered on subnet:[/green] " + f"[blue]{netuid}[/blue]." + ) + return True + logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}") + time.sleep(0.5) + + # Successful registration, final check for neuron and pubkey + if success: + logging.info(":satellite: Checking Registration status...") + is_registered = subtensor.is_hotkey_registered( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address + ) + if is_registered: + logging.success( + ":white_heavy_check_mark: [green]Registered[/green]" + ) + return True + else: + # neuron not found, try again + logging.error( + ":cross_mark: [red]Unknown error. Neuron not found.[/red]" + ) + continue + else: + # Exited loop because pow is no longer valid. + logging.error("[red]POW is stale.[/red]") + # Try again. + + if attempts < max_allowed_attempts: + # Failed registration, retry pow + attempts += 1 + logging.error( + f":satellite: [magenta]Failed registration, retrying pow ...[/magenta] " + f"[blue]({attempts}/{max_allowed_attempts})[/blue]" + ) + else: + # Failed to register after max attempts. + logging.error("[red]No more attempts.[/red]") + return False diff --git a/bittensor/core/extrinsics/root.py b/bittensor/core/extrinsics/root.py index a0312a4211..bf49e8023a 100644 --- a/bittensor/core/extrinsics/root.py +++ b/bittensor/core/extrinsics/root.py @@ -1,37 +1,193 @@ +import time from typing import Union, TYPE_CHECKING import numpy as np from numpy.typing import NDArray -from bittensor.core.extrinsics.asyncex.root import ( - root_register_extrinsic as async_root_register_extrinsic, - set_root_weights_extrinsic as async_set_root_weights_extrinsic, +from bittensor.core.errors import SubstrateRequestException +from bittensor.utils import ( + u16_normalized_float, + format_error_message, + unlock_key, + torch, +) +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import ( + normalize_max_weight, + convert_weights_and_uids_for_emit, ) -from bittensor.utils import execute_coroutine -from bittensor.utils.registration import torch if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.subtensor import Subtensor +def _get_limits(subtensor: "Subtensor") -> 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 (Subtensor): 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 = subtensor.get_hyperparameter("MinAllowedWeights", netuid=0) + mwl = subtensor.get_hyperparameter("MaxWeightsLimit", netuid=0) + min_allowed_weights = int(maw) + max_weight_limit = u16_normalized_float(int(mwl)) + return min_allowed_weights, max_weight_limit + + def root_register_extrinsic( subtensor: "Subtensor", wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> bool: - return execute_coroutine( - coroutine=async_root_register_extrinsic( - subtensor=subtensor.async_subtensor, - wallet=wallet, - netuid=0, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), - event_loop=subtensor.event_loop, + """Registers the wallet to root network. + + Arguments: + subtensor (bittensor.core.subtensor.Subtensor): The Subtensor object + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning `True`, or returns + `False` if the extrinsic fails to enter the block within the timeout. + 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`. + """ + netuid = 0 + + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + logging.debug( + f"Checking if hotkey ([blue]{wallet.hotkey_str}[/blue]) is registered on root." + ) + is_registered = subtensor.is_hotkey_registered( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address + ) + if is_registered: + logging.error( + ":white_heavy_check_mark: [green]Already registered on root network.[/green]" + ) + return True + + logging.info(":satellite: [magenta]Registering to root network...[/magenta]") + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="root_register", + call_params={"hotkey": wallet.hotkey.ss58_address}, + ) + success, err_msg = subtensor.sign_and_send_extrinsic( + call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not success: + logging.error(f":cross_mark: [red]Failed error:[/red] {err_msg}") + time.sleep(0.5) + return False + + # Successful registration, final check for neuron and pubkey + else: + uid = subtensor.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, wallet.hotkey.ss58_address], + ) + if uid is not None: + logging.info( + f":white_heavy_check_mark: [green]Registered with UID[/green] [blue]{uid}[/blue]." + ) + return True + else: + # neuron not found, try again + logging.error(":cross_mark: [red]Unknown error. Neuron not found.[/red]") + return False + + +def _do_set_root_weights( + subtensor: "Subtensor", + wallet: "Wallet", + netuids: Union[NDArray[np.int64], list[int]], + weights: Union[NDArray[np.float32], list[float]], + netuid: int = 0, + version_key: int = 0, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + period: int = 5, +) -> tuple[bool, str]: + """ + Sets the root weights on the Subnet for the given wallet hotkey account. + + This function constructs and submits an extrinsic to set the root weights for the given wallet hotkey account. + It waits for inclusion or finalization of the extrinsic based on the provided parameters. + + Arguments: + subtensor (bittensor.core.subtensor.Subtensor): The Subtensor object used to interact with the + blockchain. + wallet (bittensor_wallet.Wallet): The wallet containing the hotkey and coldkey for the transaction. + netuids (Union[NDArray[np.int64], list[int]]): List of UIDs to set weights for. + weights (Union[NDArray[np.float32], list[float]]): Corresponding weights to set for each UID. + netuid (int): The netuid of the subnet to set weights for. Defaults to 0. + version_key (int, optional): The version key of the validator. Defaults to 0. + wait_for_inclusion (bool, optional): If True, waits for the extrinsic to be included in a block. Defaults to + False. + wait_for_finalization (bool, optional): If True, waits for the extrinsic to be finalized on the chain. Defaults + to False. + period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. + + Returns: + tuple: Returns a tuple containing a boolean indicating success and a message describing the result of the + operation. + """ + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_root_weights", + call_params={ + "dests": netuids, + "weights": weights, + "netuid": netuid, + "version_key": version_key, + "hotkey": wallet.hotkey.ss58_address, + }, ) + next_nonce = subtensor.substrate.get_account_next_index(wallet.hotkey.ss58_address) + + # Period dictates how long the extrinsic will stay as part of waiting pool + extrinsic = subtensor.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.coldkey, + era={"period": period}, + nonce=next_nonce, + ) + response = subtensor.substrate.submit_extrinsic( + 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." + + if response.is_success: + return True, "Successfully set weights." + + return False, format_error_message(response.error_message) + def set_root_weights_extrinsic( subtensor: "Subtensor", @@ -42,15 +198,88 @@ def set_root_weights_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, ) -> bool: - return execute_coroutine( - coroutine=async_set_root_weights_extrinsic( - subtensor=subtensor.async_subtensor, + """Sets the given weights and values on chain for wallet hotkey account. + + Arguments: + subtensor (bittensor.core.subtensor.Subtensor): The Subtensor 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`. + """ + my_uid = subtensor.substrate.query( + "SubtensorModule", "Uids", [0, wallet.hotkey.ss58_address] + ) + + if my_uid is None: + logging.error("Your hotkey is not registered to the root network.") + return False + + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + # First convert types. + if isinstance(netuids, list): + netuids = np.array(netuids, dtype=np.int64) + if isinstance(weights, list): + weights = np.array(weights, dtype=np.float32) + + logging.debug("Fetching weight limits") + min_allowed_weights, max_weight_limit = _get_limits(subtensor) + + # Get non zero values. + non_zero_weight_idx = np.argwhere(weights > 0).squeeze(axis=1) + non_zero_weights = weights[non_zero_weight_idx] + if non_zero_weights.size < min_allowed_weights: + raise ValueError( + "The minimum number of weights required to set weights is {}, got {}".format( + min_allowed_weights, non_zero_weights.size + ) + ) + + # Normalize the weights to max value. + logging.info("Normalizing weights") + formatted_weights = normalize_max_weight(x=weights, limit=max_weight_limit) + logging.info( + f"Raw weights -> Normalized weights: [blue]{weights}[/blue] -> [green]{formatted_weights}[/green]" + ) + + try: + logging.info(":satellite: [magenta]Setting root weights...[magenta]") + weight_uids, weight_vals = convert_weights_and_uids_for_emit(netuids, weights) + + success, error_message = _do_set_root_weights( + subtensor=subtensor, wallet=wallet, - netuids=netuids, - weights=weights, + netuids=weight_uids, + weights=weight_vals, version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - ), - event_loop=subtensor.event_loop, - ) + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True + + if success is True: + logging.info(":white_heavy_check_mark: [green]Finalized[/green]") + return True + else: + fmt_err = error_message + logging.error(f":cross_mark: [red]Failed error:[/red] {fmt_err}") + return False + + except SubstrateRequestException as e: + fmt_err = format_error_message(e) + logging.error(f":cross_mark: [red]Failed error:[/red] {fmt_err}") + return False diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index 8a8d9c82d1..2dff1e61f2 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -1,38 +1,161 @@ from typing import Optional, TYPE_CHECKING -from bittensor.core.extrinsics.asyncex.serving import ( - do_serve_axon as async_do_serve_axon, - serve_axon_extrinsic as async_serve_axon_extrinsic, - publish_metadata as async_publish_metadata, - get_metadata as async_get_metadata, +from bittensor.core.errors import MetadataError +from bittensor.core.settings import version_as_int +from bittensor.utils import ( + format_error_message, + networking as net, + unlock_key, + Certificate, ) -from bittensor.utils import execute_coroutine +from bittensor.utils.btlogging import logging +from bittensor.core.types import AxonServeCallParams if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.axon import Axon from bittensor.core.subtensor import Subtensor - from bittensor.core.types import AxonServeCallParams - from bittensor.utils import Certificate def do_serve_axon( - self: "Subtensor", + subtensor: "Subtensor", wallet: "Wallet", call_params: "AxonServeCallParams", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> tuple[bool, Optional[dict]]: - return execute_coroutine( - coroutine=async_do_serve_axon( - subtensor=self.async_subtensor, - wallet=wallet, - call_params=call_params, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), - event_loop=self.event_loop, + """ + Internal method to submit a serve axon transaction to the Bittensor blockchain. This method creates and submits a + transaction, enabling a neuron's ``Axon`` to serve requests on the network. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance object. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron. + call_params (bittensor.core.types.AxonServeCallParams): Parameters required for the serve axon call. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + + Returns: + tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + + This function is crucial for initializing and announcing a neuron's ``Axon`` service on the network, enhancing the + decentralized computation capabilities of Bittensor. + """ + if call_params.certificate is None: + call_function = "serve_axon" + else: + call_function = "serve_axon_tls" + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=call_params.dict(), ) + extrinsic = subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.hotkey + ) + response = subtensor.substrate.submit_extrinsic( + extrinsic=extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + if wait_for_inclusion or wait_for_finalization: + if response.is_success: + return True, None + + return False, response.error_message + + return True, None + + +def serve_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + ip: str, + port: int, + protocol: int, + netuid: int, + placeholder1: int = 0, + placeholder2: int = 0, + wait_for_inclusion: bool = False, + wait_for_finalization=True, + certificate: Optional[Certificate] = None, +) -> bool: + """Subscribes a Bittensor endpoint to the subtensor chain. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance object. + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + ip (str): Endpoint host port i.e., ``192.122.31.4``. + port (int): Endpoint port number i.e., ``9221``. + protocol (int): An ``int`` representation of the protocol. + netuid (int): The network uid to serve on. + placeholder1 (int): A placeholder for future use. + placeholder2 (int): A placeholder for future use. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``True``, or + returns ``False`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. + Defaults to ``None``. + + 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``. + """ + # Decrypt hotkey + if not (unlock := unlock_key(wallet, "hotkey")).success: + logging.error(unlock.message) + return False + + params = AxonServeCallParams( + **{ + "version": version_as_int, + "ip": net.ip_to_int(ip), + "port": port, + "ip_type": net.ip_version(ip), + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + "coldkey": wallet.coldkeypub.ss58_address, + "protocol": protocol, + "placeholder1": placeholder1, + "placeholder2": placeholder2, + "certificate": certificate, + } + ) + logging.debug("Checking axon ...") + neuron = subtensor.get_neuron_for_pubkey_and_subnet( + wallet.hotkey.ss58_address, netuid=netuid + ) + neuron_up_to_date = not neuron.is_null and params == neuron + if neuron_up_to_date: + logging.debug( + f"Axon already served on: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) " + ) + return True + + logging.debug( + f"Serving axon with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) -> {subtensor.network}:{netuid}" + ) + success, error_message = do_serve_axon( + subtensor=subtensor, + wallet=wallet, + call_params=params, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + ) + + if wait_for_inclusion or wait_for_finalization: + if success is True: + logging.debug( + f"Axon served with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) on {subtensor.network}:{netuid} " + ) + return True + else: + logging.error(f"Failed: {format_error_message(error_message)}") + return False + else: + return True def serve_axon_extrinsic( @@ -43,21 +166,59 @@ def serve_axon_extrinsic( wait_for_finalization: bool = True, certificate: Optional["Certificate"] = None, ) -> bool: - return execute_coroutine( - coroutine=async_serve_axon_extrinsic( - subtensor=subtensor.async_subtensor, - netuid=netuid, - axon=axon, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - certificate=certificate, - ), - event_loop=subtensor.event_loop, + """Serves the axon to the network. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance object. + netuid (int): The ``netuid`` being served on. + axon (bittensor.core.axon.Axon): Axon to serve. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``True``, or + returns ``False`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. + Defaults to ``None``. + + 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``. + """ + if not (unlock := unlock_key(axon.wallet, "hotkey")).success: + logging.error(unlock.message) + return False + external_port = axon.external_port + + # ---- Get external ip ---- + if axon.external_ip is None: + try: + external_ip = net.get_external_ip() + logging.success( + f":white_heavy_check_mark: [green]Found external ip:[/green] [blue]{external_ip}[/blue]" + ) + except Exception as e: + raise ConnectionError( + f"Unable to attain your external ip. Check your internet connection. error: {e}" + ) from e + else: + external_ip = axon.external_ip + + # ---- Subscribe to chain ---- + serve_success = serve_extrinsic( + subtensor=subtensor, + wallet=axon.wallet, + ip=external_ip, + port=external_port, + protocol=4, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + certificate=certificate, ) + return serve_success def publish_metadata( - self: "Subtensor", + subtensor: "Subtensor", wallet: "Wallet", netuid: int, data_type: str, @@ -65,29 +226,69 @@ def publish_metadata( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> bool: - return execute_coroutine( - coroutine=async_publish_metadata( - subtensor=self.async_subtensor, - wallet=wallet, - netuid=netuid, - data_type=data_type, - data=data, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), - event_loop=self.event_loop, + """ + Publishes metadata on the Bittensor network using the specified wallet and network identifier. + + Args: + subtensor (bittensor.subtensor): The subtensor instance representing the Bittensor blockchain connection. + wallet (bittensor.wallet): The wallet object used for authentication in the transaction. + netuid (int): Network UID on which the metadata is to be published. + data_type (str): The data type of the information being submitted. It should be one of the following: + ``'Sha256'``, ``'Blake256'``, ``'Keccak256'``, or ``'Raw0-128'``. This specifies the format or hashing + algorithm used for the data. + data (str): The actual metadata content to be published. This should be formatted or hashed according to the + ``type`` specified. (Note: max ``str`` length is 128 bytes) + wait_for_inclusion (bool, optional): If ``True``, the function will wait for the extrinsic to be included in a + block before returning. Defaults to ``False``. + wait_for_finalization (bool, optional): If ``True``, the function will wait for the extrinsic to be finalized + on the chain before returning. Defaults to ``True``. + + Returns: + bool: ``True`` if the metadata was successfully published (and finalized if specified). ``False`` otherwise. + + Raises: + MetadataError: If there is an error in submitting the extrinsic or if the response from the blockchain indicates + failure. + """ + + if not (unlock := unlock_key(wallet, "hotkey")).success: + logging.error(unlock.message) + return False + + call = subtensor.substrate.compose_call( + call_module="Commitments", + call_function="set_commitment", + call_params={ + "netuid": netuid, + "info": {"fields": [[{f"{data_type}": data}]]}, + }, ) + extrinsic = subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.hotkey + ) + response = subtensor.substrate.submit_extrinsic( + extrinsic=extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True + + if response.is_success: + return True + raise MetadataError(format_error_message(response.error_message)) + def get_metadata( - self: "Subtensor", netuid: int, hotkey: str, block: Optional[int] = None + subtensor: "Subtensor", netuid: int, hotkey: str, block: Optional[int] = None ) -> str: - return execute_coroutine( - coroutine=async_get_metadata( - subtensor=self.async_subtensor, - netuid=netuid, - hotkey=hotkey, - block=block, - ), - event_loop=self.event_loop, + """Fetches metadata from the blockchain for a given hotkey and netuid.""" + commit_data = subtensor.substrate.query( + module="Commitments", + storage_function="CommitmentOf", + params=[netuid, hotkey], + block_hash=subtensor.determine_block_hash(block), ) + return getattr(commit_data, "value", None) diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 913ae29a8b..5e86c9110e 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -1,19 +1,85 @@ """Module sync setting weights extrinsic.""" -from typing import Union, TYPE_CHECKING +from typing import Union, TYPE_CHECKING, Optional import numpy as np from numpy.typing import NDArray -from bittensor.core.extrinsics.asyncex.weights import ( - set_weights_extrinsic as async_set_weights_extrinsic, -) -from bittensor.utils import execute_coroutine -from bittensor.utils.registration import torch +from bittensor.core.settings import version_as_int +from bittensor.utils import format_error_message, weight_utils +from bittensor.utils.btlogging import logging if TYPE_CHECKING: from bittensor.core.subtensor import Subtensor from bittensor_wallet import Wallet + from bittensor.utils.registration import torch + + +def _do_set_weights( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + uids: list[int], + vals: list[int], + version_key: int = version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + period: int = 5, +) -> tuple[bool, Optional[str]]: # (success, error_message) + """ + Internal method to send a transaction to the Bittensor blockchain, setting weights + for specified neurons. This method constructs and submits the transaction, handling + retries and blockchain communication. + + Args: + subtensor (subtensor.core.subtensor.Subtensor): Subtensor instance. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. + uids (List[int]): List of neuron UIDs for which weights are being set. + vals (List[int]): List of weight values corresponding to each UID. + netuid (int): Unique identifier for the network. + version_key (int, optional): Version key for compatibility with the network. + 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. + period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. + + 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 = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_weights", + call_params={ + "dests": uids, + "weights": vals, + "netuid": netuid, + "version_key": version_key, + }, + ) + next_nonce = subtensor.substrate.get_account_next_index(wallet.hotkey.ss58_address) + # Period dictates how long the extrinsic will stay as part of waiting pool + extrinsic = subtensor.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + era={"period": period}, + nonce=next_nonce, + ) + response = subtensor.substrate.submit_extrinsic( + 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." + + if response.is_success: + return True, "Successfully set weights." + + return False, format_error_message(response.error_message) def set_weights_extrinsic( @@ -26,16 +92,62 @@ def set_weights_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, ) -> tuple[bool, str]: - return execute_coroutine( - coroutine=async_set_weights_extrinsic( - subtensor=subtensor.async_subtensor, + """Sets the given weights and values on chain for wallet hotkey account. + + Args: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): Bittensor subtensor object. + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + netuid (int): The ``netuid`` of the subnet to set weights for. + uids (Union[NDArray[np.int64], torch.LongTensor, list]): The ``uint64`` uids of destination neurons. + weights (Union[NDArray[np.float32], torch.FloatTensor, list]): The weights to set. These must be ``float`` s + and correspond to the passed ``uid`` s. + version_key (int): The version key of the validator. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``True``, or + returns ``False`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + + Returns: + 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 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: [magenta]Setting weights on [/magenta][blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + ) + try: + success, error_message = _do_set_weights( + subtensor=subtensor, wallet=wallet, netuid=netuid, - uids=uids, - weights=weights, + uids=weight_uids, + vals=weight_vals, version_key=version_key, - wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - ), - event_loop=subtensor.event_loop, - ) + 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: [green]{message}[/green]") + return True, message + + logging.error(f"[red]Failed[/red] set weights. Error: {error_message}") + return False, error_message + + except Exception as error: + logging.error(f":cross_mark: [red]Failed[/red] set weights. Error: {error}") + return False, str(error) diff --git a/bittensor/core/extrinsics/staking.py b/bittensor/core/extrinsics/staking.py index 7245ce98e9..9b35cda557 100644 --- a/bittensor/core/extrinsics/staking.py +++ b/bittensor/core/extrinsics/staking.py @@ -1,15 +1,14 @@ -from typing import Union, Optional, TYPE_CHECKING +import time +from typing import Union, Optional, TYPE_CHECKING, Sequence -from bittensor.core.extrinsics.asyncex.staking import ( - add_stake_extrinsic as async_add_stake_extrinsic, - add_stake_multiple_extrinsic as async_add_stake_multiple_extrinsic, -) -from bittensor.utils import execute_coroutine +from bittensor.core.errors import StakeError, NotRegisteredError +from bittensor.utils import unlock_key +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.subtensor import Subtensor - from bittensor.utils.balance import Balance def add_stake_extrinsic( @@ -20,17 +19,169 @@ def add_stake_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - return execute_coroutine( - coroutine=async_add_stake_extrinsic( - subtensor=subtensor.async_subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), - event_loop=subtensor.event_loop, + """ + Adds the specified amount of stake to passed hotkey `uid`. + + Arguments: + subtensor: the Subtensor object to use + wallet: Bittensor wallet object. + hotkey_ss58: The `ss58` address of the hotkey account to stake to defaults to the wallet's hotkey. + amount: Amount to stake as Bittensor balance, `None` if staking all. + wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns + `False` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, + or returns `False` if the extrinsic fails to be finalized within the timeout. + + Returns: + success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for + finalization/inclusion, the response is `True`. + """ + + def _check_threshold_amount( + balance: "Balance", + block_hash: str, + min_req_stake: Optional["Balance"] = None, + ) -> tuple[bool, "Balance"]: + """Checks if the new stake balance will be above the minimum required stake threshold.""" + if not min_req_stake: + min_req_stake_ = subtensor.substrate.query( + module="SubtensorModule", + storage_function="NominatorMinRequiredStake", + block_hash=block_hash, + ) + min_req_stake = Balance.from_rao(min_req_stake_) + if min_req_stake > balance: + return False, min_req_stake + return True, min_req_stake + + # Decrypt keys, + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + # Default to wallet's own hotkey if the value is not passed. + if hotkey_ss58 is None: + hotkey_ss58 = wallet.hotkey.ss58_address + + # Flag to indicate if we are using the wallet's own hotkey. + own_hotkey: bool + + logging.info( + f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" ) + old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) + block = subtensor.get_current_block() + + # Get hotkey owner + hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58=hotkey_ss58, block=block) + own_hotkey = wallet.coldkeypub.ss58_address == hotkey_owner + if not own_hotkey: + # This is not the wallet's own hotkey, so we are delegating. + if not subtensor.is_hotkey_delegate(hotkey_ss58, block=block): + logging.debug(f"Hotkey {hotkey_ss58} is not a delegate on the chain.") + return False + + # Get current stake and existential deposit + old_stake = subtensor.get_stake_for_coldkey_and_hotkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + block=block, + ) + existential_deposit = subtensor.get_existential_deposit(block=block) + + # Convert to bittensor.Balance + if amount is None: + # Stake it all. + staking_balance = Balance.from_tao(old_balance.tao) + elif not isinstance(amount, Balance): + staking_balance = Balance.from_tao(amount) + else: + staking_balance = amount + + # Leave existential balance to keep key alive. + if staking_balance > old_balance - existential_deposit: + # If we are staking all, we need to leave at least the existential deposit. + staking_balance = old_balance - existential_deposit + else: + staking_balance = staking_balance + + # Check enough to stake. + if staking_balance > old_balance: + logging.error(":cross_mark: [red]Not enough stake:[/red]") + logging.error(f"\t\tbalance:{old_balance}") + logging.error(f"\t\tamount: {staking_balance}") + logging.error(f"\t\twallet: {wallet.name}") + return False + + # If nominating, we need to check if the new stake balance will be above the minimum required stake threshold. + if not own_hotkey: + new_stake_balance = old_stake + staking_balance + is_above_threshold, threshold = _check_threshold_amount( + new_stake_balance, block_hash=subtensor.get_block_hash(block) + ) + if not is_above_threshold: + logging.error( + f":cross_mark: [red]New stake balance of {new_stake_balance} is below the minimum required " + f"nomination stake threshold {threshold}.[/red]" + ) + return False + + try: + logging.info( + f":satellite: [magenta]Staking to:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + ) + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake", + call_params={"hotkey": hotkey_ss58, "amount_staked": staking_balance.rao}, + ) + staking_response, err_msg = subtensor.sign_and_send_extrinsic( + call, wallet, wait_for_inclusion, wait_for_finalization + ) + if staking_response is True: # If we successfully staked. + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] " + "[magenta]...[/magenta]" + ) + new_block = subtensor.get_current_block() + new_balance = subtensor.get_balance( + wallet.coldkeypub.ss58_address, block=new_block + ) + new_stake = subtensor.get_stake_for_coldkey_and_hotkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + block=new_block, + ) + logging.info("Balance:") + logging.info( + f"[blue]{old_balance}[/blue] :arrow_right: {new_balance}[/green]" + ) + logging.info("Stake:") + logging.info( + f"[blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) + return True + else: + logging.error(":cross_mark: [red]Failed[/red]: Error unknown.") + return False + + # TODO I don't think these are used. Maybe should just catch SubstrateRequestException? + except NotRegisteredError: + logging.error( + ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( + wallet.hotkey_str + ) + ) + return False + except StakeError as e: + logging.error(f":cross_mark: [red]Stake Error: {e}[/red]") + return False def add_stake_multiple_extrinsic( @@ -41,14 +192,200 @@ def add_stake_multiple_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - return execute_coroutine( - coroutine=async_add_stake_multiple_extrinsic( - subtensor=subtensor.async_subtensor, - wallet=wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), - event_loop=subtensor.event_loop, + """Adds stake to each ``hotkey_ss58`` in the list, using each amount, from a common coldkey. + + Arguments: + subtensor: The initialized SubtensorInterface object. + wallet: Bittensor wallet object for the coldkey. + hotkey_ss58s: List of hotkeys to stake to. + amounts: List of amounts to stake. If `None`, stake all to the first hotkey. + wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` + if the extrinsic fails to enter the block within the timeout. + wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, or + returns `False` if the extrinsic fails to be finalized within the timeout. + + Returns: + success: `True` if extrinsic was finalized or included in the block. `True` if any wallet was staked. If we did + not wait for finalization/inclusion, the response is `True`. + """ + + def get_old_stakes(block_hash: str) -> dict[str, Balance]: + calls = [ + ( + subtensor.substrate.create_storage_key( + "SubtensorModule", + "Stake", + [hotkey_ss58, wallet.coldkeypub.ss58_address], + block_hash=block_hash, + ) + ) + for hotkey_ss58 in hotkey_ss58s + ] + batch_call = subtensor.substrate.query_multi(calls, block_hash=block_hash) + results = {} + for item in batch_call: + results.update({item[0].params[0]: Balance.from_rao(item[1] or 0)}) + return results + + if not isinstance(hotkey_ss58s, list) or not all( + isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s + ): + raise TypeError("hotkey_ss58s must be a list of str") + + if len(hotkey_ss58s) == 0: + return True + + if amounts is not None and len(amounts) != len(hotkey_ss58s): + raise ValueError("amounts must be a list of the same length as hotkey_ss58s") + + new_amounts: Sequence[Optional[Balance]] + if amounts is None: + new_amounts = [None] * len(hotkey_ss58s) + else: + new_amounts = [Balance.from_tao(amount) for amount in amounts] + if sum(amount.tao for amount in new_amounts) == 0: + # Staking 0 tao + return True + + # Decrypt keys, + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + logging.info( + f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" ) + block = subtensor.get_current_block() + old_stakes: dict[str, Balance] = get_old_stakes(subtensor.get_block_hash(block)) + + # Remove existential balance to keep key alive. + # Keys must maintain a balance of at least 1000 rao to stay alive. + total_staking_rao = sum( + [amount.rao if amount is not None else 0 for amount in new_amounts] + ) + old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address, block=block) + if total_staking_rao == 0: + # Staking all to the first wallet. + if old_balance.rao > 1000: + old_balance -= Balance.from_rao(1000) + + elif total_staking_rao < 1000: + # Staking less than 1000 rao to the wallets. + pass + else: + # Staking more than 1000 rao to the wallets. + # Reduce the amount to stake to each wallet to keep the balance above 1000 rao. + percent_reduction = 1 - (1000 / total_staking_rao) + new_amounts = [ + Balance.from_tao(amount.tao * percent_reduction) for amount in new_amounts + ] + + successful_stakes = 0 + for idx, (hotkey_ss58, amount) in enumerate(zip(hotkey_ss58s, new_amounts)): + staking_all = False + # Convert to bittensor.Balance + if amount is None: + # Stake it all. + staking_balance = Balance.from_tao(old_balance.tao) + staking_all = True + else: + # Amounts are cast to balance earlier in the function + assert isinstance(amount, Balance) + staking_balance = amount + + # Check enough to stake + if staking_balance > old_balance: + logging.error( + f":cross_mark: [red]Not enough balance[/red]: [green]{old_balance}[/green] to stake: " + f"[blue]{staking_balance}[/blue] from wallet: [white]{wallet.name}[/white]" + ) + continue + + try: + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake", + call_params={ + "hotkey": hotkey_ss58, + "amount_staked": staking_balance.rao, + }, + ) + staking_response, err_msg = subtensor.sign_and_send_extrinsic( + call, wallet, wait_for_inclusion, wait_for_finalization + ) + + if staking_response is True: # If we successfully staked. + # We only wait here if we expect finalization. + + if idx < len(hotkey_ss58s) - 1: + # Wait for tx rate limit. + tx_query = subtensor.substrate.query( + module="SubtensorModule", storage_function="TxRateLimit" + ) + tx_rate_limit_blocks: int = tx_query + if tx_rate_limit_blocks > 0: + logging.error( + f":hourglass: [yellow]Waiting for tx rate limit: [white]{tx_rate_limit_blocks}[/white] " + f"blocks[/yellow]" + ) + # 12 seconds per block + time.sleep(tx_rate_limit_blocks * 12) + + if not wait_for_finalization and not wait_for_inclusion: + old_balance -= staking_balance + successful_stakes += 1 + if staking_all: + # If staked all, no need to continue + break + + continue + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + new_block = subtensor.get_current_block() + new_stake = subtensor.get_stake_for_coldkey_and_hotkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + block=new_block, + ) + new_balance = subtensor.get_balance( + wallet.coldkeypub.ss58_address, block=new_block + ) + logging.info( + "Stake ({}): [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( + hotkey_ss58, old_stakes[hotkey_ss58], new_stake + ) + ) + old_balance = new_balance + successful_stakes += 1 + if staking_all: + # If staked all, no need to continue + break + + else: + logging.error(":cross_mark: [red]Failed[/red]: Error unknown.") + continue + + except NotRegisteredError: + logging.error( + ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( + hotkey_ss58 + ) + ) + continue + except StakeError as e: + logging.error(":cross_mark: [red]Stake Error: {}[/red]".format(e)) + continue + + if successful_stakes != 0: + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] ([blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" + ) + new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) + logging.info( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + return True + + return False diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index fbf6267b19..5257fc347d 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -1,14 +1,67 @@ from typing import Union, TYPE_CHECKING -from bittensor.core.extrinsics.asyncex.transfer import ( - transfer_extrinsic as async_transfer_extrinsic, +from bittensor.core.settings import NETWORK_EXPLORER_MAP +from bittensor.utils.balance import Balance +from bittensor.utils import ( + is_valid_bittensor_address_or_public_key, + unlock_key, + get_explorer_url_for_network, + format_error_message, ) -from bittensor.utils import execute_coroutine +from bittensor.utils.btlogging import logging if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.subtensor import Subtensor - from bittensor.utils.balance import Balance + + +def _do_transfer( + subtensor: "Subtensor", + wallet: "Wallet", + destination: str, + amount: "Balance", + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, str, str]: + """ + Makes transfer from wallet to destination public key address. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): the Subtensor object used for transfer + wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. + destination (str): Destination public key address (ss58_address or ed25519) of recipient. + amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning `True`, or returns + `False` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + + Returns: + success, block hash, formatted error message + """ + call = subtensor.substrate.compose_call( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": destination, "value": amount.rao}, + ) + extrinsic = subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = subtensor.substrate.submit_extrinsic( + 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, "", "Success, extrinsic submitted without waiting." + + # Otherwise continue with finalization. + if response.is_success: + block_hash_ = response.block_hash + return True, block_hash_, "Success with response." + + return False, "", format_error_message(response.error_message) def transfer_extrinsic( @@ -21,16 +74,100 @@ def transfer_extrinsic( wait_for_finalization: bool = False, keep_alive: bool = True, ) -> bool: - return execute_coroutine( - coroutine=async_transfer_extrinsic( - subtensor=subtensor.async_subtensor, - wallet=wallet, - destination=dest, - amount=amount, - transfer_all=transfer_all, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - keep_alive=keep_alive, - ), - event_loop=subtensor.event_loop, + """Transfers funds from this wallet to the destination public key address. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): the Subtensor object used for transfer + wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. + dest (str): Destination public key address (ss58_address or ed25519) of recipient. + amount (bittensor.utils.balance.Balance): Amount to stake as Bittensor balance. + transfer_all (bool): Whether to transfer all funds from this wallet to the destination address. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning `True`, or returns + `False` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + keep_alive (bool): If set, keeps the account alive by keeping the balance above the existential deposit. + + Returns: + success (bool): Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for + finalization / inclusion, the response is `True`, regardless of its inclusion. + """ + destination = dest + # Validate destination address. + if not is_valid_bittensor_address_or_public_key(destination): + logging.error( + f":cross_mark: [red]Invalid destination SS58 address[/red]: {destination}" + ) + return False + logging.info(f"Initiating transfer on network: {subtensor.network}") + # Unlock wallet coldkey. + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + # Check balance. + logging.info( + f":satellite: [magenta]Checking balance and fees on chain [/magenta] [blue]{subtensor.network}[/blue]" + ) + # check existential deposit and fee + logging.debug("Fetching existential and fee") + block = subtensor.get_current_block() + account_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address, block=block) + if not keep_alive: + # Check if the transfer should keep_alive the account + existential_deposit = Balance(0) + else: + existential_deposit = subtensor.get_existential_deposit(block=block) + + fee = subtensor.get_transfer_fee(wallet=wallet, dest=destination, value=amount.rao) + + # Check if we have enough balance. + if transfer_all is True: + amount = account_balance - fee - existential_deposit + if amount < Balance(0): + logging.error("Not enough balance to transfer") + return False + + if account_balance < (amount + fee + existential_deposit): + logging.error(":cross_mark: [red]Not enough balance[/red]") + logging.error(f"\t\tBalance:\t[blue]{account_balance}[/blue]") + logging.error(f"\t\tAmount:\t[blue]{amount}[/blue]") + logging.error(f"\t\tFor fee:\t[blue]{fee}[/blue]") + return False + + logging.info(":satellite: [magenta]Transferring... bool: + """ + Checks if the remaining stake balance is above the minimum required stake threshold. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. + stake_balance (bittensor.utils.balance.Balance): the balance to check for threshold limits. + + Returns: + success (bool): `True` if the unstaking is above the threshold or 0, or `False` if the unstaking is below the + threshold, but not 0. + """ + min_req_stake: Balance = subtensor.get_minimum_required_stake() + + if min_req_stake > stake_balance > 0: + logging.warning( + f":cross_mark: [yellow]Remaining stake balance of {stake_balance} less than minimum of " + f"{min_req_stake} TAO[/yellow]" + ) + return False + else: + return True + + +def _do_unstake( + subtensor: "Subtensor", + wallet: "Wallet", + hotkey_ss58: str, + amount: "Balance", + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> bool: + """Sends an unstake extrinsic to the chain. + + Args: + wallet (bittensor_wallet.Wallet): Wallet object that can sign the extrinsic. + hotkey_ss58 (str): Hotkey ``ss58`` address to unstake from. + amount (bittensor.utils.balance.Balance): Amount to unstake. + wait_for_inclusion (bool): If ``True``, waits for inclusion before returning. + wait_for_finalization (bool): If ``True``, waits for finalization before returning. + + Returns: + success (bool): ``True`` if the extrinsic was successful. + + Raises: + StakeError: If the extrinsic failed. + """ + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={"hotkey": hotkey_ss58, "amount_unstaked": amount.rao}, + ) + success, err_msg = subtensor.sign_and_send_extrinsic( + call, wallet, wait_for_inclusion, wait_for_finalization + ) + if success: + return success + else: + raise StakeError(err_msg) + + +def __do_remove_stake_single( + subtensor: "Subtensor", + wallet: "Wallet", + hotkey_ss58: str, + amount: "Balance", + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> bool: + """ + Executes an unstake call to the chain using the wallet and the amount specified. + + Args: + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + hotkey_ss58 (str): Hotkey address to unstake from. + amount (bittensor.utils.balance.Balance): Amount to unstake as Bittensor balance object. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``True``, or + returns ``False`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + + Returns: + success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for + finalization / inclusion, the response is ``True``. + + Raises: + bittensor.core.errors.StakeError: If the extrinsic fails to be finalized or included in the block. + bittensor.core.errors.NotRegisteredError: If the hotkey is not registered in any subnets. + + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + success = _do_unstake( + subtensor=subtensor, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + amount=amount, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if success: + return True + + def unstake_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -20,17 +127,126 @@ def unstake_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - return execute_coroutine( - coroutine=async_unstake_extrinsic( - subtensor=subtensor.async_subtensor, + """Removes stake into the wallet coldkey from the specified hotkey ``uid``. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey to unstake from. By default, the wallet hotkey + is used. + amount (Union[Balance, float]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``True``, or + returns ``False`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + + Returns: + success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for + finalization / inclusion, the response is ``True``. + """ + # Decrypt keys, + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + if hotkey_ss58 is None: + hotkey_ss58 = wallet.hotkey.ss58_address # Default to wallet's own hotkey. + + logging.info( + f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + ) + block = subtensor.get_current_block() + old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address, block=block) + old_stake = subtensor.get_stake_for_coldkey_and_hotkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + block=block, + ) + hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58, block=block) + own_hotkey: bool = wallet.coldkeypub.ss58_address == hotkey_owner + + # Convert to bittensor.Balance + if amount is None: + # Unstake it all. + unstaking_balance = old_stake + elif not isinstance(amount, Balance): + unstaking_balance = Balance.from_tao(amount) + else: + unstaking_balance = amount + + # Check enough to unstake. + stake_on_uid = old_stake + if unstaking_balance > stake_on_uid: + logging.error( + f":cross_mark: [red]Not enough stake[/red]: [green]{stake_on_uid}[/green] to unstake: " + f"[blue]{unstaking_balance}[/blue] from hotkey: [yellow]{wallet.hotkey_str}[/yellow]" + ) + return False + + # If nomination stake, check threshold. + if not own_hotkey and not _check_threshold_amount( + subtensor=subtensor, stake_balance=(stake_on_uid - unstaking_balance) + ): + logging.warning( + ":warning: [yellow]This action will unstake the entire staked balance![/yellow]" + ) + unstaking_balance = stake_on_uid + + try: + logging.info( + f":satellite: [magenta]Unstaking from chain:[/magenta] [blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" + ) + staking_response: bool = __do_remove_stake_single( + subtensor=subtensor, wallet=wallet, hotkey_ss58=hotkey_ss58, - amount=amount, + amount=unstaking_balance, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - ), - event_loop=subtensor.event_loop, - ) + ) + + if staking_response is True: # If we successfully unstaked. + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" + ) + block = subtensor.get_current_block() + new_balance = subtensor.get_balance( + wallet.coldkeypub.ss58_address, block=block + ) + new_stake = subtensor.get_stake_for_coldkey_and_hotkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + block=block, + ) + logging.info("Balance:") + logging.info( + f"\t\t[blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + logging.info("Stake:") + logging.info( + f"\t\t[blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) + return True + else: + logging.error(":cross_mark: [red]Failed[/red]: Unknown Error.") + return False + + except NotRegisteredError: + logging.error( + f":cross_mark: [red]Hotkey: {wallet.hotkey_str} is not registered.[/red]" + ) + return False + except StakeError as e: + logging.error(f":cross_mark: [red]Stake Error: {e}[/red]") + return False def unstake_multiple_extrinsic( @@ -41,14 +257,182 @@ def unstake_multiple_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - return execute_coroutine( - coroutine=async_unstake_multiple_extrinsic( - subtensor=subtensor.async_subtensor, - wallet=wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), - event_loop=subtensor.event_loop, + """Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. + wallet (bittensor_wallet.Wallet): The wallet with the coldkey to unstake to. + hotkey_ss58s (List[str]): List of hotkeys to unstake from. + amounts (List[Union[Balance, float]]): List of amounts to unstake. If ``None``, unstake all. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``True``, or + returns ``False`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning + ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + + Returns: + success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. Flag is ``True`` if any + wallet was unstaked. If we did not wait for finalization / inclusion, the response is ``True``. + """ + if not isinstance(hotkey_ss58s, list) or not all( + isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s + ): + raise TypeError("hotkey_ss58s must be a list of str") + + if len(hotkey_ss58s) == 0: + return True + + if amounts is not None and len(amounts) != len(hotkey_ss58s): + raise ValueError("amounts must be a list of the same length as hotkey_ss58s") + + if amounts is not None and not all( + isinstance(amount, (Balance, float)) for amount in amounts + ): + raise TypeError( + "amounts must be a [list of bittensor.Balance or float] or None" + ) + + if amounts is None: + amounts = [None] * len(hotkey_ss58s) + else: + # Convert to Balance + amounts = [ + Balance.from_tao(amount) if isinstance(amount, float) else amount + for amount in amounts + ] + + if sum(amount.tao for amount in amounts) == 0: + # Staking 0 tao + return True + + # Unlock coldkey. + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + logging.info( + f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" ) + block = subtensor.get_current_block() + old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address, block=block) + # these calls can probably be sped up with query_multi, but honestly if you're looking for + # concurrency speed, use AsyncSubtensor + old_stakes = [ + subtensor.get_stake_for_coldkey_and_hotkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + block=block, + ) + for hotkey_ss58 in hotkey_ss58s + ] + hotkeys_ = [ + subtensor.get_hotkey_owner(hotkey_ss58, block=block) + for hotkey_ss58 in hotkey_ss58s + ] + + own_hotkeys = [ + (wallet.coldkeypub.ss58_address == hotkey_owner) for hotkey_owner in hotkeys_ + ] + + successful_unstakes = 0 + for idx, (hotkey_ss58, amount, old_stake, own_hotkey) in enumerate( + zip(hotkey_ss58s, amounts, old_stakes, own_hotkeys) + ): + # Covert to bittensor.Balance + if amount is None: + # Unstake it all. + unstaking_balance = old_stake + else: + unstaking_balance = ( + amount if isinstance(amount, Balance) else Balance.from_tao(amount) + ) + + # Check enough to unstake. + stake_on_uid = old_stake + if unstaking_balance > stake_on_uid: + logging.error( + f":cross_mark: [red]Not enough stake[/red]: [green]{stake_on_uid}[/green] to unstake: " + f"[blue]{unstaking_balance}[/blue] from hotkey: [blue]{wallet.hotkey_str}[/blue]." + ) + continue + + # If nomination stake, check threshold. + if not own_hotkey and not _check_threshold_amount( + subtensor=subtensor, stake_balance=(stake_on_uid - unstaking_balance) + ): + logging.warning( + ":warning: [yellow]This action will unstake the entire staked balance![/yellow]" + ) + unstaking_balance = stake_on_uid + + try: + logging.info( + f":satellite: [magenta]Unstaking from chain:[/magenta] [blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" + ) + staking_response: bool = __do_remove_stake_single( + subtensor=subtensor, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + amount=unstaking_balance, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if staking_response is True: # If we successfully unstaked. + # We only wait here if we expect finalization. + + if idx < len(hotkey_ss58s) - 1: + # Wait for tx rate limit. + tx_rate_limit_blocks = subtensor.tx_rate_limit() + if tx_rate_limit_blocks > 0: + logging.info( + f":hourglass: [yellow]Waiting for tx rate limit: " + f"[white]{tx_rate_limit_blocks}[/white] blocks[/yellow]" + ) + time.sleep(tx_rate_limit_blocks * 12) # 12 seconds per block + + if not wait_for_finalization and not wait_for_inclusion: + successful_unstakes += 1 + continue + + logging.info(":white_heavy_check_mark: [green]Finalized[/green]") + + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]..." + ) + block = subtensor.get_current_block() + new_stake = subtensor.get_stake_for_coldkey_and_hotkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + block=block, + ) + logging.info( + f"Stake ({hotkey_ss58}): [blue]{stake_on_uid}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) + successful_unstakes += 1 + else: + logging.error(":cross_mark: [red]Failed: Unknown Error.[/red]") + continue + + except NotRegisteredError: + logging.error( + f":cross_mark: [red]Hotkey[/red] [blue]{hotkey_ss58}[/blue] [red]is not registered.[/red]" + ) + continue + except StakeError as e: + logging.error(":cross_mark: [red]Stake Error: {}[/red]".format(e)) + continue + + if successful_unstakes != 0: + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] ([blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" + ) + new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) + logging.info( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + return True + + return False diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index ae3e898cdb..689613a9f5 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -1,5 +1,5 @@ -import asyncio import copy +import importlib import os import pickle import typing @@ -20,7 +20,6 @@ ) from bittensor.core import settings from bittensor.core.chain_data import AxonInfo -from bittensor.utils import execute_coroutine # For annotation purposes if typing.TYPE_CHECKING: @@ -143,7 +142,7 @@ def determine_chain_endpoint_and_network(network: str) -> tuple[str, str]: return "unknown", network -class AsyncMetagraphMixin(ABC): +class MetagraphMixin(ABC): """ The metagraph class is a core component of the Bittensor network, representing the neural graph that forms the backbone of the decentralized machine learning system. @@ -162,6 +161,8 @@ class AsyncMetagraphMixin(ABC): Args: netuid (int): A unique identifier that distinguishes between different instances or versions of the Bittensor network. network (str): The name of the network, signifying specific configurations or iterations within the Bittensor ecosystem. + + Attributes: version (NDArray): The version number of the network, integral for tracking network updates. n (NDArray): The total number of neurons in the network, reflecting its size and complexity. block (NDArray): The current block number in the blockchain, crucial for synchronizing with the network's latest state. @@ -233,6 +234,7 @@ class AsyncMetagraphMixin(ABC): axons: list[AxonInfo] chain_endpoint: Optional[str] subtensor: Optional["AsyncSubtensor"] + _dtype_registry = {"int64": np.int64, "float32": np.float32, "bool": bool} @property def S(self) -> Union[NDArray, "torch.nn.Parameter"]: @@ -455,7 +457,7 @@ def __init__( network: str = settings.DEFAULT_NETWORK, lite: bool = True, sync: bool = True, - subtensor: "AsyncSubtensor" = None, + subtensor: Optional[Union["AsyncSubtensor", "Subtensor"]] = None, ): """ Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the @@ -475,7 +477,7 @@ def __init__( Example: Initializing a metagraph object for the Bittensor network with a specific network UID:: - metagraph = metagraph(netuid=123, network="finney", lite=True, sync=True) + metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) """ @@ -568,150 +570,6 @@ def state_dict(self): "neurons": self.neurons, } - async def sync( - self, - block: Optional[int] = None, - lite: bool = True, - subtensor: Optional["AsyncSubtensor"] = None, - ): - """ - Synchronizes the metagraph with the Bittensor network's current state. It updates the metagraph's attributes to - reflect the latest data from the network, ensuring the metagraph represents the most current state of the - network. - - Args: - block (Optional[int]): A specific block number to synchronize with. If None, the metagraph syncs with the - latest block. This allows for historical analysis or specific state examination of the network. - lite (bool): If True, a lite version of the metagraph is used for quicker synchronization. This is - beneficial when full detail is not necessary, allowing for reduced computational and time overhead. - subtensor (Optional[bittensor.core.subtensor.Subtensor]): An instance of the subtensor class from Bittensor, - providing an interface to the underlying blockchain data. If provided, this instance is used for data - retrieval during synchronization. - - Example: - Sync the metagraph with the latest block from the subtensor, using the lite version for efficiency:: - - from bittensor.core.subtensor import Subtensor - - subtensor = Subtensor() - metagraph.sync(subtensor=subtensor) - - Sync with a specific block number for detailed analysis:: - - from bittensor.core.subtensor import Subtensor - - subtensor = Subtensor() - metagraph.sync(block=12345, lite=False, subtensor=subtensor) - - NOTE: - If attempting to access data beyond the previous 300 blocks, you **must** use the ``archive`` network for - subtensor. Light nodes are configured only to store the previous 300 blocks if connecting to finney or - test networks. - - For example:: - - from bittensor.core.subtensor import Subtensor - - subtensor = Subtensor(network='archive') - current_block = subtensor.get_current_block() - history_block = current_block - 1200 - - metagraph.sync(block=history_block, lite=False, subtensor=subtensor) - """ - # Initialize subtensor - subtensor = self._initialize_subtensor(subtensor) - - if ( - subtensor.chain_endpoint != settings.ARCHIVE_ENTRYPOINT - or subtensor.network != "archive" - ): - cur_block = await subtensor.get_current_block() - if block and block < (cur_block - 300): - logging.warning( - "Attempting to sync longer than 300 blocks ago on a non-archive node. Please use the 'archive' " - "network for subtensor and retry." - ) - - # Assign neurons based on 'lite' flag - await self._assign_neurons(block, lite, subtensor) - - # Set attributes for metagraph - await self._set_metagraph_attributes(block, subtensor) - - # If not a 'lite' version, compute and set weights and bonds for each neuron - if not lite: - await self._set_weights_and_bonds(subtensor=subtensor) - - def _initialize_subtensor(self, subtensor: "AsyncSubtensor") -> "AsyncSubtensor": - """ - Initializes the subtensor to be used for syncing the metagraph. - - This method ensures that a subtensor instance is available and properly set up for data retrieval during the - synchronization process. - - If no subtensor is provided, this method is responsible for creating a new instance of the subtensor, configured - according to the current network settings. - - Args: - subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance provided for - initialization. If ``None``, a new subtensor instance is created using the current network configuration. - - Returns: - subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The initialized subtensor instance, ready to be - used for syncing the metagraph. - - Internal Usage: - Used internally during the sync process to ensure a valid subtensor instance is available:: - - subtensor = self._initialize_subtensor(subtensor) - """ - if subtensor and subtensor != self.subtensor: - self.subtensor = subtensor - if not subtensor and self.subtensor: - subtensor = self.subtensor - if not subtensor: - # TODO: Check and test the initialization of the new subtensor - # Lazy import due to circular import (subtensor -> metagraph, metagraph -> subtensor) - from bittensor.core.subtensor import AsyncSubtensor - - subtensor = AsyncSubtensor(network=self.chain_endpoint) - self.subtensor = subtensor - return subtensor - - async def _assign_neurons( - self, block: int, lite: bool, subtensor: "AsyncSubtensor" - ): - """ - Assigns neurons to the metagraph based on the provided block number and the lite flag. - - This method is responsible for fetching and setting the neuron data in the metagraph, which includes neuron - attributes like UID, stake, trust, and other relevant information. - - Args: - block (int): The block number for which the neuron data needs to be fetched. If ``None``, the latest block - data is used. - lite (bool): A boolean flag indicating whether to use a lite version of the neuron data. The lite version - typically includes essential information and is quicker to fetch and process. - subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for fetching neuron - data from the network. - - Internal Usage: - Used internally during the sync process to fetch and set neuron data:: - - from bittensor.core.subtensor import Subtensor - - block = 12345 - lite = False - subtensor = Subtensor() - self._assign_neurons(block, lite, subtensor) - """ - if lite: - self.neurons = await subtensor.neurons_lite(block=block, netuid=self.netuid) - - else: - self.neurons = await subtensor.neurons(block=block, netuid=self.netuid) - self.lite = lite - @staticmethod def _create_tensor(data, dtype) -> Union[NDArray, "torch.nn.Parameter"]: """ @@ -737,38 +595,6 @@ def _create_tensor(data, dtype) -> Union[NDArray, "torch.nn.Parameter"]: else np.array(data, dtype=dtype) ) - async def _set_weights_and_bonds( - self, subtensor: Optional["AsyncSubtensor"] = None - ): - """ - Computes and sets the weights and bonds for each neuron in the metagraph. This method is responsible for - processing the raw weight and bond data obtained from the network and converting it into a structured format - suitable for the metagraph model. - - Args: - subtensor: The subtensor instance used for fetching weights and bonds data. If ``None``, the weights and - bonds are not updated. - - Internal Usage: - Used internally during the sync process to update the weights and bonds of the neurons:: - - self._set_weights_and_bonds(subtensor=subtensor) - """ - # TODO: Check and test the computation of weights and bonds - if self.netuid == 0: - self.weights = await self._process_root_weights( - [neuron.weights for neuron in self.neurons], - "weights", - subtensor, - ) - else: - self.weights = self._process_weights_or_bonds( - [neuron.weights for neuron in self.neurons], "weights" - ) - self.bonds = self._process_weights_or_bonds( - [neuron.bonds for neuron in self.neurons], "bonds" - ) - def _process_weights_or_bonds( self, data, attribute: str ) -> Union[NDArray, "torch.nn.Parameter"]: @@ -833,85 +659,96 @@ def _process_weights_or_bonds( ) return tensor_param - @abstractmethod - async def _set_metagraph_attributes(self, block, subtensor): - pass + def _set_metagraph_attributes(self, block: int): + """ + Sets various attributes of the metagraph based on the latest network data fetched from the subtensor. This + method updates parameters like the number of neurons, block number, stakes, trusts, ranks, and other + neuron-specific information. - async def _process_root_weights( - self, data: list, attribute: str, subtensor: "AsyncSubtensor" - ) -> Union[NDArray, "torch.nn.Parameter"]: + Args: + block (int): The block number for which the metagraph attributes need to be set. + + Internal Usage: + Used internally during the sync process to update the metagraph's attributes:: + + self._set_metagraph_attributes(block) """ - Specifically processes the root weights data for the metagraph. This method is similar to :func:`_process_weights_or_bonds` - but is tailored for processing root weights, which have a different structure and significance in the network. + self.n = self._create_tensor( + len(self.neurons), dtype=self._dtype_registry["int64"] + ) + self.version = self._create_tensor( + [settings.version_as_int], dtype=self._dtype_registry["int64"] + ) + self.block = self._create_tensor(block, dtype=self._dtype_registry["int64"]) + self.uids = self._create_tensor( + [neuron.uid for neuron in self.neurons], dtype=self._dtype_registry["int64"] + ) + self.trust = self._create_tensor( + [neuron.trust for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.consensus = self._create_tensor( + [neuron.consensus for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.incentive = self._create_tensor( + [neuron.incentive for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.dividends = self._create_tensor( + [neuron.dividends for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.ranks = self._create_tensor( + [neuron.rank for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.emission = self._create_tensor( + [neuron.emission for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.active = self._create_tensor( + [neuron.active for neuron in self.neurons], + dtype=self._dtype_registry["int64"], + ) + self.last_update = self._create_tensor( + [neuron.last_update for neuron in self.neurons], + dtype=self._dtype_registry["int64"], + ) + self.validator_permit = self._create_tensor( + [neuron.validator_permit for neuron in self.neurons], dtype=bool + ) + self.validator_trust = self._create_tensor( + [neuron.validator_trust for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.total_stake = self._create_tensor( + [neuron.total_stake.tao for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.stake = self._create_tensor( + [neuron.stake for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.axons = [n.axon_info for n in self.neurons] + + def save(self, root_dir: Optional[list[str]] = None) -> "AsyncMetagraph": + """ + Saves the current state of the metagraph to a file on disk. This function is crucial for persisting the current + state of the network's metagraph, which can later be reloaded or analyzed. The save operation includes all + neuron attributes and parameters, ensuring a complete snapshot of the metagraph's state. Args: - data (list): The raw root weights data to be processed. - attribute (str): A string indicating the attribute type, here it's typically ``weights``. - subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for additional data - and context needed in processing. + root_dir: list to the file path for the root directory of your metagraph saves + (i.e. ['/', 'tmp', 'metagraphs'], defaults to ["~", ".bittensor", "metagraphs"] Returns: - A tensor parameter encapsulating the processed root weights data. + metagraph (bittensor.core.metagraph.Metagraph): The metagraph instance after saving its state. - Internal Usage: - Used internally to process and set root weights for the metagraph:: + Example: + Save the current state of the metagraph to the default directory:: - self.root_weights = self._process_root_weights(raw_root_weights_data, "weights", subtensor) - """ - data_array = [] - n_subnets = await subtensor.get_total_subnets() or 0 - subnets = await subtensor.get_subnets() - for item in data: - if len(item) == 0: - if use_torch(): - data_array.append(torch.zeros(n_subnets)) - else: - data_array.append(np.zeros(n_subnets, dtype=np.float32)) - else: - uids, values = zip(*item) - # TODO: Validate and test the conversion of uids and values to tensor - data_array.append( - convert_root_weight_uids_and_vals_to_tensor( - n_subnets, list(uids), list(values), subnets - ) - ) - - tensor_param: Union[NDArray, "torch.nn.Parameter"] = ( - ( - torch.nn.Parameter(torch.stack(data_array), requires_grad=False) - if len(data_array) - else torch.nn.Parameter() - ) - if use_torch() - else ( - np.stack(data_array) - if len(data_array) - else np.array([], dtype=np.float32) - ) - ) - if len(data_array) == 0: - logging.warning( - f"Empty {attribute}_array on metagraph.sync(). The '{attribute}' tensor is empty." - ) - return tensor_param - - def save(self, root_dir: Optional[list[str]] = None) -> "AsyncMetagraph": - """ - Saves the current state of the metagraph to a file on disk. This function is crucial for persisting the current - state of the network's metagraph, which can later be reloaded or analyzed. The save operation includes all - neuron attributes and parameters, ensuring a complete snapshot of the metagraph's state. - - Args: - root_dir: list to the file path for the root directory of your metagraph saves - (i.e. ['/', 'tmp', 'metagraphs'], defaults to ["~", ".bittensor", "metagraphs"] - - Returns: - metagraph (bittensor.core.metagraph.Metagraph): The metagraph instance after saving its state. - - Example: - Save the current state of the metagraph to the default directory:: - - metagraph.save() + metagraph.save() The saved state can later be loaded to restore or analyze the metagraph's state at this point. @@ -1046,14 +883,14 @@ def __copy__(self): """ -class AsyncTorchMetaGraph(AsyncMetagraphMixin, BaseClass): +class TorchMetagraph(MetagraphMixin, BaseClass): def __init__( self, netuid: int, network: str = settings.DEFAULT_NETWORK, lite: bool = True, sync: bool = True, - subtensor: "AsyncSubtensor" = None, + subtensor: Optional[Union["AsyncSubtensor", "Subtensor"]] = None, ): """ Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the @@ -1078,11 +915,16 @@ def __init__( metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) """ torch.nn.Module.__init__(self) - AsyncMetagraphMixin.__init__(self, netuid, network, lite, sync, subtensor) + MetagraphMixin.__init__(self, netuid, network, lite, sync, subtensor) self.netuid = netuid self.network, self.chain_endpoint = determine_chain_endpoint_and_network( network ) + self._dtype_registry = { + "int64": torch.int64, + "float32": torch.float32, + "bool": torch.bool, + } self.version = torch.nn.Parameter( torch.tensor([settings.version_as_int], dtype=torch.int64), requires_grad=False, @@ -1143,85 +985,6 @@ def __init__( self.subtensor = subtensor self.should_sync = sync - if self.should_sync: - execute_coroutine(self.sync(block=None, lite=lite, subtensor=subtensor)) - - async def __aenter__(self): - if self.should_sync: - await self.sync(block=None, lite=self.lite, subtensor=self.subtensor) - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - pass - - async def _set_metagraph_attributes(self, block: int, subtensor: "AsyncSubtensor"): - """ - Sets various attributes of the metagraph based on the latest network data fetched from the subtensor. - This method updates parameters like the number of neurons, block number, stakes, trusts, ranks, and other - neuron-specific information. - - Args: - block (int): The block number for which the metagraph attributes need to be set. If ``None``, the latest - block data is used. - subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for fetching the - latest network data. - - Internal Usage: - Used internally during the sync process to update the metagraph's attributes:: - - from bittensor.core.subtensor import Subtensor - - subtensor = Subtensor() - block = subtensor.get_current_block() - - self._set_metagraph_attributes(block, subtensor) - """ - self.n = self._create_tensor(len(self.neurons), dtype=torch.int64) - self.version = self._create_tensor([settings.version_as_int], dtype=torch.int64) - self.block = self._create_tensor( - block if block else await subtensor.block, dtype=torch.int64 - ) - self.uids = self._create_tensor( - [neuron.uid for neuron in self.neurons], dtype=torch.int64 - ) - self.trust = self._create_tensor( - [neuron.trust for neuron in self.neurons], dtype=torch.float32 - ) - self.consensus = self._create_tensor( - [neuron.consensus for neuron in self.neurons], dtype=torch.float32 - ) - self.incentive = self._create_tensor( - [neuron.incentive for neuron in self.neurons], dtype=torch.float32 - ) - self.dividends = self._create_tensor( - [neuron.dividends for neuron in self.neurons], dtype=torch.float32 - ) - self.ranks = self._create_tensor( - [neuron.rank for neuron in self.neurons], dtype=torch.float32 - ) - self.emission = self._create_tensor( - [neuron.emission for neuron in self.neurons], dtype=torch.float32 - ) - self.active = self._create_tensor( - [neuron.active for neuron in self.neurons], dtype=torch.int64 - ) - self.last_update = self._create_tensor( - [neuron.last_update for neuron in self.neurons], dtype=torch.int64 - ) - self.validator_permit = self._create_tensor( - [neuron.validator_permit for neuron in self.neurons], dtype=torch.bool - ) - self.validator_trust = self._create_tensor( - [neuron.validator_trust for neuron in self.neurons], dtype=torch.float32 - ) - self.total_stake = self._create_tensor( - [neuron.total_stake.tao for neuron in self.neurons], dtype=torch.float32 - ) - self.stake = self._create_tensor( - [neuron.stake for neuron in self.neurons], dtype=torch.float32 - ) - self.axons = [n.axon_info for n in self.neurons] - def load_from_path(self, dir_path: str) -> "AsyncMetagraph": """ Loads the metagraph state from a specified directory path. @@ -1286,14 +1049,14 @@ def load_from_path(self, dir_path: str) -> "AsyncMetagraph": return self -class AsyncNonTorchMetagraph(AsyncMetagraphMixin): +class NonTorchMetagraph(MetagraphMixin): def __init__( self, netuid: int, network: str = settings.DEFAULT_NETWORK, lite: bool = True, sync: bool = True, - subtensor: "AsyncSubtensor" = None, + subtensor: Optional[Union["AsyncSubtensor", "Subtensor"]] = None, ): """ Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the @@ -1318,7 +1081,7 @@ def __init__( metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) """ # super(metagraph, self).__init__() - AsyncMetagraphMixin.__init__(self, netuid, network, lite, sync, subtensor) + MetagraphMixin.__init__(self, netuid, network, lite, sync, subtensor) self.netuid = netuid self.network, self.chain_endpoint = determine_chain_endpoint_and_network( @@ -1347,81 +1110,6 @@ def __init__( self.subtensor = subtensor self.should_sync = sync - if self.should_sync: - execute_coroutine(self.sync(block=None, lite=lite, subtensor=subtensor)) - - async def __aenter__(self): - if self.should_sync: - await self.sync(block=None, lite=self.lite, subtensor=self.subtensor) - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - pass - - async def _set_metagraph_attributes(self, block: int, subtensor: "AsyncSubtensor"): - """ - Sets various attributes of the metagraph based on the latest network data fetched from the subtensor. This - method updates parameters like the number of neurons, block number, stakes, trusts, ranks, and other - neuron-specific information. - - Args: - block (int): The block number for which the metagraph attributes need to be set. If ``None``, the latest - block data is used. - subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for fetching the - latest network data. - - Internal Usage: - Used internally during the sync process to update the metagraph's attributes:: - - self._set_metagraph_attributes(block, subtensor) - """ - # TODO: Check and test the setting of each attribute - self.n = self._create_tensor(len(self.neurons), dtype=np.int64) - self.version = self._create_tensor([settings.version_as_int], dtype=np.int64) - self.block = self._create_tensor( - block if block else await subtensor.block, dtype=np.int64 - ) - self.uids = self._create_tensor( - [neuron.uid for neuron in self.neurons], dtype=np.int64 - ) - self.trust = self._create_tensor( - [neuron.trust for neuron in self.neurons], dtype=np.float32 - ) - self.consensus = self._create_tensor( - [neuron.consensus for neuron in self.neurons], dtype=np.float32 - ) - self.incentive = self._create_tensor( - [neuron.incentive for neuron in self.neurons], dtype=np.float32 - ) - self.dividends = self._create_tensor( - [neuron.dividends for neuron in self.neurons], dtype=np.float32 - ) - self.ranks = self._create_tensor( - [neuron.rank for neuron in self.neurons], dtype=np.float32 - ) - self.emission = self._create_tensor( - [neuron.emission for neuron in self.neurons], dtype=np.float32 - ) - self.active = self._create_tensor( - [neuron.active for neuron in self.neurons], dtype=np.int64 - ) - self.last_update = self._create_tensor( - [neuron.last_update for neuron in self.neurons], dtype=np.int64 - ) - self.validator_permit = self._create_tensor( - [neuron.validator_permit for neuron in self.neurons], dtype=bool - ) - self.validator_trust = self._create_tensor( - [neuron.validator_trust for neuron in self.neurons], dtype=np.float32 - ) - self.total_stake = self._create_tensor( - [neuron.total_stake.tao for neuron in self.neurons], dtype=np.float32 - ) - self.stake = self._create_tensor( - [neuron.stake for neuron in self.neurons], dtype=np.float32 - ) - self.axons = [n.axon_info for n in self.neurons] - def load_from_path(self, dir_path: str) -> "AsyncMetagraph": """ Loads the state of the Metagraph from a specified directory path. @@ -1485,9 +1173,9 @@ def load_from_path(self, dir_path: str) -> "AsyncMetagraph": if use_torch(): - AsyncMetagraph = AsyncTorchMetaGraph + NumpyOrTorch = TorchMetagraph else: - AsyncMetagraph = AsyncNonTorchMetagraph + NumpyOrTorch = NonTorchMetagraph """Metagraph class that uses :class:`TorchMetaGraph` if PyTorch is available; otherwise, it falls back to :class:`NonTorchMetagraph`. - **With PyTorch**: When `use_torch()` returns `True`, `Metagraph` is set to :class:`TorchMetaGraph`, which utilizes PyTorch functionalities. @@ -1495,18 +1183,9 @@ def load_from_path(self, dir_path: str) -> "AsyncMetagraph": """ -class Metagraph(AsyncMetagraph): +class AsyncMetagraph(NumpyOrTorch): """ - Represents a wrapper for the asynchronous metagraph functionality. - - This class provides a synchronous interface to interact with an asynchronous metagraph. It is initialized with - configuration related to the network and provides methods for synchronizing and accessing asynchronous metagraph - attributes. - - If you want to get the description of any method from the `bittensor.core.metagraph.Metagraph` class, then simply - get the corresponding method from the `bittensor.core.metagraph.AsyncMetagraph` class. - `AsyncMetagraph` is the class related with `AsyncTorchMetaGraph` or `AsyncNonTorchMetagraph` depending on the use - of the use of the env var `USE_TORCH` + TODO docstring. Advise user to use `async_metagraph` factory fn if they want to sync at init """ def __init__( @@ -1515,51 +1194,517 @@ def __init__( network: str = settings.DEFAULT_NETWORK, lite: bool = True, sync: bool = True, - subtensor: "Subtensor" = None, + subtensor: Optional["AsyncSubtensor"] = None, ): - self.subtensor: Optional["Subtensor"] = subtensor - self._async_metagraph = AsyncMetagraph( - netuid=netuid, - network=network, - lite=lite, - sync=sync, - subtensor=subtensor.async_subtensor if subtensor else None, - ) + super().__init__(netuid, network, lite, sync, subtensor) - def sync( + async def __aenter__(self): + if self.should_sync: + await self.sync(block=None, lite=self.lite, subtensor=self.subtensor) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + pass + + async def sync( self, block: Optional[int] = None, lite: bool = True, - subtensor: Optional["Subtensor"] = None, + subtensor: Optional["AsyncSubtensor"] = None, ): - """Synchronizes the metagraph to the specified block, lite, and subtensor instance if available.""" - if subtensor: - event_loop = subtensor.event_loop - elif self.subtensor: - event_loop = self.subtensor.event_loop - else: - event_loop = None - execute_coroutine( - self._async_metagraph.sync( - block=block, - lite=lite, - subtensor=subtensor.async_subtensor if subtensor else None, - ), - event_loop=event_loop, - ) + """ + Synchronizes the metagraph with the Bittensor network's current state. It updates the metagraph's attributes to + reflect the latest data from the network, ensuring the metagraph represents the most current state of the + network. - def __getattr__(self, name): - attr = getattr(self._async_metagraph, name) - if callable(attr): - if asyncio.iscoroutinefunction(attr): - - def wrapper(*args, **kwargs): - return execute_coroutine( - attr(*args, **kwargs), - event_loop=self.subtensor.event_loop - if self.subtensor - else None, - ) + Args: + block (Optional[int]): A specific block number to synchronize with. If None, the metagraph syncs with the + latest block. This allows for historical analysis or specific state examination of the network. + lite (bool): If True, a lite version of the metagraph is used for quicker synchronization. This is + beneficial when full detail is not necessary, allowing for reduced computational and time overhead. + subtensor (Optional[bittensor.core.subtensor.Subtensor]): An instance of the subtensor class from Bittensor, + providing an interface to the underlying blockchain data. If provided, this instance is used for data + retrieval during synchronization. + + Example: + Sync the metagraph with the latest block from the subtensor, using the lite version for efficiency:: + + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor() + metagraph.sync(subtensor=subtensor) + + Sync with a specific block number for detailed analysis:: + + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor() + metagraph.sync(block=12345, lite=False, subtensor=subtensor) + + NOTE: + If attempting to access data beyond the previous 300 blocks, you **must** use the ``archive`` network for + subtensor. Light nodes are configured only to store the previous 300 blocks if connecting to finney or + test networks. - return wrapper - return attr + For example:: + + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor(network='archive') + current_block = subtensor.get_current_block() + history_block = current_block - 1200 + + metagraph.sync(block=history_block, lite=False, subtensor=subtensor) + """ + subtensor = await self._initialize_subtensor(subtensor) + + if ( + subtensor.chain_endpoint != settings.ARCHIVE_ENTRYPOINT + or subtensor.network != "archive" + ): + cur_block = await subtensor.get_current_block() + if block and block < (cur_block - 300): + logging.warning( + "Attempting to sync longer than 300 blocks ago on a non-archive node. Please use the 'archive' " + "network for subtensor and retry." + ) + if block is None: + block = await subtensor.get_current_block() + + # Assign neurons based on 'lite' flag + await self._assign_neurons(block, lite, subtensor) + + # Set attributes for metagraph + self._set_metagraph_attributes(block) + + # If not a 'lite' version, compute and set weights and bonds for each neuron + if not lite: + await self._set_weights_and_bonds(subtensor=subtensor) + + async def _initialize_subtensor( + self, subtensor: "AsyncSubtensor" + ) -> "AsyncSubtensor": + """ + Initializes the subtensor to be used for syncing the metagraph. + + This method ensures that a subtensor instance is available and properly set up for data retrieval during the + synchronization process. + + If no subtensor is provided, this method is responsible for creating a new instance of the subtensor, configured + according to the current network settings. + + Args: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance provided for + initialization. If ``None``, a new subtensor instance is created using the current network configuration. + + Returns: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The initialized subtensor instance, ready to be + used for syncing the metagraph. + + Internal Usage: + Used internally during the sync process to ensure a valid subtensor instance is available:: + + subtensor = await self._initialize_subtensor(subtensor) + """ + if subtensor and subtensor != self.subtensor: + self.subtensor = subtensor + if not subtensor and self.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", + ) + + async with AsyncSubtensor(network=self.chain_endpoint) as subtensor: + self.subtensor = subtensor + return subtensor + + async def _assign_neurons( + self, block: int, lite: bool, subtensor: "AsyncSubtensor" + ): + """ + Assigns neurons to the metagraph based on the provided block number and the lite flag. + + This method is responsible for fetching and setting the neuron data in the metagraph, which includes neuron + attributes like UID, stake, trust, and other relevant information. + + Args: + block (int): The block number for which the neuron data needs to be fetched. If ``None``, the latest block + data is used. + lite (bool): A boolean flag indicating whether to use a lite version of the neuron data. The lite version + typically includes essential information and is quicker to fetch and process. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for fetching neuron + data from the network. + + Internal Usage: + Used internally during the sync process to fetch and set neuron data:: + + from bittensor.core.subtensor import Subtensor + + block = 12345 + lite = False + subtensor = Subtensor() + self._assign_neurons(block, lite, subtensor) + """ + if lite: + self.neurons = await subtensor.neurons_lite(block=block, netuid=self.netuid) + + else: + self.neurons = await subtensor.neurons(block=block, netuid=self.netuid) + self.lite = lite + + async def _set_weights_and_bonds( + self, subtensor: Optional["AsyncSubtensor"] = None + ): + """ + Computes and sets the weights and bonds for each neuron in the metagraph. This method is responsible for + processing the raw weight and bond data obtained from the network and converting it into a structured format + suitable for the metagraph model. + + Args: + subtensor: The subtensor instance used for fetching weights and bonds data. If ``None``, the weights and + bonds are not updated. + + Internal Usage: + Used internally during the sync process to update the weights and bonds of the neurons:: + + self._set_weights_and_bonds(subtensor=subtensor) + """ + # TODO: Check and test the computation of weights and bonds + if self.netuid == 0: + self.weights = await self._process_root_weights( + [neuron.weights for neuron in self.neurons], + "weights", + subtensor, + ) + else: + self.weights = self._process_weights_or_bonds( + [neuron.weights for neuron in self.neurons], "weights" + ) + self.bonds = self._process_weights_or_bonds( + [neuron.bonds for neuron in self.neurons], "bonds" + ) + + async def _process_root_weights( + self, data: list, attribute: str, subtensor: "AsyncSubtensor" + ) -> Union[NDArray, "torch.nn.Parameter"]: + """ + Specifically processes the root weights data for the metagraph. This method is similar to :func:`_process_weights_or_bonds` + but is tailored for processing root weights, which have a different structure and significance in the network. + + Args: + data (list): The raw root weights data to be processed. + attribute (str): A string indicating the attribute type, here it's typically ``weights``. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for additional data + and context needed in processing. + + Returns: + A tensor parameter encapsulating the processed root weights data. + + Internal Usage: + Used internally to process and set root weights for the metagraph:: + + self.root_weights = self._process_root_weights(raw_root_weights_data, "weights", subtensor) + """ + data_array = [] + n_subnets = await subtensor.get_total_subnets() or 0 + subnets = await subtensor.get_subnets() + for item in data: + if len(item) == 0: + if use_torch(): + data_array.append(torch.zeros(n_subnets)) + else: + data_array.append(np.zeros(n_subnets, dtype=np.float32)) + else: + uids, values = zip(*item) + # TODO: Validate and test the conversion of uids and values to tensor + data_array.append( + convert_root_weight_uids_and_vals_to_tensor( + n_subnets, list(uids), list(values), subnets + ) + ) + + tensor_param: Union[NDArray, "torch.nn.Parameter"] = ( + ( + torch.nn.Parameter(torch.stack(data_array), requires_grad=False) + if len(data_array) + else torch.nn.Parameter() + ) + if use_torch() + else ( + np.stack(data_array) + if len(data_array) + else np.array([], dtype=np.float32) + ) + ) + if len(data_array) == 0: + logging.warning( + f"Empty {attribute}_array on metagraph.sync(). The '{attribute}' tensor is empty." + ) + return tensor_param + + +class Metagraph(NumpyOrTorch): + def __init__( + self, + netuid: int, + network: str = settings.DEFAULT_NETWORK, + lite: bool = True, + sync: bool = True, + subtensor: Optional["Subtensor"] = None, + ): + super().__init__(netuid, network, lite, sync, subtensor) + if sync: + self.sync() + + def sync( + self, + block: Optional[int] = None, + lite: bool = True, + subtensor: Optional["Subtensor"] = None, + ): + """ + Synchronizes the metagraph with the Bittensor network's current state. It updates the metagraph's attributes to + reflect the latest data from the network, ensuring the metagraph represents the most current state of the + network. + + Args: + block (Optional[int]): A specific block number to synchronize with. If None, the metagraph syncs with the + latest block. This allows for historical analysis or specific state examination of the network. + lite (bool): If True, a lite version of the metagraph is used for quicker synchronization. This is + beneficial when full detail is not necessary, allowing for reduced computational and time overhead. + subtensor (Optional[bittensor.core.subtensor.Subtensor]): An instance of the subtensor class from Bittensor, + providing an interface to the underlying blockchain data. If provided, this instance is used for data + retrieval during synchronization. + + Example: + Sync the metagraph with the latest block from the subtensor, using the lite version for efficiency:: + + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor() + metagraph.sync(subtensor=subtensor) + + Sync with a specific block number for detailed analysis:: + + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor() + metagraph.sync(block=12345, lite=False, subtensor=subtensor) + + NOTE: + If attempting to access data beyond the previous 300 blocks, you **must** use the ``archive`` network for + subtensor. Light nodes are configured only to store the previous 300 blocks if connecting to finney or + test networks. + + For example:: + + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor(network='archive') + current_block = subtensor.get_current_block() + history_block = current_block - 1200 + + metagraph.sync(block=history_block, lite=False, subtensor=subtensor) + """ + subtensor = self._initialize_subtensor(subtensor) + + if ( + subtensor.chain_endpoint != settings.ARCHIVE_ENTRYPOINT + or subtensor.network != "archive" + ): + cur_block = subtensor.get_current_block() + if block and block < (cur_block - 300): + logging.warning( + "Attempting to sync longer than 300 blocks ago on a non-archive node. Please use the 'archive' " + "network for subtensor and retry." + ) + + if block is None: + block = subtensor.get_current_block() + + # Assign neurons based on 'lite' flag + self._assign_neurons(block, lite, subtensor) + + # Set attributes for metagraph + self._set_metagraph_attributes(block) + + # If not a 'lite' version, compute and set weights and bonds for each neuron + if not lite: + self._set_weights_and_bonds(subtensor=subtensor) + + def _initialize_subtensor(self, subtensor: "Subtensor") -> "Subtensor": + """ + Initializes the subtensor to be used for syncing the metagraph. + + This method ensures that a subtensor instance is available and properly set up for data retrieval during the + synchronization process. + + If no subtensor is provided, this method is responsible for creating a new instance of the subtensor, configured + according to the current network settings. + + Args: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance provided for + initialization. If ``None``, a new subtensor instance is created using the current network configuration. + + Returns: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The initialized subtensor instance, ready to be + used for syncing the metagraph. + + Internal Usage: + Used internally during the sync process to ensure a valid subtensor instance is available:: + + subtensor = self._initialize_subtensor(subtensor) + """ + if subtensor and subtensor != self.subtensor: + self.subtensor = subtensor + if not subtensor and self.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" + ) + subtensor = Subtensor(network=self.chain_endpoint) + + self.subtensor = subtensor + return subtensor + + def _assign_neurons(self, block: int, lite: bool, subtensor: "Subtensor"): + """ + Assigns neurons to the metagraph based on the provided block number and the lite flag. + + This method is responsible for fetching and setting the neuron data in the metagraph, which includes neuron + attributes like UID, stake, trust, and other relevant information. + + Args: + block (int): The block number for which the neuron data needs to be fetched. If ``None``, the latest block + data is used. + lite (bool): A boolean flag indicating whether to use a lite version of the neuron data. The lite version + typically includes essential information and is quicker to fetch and process. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for fetching neuron + data from the network. + + Internal Usage: + Used internally during the sync process to fetch and set neuron data:: + + from bittensor.core.subtensor import Subtensor + + block = 12345 + lite = False + subtensor = Subtensor() + self._assign_neurons(block, lite, subtensor) + """ + if lite: + self.neurons = subtensor.neurons_lite(block=block, netuid=self.netuid) + + else: + self.neurons = subtensor.neurons(block=block, netuid=self.netuid) + self.lite = lite + + def _set_weights_and_bonds(self, subtensor: Optional["Subtensor"] = None): + """ + Computes and sets the weights and bonds for each neuron in the metagraph. This method is responsible for + processing the raw weight and bond data obtained from the network and converting it into a structured format + suitable for the metagraph model. + + Args: + subtensor: The subtensor instance used for fetching weights and bonds data. If ``None``, the weights and + bonds are not updated. + + Internal Usage: + Used internally during the sync process to update the weights and bonds of the neurons:: + + self._set_weights_and_bonds(subtensor=subtensor) + """ + if self.netuid == 0: + self.weights = self._process_root_weights( + [neuron.weights for neuron in self.neurons], + "weights", + subtensor, + ) + else: + self.weights = self._process_weights_or_bonds( + [neuron.weights for neuron in self.neurons], "weights" + ) + self.bonds = self._process_weights_or_bonds( + [neuron.bonds for neuron in self.neurons], "bonds" + ) + + def _process_root_weights( + self, data: list, attribute: str, subtensor: "Subtensor" + ) -> Union[NDArray, "torch.nn.Parameter"]: + """ + Specifically processes the root weights data for the metagraph. This method is similar to :func:`_process_weights_or_bonds` + but is tailored for processing root weights, which have a different structure and significance in the network. + + Args: + data (list): The raw root weights data to be processed. + attribute (str): A string indicating the attribute type, here it's typically ``weights``. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for additional data + and context needed in processing. + + Returns: + A tensor parameter encapsulating the processed root weights data. + + Internal Usage: + Used internally to process and set root weights for the metagraph:: + + self.root_weights = self._process_root_weights(raw_root_weights_data, "weights", subtensor) + """ + data_array = [] + n_subnets = subtensor.get_total_subnets() or 0 + subnets = subtensor.get_subnets() + for item in data: + if len(item) == 0: + if use_torch(): + data_array.append(torch.zeros(n_subnets)) + else: + data_array.append(np.zeros(n_subnets, dtype=np.float32)) + else: + uids, values = zip(*item) + # TODO: Validate and test the conversion of uids and values to tensor + data_array.append( + convert_root_weight_uids_and_vals_to_tensor( + n_subnets, list(uids), list(values), subnets + ) + ) + + tensor_param: Union[NDArray, "torch.nn.Parameter"] = ( + ( + torch.nn.Parameter(torch.stack(data_array), requires_grad=False) + if len(data_array) + else torch.nn.Parameter() + ) + if use_torch() + else ( + np.stack(data_array) + if len(data_array) + else np.array([], dtype=np.float32) + ) + ) + if len(data_array) == 0: + logging.warning( + f"Empty {attribute}_array on metagraph.sync(). The '{attribute}' tensor is empty." + ) + return tensor_param + + +async def async_metagraph( + netuid: int, + network: str = settings.DEFAULT_NETWORK, + lite: bool = True, + sync: bool = True, + subtensor: "AsyncSubtensor" = None, +) -> "AsyncMetagraph": + """ + Factory function to create an instantiated AsyncMetagraph, mainly for the ability to use sync at instantiation. + """ + metagraph_ = AsyncMetagraph( + netuid=netuid, network=network, lite=lite, sync=sync, subtensor=subtensor + ) + if sync: + await metagraph_.sync() + return metagraph_ diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 3786fd783f..60b98d3805 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -2,7 +2,6 @@ import os import re -import warnings from pathlib import Path from munch import munchify @@ -312,15 +311,7 @@ def __apply_nest_asyncio(): If not set, warn the user that the default will change in the future. """ nest_asyncio_env = os.getenv("NEST_ASYNCIO") - if nest_asyncio_env == "1" or nest_asyncio_env is None: - if nest_asyncio_env is None: - warnings.warn( - """NEST_ASYNCIO implicitly set to '1'. In the future, the default value will be '0'. - If you use `nest_asyncio`, make sure to add it explicitly to your project dependencies, - as it will be removed from `bittensor` package dependencies in the future. - To silence this warning, explicitly set the environment variable, e.g. `export NEST_ASYNCIO=0`.""", - DeprecationWarning, - ) + if nest_asyncio_env == "1": # Install and apply nest asyncio to allow the async functions to run in a .ipynb import nest_asyncio diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 4fc0b5b9c5..bd7aca83d3 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1,55 +1,90 @@ -import warnings +import copy from functools import lru_cache -from typing import TYPE_CHECKING, Any, Iterable, Optional, Union +from typing import TYPE_CHECKING, Any, Iterable, Optional, Union, cast +from async_substrate_interface.errors import SubstrateRequestException +from async_substrate_interface.sync_substrate import SubstrateInterface +from async_substrate_interface.utils import hex_to_bytes, json import numpy as np -from async_substrate_interface import SubstrateInterface from numpy.typing import NDArray - -from bittensor.core.async_subtensor import AsyncSubtensor +import requests +import scalecodec +from scalecodec.base import RuntimeConfiguration +from scalecodec.type_registry import load_type_registry_preset + +from bittensor.core.types import SubtensorMixin +from bittensor.core.chain_data import ( + custom_rpc_type_registry, + decode_account_id, + WeightCommitInfo, +) +from bittensor.core.extrinsics.commit_reveal import commit_reveal_v3_extrinsic +from bittensor.core.extrinsics.commit_weights import ( + commit_weights_extrinsic, + reveal_weights_extrinsic, +) +from bittensor.core.extrinsics.registration import ( + burned_register_extrinsic, + register_extrinsic, +) +from bittensor.core.extrinsics.root import ( + root_register_extrinsic, + set_root_weights_extrinsic, +) +from bittensor.core.extrinsics.set_weights import set_weights_extrinsic +from bittensor.core.extrinsics.staking import ( + add_stake_extrinsic, + add_stake_multiple_extrinsic, +) +from bittensor.core.extrinsics.transfer import transfer_extrinsic +from bittensor.core.extrinsics.unstaking import ( + unstake_extrinsic, + unstake_multiple_extrinsic, +) from bittensor.core.metagraph import Metagraph -from bittensor.core.settings import version_as_int +from bittensor.core.extrinsics.serving import ( + publish_metadata, + get_metadata, + serve_axon_extrinsic, +) +from bittensor.core.settings import ( + version_as_int, + SS58_FORMAT, + TYPE_REGISTRY, + DELEGATES_DETAILS_URL, +) +from bittensor.core.types import ParamWithTypes from bittensor.utils import ( - execute_coroutine, torch, - get_event_loop, - event_loop_is_running, + format_error_message, + ss58_to_vec_u8, + decode_hex_identity_dict, + u16_normalized_float, + _decode_hex_identity_dict, ) +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import generate_weight_hash +from bittensor.core.async_subtensor import ProposalVoteData +from bittensor.core.axon import Axon +from bittensor.core.config import Config +from bittensor.core.chain_data.delegate_info import DelegateInfo +from bittensor.core.chain_data.neuron_info import NeuronInfo +from bittensor.core.chain_data.neuron_info_lite import NeuronInfoLite +from bittensor.core.chain_data.stake_info import StakeInfo +from bittensor.core.chain_data.subnet_hyperparameters import SubnetHyperparameters +from bittensor.core.chain_data.subnet_info import SubnetInfo +from bittensor.utils.balance import Balance if TYPE_CHECKING: from bittensor_wallet import Wallet - from bittensor.core.async_subtensor import ProposalVoteData - from bittensor.core.axon import Axon - from bittensor.core.config import Config - from bittensor.core.chain_data.delegate_info import DelegateInfo - from bittensor.core.chain_data.neuron_info import NeuronInfo - from bittensor.core.chain_data.neuron_info_lite import NeuronInfoLite - from bittensor.core.chain_data.stake_info import StakeInfo - from bittensor.core.chain_data.subnet_hyperparameters import SubnetHyperparameters - from bittensor.core.chain_data.subnet_info import SubnetInfo - from bittensor.utils.balance import Balance from bittensor.utils import Certificate - from async_substrate_interface import QueryMapResult + from async_substrate_interface.sync_substrate import QueryMapResult from bittensor.utils.delegates_details import DelegatesDetails - from scalecodec.types import ScaleType - + from scalecodec.types import ScaleType, GenericCall -class Subtensor: - """ - Represents a synchronous interface for `bittensor.core.async_subtensor.AsyncSubtensor`. - If you want to get the description of any method from the `bittensor.core.subtensor.Subtensor` class, then simply - get the corresponding method from the `bittensor.core.async_subtensor.AsyncSubtensor` class. - """ - - # get static methods from AsyncSubtensor - config = AsyncSubtensor.config - setup_config = AsyncSubtensor.setup_config - help = AsyncSubtensor.help - add_args = AsyncSubtensor.add_args - determine_chain_endpoint_and_network = ( - AsyncSubtensor.determine_chain_endpoint_and_network - ) +class Subtensor(SubtensorMixin): + """Thin layer for interacting with Substrate Interface. Mostly a collection of frequently-used calls.""" def __init__( self, @@ -58,51 +93,76 @@ def __init__( _mock: bool = False, log_verbose: bool = False, ): - if event_loop_is_running(): - warnings.warn( - "You are calling this from an already running event loop. Some features may not work correctly. You " - "should instead use `AsyncSubtensor`." - ) - self.event_loop = get_event_loop() - self.network = network - self._config = config + """ + Initializes an instance of the Subtensor class. + + Arguments: + network (str): The network name or type to connect to. + config (Optional[Config]): Configuration object for the AsyncSubtensor instance. + _mock: Whether this is a mock instance. Mainly just for use in testing. + log_verbose (bool): Enables or disables verbose logging. + + Raises: + Any exceptions raised during the setup, configuration, or connection process. + """ + if config is None: + config = self.config() + self._config = copy.deepcopy(config) + self.chain_endpoint, self.network = self.setup_config(network, self._config) + self._mock = _mock + self.log_verbose = log_verbose - self.async_subtensor = AsyncSubtensor( - network=network, - config=config, - log_verbose=log_verbose, - event_loop=self.event_loop, - _mock=_mock, - ) + self._check_and_log_network_settings() + logging.debug( + f"Connecting to ..." + ) self.substrate = SubstrateInterface( - url=self.async_subtensor.chain_endpoint, + url=self.chain_endpoint, + ss58_format=SS58_FORMAT, + type_registry=TYPE_REGISTRY, + use_remote_preset=True, + chain_name="Bittensor", _mock=_mock, - substrate=self.async_subtensor.substrate, ) - self.chain_endpoint = self.async_subtensor.chain_endpoint - - def __str__(self): - return self.async_subtensor.__str__() - - def __repr__(self): - return self.async_subtensor.__repr__() - - def execute_coroutine(self, coroutine) -> Any: - return execute_coroutine(coroutine, self.event_loop) + if self.log_verbose: + logging.info( + f"Connected to {self.network} network and {self.chain_endpoint}." + ) def close(self): - execute_coroutine(self.async_subtensor.close()) + """ + Does nothing. Exists for backwards compatibility purposes. + """ + pass # Subtensor queries =========================================================================================== def query_constant( self, module_name: str, constant_name: str, block: Optional[int] = None ) -> Optional["ScaleType"]: - return self.execute_coroutine( - self.async_subtensor.query_constant( - module_name=module_name, constant_name=constant_name, block=block - ) + """ + Retrieves a constant from the specified module on the Bittensor blockchain. This function is used to access + fixed parameters or values defined within the blockchain's modules, which are essential for understanding + the network's configuration and rules. + + Args: + module_name: The name of the module containing the constant. + constant_name: The name of the constant to retrieve. + block: The blockchain block number at which to query the constant. + + Returns: + Optional[scalecodec.ScaleType]: The value of the constant if found, `None` otherwise. + + Constants queried through this function can include critical network parameters such as inflation rates, + consensus rules, or validation thresholds, providing a deeper understanding of the Bittensor network's + operational parameters. + """ + return self.substrate.get_constant( + module_name=module_name, + constant_name=constant_name, + block_hash=self.determine_block_hash(block), ) def query_map( @@ -112,19 +172,54 @@ def query_map( block: Optional[int] = None, params: Optional[list] = None, ) -> "QueryMapResult": - return self.execute_coroutine( - self.async_subtensor.query_map( - module=module, name=name, block=block, params=params - ) - ) + """ + Queries map storage from any module on the Bittensor blockchain. This function retrieves data structures that + represent key-value mappings, essential for accessing complex and structured data within the blockchain + modules. + + Args: + module: The name of the module from which to query the map storage. + name: The specific storage function within the module to query. + block: The blockchain block number at which to perform the query. + params: Parameters to be passed to the query. + + Returns: + result: A data structure representing the map storage if found, `None` otherwise. + + This function is particularly useful for retrieving detailed and structured data from various blockchain + modules, offering insights into the network's state and the relationships between its different components. + """ + result = self.substrate.query_map( + module=module, + storage_function=name, + params=params, + block_hash=self.determine_block_hash(block=block), + ) + return getattr(result, "value", None) def query_map_subtensor( self, name: str, block: Optional[int] = None, params: Optional[list] = None ) -> "QueryMapResult": - return self.execute_coroutine( - self.async_subtensor.query_map_subtensor( - name=name, block=block, params=params - ) + """ + Queries map storage from the Subtensor module on the Bittensor blockchain. This function is designed to retrieve + a map-like data structure, which can include various neuron-specific details or network-wide attributes. + + Args: + name: The name of the map storage function to query. + block: The blockchain block number at which to perform the query. + params: A list of parameters to pass to the query function. + + Returns: + An object containing the map-like data structure, or `None` if not found. + + This function is particularly useful for analyzing and understanding complex network structures and + relationships within the Bittensor ecosystem, such as interneuronal connections and stake distributions. + """ + return self.substrate.query_map( + module="SubtensorModule", + storage_function=name, + params=params, + block_hash=self.determine_block_hash(block), ) def query_module( @@ -134,43 +229,135 @@ def query_module( block: Optional[int] = None, params: Optional[list] = None, ) -> "ScaleType": - return self.execute_coroutine( - self.async_subtensor.query_module( - module=module, - name=name, - block=block, - params=params, - ) + """ + Queries any module storage on the Bittensor blockchain with the specified parameters and block number. This + function is a generic query interface that allows for flexible and diverse data retrieval from various + blockchain modules. + + Args: + module (str): The name of the module from which to query data. + name (str): The name of the storage function within the module. + block (Optional[int]): The blockchain block number at which to perform the query. + params (Optional[list[object]]): A list of parameters to pass to the query function. + + Returns: + An object containing the requested data if found, `None` otherwise. + + This versatile query function is key to accessing a wide range of data and insights from different parts of the + Bittensor blockchain, enhancing the understanding and analysis of the network's state and dynamics. + """ + return self.substrate.query( + module=module, + storage_function=name, + params=params, + block_hash=self.determine_block_hash(block), ) def query_runtime_api( self, runtime_api: str, method: str, - params: Optional[Union[list[int], dict[str, int]]] = None, + params: Optional[Union[list[list[int]], dict[str, int], list[int]]] = None, block: Optional[int] = None, ) -> Optional[str]: - return self.execute_coroutine( - coroutine=self.async_subtensor.query_runtime_api( - runtime_api=runtime_api, - method=method, - params=params, - block=block, - ) + """ + Queries the runtime API of the Bittensor blockchain, providing a way to interact with the underlying runtime and + retrieve data encoded in Scale Bytes format. This function is essential for advanced users who need to + interact with specific runtime methods and decode complex data types. + + Args: + runtime_api: The name of the runtime API to query. + method: The specific method within the runtime API to call. + params: The parameters to pass to the method call. + block: the block number for this query. + + 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. + """ + # TODO why doesn't this just use SubstrateInterface.runtime_call ? + block_hash = self.determine_block_hash(block) + + call_definition = TYPE_REGISTRY["runtime_api"][runtime_api]["methods"][method] + + data = ( + "0x" + if params is None + else self.encode_params(call_definition=call_definition, params=params) + ) + api_method = f"{runtime_api}_{method}" + + json_result = self.substrate.rpc_request( + method="state_call", + params=[api_method, data, block_hash] if block_hash else [api_method, data], ) + if json_result is None: + return None + + return_type = call_definition["type"] + + as_scale_bytes = scalecodec.ScaleBytes(json_result["result"]) # type: ignore + + rpc_runtime_config = RuntimeConfiguration() + rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) + rpc_runtime_config.update_type_registry(custom_rpc_type_registry) + + obj = rpc_runtime_config.create_scale_object(return_type, as_scale_bytes) + if obj.data.to_hex() == "0x0400": # RPC returned None result + return None + + return obj.decode() + def query_subtensor( self, name: str, block: Optional[int] = None, params: Optional[list] = None ) -> "ScaleType": - return self.execute_coroutine( - self.async_subtensor.query_subtensor(name=name, block=block, params=params) + """ + Queries named storage from the Subtensor module on the Bittensor blockchain. This function is used to retrieve + specific data or parameters from the blockchain, such as stake, rank, or other neuron-specific attributes. + + Args: + name: The name of the storage function to query. + block: The blockchain block number at which to perform the query. + params: A list of parameters to pass to the query function. + + Returns: + query_response (scalecodec.ScaleType): An object containing the requested data. + + This query function is essential for accessing detailed information about the network and its neurons, providing + valuable insights into the state and dynamics of the Bittensor ecosystem. + """ + return self.substrate.query( + module="SubtensorModule", + storage_function=name, + params=params, + block_hash=self.determine_block_hash(block), ) def state_call( self, method: str, data: str, block: Optional[int] = None ) -> dict[Any, Any]: - return self.execute_coroutine( - self.async_subtensor.state_call(method=method, data=data, block=block) + """ + Makes a state call to the Bittensor blockchain, allowing for direct queries of the blockchain's state. This + function is typically used for advanced queries that require specific method calls and data inputs. + + Args: + method: The method name for the state call. + data: The data to be passed to the method. + block: The blockchain block number at which to perform the state call. + + Returns: + result (dict[Any, Any]): The result of the rpc call. + + The state call function provides a more direct and flexible way of querying blockchain data, useful for specific + use cases where standard queries are insufficient. + """ + block_hash = self.determine_block_hash(block) + return self.substrate.rpc_request( + method="state_call", + params=[method, data, block_hash] if block_hash else [method, data], ) # Common subtensor calls =========================================================================================== @@ -180,282 +367,1100 @@ def block(self) -> int: return self.get_current_block() def blocks_since_last_update(self, netuid: int, uid: int) -> Optional[int]: - return self.execute_coroutine( - self.async_subtensor.blocks_since_last_update(netuid=netuid, uid=uid) - ) + """ + Returns the number of blocks since the last update for a specific UID in the subnetwork. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + uid (int): The unique identifier of the neuron. + + Returns: + Optional[int]: The number of blocks since the last update, or ``None`` if the subnetwork or UID does not + exist. + """ + call = self.get_hyperparameter(param_name="LastUpdate", netuid=netuid) + return None if call is None else (self.get_current_block() - int(call[uid])) def bonds( self, netuid: int, block: Optional[int] = None ) -> list[tuple[int, list[tuple[int, int]]]]: - return self.execute_coroutine( - self.async_subtensor.bonds(netuid=netuid, block=block), - ) + """ + Retrieves the bond distribution set by neurons within a specific subnet of the Bittensor network. + Bonds represent the investments or commitments made by neurons in one another, indicating a level of trust + and perceived value. This bonding mechanism is integral to the network's market-based approach to + measuring and rewarding machine intelligence. + + Args: + netuid: The network UID of the subnet to query. + block: the block number for this query. + + 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. + """ + b_map_encoded = self.substrate.query_map( + module="SubtensorModule", + storage_function="Bonds", + params=[netuid], + block_hash=self.determine_block_hash(block), + ) + b_map = [] + for uid, b in b_map_encoded: + b_map.append((uid, b.value)) + + return b_map def commit(self, wallet, netuid: int, data: str) -> bool: - return self.execute_coroutine( - self.async_subtensor.commit(wallet=wallet, netuid=netuid, data=data) + """ + Commits arbitrary data to the Bittensor network by publishing metadata. + + Arguments: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the data. + netuid (int): The unique identifier of the subnetwork. + data (str): The data to be committed to the network. + """ + return publish_metadata( + subtensor=self, + wallet=wallet, + netuid=netuid, + data_type=f"Raw{len(data)}", + data=data.encode(), ) def commit_reveal_enabled( self, netuid: int, block: Optional[int] = None ) -> Optional[bool]: - return self.execute_coroutine( - self.async_subtensor.commit_reveal_enabled(netuid=netuid, block=block) + """ + Check if commit-reveal mechanism is enabled for a given network at a specific block. + + Arguments: + netuid: The network identifier for which to check the commit-reveal mechanism. + block: The block number to query. + + Returns: + Returns the integer value of the hyperparameter if available; otherwise, returns None. + """ + call = self.get_hyperparameter( + param_name="CommitRevealWeightsEnabled", block=block, netuid=netuid ) + return True if call is True else False def difficulty(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - return self.execute_coroutine( - self.async_subtensor.difficulty(netuid=netuid, block=block), + """ + Retrieves the 'Difficulty' hyperparameter for a specified subnet in the Bittensor network. + + This parameter is instrumental in determining the computational challenge required for neurons to participate in + consensus and validation processes. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + Optional[int]: The value of the 'Difficulty' hyperparameter if the subnet exists, ``None`` otherwise. + + The 'Difficulty' parameter directly impacts the network's security and integrity by setting the computational + effort required for validating transactions and participating in the network's consensus mechanism. + """ + call = self.get_hyperparameter( + param_name="Difficulty", netuid=netuid, block=block ) + if call is None: + return None + return int(call) def does_hotkey_exist(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: - return self.execute_coroutine( - self.async_subtensor.does_hotkey_exist(hotkey_ss58=hotkey_ss58, block=block) - ) + """ + Returns true if the hotkey is known by the chain and there are accounts. + + Args: + hotkey_ss58: The SS58 address of the hotkey. + block: the block number for this query. + + Returns: + `True` if the hotkey is known by the chain and there are accounts, `False` otherwise. + """ + _result = self.substrate.query( + module="SubtensorModule", + storage_function="Owner", + params=[hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + result = decode_account_id(_result.value[0]) + return_val = ( + False + if result is None + else result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" + ) + return return_val def get_all_subnets_info(self, block: Optional[int] = None) -> list["SubnetInfo"]: - return self.execute_coroutine( - self.async_subtensor.get_all_subnets_info(block=block), + """ + Retrieves detailed information about all subnets within the Bittensor network. This function provides + comprehensive data on each subnet, including its characteristics and operational parameters. + + Arguments: + block: The blockchain block number for the query. + + Returns: + list[SubnetInfo]: A list of SubnetInfo objects, each containing detailed information about a subnet. + + Gaining insights into the subnets' details assists in understanding the network's composition, the roles of + different subnets, and their unique features. + """ + hex_bytes_result = self.query_runtime_api( + "SubnetInfoRuntimeApi", "get_subnets_info", params=[], block=block ) + if not hex_bytes_result: + return [] + else: + return SubnetInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) def get_balance(self, address: str, block: Optional[int] = None) -> "Balance": - return self.execute_coroutine( - self.async_subtensor.get_balance(address, block=block), + """ + Retrieves the balance for given coldkey. + + Arguments: + address (str): coldkey address. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Balance object. + """ + balance = self.substrate.query( + module="System", + storage_function="Account", + params=[address], + block_hash=self.determine_block_hash(block), ) + return Balance(balance["data"]["free"]) def get_balances( self, *addresses: str, block: Optional[int] = None, ) -> dict[str, "Balance"]: - return self.execute_coroutine( - self.async_subtensor.get_balances(*addresses, block=block), - ) + """ + Retrieves the balance for given coldkey(s) + + Arguments: + addresses (str): coldkey addresses(s). + block (Optional[int]): The blockchain block number for the query. + + Returns: + Dict of {address: Balance objects}. + """ + if not (block_hash := self.determine_block_hash(block)): + block_hash = self.substrate.get_chain_head() + calls = [ + ( + self.substrate.create_storage_key( + "System", "Account", [address], block_hash=block_hash + ) + ) + for address in addresses + ] + batch_call = self.substrate.query_multi(calls, block_hash=block_hash) + results = {} + for item in batch_call: + value = item[1] or {"data": {"free": 0}} + results.update({item[0].params[0]: Balance(value["data"]["free"])}) + return results def get_current_block(self) -> int: - return self.execute_coroutine( - coroutine=self.async_subtensor.get_current_block(), - ) + """ + Returns the current block number on the Bittensor blockchain. This function provides the latest block number, + indicating the most recent state of the blockchain. + + Returns: + int: The current chain block number. + + Knowing the current block number is essential for querying real-time data and performing time-sensitive + operations on the blockchain. It serves as a reference point for network activities and data + synchronization. + """ + return self.substrate.get_block_number(None) @lru_cache(maxsize=128) - def get_block_hash(self, block: Optional[int] = None) -> str: - return self.execute_coroutine( - coroutine=self.async_subtensor.get_block_hash(block=block), - ) + def _get_block_hash(self, block_id: int): + return self.substrate.get_block_hash(block_id) + + def get_block_hash(self, block: Optional[int] = None): + """ + Retrieves the hash of a specific block on the Bittensor blockchain. The block hash is a unique identifier + representing the cryptographic hash of the block's content, ensuring its integrity and immutability. + + Arguments: + block (int): The block number for which the hash is to be retrieved. + + Returns: + str: The cryptographic hash of the specified block. + + The block hash is a fundamental aspect of blockchain technology, providing a secure reference to each block's + data. It is crucial for verifying transactions, ensuring data consistency, and maintaining the + trustworthiness of the blockchain. + """ + if block: + return self._get_block_hash(block) + else: + return self.substrate.get_chain_head() + + def determine_block_hash(self, block: Optional[int]) -> Optional[str]: + if block is None: + return None + else: + return self.get_block_hash(block=block) + + def encode_params( + self, + call_definition: dict[str, list["ParamWithTypes"]], + params: Union[list[Any], dict[str, Any]], + ) -> str: + """Returns a hex encoded string of the params using their types.""" + param_data = scalecodec.ScaleBytes(b"") + + for i, param in enumerate(call_definition["params"]): + scale_obj = self.substrate.create_scale_object(param["type"]) + if isinstance(params, list): + param_data += scale_obj.encode(params[i]) + else: + if param["name"] not in params: + raise ValueError(f"Missing param {param['name']} in params dict.") + + param_data += scale_obj.encode(params[param["name"]]) + + return param_data.to_hex() + + def get_hyperparameter( + self, param_name: str, netuid: int, block: Optional[int] = None + ) -> Optional[Any]: + """ + Retrieves a specified hyperparameter for a specific subnet. + + Arguments: + param_name (str): The name of the hyperparameter to retrieve. + netuid (int): The unique identifier of the subnet. + block: the block number at which to retrieve the hyperparameter. + + Returns: + The value of the specified hyperparameter if the subnet exists, or None + """ + block_hash = self.determine_block_hash(block) + if not self.subnet_exists(netuid, block=block): + logging.error(f"subnet {netuid} does not exist") + return None + + result = self.substrate.query( + module="SubtensorModule", + storage_function=param_name, + params=[netuid], + block_hash=block_hash, + ) + + return getattr(result, "value", result) def get_children( self, hotkey: str, netuid: int, block: Optional[int] = None ) -> tuple[bool, list, str]: - return self.execute_coroutine( - self.async_subtensor.get_children( - hotkey=hotkey, netuid=netuid, block=block - ), - ) + """ + 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. + + Arguments: + hotkey (str): The hotkey value. + netuid (int): The netuid value. + block (Optional[int]): The block number for which the children are to be retrieved. + + Returns: + A tuple containing a boolean indicating success or failure, a list of formatted children, and an error + message (if applicable) + """ + try: + children = self.substrate.query( + module="SubtensorModule", + storage_function="ChildKeys", + params=[hotkey, netuid], + block_hash=self.determine_block_hash(block), + ) + if children: + formatted_children = [] + for proportion, child in children.value: + # Convert U64 to int + formatted_child = decode_account_id(child[0]) + int_proportion = int(proportion) + formatted_children.append((int_proportion, formatted_child)) + return True, formatted_children, "" + else: + return True, [], "" + except SubstrateRequestException as e: + return False, [], format_error_message(e) def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> str: - return self.execute_coroutine( - self.async_subtensor.get_commitment(netuid=netuid, uid=uid, block=block), - ) + """ + Retrieves the on-chain commitment for a specific neuron in the Bittensor network. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + uid (int): The unique identifier of the neuron. + block (Optional[int]): The block number to retrieve the commitment from. If None, the latest block is used. + Default is ``None``. + + Returns: + str: The commitment data as a string. + """ + metagraph = self.metagraph(netuid) + try: + hotkey = metagraph.hotkeys[uid] # type: ignore + except IndexError: + logging.error( + "Your uid is not in the hotkeys. Please double-check your UID." + ) + return "" + + metadata = get_metadata(self, netuid, hotkey, block) + try: + commitment = metadata["info"]["fields"][0] # type: ignore + hex_data = commitment[list(commitment.keys())[0]][2:] # type: ignore + return bytes.fromhex(hex_data).decode() + + except TypeError: + return "" def get_current_weight_commit_info( self, netuid: int, block: Optional[int] = None ) -> list: - return self.execute_coroutine( - self.async_subtensor.get_current_weight_commit_info( - netuid=netuid, block=block - ), + """ + Retrieves CRV3 weight commit information for a specific subnet. + + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. Default is ``None``. + + Returns: + list: A list of commit details, where each entry is a dictionary with keys 'who', 'serialized_commit', and + 'reveal_round', or an empty list if no data is found. + """ + result = self.substrate.query_map( + module="SubtensorModule", + storage_function="CRV3WeightCommits", + params=[netuid], + block_hash=self.determine_block_hash(block), ) + commits = result.records[0][1] if result.records else [] + return [WeightCommitInfo.from_vec_u8(commit) for commit in commits] + def get_delegate_by_hotkey( self, hotkey_ss58: str, block: Optional[int] = None ) -> Optional["DelegateInfo"]: - return self.execute_coroutine( - self.async_subtensor.get_delegate_by_hotkey( - hotkey_ss58=hotkey_ss58, block=block - ), + """ + Retrieves detailed information about a delegate neuron based on its hotkey. This function provides a + comprehensive view of the delegate's status, including its stakes, nominators, and reward distribution. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the delegate's hotkey. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[DelegateInfo]: Detailed information about the delegate neuron, ``None`` if not found. + + This function is essential for understanding the roles and influence of delegate neurons within the Bittensor + network's consensus and governance structures. + """ + + block_hash = self.determine_block_hash(block) + encoded_hotkey = ss58_to_vec_u8(hotkey_ss58) + + json_body = self.substrate.rpc_request( + method="delegateInfo_getDelegate", # custom rpc method + params=([encoded_hotkey, block_hash] if block_hash else [encoded_hotkey]), ) + if not (result := json_body.get("result", None)): + return None + + return DelegateInfo.from_vec_u8(bytes(result)) + def get_delegate_identities( self, block: Optional[int] = None ) -> dict[str, "DelegatesDetails"]: - return self.execute_coroutine( - self.async_subtensor.get_delegate_identities(block=block), - ) + """ + 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. + + Arguments: + block (Optional[int]): The blockchain block number for the query. + + Returns: + Dict {ss58: DelegatesDetails, ...} + + """ + block_hash = self.determine_block_hash(block) + response = requests.get(DELEGATES_DETAILS_URL) + identities_info = self.substrate.query_map( + module="Registry", storage_function="IdentityOf", block_hash=block_hash + ) + + all_delegates_details = {} + for ss58_address, identity in identities_info: + all_delegates_details.update( + { + decode_account_id( + ss58_address[0] + ): DelegatesDetails.from_chain_data( + decode_hex_identity_dict(identity.value["info"]) + ) + } + ) + if response.ok: + all_delegates: dict[str, Any] = json.loads(response.content) + + for delegate_hotkey, delegate_details in all_delegates.items(): + delegate_info = all_delegates_details.setdefault( + delegate_hotkey, + DelegatesDetails( + display=delegate_details.get("name", ""), + web=delegate_details.get("url", ""), + additional=delegate_details.get("description", ""), + pgp_fingerprint=delegate_details.get("fingerprint", ""), + ), + ) + delegate_info.display = delegate_info.display or delegate_details.get( + "name", "" + ) + delegate_info.web = delegate_info.web or delegate_details.get("url", "") + delegate_info.additional = ( + delegate_info.additional or delegate_details.get("description", "") + ) + delegate_info.pgp_fingerprint = ( + delegate_info.pgp_fingerprint + or delegate_details.get("fingerprint", "") + ) + + return all_delegates_details def get_delegate_take( self, hotkey_ss58: str, block: Optional[int] = None ) -> Optional[float]: - return self.execute_coroutine( - self.async_subtensor.get_delegate_take( - hotkey_ss58=hotkey_ss58, block=block - ), + """ + Retrieves the delegate 'take' percentage for a neuron identified by its hotkey. The 'take' represents the + percentage of rewards that the delegate claims from its nominators' stakes. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[float]: The delegate take percentage, None if not available. + + The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of + rewards among neurons and their nominators. + """ + result = self.query_subtensor( + name="Delegates", + block=block, + params=[hotkey_ss58], + ) + return ( + None + if result is None + else u16_normalized_float(getattr(result, "value", 0)) ) def get_delegated( self, coldkey_ss58: str, block: Optional[int] = None ) -> list[tuple["DelegateInfo", "Balance"]]: - return self.execute_coroutine( - self.async_subtensor.get_delegated(coldkey_ss58=coldkey_ss58, block=block), + """ + 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. + + Arguments: + coldkey_ss58 (str): The `SS58` address of the account's coldkey. + block (Optional[int]): The blockchain block number for the query. + + 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. + """ + + block_hash = self.determine_block_hash(block) + encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) + json_body = self.substrate.rpc_request( + method="delegateInfo_getDelegated", + params=([block_hash, encoded_coldkey] if block_hash else [encoded_coldkey]), ) + if not (result := json_body.get("result")): + return [] + + return DelegateInfo.delegated_list_from_vec_u8(bytes(result)) + def get_delegates(self, block: Optional[int] = None) -> list["DelegateInfo"]: - return self.execute_coroutine( - self.async_subtensor.get_delegates(block=block), - ) + """ + Fetches all delegates on the chain + + Arguments: + block (Optional[int]): The blockchain block number for the query. + + Returns: + List of DelegateInfo objects, or an empty list if there are no delegates. + """ + hex_bytes_result = self.query_runtime_api( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegates", + params=[], + block=block, + ) + if hex_bytes_result: + return DelegateInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) + else: + return [] def get_existential_deposit( self, block: Optional[int] = None ) -> Optional["Balance"]: - return self.execute_coroutine( - self.async_subtensor.get_existential_deposit(block=block), + """ + 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. + + Arguments: + block (Optional[int]): The blockchain block number for the query. + + 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. + """ + result = self.substrate.get_constant( + module_name="Balances", + constant_name="ExistentialDeposit", + block_hash=self.determine_block_hash(block), ) + if result is None: + raise Exception("Unable to retrieve existential deposit amount.") + + return Balance.from_rao(getattr(result, "value", 0)) + def get_hotkey_owner( self, hotkey_ss58: str, block: Optional[int] = None ) -> Optional[str]: - return self.execute_coroutine( - self.async_subtensor.get_hotkey_owner(hotkey_ss58=hotkey_ss58, block=block), - ) + """ + 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. + + Arguments: + hotkey_ss58 (str): The SS58 address of the hotkey. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[str]: The SS58 address of the owner if the hotkey exists, or None if it doesn't. + """ + hk_owner_query = self.substrate.query( + module="SubtensorModule", + storage_function="Owner", + params=[hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + exists = False + val = None + if hasattr(hk_owner_query, "value"): + val = decode_account_id(hk_owner_query.value[0]) + if val: + exists = self.does_hotkey_exist(hotkey_ss58, block=block) + hotkey_owner = val if exists else None + return hotkey_owner def get_minimum_required_stake(self) -> "Balance": - return self.execute_coroutine( - self.async_subtensor.get_minimum_required_stake(), + """ + Returns the minimum required stake for nominators in the Subtensor network. + This method retries the substrate call up to three times with exponential backoff in case of failures. + + Returns: + Balance: The minimum required stake as a Balance object. + + Raises: + Exception: If the substrate call fails after the maximum number of retries. + """ + result = self.substrate.query( + module="SubtensorModule", storage_function="NominatorMinRequiredStake" ) + return Balance.from_rao(getattr(result, "value", 0)) + def get_netuids_for_hotkey( - self, hotkey_ss58: str, block: Optional[int] = None, reuse_block: bool = False + self, hotkey_ss58: str, block: Optional[int] = None ) -> list[int]: - return self.execute_coroutine( - self.async_subtensor.get_netuids_for_hotkey( - hotkey_ss58=hotkey_ss58, block=block, reuse_block=reuse_block - ), - ) + """ + 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. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + block (Optional[int]): The blockchain block number for the query. + + Returns: + A list of netuids where the neuron is a member. + """ + result = self.substrate.query_map( + module="SubtensorModule", + storage_function="IsNetworkMember", + params=[hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + netuids = [] + if result.records: + for record in result: + if record[1].value: + netuids.append(record[0]) + return netuids def get_neuron_certificate( self, hotkey: str, netuid: int, block: Optional[int] = None ) -> Optional["Certificate"]: - return self.execute_coroutine( - self.async_subtensor.get_neuron_certificate(hotkey, netuid, block=block), - ) + """ + Retrieves the TLS certificate for a specific neuron identified by its unique identifier (UID) within a + specified subnet (netuid) of the Bittensor network. + + Arguments: + hotkey: The hotkey to query. + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + the certificate of the neuron if found, `None` otherwise. + + This function is used for certificate discovery for setting up mutual tls communication between neurons. + """ + certificate = self.query_module( + module="SubtensorModule", + name="NeuronCertificates", + block=block, + params=[netuid, hotkey], + ) + try: + if certificate: + tuple_ascii = certificate["public_key"][0] + return chr(certificate["algorithm"]) + "".join( + chr(i) for i in tuple_ascii + ) + except AttributeError: + return None + return None def get_neuron_for_pubkey_and_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None ) -> Optional["NeuronInfo"]: - return self.execute_coroutine( - self.async_subtensor.get_neuron_for_pubkey_and_subnet( - hotkey_ss58, netuid, block=block - ), + """ + Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific subnet UID + (netuid). This function provides detailed neuron information for a particular subnet within the Bittensor + network. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[bittensor.core.chain_data.neuron_info.NeuronInfo]: Detailed information about the neuron if found, + ``None`` otherwise. + + This function is crucial for accessing specific neuron data and understanding its status, stake, and other + attributes within a particular subnet of the Bittensor ecosystem. + """ + block_hash = self.determine_block_hash(block) + uid = self.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, hotkey_ss58], + block_hash=block_hash, + ) + if uid is None: + return NeuronInfo.get_null_neuron() + + params = [netuid, uid.value] + json_body = self.substrate.rpc_request( + method="neuronInfo_getNeuron", params=params, block_hash=block_hash ) + if not (result := json_body.get("result", None)): + return NeuronInfo.get_null_neuron() + + return NeuronInfo.from_vec_u8(bytes(result)) + def get_stake_for_coldkey_and_hotkey( self, hotkey_ss58: str, coldkey_ss58: str, block: Optional[int] = None ) -> Optional["Balance"]: - return self.execute_coroutine( - self.async_subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey_ss58, coldkey_ss58=coldkey_ss58, block=block - ), + """ + Retrieves stake information associated with a specific coldkey and hotkey. + + Arguments: + hotkey_ss58 (str): the hotkey SS58 address to query + coldkey_ss58 (str): the coldkey SS58 address to query + block (Optional[int]): the block number to query + + Returns: + Stake Balance for the given coldkey and hotkey + """ + result = self.substrate.query( + module="SubtensorModule", + storage_function="Stake", + params=[hotkey_ss58, coldkey_ss58], + block_hash=self.determine_block_hash(block), ) + return Balance.from_rao(getattr(result, "value", 0)) def get_stake_info_for_coldkey( self, coldkey_ss58: str, block: Optional[int] = None ) -> list["StakeInfo"]: - return self.execute_coroutine( - self.async_subtensor.get_stake_info_for_coldkey( - coldkey_ss58=coldkey_ss58, block=block - ), + """ + 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. + + Arguments: + coldkey_ss58 (str): The ``SS58`` address of the account's coldkey. + block (Optional[int]): The blockchain block number for the query. + + Returns: + A list of StakeInfo objects detailing the stake allocations for the account. + + Stake information is vital for account holders to assess their investment and participation in the network's + delegation and consensus processes. + """ + encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) + + hex_bytes_result = self.query_runtime_api( + runtime_api="StakeInfoRuntimeApi", + method="get_stake_info_for_coldkey", + params=[encoded_coldkey], + block=block, ) + if not hex_bytes_result: + return [] + + return StakeInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) + def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[str]: - return self.execute_coroutine( - self.async_subtensor.get_subnet_burn_cost(block=block), + """ + 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. + + Arguments: + block (Optional[int]): The blockchain block number for the query. + + Returns: + int: The burn cost for subnet registration. + + The subnet burn cost is an important economic parameter, reflecting the network's mechanisms for controlling + the proliferation of subnets and ensuring their commitment to the network's long-term viability. + """ + lock_cost = self.query_runtime_api( + runtime_api="SubnetRegistrationRuntimeApi", + method="get_network_registration_cost", + params=[], + block=block, ) + return lock_cost + def get_subnet_hyperparameters( self, netuid: int, block: Optional[int] = None ) -> Optional[Union[list, "SubnetHyperparameters"]]: - return self.execute_coroutine( - self.async_subtensor.get_subnet_hyperparameters(netuid=netuid, block=block), + """ + Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters define + the operational settings and rules governing the subnet's behavior. + + Arguments: + netuid (int): The network UID of the subnet to query. + block (Optional[int]): The blockchain block number for the query. + + 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. + """ + hex_bytes_result = self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams", + params=[netuid], + block=block, ) + if not hex_bytes_result: + return None + + return SubnetHyperparameters.from_vec_u8(hex_to_bytes(hex_bytes_result)) + def get_subnet_reveal_period_epochs( self, netuid: int, block: Optional[int] = None ) -> int: - return self.execute_coroutine( - self.async_subtensor.get_subnet_reveal_period_epochs( - netuid=netuid, block=block + """Retrieve the SubnetRevealPeriodEpochs hyperparameter.""" + return cast( + int, + self.get_hyperparameter( + param_name="RevealPeriodEpochs", block=block, netuid=netuid ), ) def get_subnets(self, block: Optional[int] = None) -> list[int]: - return self.execute_coroutine( - self.async_subtensor.get_subnets(block=block), - ) + """ + Retrieves the list of all subnet unique identifiers (netuids) currently present in the Bittensor network. + + Arguments: + block (Optional[int]): The blockchain block number for the query. + + 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. + """ + result = self.substrate.query_map( + module="SubtensorModule", + storage_function="NetworksAdded", + block_hash=self.determine_block_hash(block), + ) + subnets = [] + if result.records: + for netuid, exists in result: + if exists: + subnets.append(netuid) + return subnets def get_total_stake_for_coldkey( self, ss58_address: str, block: Optional[int] = None ) -> "Balance": - result = self.execute_coroutine( - self.async_subtensor.get_total_stake_for_coldkey(ss58_address, block=block), + """ + Returns the total stake held on a coldkey. + + Arguments: + ss58_address (str): The SS58 address of the coldkey + block (Optional[int]): The blockchain block number for the query. + + Returns: + Balance of the stake held on the coldkey. + """ + result = self.substrate.query( + module="SubtensorModule", + storage_function="TotalColdkeyStake", + params=[ss58_address], + block_hash=self.determine_block_hash(block), ) - return result + return Balance.from_rao(getattr(result, "value", 0)) def get_total_stake_for_coldkeys( self, *ss58_addresses: str, block: Optional[int] = None ) -> dict[str, "Balance"]: - return self.execute_coroutine( - self.async_subtensor.get_total_stake_for_coldkeys( - *ss58_addresses, block=block - ), - ) + """ + Returns the total stake held on multiple coldkeys. + + Arguments: + ss58_addresses (tuple[str]): The SS58 address(es) of the coldkey(s) + block (Optional[int]): The blockchain block number for the query. + + Returns: + Dict in view {address: Balance objects}. + """ + if not (block_hash := self.determine_block_hash(block)): + block_hash = self.substrate.get_chain_head() + calls = [ + ( + self.substrate.create_storage_key( + "SubtensorModule", + "TotalColdkeyStake", + [address], + block_hash=block_hash, + ) + ) + for address in ss58_addresses + ] + batch_call = self.substrate.query_multi(calls, block_hash=block_hash) + results = {} + for item in batch_call: + results.update({item[0].params[0]: Balance.from_rao(item[1] or 0)}) + return results def get_total_stake_for_hotkey( self, ss58_address: str, block: Optional[int] = None ) -> "Balance": - result = self.execute_coroutine( - self.async_subtensor.get_total_stake_for_hotkey(ss58_address, block=block), + """ + Returns the total stake held on a hotkey. + + Arguments: + ss58_address (str): The SS58 address of the hotkey + block (Optional[int]): The blockchain block number for the query. + + Returns: + Balance of the stake held on the hotkey. + """ + result = self.substrate.query( + module="SubtensorModule", + storage_function="TotalHotkeyStake", + params=[ss58_address], + block_hash=self.determine_block_hash(block), ) - return result + return Balance.from_rao(getattr(result, "value", 0)) def get_total_stake_for_hotkeys( self, *ss58_addresses: str, block: Optional[int] = None ) -> dict[str, "Balance"]: - return self.execute_coroutine( - self.async_subtensor.get_total_stake_for_hotkeys( - *ss58_addresses, block=block - ), + """ + Returns the total stake held on hotkeys. + + Arguments: + ss58_addresses (tuple[str]): The SS58 address(es) of the hotkey(s) + block (Optional[int]): The blockchain block number for the query. + + Returns: + Dict {address: Balance objects}. + """ + results = self.substrate.query_multiple( + params=[s for s in ss58_addresses], + module="SubtensorModule", + storage_function="TotalHotkeyStake", + block_hash=self.determine_block_hash(block), ) + return {k: Balance.from_rao(r or 0) for (k, r) in results.items()} def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: - return self.execute_coroutine( - self.async_subtensor.get_total_subnets(block=block), + """ + Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. + + Arguments: + block (Optional[int]): The blockchain block number for the query. + + 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 = self.substrate.query( + module="SubtensorModule", + storage_function="TotalNetworks", + params=[], + block_hash=self.determine_block_hash(block), ) + return getattr(result, "value", None) def get_transfer_fee( self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] ) -> "Balance": - return self.execute_coroutine( - self.async_subtensor.get_transfer_fee( - wallet=wallet, dest=dest, value=value - ), - ) + """ + 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. + + Arguments: + wallet (bittensor_wallet.Wallet): The wallet from which the transfer is initiated. + dest (str): The ``SS58`` address of the destination account. + value (Union[bittensor.utils.balance.Balance, float, int]): The amount of tokens to be transferred, + specified as a Balance object, or in Tao (float) or Rao (int) units. + + Returns: + bittensor.utils.balance.Balance: The estimated transaction fee for the transfer, represented as a Balance + object. + + Estimating the transfer fee is essential for planning and executing token transactions, ensuring that the wallet + has sufficient funds to cover both the transfer amount and the associated costs. This function provides a + crucial tool for managing financial operations within the Bittensor network. + """ + if isinstance(value, float): + value = Balance.from_tao(value) + elif isinstance(value, int): + value = Balance.from_rao(value) + + if isinstance(value, Balance): + call = self.substrate.compose_call( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": dest, "value": str(value.rao)}, + ) + + try: + payment_info = self.substrate.get_payment_info( + call=call, keypair=wallet.coldkeypub + ) + except Exception as e: + logging.error( + f":cross_mark: [red]Failed to get payment info: [/red]{e}" + ) + payment_info = {"partialFee": int(2e7)} # assume 0.02 Tao + + return Balance.from_rao(payment_info["partialFee"]) + else: + fee = Balance.from_rao(int(2e7)) + logging.error( + "To calculate the transaction fee, the value must be Balance, float, or int. Received type: %s. Fee " + "is %s", + type(value), + 2e7, + ) + return fee def get_vote_data( self, proposal_hash: str, block: Optional[int] = None ) -> Optional["ProposalVoteData"]: - return self.execute_coroutine( - self.async_subtensor.get_vote_data( - proposal_hash=proposal_hash, block=block - ), - ) + """ + Retrieves the voting data for a specific proposal on the Bittensor blockchain. This data includes information + about how senate members have voted on the proposal. + + Arguments: + proposal_hash (str): The hash of the proposal for which voting data is requested. + block (Optional[int]): The blockchain block number for the query. + + 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. + """ + vote_data = self.substrate.query( + module="Triumvirate", + storage_function="Voting", + params=[proposal_hash], + block_hash=self.determine_block_hash(block), + ) + if vote_data is None: + return None + else: + return ProposalVoteData(vote_data) def get_uid_for_hotkey_on_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None ) -> Optional[int]: - return self.execute_coroutine( - self.async_subtensor.get_uid_for_hotkey_on_subnet( - hotkey_ss58=hotkey_ss58, netuid=netuid, block=block - ), + """ + Retrieves the unique identifier (UID) for a neuron's hotkey on a specific subnet. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The UID of the neuron if it is registered on the subnet, ``None`` otherwise. + + The UID is a critical identifier within the network, linking the neuron's hotkey to its operational and + governance activities on a particular subnet. + """ + result = self.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, hotkey_ss58], + block_hash=self.determine_block_hash(block), ) + return getattr(result, "value", result) def filter_netuids_by_registered_hotkeys( self, @@ -464,28 +1469,90 @@ def filter_netuids_by_registered_hotkeys( all_hotkeys: Iterable["Wallet"], block: Optional[int], ) -> list[int]: - return self.execute_coroutine( - self.async_subtensor.filter_netuids_by_registered_hotkeys( - all_netuids=all_netuids, - filter_for_netuids=filter_for_netuids, - all_hotkeys=all_hotkeys, - block=block, - ), - ) + """ + Filters a given list of all netuids for certain specified netuids and hotkeys + + Arguments: + all_netuids (Iterable[int]): A list of netuids to filter. + filter_for_netuids (Iterable[int]): A subset of all_netuids to filter from the main list. + all_hotkeys (Iterable[Wallet]): Hotkeys to filter from the main list. + block (Optional[int]): The blockchain block number for the query. + + Returns: + The filtered list of netuids. + """ + self._get_block_hash(block) # just used to cache the block hash + netuids_with_registered_hotkeys = [ + item + for sublist in [ + self.get_netuids_for_hotkey( + wallet.hotkey.ss58_address, + block=block, + ) + for wallet in all_hotkeys + ] + for item in sublist + ] + + if not filter_for_netuids: + all_netuids = netuids_with_registered_hotkeys + + else: + filtered_netuids = [ + netuid for netuid in all_netuids if netuid in filter_for_netuids + ] + + registered_hotkeys_filtered = [ + netuid + for netuid in netuids_with_registered_hotkeys + if netuid in filter_for_netuids + ] + + # Combine both filtered lists + all_netuids = filtered_netuids + registered_hotkeys_filtered + + return list(set(all_netuids)) def immunity_period( self, netuid: int, block: Optional[int] = None ) -> Optional[int]: - return self.execute_coroutine( - self.async_subtensor.immunity_period(netuid=netuid, block=block), + """ + Retrieves the 'ImmunityPeriod' hyperparameter for a specific subnet. This parameter defines the duration during + which new neurons are protected from certain network penalties or restrictions. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The value of the 'ImmunityPeriod' hyperparameter if the subnet exists, ``None`` otherwise. + + The 'ImmunityPeriod' is a critical aspect of the network's governance system, ensuring that new participants + have a grace period to establish themselves and contribute to the network without facing immediate + punitive actions. + """ + call = self.get_hyperparameter( + param_name="ImmunityPeriod", netuid=netuid, block=block ) + return None if call is None else int(call) def is_hotkey_delegate(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: - return self.execute_coroutine( - self.async_subtensor.is_hotkey_delegate( - hotkey_ss58=hotkey_ss58, block=block - ), - ) + """ + Determines whether a given hotkey (public key) is a delegate on the Bittensor network. This function checks if + the neuron associated with the hotkey is part of the network's delegation system. + + Arguments: + hotkey_ss58 (str): The SS58 address of the neuron's hotkey. + block (Optional[int]): The blockchain block number for the query. + + 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. + """ + delegates = self.get_delegates(block) + return hotkey_ss58 in [info.hotkey_ss58 for info in delegates] def is_hotkey_registered( self, @@ -493,40 +1560,90 @@ def is_hotkey_registered( netuid: Optional[int] = None, block: Optional[int] = None, ) -> bool: - return self.execute_coroutine( - self.async_subtensor.is_hotkey_registered( - hotkey_ss58=hotkey_ss58, netuid=netuid, block=block - ), - ) + """ + Determines whether a given hotkey (public key) is registered in the Bittensor network, either globally across + any subnet or specifically on a specified subnet. This function checks the registration status of a neuron + identified by its hotkey, which is crucial for validating its participation and activities within the + network. + + Args: + hotkey_ss58: The SS58 address of the neuron's hotkey. + netuid: The unique identifier of the subnet to check the registration. If `None`, the + registration is checked across all subnets. + block: The blockchain block number at which to perform the query. + + Returns: + bool: `True` if the hotkey is registered in the specified context (either any subnet or a specific subnet), + `False` otherwise. + + This function is important for verifying the active status of neurons in the Bittensor network. It aids in + understanding whether a neuron is eligible to participate in network processes such as consensus, + validation, and incentive distribution based on its registration status. + """ + if netuid is None: + return self.is_hotkey_registered_any(hotkey_ss58, block) + else: + return self.is_hotkey_registered_on_subnet(hotkey_ss58, netuid, block) def is_hotkey_registered_any( self, hotkey_ss58: str, block: Optional[int] = None, ) -> bool: - return self.execute_coroutine( - self.async_subtensor.is_hotkey_registered_any( - hotkey_ss58=hotkey_ss58, - block=block, - ), - ) + """ + Checks if a neuron's hotkey is registered on any subnet within the Bittensor network. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + block (Optional[int]): The blockchain block number for the query. + + Returns: + bool: ``True`` if the hotkey is registered on any subnet, False otherwise. + + This function is essential for determining the network-wide presence and participation of a neuron. + """ + hotkeys = self.get_netuids_for_hotkey(hotkey_ss58, block) + return len(hotkeys) > 0 def is_hotkey_registered_on_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None ) -> bool: - return self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block) is not None + """Checks if the hotkey is registered on a given netuid.""" + return ( + self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block=block) + is not None + ) def last_drand_round(self) -> Optional[int]: - return self.execute_coroutine( - self.async_subtensor.last_drand_round(), + """ + Retrieves the last drand round emitted in bittensor. This corresponds when committed weights will be revealed. + + Returns: + int: The latest Drand round emitted in bittensor. + """ + result = self.substrate.query( + module="Drand", storage_function="LastStoredRound" ) + return getattr(result, "value", None) def max_weight_limit( self, netuid: int, block: Optional[int] = None ) -> Optional[float]: - return self.execute_coroutine( - self.async_subtensor.max_weight_limit(netuid=netuid, block=block), + """ + Returns network MaxWeightsLimit hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[float]: The value of the MaxWeightsLimit hyperparameter, or ``None`` if the subnetwork does not + exist or the parameter is not found. + """ + call = self.get_hyperparameter( + param_name="MaxWeightsLimit", netuid=netuid, block=block ) + return None if call is None else u16_normalized_float(int(call)) def metagraph( self, netuid: int, lite: bool = True, block: Optional[int] = None @@ -545,72 +1662,333 @@ def metagraph( def min_allowed_weights( self, netuid: int, block: Optional[int] = None ) -> Optional[int]: - return self.execute_coroutine( - self.async_subtensor.min_allowed_weights(netuid=netuid, block=block), + """ + Returns network MinAllowedWeights hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The value of the MinAllowedWeights hyperparameter, or ``None`` if the subnetwork does not + exist or the parameter is not found. + """ + call = self.get_hyperparameter( + param_name="MinAllowedWeights", netuid=netuid, block=block ) + return None if call is None else int(call) def neuron_for_uid( self, uid: int, netuid: int, block: Optional[int] = None ) -> "NeuronInfo": - return self.execute_coroutine( - self.async_subtensor.neuron_for_uid(uid=uid, netuid=netuid, block=block), + """ + Retrieves detailed information about a specific neuron identified by its unique identifier (UID) within a + specified subnet (netuid) of the Bittensor network. This function provides a comprehensive view of a + neuron's attributes, including its stake, rank, and operational status. + + Arguments: + uid (int): The unique identifier of the neuron. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Detailed information about the neuron if found, a null neuron otherwise + + This function is crucial for analyzing individual neurons' contributions and status within a specific subnet, + offering insights into their roles in the network's consensus and validation mechanisms. + """ + if uid is None: + return NeuronInfo.get_null_neuron() + + block_hash = self.determine_block_hash(block) + + params = [netuid, uid, block_hash] if block_hash else [netuid, uid] + json_body = self.substrate.rpc_request( + method="neuronInfo_getNeuron", # custom rpc method + params=params, ) + if not (result := json_body.get("result", None)): + return NeuronInfo.get_null_neuron() + + bytes_result = bytes(result) + return NeuronInfo.from_vec_u8(bytes_result) def neurons(self, netuid: int, block: Optional[int] = None) -> list["NeuronInfo"]: - return self.execute_coroutine( - self.async_subtensor.neurons(netuid=netuid, block=block), + """ + 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. + + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + 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. + """ + hex_bytes_result = self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons", + params=[netuid], + block=block, ) + if not hex_bytes_result: + return [] + + return NeuronInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) + def neurons_lite( self, netuid: int, block: Optional[int] = None ) -> list["NeuronInfoLite"]: - return self.execute_coroutine( - self.async_subtensor.neurons_lite(netuid=netuid, block=block), - ) - - def query_identity(self, key: str, block: Optional[int] = None) -> Optional[str]: - return self.execute_coroutine( - self.async_subtensor.query_identity(key=key, block=block), - ) + """ + 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. + + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + 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. + """ + hex_bytes_result = self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons_lite", + params=[ + netuid + ], # TODO check to see if this can accept more than one at a time + block=block, + ) + + if not hex_bytes_result: + return [] + + return NeuronInfoLite.list_from_vec_u8(hex_to_bytes(hex_bytes_result)) + + def query_identity(self, key: str, block: Optional[int] = None) -> 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. + + Arguments: + key (str): The key used to query the neuron's identity, typically the neuron's SS58 address. + block (Optional[int]): The blockchain block number for the query. + + Returns: + 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. + + Note: + See the `Bittensor CLI documentation `_ for supported identity + parameters. + """ + identity_info = self.substrate.query( + module="Registry", + storage_function="IdentityOf", + params=[key], + block_hash=self.determine_block_hash(block), + ) + try: + return _decode_hex_identity_dict(identity_info["info"]) + except TypeError: + return {} def recycle(self, netuid: int, block: Optional[int] = None) -> Optional["Balance"]: - return self.execute_coroutine( - self.async_subtensor.recycle(netuid=netuid, block=block), - ) + """ + Retrieves the 'Burn' hyperparameter for a specified subnet. The 'Burn' parameter represents the amount of Tao + that is effectively recycled within the Bittensor network. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[Balance]: The value of the 'Burn' hyperparameter if the subnet exists, None otherwise. + + Understanding the 'Burn' rate is essential for analyzing the network registration usage, particularly how it is + correlated with user activity and the overall cost of participation in a given subnet. + """ + call = self.get_hyperparameter(param_name="Burn", netuid=netuid, block=block) + return None if call is None else Balance.from_rao(int(call)) def subnet_exists(self, netuid: int, block: Optional[int] = None) -> bool: - return self.execute_coroutine( - self.async_subtensor.subnet_exists(netuid=netuid, block=block), + """ + Checks if a subnet with the specified unique identifier (netuid) exists within the Bittensor network. + + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + 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. + """ + result = self.substrate.query( + module="SubtensorModule", + storage_function="NetworksAdded", + params=[netuid], + block_hash=self.determine_block_hash(block), ) + return getattr(result, "value", False) def subnetwork_n(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - return self.execute_coroutine( - self.async_subtensor.subnetwork_n(netuid=netuid, block=block), + """ + Returns network SubnetworkN hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The value of the SubnetworkN hyperparameter, or ``None`` if the subnetwork does not exist or + the parameter is not found. + """ + call = self.get_hyperparameter( + param_name="SubnetworkN", netuid=netuid, block=block ) + return None if call is None else int(call) def tempo(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - return self.execute_coroutine( - self.async_subtensor.tempo(netuid=netuid, block=block), - ) + """ + Returns network Tempo hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The value of the Tempo hyperparameter, or ``None`` if the subnetwork does not exist or the + parameter is not found. + """ + call = self.get_hyperparameter(param_name="Tempo", netuid=netuid, block=block) + return None if call is None else int(call) def tx_rate_limit(self, block: Optional[int] = None) -> Optional[int]: - return self.execute_coroutine( - self.async_subtensor.tx_rate_limit(block=block), - ) + """ + Retrieves the transaction rate limit for the Bittensor network as of a specific blockchain block. + This rate limit sets the maximum number of transactions that can be processed within a given time frame. + + Args: + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The transaction rate limit of the network, None if not available. + + The transaction rate limit is an essential parameter for ensuring the stability and scalability of the Bittensor + network. It helps in managing network load and preventing congestion, thereby maintaining efficient and + timely transaction processing. + """ + result = self.query_subtensor("TxRateLimit", block=block) + return getattr(result, "value", None) def weights( self, netuid: int, block: Optional[int] = None ) -> list[tuple[int, list[tuple[int, int]]]]: - return self.execute_coroutine( - self.async_subtensor.weights(netuid=netuid, block=block), + """ + 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. + + Arguments: + netuid (int): The network UID of the subnet to query. + block (Optional[int]): Block number for synchronization, or ``None`` for the latest block. + + 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. + """ + w_map_encoded = self.substrate.query_map( + module="SubtensorModule", + storage_function="Weights", + params=[netuid], + block_hash=self.determine_block_hash(block), ) + w_map = [(uid, w.value or []) for uid, w in w_map_encoded] + + return w_map def weights_rate_limit( self, netuid: int, block: Optional[int] = None ) -> Optional[int]: - return self.execute_coroutine( - self.async_subtensor.weights_rate_limit(netuid=netuid, block=block), + """ + Returns network WeightsSetRateLimit hyperparameter. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The value of the WeightsSetRateLimit hyperparameter, or ``None`` if the subnetwork does not + exist or the parameter is not found. + """ + call = self.get_hyperparameter( + param_name="WeightsSetRateLimit", netuid=netuid, block=block ) + return None if call is None else int(call) + + # Extrinsics helper ================================================================================================ + + def sign_and_send_extrinsic( + self, + call: "GenericCall", + wallet: "Wallet", + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + sign_with: str = "coldkey", + ) -> tuple[bool, str]: + """ + Helper method to sign and submit an extrinsic call to chain. + + Arguments: + call (scalecodec.types.GenericCall): a prepared Call object + wallet (bittensor_wallet.Wallet): the wallet whose coldkey will be used to sign the extrinsic + wait_for_inclusion (bool): whether to wait until the extrinsic call is included on the chain + wait_for_finalization (bool): whether to wait until the extrinsic call is finalized on the chain + sign_with: the wallet's keypair to use for the signing. Options are "coldkey", "hotkey", "coldkeypub" + + Returns: + (success, error message) + """ + if sign_with not in ("coldkey", "hotkey", "coldkeypub"): + raise AttributeError( + f"'sign_with' must be either 'coldkey', 'hotkey' or 'coldkeypub', not '{sign_with}'" + ) + + extrinsic = self.substrate.create_signed_extrinsic( + call=call, keypair=getattr(wallet, sign_with) + ) + try: + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True, "" + + if response.is_success: + return True, "" + + return False, format_error_message(response.error_message) + + except SubstrateRequestException as e: + return False, format_error_message(e) # Extrinsics ======================================================================================================= @@ -622,14 +2000,31 @@ def add_stake( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - return self.execute_coroutine( - self.async_subtensor.add_stake( - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), + """ + Adds the specified amount of stake to a neuron identified by the hotkey ``SS58`` address. + Staking is a fundamental process in the Bittensor network that enables neurons to participate actively and earn + incentives. + + Args: + wallet (bittensor_wallet.Wallet): The wallet to be used for staking. + hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey associated with the neuron. + amount (Union[Balance, float]): The amount of TAO to stake. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + + Returns: + bool: ``True`` if the staking is successful, False otherwise. + + This function enables neurons to increase their stake in the network, enhancing their influence and potential + rewards in line with Bittensor's consensus and reward mechanisms. + """ + return add_stake_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + amount=amount, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) def add_stake_multiple( @@ -640,14 +2035,30 @@ def add_stake_multiple( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - return self.execute_coroutine( - self.async_subtensor.add_stake_multiple( - wallet=wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), + """ + Adds stakes to multiple neurons identified by their hotkey SS58 addresses. + This bulk operation allows for efficient staking across different neurons from a single wallet. + + Args: + wallet (bittensor_wallet.Wallet): The wallet used for staking. + hotkey_ss58s (list[str]): List of ``SS58`` addresses of hotkeys to stake to. + amounts (list[Union[Balance, float]]): Corresponding amounts of TAO to stake for each hotkey. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + + Returns: + bool: ``True`` if the staking is successful for all specified neurons, False otherwise. + + This function is essential for managing stakes across multiple neurons, reflecting the dynamic and collaborative + nature of the Bittensor network. + """ + return add_stake_multiple_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) def burned_register( @@ -657,13 +2068,27 @@ def burned_register( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> bool: - return self.execute_coroutine( - self.async_subtensor.burned_register( - wallet=wallet, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), + """ + Registers a neuron on the Bittensor network by recycling TAO. This method of registration involves recycling + TAO tokens, allowing them to be re-mined by performing work on the network. + + Args: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron to be registered. + netuid (int): The unique identifier of the subnet. + wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Defaults to + `False`. + wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. + Defaults to `True`. + + Returns: + bool: ``True`` if the registration is successful, False otherwise. + """ + return burned_register_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) def commit_weights( @@ -678,19 +2103,67 @@ def commit_weights( wait_for_finalization: bool = False, max_retries: int = 5, ) -> tuple[bool, str]: - return self.execute_coroutine( - self.async_subtensor.commit_weights( - wallet=wallet, - netuid=netuid, - salt=salt, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - max_retries=max_retries, - ), - ) + """ + Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. + This action serves as a commitment or snapshot of the neuron's current weight distribution. + + Arguments: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the weights. + netuid (int): The unique identifier of the subnet. + salt (list[int]): list of randomly generated integers as salt to generated weighted hash. + uids (np.ndarray): NumPy array of neuron UIDs for which weights are being committed. + weights (np.ndarray): NumPy array of weight values corresponding to each UID. + version_key (int): Version key for compatibility with the network. Default is ``int representation of + Bittensor version.``. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is + ``False``. + max_retries (int): The number of maximum attempts to commit weights. Default is ``5``. + + Returns: + 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}, " + f"version_key={version_key}" + ) + + # Generate the hash of the weights + commit_hash = generate_weight_hash( + address=wallet.hotkey.ss58_address, + netuid=netuid, + uids=list(uids), + values=list(weights), + salt=salt, + version_key=version_key, + ) + + while retries < max_retries and success is False: + try: + success, message = 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 def register( self, @@ -707,21 +2180,48 @@ def register( update_interval: Optional[int] = None, log_verbose: bool = False, ) -> bool: - return self.execute_coroutine( - self.async_subtensor.register( - wallet=wallet, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - max_allowed_attempts=max_allowed_attempts, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ), + """ + Registers a neuron on the Bittensor network using the provided wallet. + + Registration is a critical step for a neuron to become an active participant in the network, enabling it to + stake, set weights, and receive incentives. + + Args: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron to be registered. + netuid (int): The unique identifier of the subnet. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. Defaults to `False`. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Defaults to + `True`. + max_allowed_attempts (int): Maximum number of attempts to register the wallet. + output_in_place (bool): If true, prints the progress of the proof of work to the console in-place. Meaning + the progress is printed on the same lines. Defaults to `True`. + cuda (bool): If ``true``, the wallet should be registered using CUDA device(s). Defaults to `False`. + dev_id (Union[List[int], int]): The CUDA device id to use, or a list of device ids. Defaults to `0` (zero). + tpb (int): The number of threads per block (CUDA). Default to `256`. + num_processes (Optional[int]): The number of processes to use to register. Default to `None`. + update_interval (Optional[int]): The number of nonces to solve between updates. Default to `None`. + log_verbose (bool): If ``true``, the registration process will log more information. Default to `False`. + + Returns: + bool: ``True`` if the registration is successful, False otherwise. + + This function facilitates the entry of new neurons into the network, supporting the decentralized + growth and scalability of the Bittensor ecosystem. + """ + return register_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + max_allowed_attempts=max_allowed_attempts, + tpb=tpb, + update_interval=update_interval, + num_processes=num_processes, + cuda=cuda, + dev_id=dev_id, + output_in_place=output_in_place, + log_verbose=log_verbose, ) def reveal_weights( @@ -736,19 +2236,55 @@ def reveal_weights( wait_for_finalization: bool = False, max_retries: int = 5, ) -> tuple[bool, str]: - return self.execute_coroutine( - self.async_subtensor.reveal_weights( - wallet=wallet, - netuid=netuid, - uids=uids, - weights=weights, - salt=salt, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - max_retries=max_retries, - ), - ) + """ + Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. + This action serves as a revelation of the neuron's previously committed weight distribution. + + Args: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron revealing the weights. + netuid (int): The unique identifier of the subnet. + uids (np.ndarray): NumPy array of neuron UIDs for which weights are being revealed. + weights (np.ndarray): NumPy array of weight values corresponding to each UID. + salt (np.ndarray): NumPy array of salt values corresponding to the hash function. + version_key (int): Version key for compatibility with the network. Default is ``int representation of + Bittensor version``. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is + ``False``. + max_retries (int): The number of maximum attempts to reveal weights. Default is ``5``. + + Returns: + tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string + value describing the success or potential error. + + This function allows neurons to reveal their previously committed weight distribution, ensuring transparency + and accountability within the Bittensor network. + """ + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to reveal weights!" + + while retries < max_retries and success is False: + try: + success, message = reveal_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=list(uids), + weights=list(weights), + salt=list(salt), + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + if success: + break + except Exception as e: + logging.error(f"Error revealing weights: {e}") + finally: + retries += 1 + + return success, message def root_register( self, @@ -756,12 +2292,50 @@ def root_register( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> bool: - return execute_coroutine( - self.async_subtensor.root_register( - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), + """ + Register neuron by recycling some TAO. + + Arguments: + wallet (bittensor_wallet.Wallet): Bittensor wallet instance. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is + ``False``. + + Returns: + `True` if registration was successful, otherwise `False`. + """ + logging.info( + f"Registering on netuid [blue]0[/blue] on network: [blue]{self.network}[/blue]" + ) + + # Check current recycle amount + logging.info("Fetching recycle amount & balance.") + block = self.get_current_block() + + try: + recycle_call = cast( + int, self.get_hyperparameter(param_name="Burn", netuid=0, block=block) + ) + balance = self.get_balance(wallet.coldkeypub.ss58_address, block=block) + except TypeError as e: + logging.error(f"Unable to retrieve current recycle. {e}") + return False + + current_recycle = Balance.from_rao(int(recycle_call)) + + # Check balance is sufficient + if balance < current_recycle: + logging.error( + f"[red]Insufficient balance {balance} to register neuron. " + f"Current recycle is {current_recycle} TAO[/red]." + ) + return False + + return root_register_extrinsic( + subtensor=self, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) def root_set_weights( @@ -773,15 +2347,33 @@ def root_set_weights( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, ) -> bool: - return self.execute_coroutine( - self.async_subtensor.root_set_weights( - wallet=wallet, - netuids=netuids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), + """ + Set weights for root network. + + Arguments: + wallet (bittensor_wallet.Wallet): bittensor wallet instance. + netuids (list[int]): The list of subnet uids. + weights (list[float]): The list of weights to be set. + version_key (int, optional): Version key for compatibility with the network. Default is ``0``. + wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Defaults to + ``False``. + wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. + Defaults to ``False``. + + Returns: + `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: [blue]{self.network}[/blue]") + return set_root_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuids=netuids_, + weights=weights_, + version_key=version_key, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, ) def set_weights( @@ -795,18 +2387,92 @@ def set_weights( wait_for_finalization: bool = False, max_retries: int = 5, ) -> tuple[bool, str]: - return self.execute_coroutine( - self.async_subtensor.set_weights( - wallet=wallet, - netuid=netuid, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - max_retries=max_retries, + """ + 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. + + Arguments: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. + netuid (int): The unique identifier of the subnet. + uids (Union[NDArray[np.int64], torch.LongTensor, list]): The list of neuron UIDs that the weights are being + set for. + weights (Union[NDArray[np.float32], torch.FloatTensor, list]): The corresponding weights to be set for each + UID. + version_key (int): Version key for compatibility with the network. Default is int representation of + Bittensor version. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is + ``False``. + max_retries (int): The number of maximum attempts to set weights. Default is ``5``. + + Returns: + 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】. + """ + + def _blocks_weight_limit() -> bool: + bslu = cast(int, self.blocks_since_last_update(netuid, cast(int, uid))) + wrl = cast(int, self.weights_rate_limit(netuid)) + return bslu > wrl + + retries = 0 + success = False + if ( + uid := self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) + ) is None: + return ( + False, + f"Hotkey {wallet.hotkey.ss58_address} not registered in subnet {netuid}", ) - ) + + 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}." + ) + success, message = commit_reveal_v3_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + retries += 1 + return success, message + else: + # go with classic `set weights 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( + f"Setting weights for subnet #[blue]{netuid}[/blue]. " + f"Attempt [blue]{retries + 1} of {max_retries}[/blue]." + ) + success, message = set_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + except Exception as e: + logging.error(f"Error setting weights: {e}") + finally: + retries += 1 + + return success, message def serve_axon( self, @@ -816,14 +2482,32 @@ def serve_axon( wait_for_finalization: bool = True, certificate: Optional["Certificate"] = None, ) -> bool: - return self.execute_coroutine( - self.async_subtensor.serve_axon( - netuid=netuid, - axon=axon, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - certificate=certificate, - ), + """ + Registers an ``Axon`` serving endpoint on the Bittensor network for a specific neuron. This function is used to + set up the Axon, a key component of a neuron that handles incoming queries and data processing tasks. + + Args: + netuid (int): The unique identifier of the subnetwork. + axon (bittensor.core.axon.Axon): The Axon instance to be registered for serving. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``False``. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is + ``True``. + certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. + Defaults to ``None``. + + Returns: + bool: ``True`` if the Axon serve registration is successful, False otherwise. + + By registering an Axon, the neuron becomes an active part of the network's distributed computing infrastructure, + contributing to the collective intelligence of Bittensor. + """ + return serve_axon_extrinsic( + subtensor=self, + netuid=netuid, + axon=axon, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + certificate=certificate, ) def transfer( @@ -836,16 +2520,34 @@ def transfer( transfer_all: bool = False, keep_alive: bool = True, ) -> bool: - return self.execute_coroutine( - self.async_subtensor.transfer( - wallet=wallet, - destination=dest, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - transfer_all=transfer_all, - keep_alive=keep_alive, - ), + """ + Transfer token of amount to destination. + + Arguments: + wallet (bittensor_wallet.Wallet): Source wallet for the transfer. + dest (str): Destination address for the transfer. + amount (float): Amount of tokens to transfer. + transfer_all (bool): Flag to transfer all tokens. Default is ``False``. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. Default is ``True``. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Default is + ``False``. + keep_alive (bool): Flag to keep the connection alive. Default is ``True``. + + Returns: + `True` if the transferring was successful, otherwise `False`. + """ + if isinstance(amount, float): + amount = Balance.from_tao(amount) + + return transfer_extrinsic( + subtensor=self, + wallet=wallet, + dest=dest, + amount=amount, + transfer_all=transfer_all, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + keep_alive=keep_alive, ) def unstake( @@ -856,14 +2558,31 @@ def unstake( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - return self.execute_coroutine( - self.async_subtensor.unstake( - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), + """ + Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting + individual neuron stakes within the Bittensor network. + + Args: + wallet (bittensor_wallet.wallet): The wallet associated with the neuron from which the stake is being + removed. + hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey account to unstake from. + amount (Union[Balance, float]): The amount of TAO to unstake. If not specified, unstakes all. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + + Returns: + bool: ``True`` if the unstaking process is successful, False otherwise. + + This function supports flexible stake management, allowing neurons to adjust their network participation and + potential reward accruals. + """ + return unstake_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + amount=amount, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) def unstake_multiple( @@ -874,12 +2593,30 @@ def unstake_multiple( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - return self.execute_coroutine( - self.async_subtensor.unstake_multiple( - wallet=wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ), + """ + Performs batch unstaking from multiple hotkey accounts, allowing a neuron to reduce its staked amounts + efficiently. This function is useful for managing the distribution of stakes across multiple neurons. + + Args: + wallet (bittensor_wallet.Wallet): The wallet linked to the coldkey from which the stakes are being + withdrawn. + hotkey_ss58s (List[str]): A list of hotkey ``SS58`` addresses to unstake from. + amounts (List[Union[Balance, float]]): The amounts of TAO to unstake from each hotkey. If not provided, + unstakes all available stakes. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + + Returns: + bool: ``True`` if the batch unstaking is successful, False otherwise. + + This function allows for strategic reallocation or withdrawal of stakes, aligning with the dynamic stake + management aspect of the Bittensor network. + """ + return unstake_multiple_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) diff --git a/bittensor/core/types.py b/bittensor/core/types.py index 75092c4328..2269ea3f9a 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -1,9 +1,208 @@ +from abc import ABC +import argparse from typing import TypedDict, Optional -from bittensor.utils import Certificate +from bittensor.utils import networking, Certificate +from bittensor.utils.btlogging import logging +from bittensor.core import settings +from bittensor.core.config import Config +from bittensor.core.chain_data import NeuronInfo, NeuronInfoLite -class AxonServeCallParams(TypedDict): +class SubtensorMixin(ABC): + network: str + chain_endpoint: str + log_verbose: bool + + def __str__(self): + return f"Network: {self.network}, Chain: {self.chain_endpoint}" + + def __repr__(self): + return self.__str__() + + def _check_and_log_network_settings(self): + if self.network == settings.NETWORKS[3]: # local + logging.warning( + ":warning: Verify your local subtensor is running on port [blue]9944[/blue]." + ) + + if ( + self.network == "finney" + or self.chain_endpoint == settings.FINNEY_ENTRYPOINT + ) and self.log_verbose: + logging.info( + f"You are connecting to {self.network} network with endpoint {self.chain_endpoint}." + ) + logging.debug( + "We strongly encourage running a local subtensor node whenever possible. " + "This increases decentralization and resilience of the network." + ) + # TODO: remove or apply this warning as updated default endpoint? + logging.debug( + "In a future release, local subtensor will become the default endpoint. " + "To get ahead of this change, please run a local subtensor node and point to it." + ) + + @staticmethod # TODO can this be a class method? + def config() -> "Config": + """ + Creates and returns a Bittensor configuration object. + + Returns: + config (bittensor.core.config.Config): A Bittensor configuration object configured with arguments added by + the `subtensor.add_args` method. + """ + parser = argparse.ArgumentParser() + SubtensorMixin.add_args(parser) + return Config(parser) + + @staticmethod + def setup_config(network: Optional[str], config: "Config"): + """ + Sets up and returns the configuration for the Subtensor network and endpoint. + + This method determines the appropriate network and chain endpoint based on the provided network string or + configuration object. It evaluates the network and endpoint in the following order of precedence: + 1. Provided network string. + 2. Configured chain endpoint in the `config` object. + 3. Configured network in the `config` object. + 4. Default chain endpoint. + 5. Default network. + + Arguments: + network (Optional[str]): The name of the Subtensor network. If None, the network and endpoint will be + determined from the `config` object. + config (bittensor.core.config.Config): The configuration object containing the network and chain endpoint + settings. + + Returns: + tuple: A tuple containing the formatted WebSocket endpoint URL and the evaluated network name. + """ + if network is None: + candidates = [ + ( + config.is_set("subtensor.chain_endpoint"), + config.subtensor.chain_endpoint, + ), + (config.is_set("subtensor.network"), config.subtensor.network), + ( + config.subtensor.get("chain_endpoint"), + config.subtensor.chain_endpoint, + ), + (config.subtensor.get("network"), config.subtensor.network), + ] + for check, config_network in candidates: + if check: + network = config_network + + evaluated_network, evaluated_endpoint = ( + SubtensorMixin.determine_chain_endpoint_and_network(network) + ) + + return networking.get_formatted_ws_endpoint_url( + evaluated_endpoint + ), evaluated_network + + @classmethod + def help(cls): + """Print help to stdout.""" + parser = argparse.ArgumentParser() + cls.add_args(parser) + print(cls.__new__.__doc__) + parser.print_help() + + @classmethod + def add_args(cls, parser: "argparse.ArgumentParser", prefix: Optional[str] = None): + """ + Adds command-line arguments to the provided ArgumentParser for configuring the Subtensor settings. + + Arguments: + parser (argparse.ArgumentParser): The ArgumentParser object to which the Subtensor arguments will be added. + prefix (Optional[str]): An optional prefix for the argument names. If provided, the prefix is prepended to + each argument name. + + Arguments added: + --subtensor.network: The Subtensor network flag. Possible values are 'finney', 'test', 'archive', and + 'local'. Overrides the chain endpoint if set. + --subtensor.chain_endpoint: The Subtensor chain endpoint flag. If set, it overrides the network flag. + --subtensor._mock: If true, uses a mocked connection to the chain. + + Example: + parser = argparse.ArgumentParser() + Subtensor.add_args(parser) + """ + prefix_str = "" if prefix is None else f"{prefix}." + try: + default_network = settings.DEFAULT_NETWORK + default_chain_endpoint = settings.FINNEY_ENTRYPOINT + + parser.add_argument( + f"--{prefix_str}subtensor.network", + default=default_network, + type=str, + help="""The subtensor network flag. The likely choices are: + -- finney (main network) + -- test (test network) + -- archive (archive network +300 blocks) + -- local (local running network) + If this option is set it overloads subtensor.chain_endpoint with + an entry point node from that network. + """, + ) + parser.add_argument( + f"--{prefix_str}subtensor.chain_endpoint", + default=default_chain_endpoint, + type=str, + help="""The subtensor endpoint flag. If set, overrides the --network flag.""", + ) + parser.add_argument( + f"--{prefix_str}subtensor._mock", + default=False, + type=bool, + help="""If true, uses a mocked connection to the chain.""", + ) + + except argparse.ArgumentError: + # re-parsing arguments. + pass + + @staticmethod + def determine_chain_endpoint_and_network( + network: str, + ) -> tuple[Optional[str], Optional[str]]: + """Determines the chain endpoint and network from the passed network or chain_endpoint. + + Arguments: + network (str): The network flag. The choices are: ``finney`` (main network), ``archive`` (archive network + +300 blocks), ``local`` (local running network), ``test`` (test network). + + Returns: + tuple[Optional[str], Optional[str]]: The network and chain endpoint flag. If passed, overrides the + ``network`` argument. + """ + + if network is None: + return None, None + if network in settings.NETWORKS: + return network, settings.NETWORK_MAP[network] + + substrings_map = { + "entrypoint-finney.opentensor.ai": ("finney", settings.FINNEY_ENTRYPOINT), + "test.finney.opentensor.ai": ("test", settings.FINNEY_TEST_ENTRYPOINT), + "archive.chain.opentensor.ai": ("archive", settings.ARCHIVE_ENTRYPOINT), + "subvortex": ("subvortex", settings.SUBVORTEX_ENTRYPOINT), + "127.0.0.1": ("local", settings.LOCAL_ENTRYPOINT), + "localhost": ("local", settings.LOCAL_ENTRYPOINT), + } + + for substring, result in substrings_map.items(): + if substring in network: + return result + + return "unknown", network + + +class AxonServeCallParams_(TypedDict): """Axon serve chain call parameters.""" version: int @@ -14,6 +213,97 @@ class AxonServeCallParams(TypedDict): certificate: Optional[Certificate] +class AxonServeCallParams: + def __init__( + self, + version: int, + ip: int, + port: int, + ip_type: int, + netuid: int, + hotkey: str, + coldkey: str, + protocol: int, + placeholder1: int, + placeholder2: int, + certificate: Optional[Certificate], + ): + self.version = version + self.ip = ip + self.port = port + self.ip_type = ip_type + self.netuid = netuid + self.hotkey = hotkey + self.coldkey = coldkey + self.protocol = protocol + self.placeholder1 = placeholder1 + self.placeholder2 = placeholder2 + self.certificate = certificate + + def __eq__(self, other): + if isinstance(other, self.__class__): + return all( + getattr(self, attr) == getattr(other, attr) for attr in self.__dict__ + ) + elif isinstance(other, dict): + return all(getattr(self, attr) == other.get(attr) for attr in self.__dict__) + elif isinstance(other, (NeuronInfo, NeuronInfoLite)): + return all( + [ + self.version == other.axon_info.version, + self.ip == networking.ip_to_int(other.axon_info.ip), + self.port == other.axon_info.port, + self.ip_type == other.axon_info.ip_type, + self.netuid == other.netuid, + self.hotkey == other.hotkey, + self.coldkey == other.coldkey, + self.protocol == other.axon_info.protocol, + self.placeholder1 == other.axon_info.placeholder1, + self.placeholder2 == other.axon_info.placeholder2, + ] + ) + else: + raise NotImplementedError( + f"AxonServeCallParams equality not implemented for {type(other)}" + ) + + def copy(self) -> "AxonServeCallParams": + return self.__class__( + self.version, + self.ip, + self.port, + self.ip_type, + self.netuid, + self.hotkey, + self.coldkey, + self.protocol, + self.placeholder1, + self.placeholder2, + self.certificate, + ) + + def dict(self) -> dict: + """ + Returns a dict representation of this object. If `self.certificate` is `None`, + it is not included in this. + """ + d = { + "version": self.version, + "ip": self.ip, + "port": self.port, + "ip_type": self.ip_type, + "netuid": self.netuid, + "hotkey": self.hotkey, + "coldkey": self.coldkey, + "protocol": self.protocol, + "placeholder1": self.placeholder1, + "placeholder2": self.placeholder2, + } + if self.certificate is not None: + d["certificate"] = self.certificate + return d + + class PrometheusServeCallParams(TypedDict): """Prometheus serve chain call parameters.""" diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index ac9341d461..07c3125879 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -6,10 +6,7 @@ import scalecodec from async_substrate_interface.utils import ( - event_loop_is_running, hex_to_bytes, - get_event_loop, - execute_coroutine, ) from bittensor_wallet import Keypair from bittensor_wallet.errors import KeyFileError, PasswordError @@ -32,10 +29,7 @@ check_version = check_version VersionCheckError = VersionCheckError ss58_decode = ss58_decode -event_loop_is_running = event_loop_is_running hex_to_bytes = hex_to_bytes -get_event_loop = get_event_loop -execute_coroutine = execute_coroutine RAOPERTAO = 1e9 @@ -46,6 +40,33 @@ UnlockStatus = namedtuple("UnlockStatus", ["success", "message"]) +def _decode_hex_identity_dict(info_dictionary: dict[str, Any]) -> dict[str, Any]: + # TODO why does this exist alongside `decode_hex_identity_dict`? + """Decodes a dictionary of hexadecimal identities.""" + for k, v in info_dictionary.items(): + if isinstance(v, dict): + item = next(iter(v.values())) + else: + item = v + if isinstance(item, tuple) and item: + if len(item) > 1: + try: + info_dictionary[k] = ( + bytes(item).hex(sep=" ", bytes_per_sep=2).upper() + ) + except UnicodeDecodeError: + logging.error(f"Could not decode: {k}: {item}.") + else: + try: + info_dictionary[k] = bytes(item[0]).decode("utf-8") + except UnicodeDecodeError: + logging.error(f"Could not decode: {k}: {item}.") + else: + info_dictionary[k] = item + + return info_dictionary + + def ss58_to_vec_u8(ss58_address: str) -> list[int]: ss58_bytes: bytes = ss58_address_to_bytes(ss58_address) encoded_address: list[int] = [int(byte) for byte in ss58_bytes] diff --git a/bittensor/utils/mock/subtensor_mock.py b/bittensor/utils/mock/subtensor_mock.py index ea39e596b6..a6ff9a49a6 100644 --- a/bittensor/utils/mock/subtensor_mock.py +++ b/bittensor/utils/mock/subtensor_mock.py @@ -3,13 +3,12 @@ from hashlib import sha256 from types import SimpleNamespace from typing import Any, Optional, Union, TypedDict -from unittest.mock import MagicMock, patch, AsyncMock +from unittest.mock import MagicMock, patch from async_substrate_interface import SubstrateInterface from bittensor_wallet import Wallet import bittensor.core.subtensor as subtensor_module -from bittensor.core.async_subtensor import AsyncSubtensor from bittensor.core.chain_data import ( NeuronInfo, NeuronInfoLite, @@ -19,7 +18,7 @@ from bittensor.core.errors import ChainQueryError from bittensor.core.subtensor import Subtensor from bittensor.core.types import AxonServeCallParams, PrometheusServeCallParams -from bittensor.utils import RAOPERTAO, u16_normalized_float, get_event_loop +from bittensor.utils import RAOPERTAO, u16_normalized_float from bittensor.utils.balance import Balance # Mock Testing Constant @@ -250,9 +249,6 @@ def setup(self) -> None: self.network = "mock" self.chain_endpoint = "ws://mock_endpoint.bt" self.substrate = MagicMock(autospec=SubstrateInterface) - self.async_subtensor = AsyncMock(autospec=AsyncSubtensor) - self.async_subtensor.block = ReusableCoroutine(_async_block) - self.event_loop = get_event_loop() def __init__(self, *args, **kwargs) -> None: mock_substrate_interface = MagicMock(autospec=SubstrateInterface) @@ -267,8 +263,8 @@ def __init__(self, *args, **kwargs) -> None: if not hasattr(self, "chain_state") or getattr(self, "chain_state") is None: self.setup() - def get_block_hash(self, block_id: int) -> str: - return "0x" + sha256(str(block_id).encode()).hexdigest()[:64] + def get_block_hash(self, block: Optional[int] = None) -> str: + return "0x" + sha256(str(block).encode()).hexdigest()[:64] def create_subnet(self, netuid: int) -> None: subtensor_state = self.chain_state["SubtensorModule"] diff --git a/bittensor/utils/networking.py b/bittensor/utils/networking.py index e8edf1ef49..84b2749e87 100644 --- a/bittensor/utils/networking.py +++ b/bittensor/utils/networking.py @@ -1,10 +1,10 @@ """Utils for handling local network with ip and ports.""" -import json import os import urllib from typing import Optional +from async_substrate_interface.utils import json import netaddr import requests diff --git a/bittensor/utils/registration/__init__.py b/bittensor/utils/registration/__init__.py index 37a913e20a..ea527e4dc3 100644 --- a/bittensor/utils/registration/__init__.py +++ b/bittensor/utils/registration/__init__.py @@ -8,3 +8,14 @@ POWSolution, ) from bittensor.utils.registration.async_pow import create_pow_async + +__all__ = [ + create_pow, + legacy_torch_api_compat, + log_no_torch_error, + torch, + use_torch, + LazyLoadedTorch, + POWSolution, + create_pow_async, +] diff --git a/bittensor/utils/registration/async_pow.py b/bittensor/utils/registration/async_pow.py index ebdce4bc72..1aae6503d8 100644 --- a/bittensor/utils/registration/async_pow.py +++ b/bittensor/utils/registration/async_pow.py @@ -6,7 +6,6 @@ from queue import Empty from typing import Callable, Union, Optional, TYPE_CHECKING -from retry import retry from bittensor.core.errors import SubstrateRequestException from bittensor.utils.registration.pow import ( @@ -14,7 +13,7 @@ update_curr_block, terminate_workers_and_wait_for_exit, CUDASolver, - LazyLoadedTorch, + torch, RegistrationStatistics, RegistrationStatisticsLogger, Solver, @@ -25,12 +24,8 @@ from bittensor.core.async_subtensor import AsyncSubtensor from bittensor_wallet import Wallet from bittensor.utils.registration import POWSolution - import torch -else: - torch = LazyLoadedTorch() -@retry(Exception, tries=3, delay=1) async def _get_block_with_retry( subtensor: "AsyncSubtensor", netuid: int ) -> tuple[int, int, str]: diff --git a/bittensor/utils/registration/pow.py b/bittensor/utils/registration/pow.py index 1eac5d255e..8989e64acf 100644 --- a/bittensor/utils/registration/pow.py +++ b/bittensor/utils/registration/pow.py @@ -13,7 +13,7 @@ from datetime import timedelta from multiprocessing.queues import Queue as QueueType from queue import Empty, Full -from typing import Any, Callable, Optional, Union, TYPE_CHECKING +from typing import Callable, Optional, Union, TYPE_CHECKING import numpy from Crypto.Hash import keccak @@ -1105,7 +1105,7 @@ def create_pow( num_processes: Optional[int] = None, update_interval: Optional[int] = None, log_verbose: bool = False, -) -> Optional[dict[str, Any]]: +) -> Optional["POWSolution"]: """ Creates a proof of work for the given subtensor and wallet. diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index d7a063cffa..7bddcd4e5d 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -15,7 +15,7 @@ templates_repo, ) from bittensor.utils.balance import Balance -from bittensor.core.extrinsics.asyncex.weights import _do_set_weights +from bittensor.core.extrinsics.set_weights import _do_set_weights from bittensor.core.metagraph import Metagraph @@ -156,8 +156,8 @@ async def test_incentive(local_chain): await wait_epoch(subtensor) # Set weights by Alice on the subnet - await _do_set_weights( - subtensor=subtensor.async_subtensor, + _do_set_weights( + subtensor=subtensor, wallet=alice_wallet, uids=[1], vals=[65535], diff --git a/tests/e2e_tests/test_root_set_weights.py b/tests/e2e_tests/test_root_set_weights.py index 46fe6bbfe3..37993cf78a 100644 --- a/tests/e2e_tests/test_root_set_weights.py +++ b/tests/e2e_tests/test_root_set_weights.py @@ -8,7 +8,7 @@ wait_epoch, sudo_set_hyperparameter_values, ) -from bittensor.core.extrinsics.asyncex.root import _do_set_root_weights +from bittensor.core.extrinsics.root import _do_set_root_weights from tests.e2e_tests.utils.e2e_test_utils import ( setup_wallet, template_path, @@ -154,8 +154,8 @@ async def test_root_reg_hyperparams(local_chain): await wait_epoch(subtensor) # Set root weights to root network (0) and sn 1 - assert await _do_set_root_weights( - subtensor=subtensor.async_subtensor, + assert _do_set_root_weights( + subtensor=subtensor, wallet=alice_wallet, netuids=[0, 1], weights=weights, diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index cf28240139..af0c85b36e 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -153,7 +153,7 @@ async def wait_interval( and the provided tempo, then enters a loop where it periodically checks the current block number until the next tempo interval starts. """ - current_block = await subtensor.async_subtensor.get_current_block() + current_block = subtensor.get_current_block() next_tempo_block_start = next_tempo(current_block, tempo, netuid) last_reported = None @@ -161,7 +161,7 @@ async def wait_interval( await asyncio.sleep( 1 ) # Wait for 1 second before checking the block number again - current_block = await subtensor.async_subtensor.get_current_block() + current_block = subtensor.get_current_block() if last_reported is None or current_block - last_reported >= reporting_interval: last_reported = current_block print( diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py index a6e9f292df..70c8342009 100644 --- a/tests/helpers/helpers.py +++ b/tests/helpers/helpers.py @@ -1,18 +1,18 @@ import asyncio -from collections import deque import json +import time +from collections import deque from typing import Union -from websockets.asyncio.client import ClientConnection, ClientProtocol -from websockets.uri import parse_uri - from bittensor_wallet.mock.wallet_mock import MockWallet as _MockWallet from bittensor_wallet.mock.wallet_mock import get_mock_coldkey from bittensor_wallet.mock.wallet_mock import get_mock_hotkey from bittensor_wallet.mock.wallet_mock import get_mock_wallet +from websockets.asyncio.client import ClientConnection, ClientProtocol +from websockets.uri import parse_uri -from bittensor.utils.balance import Balance from bittensor.core.chain_data import AxonInfo, NeuronInfo, PrometheusInfo +from bittensor.utils.balance import Balance from tests.helpers.integration_websocket_data import WEBSOCKET_RESPONSES, METADATA @@ -118,17 +118,15 @@ def __init__(self, *args, seed, **kwargs): self.received = deque() self._lock = asyncio.Lock() - async def send(self, payload: str, *args, **kwargs): + def send(self, payload: str, *args, **kwargs): received = json.loads(payload) id_ = received.pop("id") - async with self._lock: - self.received.append((received, id_)) + self.received.append((received, id_)) - async def recv(self, *args, **kwargs): + def recv(self, *args, **kwargs): while len(self.received) == 0: - await asyncio.sleep(0.1) - async with self._lock: - item, _id = self.received.pop() + time.sleep(0.1) + item, _id = self.received.pop() try: if item["method"] == "state_getMetadata": response = {"jsonrpc": "2.0", "id": _id, "result": METADATA} @@ -142,5 +140,16 @@ async def recv(self, *args, **kwargs): print("ERROR", self.seed, item["method"], item["params"]) raise - async def close(self, *args, **kwargs): + def close(self, *args, **kwargs): + pass + + +class FakeConnectContextManager: + def __init__(self, seed): + self.seed = seed + + def __enter__(self): + return FakeWebsocket(seed=self.seed) + + def __exit__(self, exc_type, exc, tb): pass diff --git a/tests/helpers/integration_websocket_data.py b/tests/helpers/integration_websocket_data.py index 6bd2e926e5..340072ee96 100644 --- a/tests/helpers/integration_websocket_data.py +++ b/tests/helpers/integration_websocket_data.py @@ -6423,6 +6423,12 @@ } }, "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + "chain_getBlockHash": { + "[3264143]": { + "jsonrpc": "2.0", + "result": None, + } + }, }, "min_allowed_weights": { "chain_getHead": { diff --git a/tests/integration_tests/test_metagraph_integration.py b/tests/integration_tests/test_metagraph_integration.py index 45ce51a6b8..3344ab6ae4 100644 --- a/tests/integration_tests/test_metagraph_integration.py +++ b/tests/integration_tests/test_metagraph_integration.py @@ -33,18 +33,14 @@ def test_sync_block_0(self): self.metagraph.sync(lite=True, block=0, subtensor=self.sub) def test_load_sync_save(self): - with mock.patch.object( - self.sub.async_subtensor, "neurons_lite", return_value=[] - ): + with mock.patch.object(self.sub, "neurons_lite", return_value=[]): self.metagraph.sync(lite=True, subtensor=self.sub) self.metagraph.save() self.metagraph.load() self.metagraph.save() def test_load_sync_save_from_torch(self): - with mock.patch.object( - self.sub.async_subtensor, "neurons_lite", return_value=[] - ): + with mock.patch.object(self.sub, "neurons_lite", return_value=[]): self.metagraph.sync(lite=True, subtensor=self.sub) def deprecated_save_torch(metagraph): diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index dcf149e62e..a2154b6615 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -1,14 +1,13 @@ -import asyncio import os.path import pytest -from bittensor.utils.balance import Balance -from bittensor.core.chain_data.axon_info import AxonInfo +from bt_decode import PortableRegistry, MetadataV15 from bittensor import NeuronInfo +from bittensor.core.chain_data.axon_info import AxonInfo from bittensor.core.subtensor import Subtensor -from bt_decode import PortableRegistry, MetadataV15 -from tests.helpers.helpers import FakeWebsocket +from bittensor.utils.balance import Balance +from tests.helpers.helpers import FakeConnectContextManager @pytest.fixture @@ -32,12 +31,11 @@ async def prepare_test(mocker, seed): MetadataV15.decode_from_metadata_option(f.read()) ) subtensor = Subtensor("unknown", _mock=True) - mocker.patch.object(subtensor.substrate.ws, "ws", FakeWebsocket(seed=seed)) - mocker.patch.object(subtensor.substrate.ws, "_initialized", True) - mocker.patch.object(subtensor.substrate._async_instance, "registry", registry) - subtensor.substrate.ws._receiving_task = asyncio.create_task( - subtensor.substrate.ws._start_receiving() + mocker.patch( + "async_substrate_interface.sync_substrate.connect", + mocker.Mock(return_value=FakeConnectContextManager(seed=seed)), ) + mocker.patch.object(subtensor.substrate, "registry", registry) return subtensor diff --git a/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py b/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py index 1dd7e6aab9..24ba13c707 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py +++ b/tests/unit_tests/extrinsics/asyncex/test_commit_reveal.py @@ -91,11 +91,11 @@ async def test_do_commit_reveal_v3_success(mocker, subtensor): call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey ) mocked_submit_extrinsic.assert_awaited_once_with( - extrinsic=mocked_create_signed_extrinsic.return_value, + mocked_create_signed_extrinsic.return_value, wait_for_inclusion=False, wait_for_finalization=False, ) - assert result == (True, "Not waiting for finalization or inclusion.") + assert result == (True, "") @pytest.mark.asyncio @@ -121,7 +121,9 @@ async def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor): ) mocked_format_error_message = mocker.patch.object( - async_commit_reveal, "format_error_message", return_value="Formatted error" + subtensor_module, + "format_error_message", + return_value="Formatted error", ) # Call @@ -149,7 +151,7 @@ async def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor): call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey ) mocked_submit_extrinsic.assert_awaited_once_with( - extrinsic=mocked_create_signed_extrinsic.return_value, + mocked_create_signed_extrinsic.return_value, wait_for_inclusion=True, wait_for_finalization=True, ) diff --git a/tests/unit_tests/extrinsics/asyncex/test_registration.py b/tests/unit_tests/extrinsics/asyncex/test_registration.py index 6baffe166c..82d9e6b561 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_registration.py +++ b/tests/unit_tests/extrinsics/asyncex/test_registration.py @@ -70,10 +70,10 @@ async def test_do_pow_register_success(subtensor, mocker): call=fake_call, keypair=fake_wallet.hotkey ) subtensor.substrate.submit_extrinsic.assert_awaited_once_with( - extrinsic=fake_extrinsic, wait_for_inclusion=True, wait_for_finalization=True + fake_extrinsic, wait_for_inclusion=True, wait_for_finalization=True ) assert result is True - assert error_message is None + assert error_message == "" @pytest.mark.asyncio @@ -105,7 +105,7 @@ async def test_do_pow_register_failure(subtensor, mocker): subtensor.substrate, "submit_extrinsic", return_value=fake_response ) mocked_format_error_message = mocker.patch.object( - async_registration, "format_error_message" + async_subtensor, "format_error_message" ) # Call @@ -124,10 +124,10 @@ async def test_do_pow_register_failure(subtensor, mocker): call=fake_call, keypair=fake_wallet.hotkey ) subtensor.substrate.submit_extrinsic.asseert_awaited_once_with( - extrinsic=fake_extrinsic, wait_for_inclusion=True, wait_for_finalization=True + fake_extrinsic, wait_for_inclusion=True, wait_for_finalization=True ) - mocked_format_error_message.assert_called_once_with(error_message=fake_err_message) + mocked_format_error_message.assert_called_once_with(fake_err_message) assert result_error_message == (False, mocked_format_error_message.return_value) @@ -173,7 +173,7 @@ async def test_do_pow_register_no_waiting(subtensor, mocker): fake_extrinsic, wait_for_inclusion=False, wait_for_finalization=False ) assert result is True - assert error_message is None + assert error_message == "" @pytest.mark.asyncio @@ -214,8 +214,15 @@ async def test_register_extrinsic_success(subtensor, mocker): ) # Asserts - mocked_subnet_exists.assert_called_once_with(1) - mocked_get_neuron.assert_called_once_with(hotkey_ss58="hotkey_ss58", netuid=1) + mocked_subnet_exists.assert_called_once_with( + 1, + block_hash=subtensor.substrate.get_chain_head.return_value, + ) + mocked_get_neuron.assert_called_once_with( + hotkey_ss58="hotkey_ss58", + netuid=1, + block_hash=subtensor.substrate.get_chain_head.return_value, + ) mocked_create_pow.assert_called_once() mocked_do_pow_register.assert_called_once() mocked_is_hotkey_registered.assert_called_once_with( @@ -264,8 +271,15 @@ async def test_register_extrinsic_success_with_cuda(subtensor, mocker): ) # Asserts - mocked_subnet_exists.assert_called_once_with(1) - mocked_get_neuron.assert_called_once_with(hotkey_ss58="hotkey_ss58", netuid=1) + mocked_subnet_exists.assert_called_once_with( + 1, + block_hash=subtensor.substrate.get_chain_head.return_value, + ) + mocked_get_neuron.assert_called_once_with( + hotkey_ss58="hotkey_ss58", + netuid=1, + block_hash=subtensor.substrate.get_chain_head.return_value, + ) mocked_create_pow.assert_called_once() mocked_do_pow_register.assert_called_once() mocked_is_hotkey_registered.assert_called_once_with( @@ -303,8 +317,15 @@ async def test_register_extrinsic_failed_with_cuda(subtensor, mocker): ) # Asserts - mocked_subnet_exists.assert_called_once_with(1) - mocked_get_neuron.assert_called_once_with(hotkey_ss58="hotkey_ss58", netuid=1) + mocked_subnet_exists.assert_called_once_with( + 1, + block_hash=subtensor.substrate.get_chain_head.return_value, + ) + mocked_get_neuron.assert_called_once_with( + hotkey_ss58="hotkey_ss58", + netuid=1, + block_hash=subtensor.substrate.get_chain_head.return_value, + ) assert result is False @@ -326,7 +347,10 @@ async def test_register_extrinsic_subnet_not_exists(subtensor, mocker): ) # Asserts - mocked_subnet_exists.assert_called_once_with(1) + mocked_subnet_exists.assert_called_once_with( + 1, + block_hash=subtensor.substrate.get_chain_head.return_value, + ) assert result is False @@ -350,7 +374,9 @@ async def test_register_extrinsic_already_registered(subtensor, mocker): # Asserts mocked_get_neuron.assert_called_once_with( - hotkey_ss58=fake_wallet.hotkey.ss58_address, netuid=1 + hotkey_ss58=fake_wallet.hotkey.ss58_address, + netuid=1, + block_hash=subtensor.substrate.get_chain_head.return_value, ) assert result is True @@ -400,8 +426,15 @@ async def is_stale_side_effect(*_, **__): ) # Asserts - mocked_subnet_exists.assert_called_once_with(1) - mocked_get_neuron.assert_called_once_with(hotkey_ss58="hotkey_ss58", netuid=1) + mocked_subnet_exists.assert_called_once_with( + 1, + block_hash=subtensor.substrate.get_chain_head.return_value, + ) + mocked_get_neuron.assert_called_once_with( + hotkey_ss58="hotkey_ss58", + netuid=1, + block_hash=subtensor.substrate.get_chain_head.return_value, + ) assert mocked_create_pow.call_count == 3 assert mocked_do_pow_register.call_count == 3 diff --git a/tests/unit_tests/extrinsics/asyncex/test_root.py b/tests/unit_tests/extrinsics/asyncex/test_root.py index bc258f1da9..c1aed1d6a4 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_root.py +++ b/tests/unit_tests/extrinsics/asyncex/test_root.py @@ -81,7 +81,6 @@ async def test_root_register_extrinsic_success(subtensor, mocker): result = await async_root.root_register_extrinsic( subtensor=subtensor, wallet=fake_wallet, - netuid=1, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -89,14 +88,14 @@ async def test_root_register_extrinsic_success(subtensor, mocker): # Asserts mocked_unlock_key.assert_called_once_with(fake_wallet) mocked_is_hotkey_registered.assert_called_once_with( - netuid=1, hotkey_ss58="fake_hotkey_address" + netuid=0, hotkey_ss58="fake_hotkey_address" ) mocked_compose_call.assert_called_once() mocked_sign_and_send_extrinsic.assert_called_once() mocked_query.assert_called_once_with( module="SubtensorModule", storage_function="Uids", - params=[1, "fake_hotkey_address"], + params=[0, "fake_hotkey_address"], ) assert result is True @@ -117,7 +116,6 @@ async def test_root_register_extrinsic_unlock_failed(subtensor, mocker): result = await async_root.root_register_extrinsic( subtensor=subtensor, wallet=fake_wallet, - netuid=1, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -149,7 +147,6 @@ async def test_root_register_extrinsic_already_registered(subtensor, mocker): result = await async_root.root_register_extrinsic( subtensor=subtensor, wallet=fake_wallet, - netuid=1, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -157,7 +154,7 @@ async def test_root_register_extrinsic_already_registered(subtensor, mocker): # Asserts mocked_unlock_key.assert_called_once_with(fake_wallet) mocked_is_hotkey_registered.assert_called_once_with( - netuid=1, hotkey_ss58="fake_hotkey_address" + netuid=0, hotkey_ss58="fake_hotkey_address" ) assert result is True @@ -190,7 +187,6 @@ async def test_root_register_extrinsic_transaction_failed(subtensor, mocker): result = await async_root.root_register_extrinsic( subtensor=subtensor, wallet=fake_wallet, - netuid=1, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -198,7 +194,7 @@ async def test_root_register_extrinsic_transaction_failed(subtensor, mocker): # Asserts mocked_unlock_key.assert_called_once_with(fake_wallet) mocked_is_hotkey_registered.assert_called_once_with( - netuid=1, hotkey_ss58="fake_hotkey_address" + netuid=0, hotkey_ss58="fake_hotkey_address" ) mocked_compose_call.assert_called_once() mocked_sign_and_send_extrinsic.assert_called_once() @@ -238,7 +234,6 @@ async def test_root_register_extrinsic_uid_not_found(subtensor, mocker): result = await async_root.root_register_extrinsic( subtensor=subtensor, wallet=fake_wallet, - netuid=1, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -246,14 +241,14 @@ async def test_root_register_extrinsic_uid_not_found(subtensor, mocker): # Asserts mocked_unlock_key.assert_called_once_with(fake_wallet) mocked_is_hotkey_registered.assert_called_once_with( - netuid=1, hotkey_ss58="fake_hotkey_address" + netuid=0, hotkey_ss58="fake_hotkey_address" ) mocked_compose_call.assert_called_once() mocked_sign_and_send_extrinsic.assert_called_once() mocked_query.assert_called_once_with( module="SubtensorModule", storage_function="Uids", - params=[1, "fake_hotkey_address"], + params=[0, "fake_hotkey_address"], ) assert result is False diff --git a/tests/unit_tests/extrinsics/asyncex/test_transfer.py b/tests/unit_tests/extrinsics/asyncex/test_transfer.py index df0e788734..0d15d7b577 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_transfer.py +++ b/tests/unit_tests/extrinsics/asyncex/test_transfer.py @@ -220,7 +220,7 @@ async def test_transfer_extrinsic_success(subtensor, mocker): result = await async_transfer.transfer_extrinsic( subtensor=subtensor, wallet=fake_wallet, - destination=fake_destination, + dest=fake_destination, amount=fake_amount, transfer_all=False, wait_for_inclusion=True, @@ -285,7 +285,7 @@ async def test_transfer_extrinsic_call_successful_with_failed_response( result = await async_transfer.transfer_extrinsic( subtensor=subtensor, wallet=fake_wallet, - destination=fake_destination, + dest=fake_destination, amount=fake_amount, transfer_all=False, wait_for_inclusion=True, @@ -346,7 +346,7 @@ async def test_transfer_extrinsic_insufficient_balance(subtensor, mocker): result = await async_transfer.transfer_extrinsic( subtensor=subtensor, wallet=fake_wallet, - destination=fake_destination, + dest=fake_destination, amount=fake_amount, transfer_all=False, wait_for_inclusion=True, @@ -384,7 +384,7 @@ async def test_transfer_extrinsic_invalid_destination(subtensor, mocker): result = await async_transfer.transfer_extrinsic( subtensor=subtensor, wallet=fake_wallet, - destination=fake_destination, + dest=fake_destination, amount=fake_amount, transfer_all=False, wait_for_inclusion=True, @@ -422,7 +422,7 @@ async def test_transfer_extrinsic_unlock_key_false(subtensor, mocker): result = await async_transfer.transfer_extrinsic( subtensor=subtensor, wallet=fake_wallet, - destination=fake_destination, + dest=fake_destination, amount=fake_amount, transfer_all=False, wait_for_inclusion=True, @@ -479,7 +479,7 @@ async def test_transfer_extrinsic_keep_alive_false_and_transfer_all_true( result = await async_transfer.transfer_extrinsic( subtensor=subtensor, wallet=fake_wallet, - destination=fake_destination, + dest=fake_destination, amount=fake_amount, transfer_all=True, wait_for_inclusion=True, diff --git a/tests/unit_tests/extrinsics/asyncex/test_weights.py b/tests/unit_tests/extrinsics/asyncex/test_weights.py index 531ab802f7..3233519c16 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_weights.py +++ b/tests/unit_tests/extrinsics/asyncex/test_weights.py @@ -56,7 +56,7 @@ async def fake_is_success(): # Asserts assert result is True - assert message == "Successfully set weights." + assert message is None @pytest.mark.asyncio @@ -79,7 +79,7 @@ async def fake_is_success(): fake_response.process_events = mocker.AsyncMock() - fake_response.error_message = mocker.Mock() + fake_response.error_message = mocker.AsyncMock(return_value="Error occurred")() fake_response.process_events = mocker.AsyncMock() mocked_format_error_message = mocker.Mock() @@ -108,7 +108,7 @@ async def fake_is_success(): # Asserts assert result is False - mocked_format_error_message.assert_called_once_with(fake_response.error_message) + mocked_format_error_message.assert_called_once_with("Error occurred") assert message == mocked_format_error_message.return_value @@ -146,7 +146,7 @@ async def test_do_set_weights_no_waiting(subtensor, mocker): # Asserts assert result is True - assert message == "Not waiting for finalization or inclusion." + assert message is None @pytest.mark.asyncio @@ -336,7 +336,7 @@ async def fake_is_success(): fake_response = mocker.Mock() fake_response.is_success = fake_is_success() fake_response.process_events = mocker.AsyncMock() - fake_response.error_message = "Error occurred" + fake_response.error_message = mocker.AsyncMock(return_value="Error occurred")() mocked_format_error_message = mocker.Mock(return_value="Formatted error") mocker.patch.object( @@ -363,7 +363,7 @@ async def fake_is_success(): # Asserts assert result is False - mocked_format_error_message.assert_called_once_with(fake_response.error_message) + mocked_format_error_message.assert_called_once_with("Error occurred") assert message == "Formatted error" diff --git a/tests/unit_tests/extrinsics/test_commit_reveal.py b/tests/unit_tests/extrinsics/test_commit_reveal.py index 30bd7c0e63..f3e3266d64 100644 --- a/tests/unit_tests/extrinsics/test_commit_reveal.py +++ b/tests/unit_tests/extrinsics/test_commit_reveal.py @@ -1,48 +1,355 @@ +import numpy as np +import pytest +import torch +from bittensor_wallet import Wallet + +from bittensor.core import subtensor as subtensor_module +from bittensor.core.chain_data import SubnetHyperparameters from bittensor.core.extrinsics import commit_reveal +from bittensor.core.subtensor import Subtensor + + +@pytest.fixture +def subtensor(mocker): + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) + yield Subtensor() + + +@pytest.fixture +def hyperparams(): + yield SubnetHyperparameters( + rho=0, + kappa=0, + immunity_period=0, + min_allowed_weights=0, + max_weight_limit=0.0, + tempo=0, + min_difficulty=0, + max_difficulty=0, + weights_version=0, + weights_rate_limit=0, + adjustment_interval=0, + activity_cutoff=0, + registration_allowed=False, + target_regs_per_interval=0, + min_burn=0, + max_burn=0, + bonds_moving_avg=0, + max_regs_per_block=0, + serving_rate_limit=0, + max_validators=0, + adjustment_alpha=0, + difficulty=0, + commit_reveal_weights_interval=0, + commit_reveal_weights_enabled=True, + alpha_high=0, + alpha_low=0, + liquid_alpha_enabled=False, + ) + + +def test_do_commit_reveal_v3_success(mocker, subtensor): + """Test successful commit-reveal with wait for finalization.""" + # Preps + fake_wallet = mocker.Mock(autospec=Wallet) + fake_netuid = 1 + fake_commit = b"fake_commit" + fake_reveal_round = 1 + + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_create_signed_extrinsic = mocker.patch.object( + subtensor.substrate, "create_signed_extrinsic" + ) + mocked_submit_extrinsic = mocker.patch.object( + subtensor.substrate, "submit_extrinsic" + ) + + # Call + result = commit_reveal._do_commit_reveal_v3( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit=fake_commit, + reveal_round=fake_reveal_round, + ) + + # Asserts + mocked_compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="commit_crv3_weights", + call_params={ + "netuid": fake_netuid, + "commit": fake_commit, + "reveal_round": fake_reveal_round, + }, + ) + mocked_create_signed_extrinsic.assert_called_once_with( + call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey + ) + mocked_submit_extrinsic.assert_called_once_with( + mocked_create_signed_extrinsic.return_value, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + assert result == (True, "") + + +def test_do_commit_reveal_v3_failure_due_to_error(mocker, subtensor): + """Test commit-reveal fails due to an error in submission.""" + # Preps + fake_wallet = mocker.Mock(autospec=Wallet) + fake_netuid = 1 + fake_commit = b"fake_commit" + fake_reveal_round = 1 + + mocked_compose_call = mocker.patch.object(subtensor.substrate, "compose_call") + mocked_create_signed_extrinsic = mocker.patch.object( + subtensor.substrate, "create_signed_extrinsic" + ) + mocked_submit_extrinsic = mocker.patch.object( + subtensor.substrate, + "submit_extrinsic", + return_value=mocker.Mock(is_success=False, error_message="Mocked error"), + ) + mocked_format_error_message = mocker.patch.object( + subtensor_module, "format_error_message", return_value="Formatted error" + ) + + # Call + result = commit_reveal._do_commit_reveal_v3( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit=fake_commit, + reveal_round=fake_reveal_round, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Asserts + mocked_compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="commit_crv3_weights", + call_params={ + "netuid": fake_netuid, + "commit": fake_commit, + "reveal_round": fake_reveal_round, + }, + ) + mocked_create_signed_extrinsic.assert_called_once_with( + call=mocked_compose_call.return_value, keypair=fake_wallet.hotkey + ) + mocked_submit_extrinsic.assert_called_once_with( + mocked_create_signed_extrinsic.return_value, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + mocked_format_error_message.assert_called_once_with("Mocked error") + assert result == (False, "Formatted error") + + +def test_commit_reveal_v3_extrinsic_success_with_torch(mocker, subtensor, hyperparams): + """Test successful commit-reveal with torch tensors.""" + # Preps + fake_wallet = mocker.Mock(autospec=Wallet) + fake_netuid = 1 + fake_uids = torch.tensor([1, 2, 3], dtype=torch.int64) + fake_weights = torch.tensor([0.1, 0.2, 0.7], dtype=torch.float32) + fake_commit_for_reveal = b"mock_commit_for_reveal" + fake_reveal_round = 1 + + # Mocks + + mocked_uids = mocker.Mock() + mocked_weights = mocker.Mock() + mocked_convert_weights_and_uids_for_emit = mocker.patch.object( + commit_reveal, + "convert_weights_and_uids_for_emit", + return_value=(mocked_uids, mocked_weights), + ) + mocked_get_subnet_reveal_period_epochs = mocker.patch.object( + subtensor, "get_subnet_reveal_period_epochs" + ) + mocked_get_encrypted_commit = mocker.patch.object( + commit_reveal, + "get_encrypted_commit", + return_value=(fake_commit_for_reveal, fake_reveal_round), + ) + mock_do_commit_reveal_v3 = mocker.patch.object( + commit_reveal, "_do_commit_reveal_v3", return_value=(True, "Success") + ) + mock_block = mocker.patch.object(subtensor, "get_current_block", return_value=1) + mock_hyperparams = mocker.patch.object( + subtensor, + "get_subnet_hyperparameters", + return_value=hyperparams, + ) + + # Call + success, message = commit_reveal.commit_reveal_v3_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Asserts + assert success is True + assert message == "reveal_round:1" + mocked_convert_weights_and_uids_for_emit.assert_called_once_with( + fake_uids, fake_weights + ) + mocked_get_encrypted_commit.assert_called_once_with( + uids=mocked_uids, + weights=mocked_weights, + subnet_reveal_period_epochs=mock_hyperparams.return_value.commit_reveal_weights_interval, + version_key=commit_reveal.version_as_int, + tempo=mock_hyperparams.return_value.tempo, + netuid=fake_netuid, + current_block=mock_block.return_value, + ) + mock_do_commit_reveal_v3.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit=fake_commit_for_reveal, + reveal_round=fake_reveal_round, + wait_for_inclusion=True, + wait_for_finalization=True, + ) -def test_commit_reveal_v3_extrinsic(mocker): - """ "Verify that sync `commit_reveal_v3_extrinsic` method calls proper async method.""" +def test_commit_reveal_v3_extrinsic_success_with_numpy(mocker, subtensor, hyperparams): + """Test successful commit-reveal with numpy arrays.""" # Preps - fake_subtensor = mocker.Mock() - fake_wallet = mocker.Mock() - netuid = 1 - uids = [1, 2, 3, 4] - weights = [0.1, 0.2, 0.3, 0.4] - version_key = 2 - wait_for_inclusion = True - wait_for_finalization = True - - mocked_execute_coroutine = mocker.patch.object(commit_reveal, "execute_coroutine") - mocked_commit_reveal_v3_extrinsic = mocker.Mock() - commit_reveal.async_commit_reveal_v3_extrinsic = mocked_commit_reveal_v3_extrinsic + fake_wallet = mocker.Mock(autospec=Wallet) + fake_netuid = 1 + fake_uids = np.array([1, 2, 3], dtype=np.int64) + fake_weights = np.array([0.1, 0.2, 0.7], dtype=np.float32) + + mock_convert = mocker.patch.object( + commit_reveal, + "convert_weights_and_uids_for_emit", + return_value=(fake_uids, fake_weights), + ) + mock_encode_drand = mocker.patch.object( + commit_reveal, "get_encrypted_commit", return_value=(b"commit", 0) + ) + mock_do_commit = mocker.patch.object( + commit_reveal, "_do_commit_reveal_v3", return_value=(True, "Committed!") + ) + mocker.patch.object(subtensor, "get_current_block", return_value=1) + mocker.patch.object( + subtensor, + "get_subnet_hyperparameters", + return_value=hyperparams, + ) # Call - result = commit_reveal.commit_reveal_v3_extrinsic( - subtensor=fake_subtensor, + success, message = commit_reveal.commit_reveal_v3_extrinsic( + subtensor=subtensor, wallet=fake_wallet, - netuid=netuid, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + wait_for_inclusion=False, + wait_for_finalization=False, ) # Asserts + assert success is True + assert message == "reveal_round:0" + mock_convert.assert_called_once_with(fake_uids, fake_weights) + mock_encode_drand.assert_called_once() + mock_do_commit.assert_called_once() - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_commit_reveal_v3_extrinsic.return_value, - event_loop=fake_subtensor.event_loop, + +def test_commit_reveal_v3_extrinsic_response_false(mocker, subtensor, hyperparams): + """Test unsuccessful commit-reveal with torch.""" + # Preps + fake_wallet = mocker.Mock(autospec=Wallet) + fake_netuid = 1 + fake_uids = torch.tensor([1, 2, 3], dtype=torch.int64) + fake_weights = torch.tensor([0.1, 0.2, 0.7], dtype=torch.float32) + fake_commit_for_reveal = b"mock_commit_for_reveal" + fake_reveal_round = 1 + + # Mocks + mocker.patch.object( + commit_reveal, + "convert_weights_and_uids_for_emit", + return_value=(fake_uids, fake_weights), + ) + mocker.patch.object( + commit_reveal, + "get_encrypted_commit", + return_value=(fake_commit_for_reveal, fake_reveal_round), + ) + mock_do_commit_reveal_v3 = mocker.patch.object( + commit_reveal, "_do_commit_reveal_v3", return_value=(False, "Failed") ) - mocked_commit_reveal_v3_extrinsic.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, + mocker.patch.object(subtensor, "get_current_block", return_value=1) + mocker.patch.object( + subtensor, + "get_subnet_hyperparameters", + return_value=hyperparams, + ) + + # Call + success, message = commit_reveal.commit_reveal_v3_extrinsic( + subtensor=subtensor, wallet=fake_wallet, - netuid=netuid, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + wait_for_inclusion=True, + wait_for_finalization=True, ) - assert result == mocked_execute_coroutine.return_value + + # Asserts + assert success is False + assert message == "Failed" + mock_do_commit_reveal_v3.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit=fake_commit_for_reveal, + reveal_round=fake_reveal_round, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +def test_commit_reveal_v3_extrinsic_exception(mocker, subtensor): + """Test exception handling in commit-reveal.""" + # Preps + fake_wallet = mocker.Mock(autospec=Wallet) + fake_netuid = 1 + fake_uids = [1, 2, 3] + fake_weights = [0.1, 0.2, 0.7] + + mocker.patch.object( + commit_reveal, + "convert_weights_and_uids_for_emit", + side_effect=Exception("Test Error"), + ) + + # Call + success, message = commit_reveal.commit_reveal_v3_extrinsic( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + ) + + # Asserts + assert success is False + assert "Test Error" in message diff --git a/tests/unit_tests/extrinsics/test_commit_weights.py b/tests/unit_tests/extrinsics/test_commit_weights.py index 54a0b80b74..42cc1f2311 100644 --- a/tests/unit_tests/extrinsics/test_commit_weights.py +++ b/tests/unit_tests/extrinsics/test_commit_weights.py @@ -1,23 +1,45 @@ -from bittensor.core.extrinsics import commit_weights +import pytest +from bittensor_wallet import Wallet +from bittensor.core import subtensor as subtensor_module +from bittensor.core.settings import version_as_int +from bittensor.core.subtensor import Subtensor +from bittensor.core.extrinsics.commit_weights import ( + _do_commit_weights, + _do_reveal_weights, +) -def test_commit_weights_extrinsic(mocker): - """ "Verify that sync `commit_weights_extrinsic` method calls proper async method.""" + +@pytest.fixture +def subtensor(mocker): + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) + return Subtensor() + + +def test_do_commit_weights(subtensor, mocker): + """Successful _do_commit_weights call.""" # Preps - fake_subtensor = mocker.Mock() - fake_wallet = mocker.Mock() + fake_wallet = mocker.MagicMock() netuid = 1 - commit_hash = "0x1234567890abcdef" + commit_hash = "fake_commit_hash" wait_for_inclusion = True wait_for_finalization = True - mocked_execute_coroutine = mocker.patch.object(commit_weights, "execute_coroutine") - mocked_commit_weights_extrinsic = mocker.Mock() - commit_weights.async_commit_weights_extrinsic = mocked_commit_weights_extrinsic + subtensor.substrate.submit_extrinsic.return_value.is_success = None + + mocked_format_error_message = mocker.Mock() + mocker.patch( + "bittensor.core.extrinsics.commit_weights.format_error_message", + mocked_format_error_message, + ) # Call - result = commit_weights.commit_weights_extrinsic( - subtensor=fake_subtensor, + result = _do_commit_weights( + subtensor=subtensor, wallet=fake_wallet, netuid=netuid, commit_hash=commit_hash, @@ -25,68 +47,101 @@ def test_commit_weights_extrinsic(mocker): wait_for_finalization=wait_for_finalization, ) - # Asserts - - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_commit_weights_extrinsic.return_value, - event_loop=fake_subtensor.event_loop, + # Assertions + subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="commit_weights", + call_params={ + "netuid": netuid, + "commit_hash": commit_hash, + }, ) - mocked_commit_weights_extrinsic.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - wallet=fake_wallet, - netuid=netuid, - commit_hash=commit_hash, + + subtensor.substrate.create_signed_extrinsic.assert_called_once() + _, kwargs = subtensor.substrate.create_signed_extrinsic.call_args + assert kwargs["call"] == subtensor.substrate.compose_call.return_value + assert kwargs["keypair"] == fake_wallet.hotkey + + subtensor.substrate.submit_extrinsic.assert_called_once_with( + extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - assert result == mocked_execute_coroutine.return_value + + mocked_format_error_message.assert_called_once_with( + subtensor.substrate.submit_extrinsic.return_value.error_message, + ) + + assert result == ( + False, + mocked_format_error_message.return_value, + ) -def test_reveal_weights_extrinsic(mocker): - """Verify that sync `reveal_weights_extrinsic` method calls proper async method.""" +def test_do_reveal_weights(subtensor, mocker): + """Verifies that the `_do_reveal_weights` method interacts with the right substrate methods.""" # Preps - fake_subtensor = mocker.Mock() - fake_wallet = mocker.Mock() + fake_wallet = mocker.MagicMock(autospec=Wallet) + fake_wallet.hotkey.ss58_address = "hotkey" + netuid = 1 uids = [1, 2, 3, 4] - weights = [5, 6, 7, 8] - salt = [1, 2, 3, 4] - version_key = 2 + values = [1, 2, 3, 4] + salt = [4, 2, 2, 1] wait_for_inclusion = True wait_for_finalization = True - mocked_execute_coroutine = mocker.patch.object(commit_weights, "execute_coroutine") - mocked_reveal_weights_extrinsic = mocker.Mock() - commit_weights.async_reveal_weights_extrinsic = mocked_reveal_weights_extrinsic + subtensor.substrate.submit_extrinsic.return_value.is_success = None + + mocked_format_error_message = mocker.Mock() + mocker.patch( + "bittensor.core.extrinsics.commit_weights.format_error_message", + mocked_format_error_message, + ) # Call - result = commit_weights.reveal_weights_extrinsic( - subtensor=fake_subtensor, + result = _do_reveal_weights( + subtensor=subtensor, wallet=fake_wallet, netuid=netuid, uids=uids, - weights=weights, + values=values, salt=salt, - version_key=version_key, + version_key=version_as_int, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) # Asserts + subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="reveal_weights", + call_params={ + "netuid": netuid, + "uids": uids, + "values": values, + "salt": salt, + "version_key": version_as_int, + }, + ) - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_reveal_weights_extrinsic.return_value, - event_loop=fake_subtensor.event_loop, + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + nonce=subtensor.substrate.get_account_next_index.return_value, ) - mocked_reveal_weights_extrinsic.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - wallet=fake_wallet, - netuid=netuid, - uids=uids, - weights=weights, - salt=salt, - version_key=version_key, + + subtensor.substrate.submit_extrinsic.assert_called_once_with( + extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - assert result == mocked_execute_coroutine.return_value + + mocked_format_error_message.assert_called_once_with( + subtensor.substrate.submit_extrinsic.return_value.error_message, + ) + + assert result == ( + False, + mocked_format_error_message.return_value, + ) diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py index 99b2c3cd1d..18676619de 100644 --- a/tests/unit_tests/extrinsics/test_registration.py +++ b/tests/unit_tests/extrinsics/test_registration.py @@ -1,101 +1,224 @@ +# The MIT License (MIT) +# Copyright © 2024 Opentensor Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import pytest +from bittensor_wallet import Wallet + from bittensor.core.extrinsics import registration +from bittensor.core.subtensor import Subtensor +from bittensor.utils.registration import POWSolution + + +# Mocking external dependencies +@pytest.fixture +def mock_subtensor(mocker): + mock = mocker.MagicMock(spec=Subtensor) + mock.network = "mock_network" + mock.substrate = mocker.MagicMock() + return mock + + +@pytest.fixture +def mock_wallet(mocker): + mock = mocker.MagicMock(spec=Wallet) + mock.coldkeypub.ss58_address = "mock_address" + mock.coldkey = mocker.MagicMock() + mock.hotkey = mocker.MagicMock() + mock.hotkey.ss58_address = "fake_ss58_address" + return mock + + +@pytest.fixture +def mock_pow_solution(mocker): + mock = mocker.MagicMock(spec=POWSolution) + mock.block_number = 123 + mock.nonce = 456 + mock.seal = [0, 1, 2, 3] + mock.is_stale.return_value = False + return mock + + +@pytest.fixture +def mock_new_wallet(mocker): + mock = mocker.MagicMock(spec=Wallet) + mock.coldkeypub.ss58_address = "mock_address" + mock.coldkey = mocker.MagicMock() + mock.hotkey = mocker.MagicMock() + return mock + + +@pytest.mark.parametrize( + "subnet_exists, neuron_is_null, cuda_available, expected_result, test_id", + [ + (False, True, True, False, "subnet-does-not-exist"), + (True, False, True, True, "neuron-already-registered"), + (True, True, False, False, "cuda-unavailable"), + ], +) +def test_register_extrinsic_without_pow( + mock_subtensor, + mock_wallet, + subnet_exists, + neuron_is_null, + cuda_available, + expected_result, + test_id, + mocker, +): + # Arrange + with ( + mocker.patch.object( + mock_subtensor, "subnet_exists", return_value=subnet_exists + ), + mocker.patch.object( + mock_subtensor, + "get_neuron_for_pubkey_and_subnet", + return_value=mocker.MagicMock(is_null=neuron_is_null), + ), + mocker.patch("torch.cuda.is_available", return_value=cuda_available), + mocker.patch( + "bittensor.utils.registration.pow._get_block_with_retry", + return_value=(0, 0, "00ff11ee"), + ), + ): + # Act + result = registration.register_extrinsic( + subtensor=mock_subtensor, + wallet=mock_wallet, + netuid=123, + wait_for_inclusion=True, + wait_for_finalization=True, + max_allowed_attempts=3, + output_in_place=True, + cuda=True, + dev_id=0, + tpb=256, + num_processes=None, + update_interval=None, + log_verbose=False, + ) + + # Assert + assert result == expected_result, f"Test failed for test_id: {test_id}" + + +@pytest.mark.parametrize( + "pow_success, pow_stale, registration_success, cuda, hotkey_registered, expected_result, test_id", + [ + (True, False, True, False, False, True, "successful-with-valid-pow"), + (True, False, True, True, False, True, "successful-with-valid-cuda-pow"), + # Pow failed but key was registered already + (False, False, False, False, True, True, "hotkey-registered"), + # Pow was a success but registration failed with error 'key already registered' + (True, False, False, False, False, True, "registration-fail-key-registered"), + ], +) +def test_register_extrinsic_with_pow( + mock_subtensor, + mock_wallet, + mock_pow_solution, + pow_success, + pow_stale, + registration_success, + cuda, + hotkey_registered, + expected_result, + test_id, + mocker, +): + # Arrange + with mocker.patch( + "bittensor.utils.registration.pow._solve_for_difficulty_fast", + return_value=mock_pow_solution if pow_success else None, + ), mocker.patch( + "bittensor.utils.registration.pow._solve_for_difficulty_fast_cuda", + return_value=mock_pow_solution if pow_success else None, + ), mocker.patch( + "bittensor.core.extrinsics.registration._do_pow_register", + return_value=(registration_success, "HotKeyAlreadyRegisteredInSubNet"), + ), mocker.patch("torch.cuda.is_available", return_value=cuda): + # Act + if pow_success: + mock_pow_solution.is_stale.return_value = pow_stale + + if not pow_success and hotkey_registered: + mock_subtensor.is_hotkey_registered = mocker.MagicMock( + return_value=hotkey_registered + ) + + result = registration.register_extrinsic( + subtensor=mock_subtensor, + wallet=mock_wallet, + netuid=123, + wait_for_inclusion=True, + wait_for_finalization=True, + max_allowed_attempts=3, + output_in_place=True, + cuda=cuda, + dev_id=0, + tpb=256, + num_processes=None, + update_interval=None, + log_verbose=False, + ) + + # Assert + assert result == expected_result, f"Test failed for test_id: {test_id}." -def test_burned_register_extrinsic(mocker): - """ "Verify that sync `burned_register_extrinsic` method calls proper async method.""" - # Preps - fake_subtensor = mocker.Mock() - fake_wallet = mocker.Mock() - netuid = 1 - wait_for_inclusion = True - wait_for_finalization = True - - mocked_execute_coroutine = mocker.patch.object(registration, "execute_coroutine") - mocked_burned_register_extrinsic = mocker.Mock() - registration.async_burned_register_extrinsic = mocked_burned_register_extrinsic - - # Call - result = registration.burned_register_extrinsic( - subtensor=fake_subtensor, - wallet=fake_wallet, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # Asserts - - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_burned_register_extrinsic.return_value, - event_loop=fake_subtensor.event_loop, - ) - mocked_burned_register_extrinsic.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - wallet=fake_wallet, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - assert result == mocked_execute_coroutine.return_value - - -def test_register_extrinsic(mocker): - """ "Verify that sync `register_extrinsic` method calls proper async method.""" - # Preps - fake_subtensor = mocker.Mock() - fake_wallet = mocker.Mock() - netuid = 1 - wait_for_inclusion = True - wait_for_finalization = True - max_allowed_attempts = 7 - output_in_place = True - cuda = True - dev_id = 5 - tpb = 12 - num_processes = 8 - update_interval = 2 - log_verbose = True - - mocked_execute_coroutine = mocker.patch.object(registration, "execute_coroutine") - mocked_register_extrinsic = mocker.Mock() - registration.async_register_extrinsic = mocked_register_extrinsic - - # Call - result = registration.register_extrinsic( - subtensor=fake_subtensor, - wallet=fake_wallet, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - max_allowed_attempts=max_allowed_attempts, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - # Asserts - - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_register_extrinsic.return_value, - event_loop=fake_subtensor.event_loop, - ) - mocked_register_extrinsic.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - wallet=fake_wallet, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - max_allowed_attempts=max_allowed_attempts, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - assert result == mocked_execute_coroutine.return_value +@pytest.mark.parametrize( + "subnet_exists, neuron_is_null, recycle_success, is_registered, expected_result, test_id", + [ + # Happy paths + (True, False, None, None, True, "neuron-not-null"), + (True, True, True, True, True, "happy-path-wallet-registered"), + # Error paths + (False, True, False, None, False, "subnet-non-existence"), + (True, True, False, False, False, "error-path-recycling-failed"), + (True, True, True, False, False, "error-path-not-registered"), + ], +) +def test_burned_register_extrinsic( + mock_subtensor, + mock_wallet, + subnet_exists, + neuron_is_null, + recycle_success, + is_registered, + expected_result, + test_id, + mocker, +): + # Arrange + with mocker.patch.object( + mock_subtensor, "subnet_exists", return_value=subnet_exists + ), mocker.patch.object( + mock_subtensor, + "get_neuron_for_pubkey_and_subnet", + return_value=mocker.MagicMock(is_null=neuron_is_null), + ), mocker.patch( + "bittensor.core.extrinsics.registration._do_burned_register", + return_value=(recycle_success, "Mock error message"), + ), mocker.patch.object( + mock_subtensor, "is_hotkey_registered", return_value=is_registered + ): + # Act + result = registration.burned_register_extrinsic( + subtensor=mock_subtensor, wallet=mock_wallet, netuid=123 + ) + # Assert + assert result == expected_result, f"Test failed for test_id: {test_id}" diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py index 7fae887011..21395735fc 100644 --- a/tests/unit_tests/extrinsics/test_root.py +++ b/tests/unit_tests/extrinsics/test_root.py @@ -3,81 +3,241 @@ from bittensor.core.extrinsics import root -def test_root_register_extrinsic(mocker): - """Verify that sync `root_register_extrinsic` method calls proper async method.""" - # Preps - fake_subtensor = mocker.Mock() - fake_wallet = mocker.Mock() - wait_for_inclusion = True - wait_for_finalization = True +@pytest.fixture +def mock_subtensor(mocker): + mock = mocker.MagicMock(spec=Subtensor) + mock.network = "magic_mock" + mock.substrate = mocker.Mock() + return mock - mocked_execute_coroutine = mocker.patch.object(root, "execute_coroutine") - mocked_root_register_extrinsic = mocker.Mock() - root.async_root_register_extrinsic = mocked_root_register_extrinsic - # Call - result = root.root_register_extrinsic( - subtensor=fake_subtensor, - wallet=fake_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) +@pytest.fixture +def mock_wallet(mocker): + mock = mocker.MagicMock() + mock.hotkey.ss58_address = "fake_hotkey_address" + return mock - # Asserts - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_root_register_extrinsic.return_value, - event_loop=fake_subtensor.event_loop, +@pytest.mark.parametrize( + "wait_for_inclusion, wait_for_finalization, hotkey_registered, registration_success, expected_result", + [ + ( + False, + True, + [True, None], + True, + True, + ), # Already registered after attempt + ( + False, + True, + [False, 1], + True, + True, + ), # Registration succeeds with user confirmation + (False, True, [False, None], False, False), # Registration fails + ( + False, + True, + [False, None], + True, + False, + ), # Registration succeeds but neuron not found + ], + ids=[ + "success-already-registered", + "success-registration-succeeds", + "failure-registration-failed", + "failure-neuron-not-found", + ], +) +def test_root_register_extrinsic( + mock_subtensor, + mock_wallet, + wait_for_inclusion, + wait_for_finalization, + hotkey_registered, + registration_success, + expected_result, + mocker, +): + # Arrange + mock_subtensor.is_hotkey_registered.return_value = hotkey_registered[0] + + # Preps + mocked_sign_and_send_extrinsic = mocker.patch.object( + mock_subtensor, + "sign_and_send_extrinsic", + return_value=(registration_success, "Error registering"), ) - mocked_root_register_extrinsic.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - wallet=fake_wallet, - netuid=0, + mocker.patch.object( + mock_subtensor.substrate, + "query", + return_value=hotkey_registered[1], + ) + + # Act + result = root.root_register_extrinsic( + subtensor=mock_subtensor, + wallet=mock_wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - assert result == mocked_execute_coroutine.return_value + # Assert + assert result == expected_result + if not hotkey_registered[0]: + mock_subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="root_register", + call_params={"hotkey": "fake_hotkey_address"}, + ) + mocked_sign_and_send_extrinsic.assert_called_once_with( + mock_subtensor.substrate.compose_call.return_value, + wallet=mock_wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) -def test_set_root_weights_extrinsic(mocker): - """Verify that sync `set_root_weights_extrinsic` method calls proper async method.""" - # Preps - fake_subtensor = mocker.Mock() - fake_wallet = mocker.Mock() - netuids = [1, 2, 3, 4] - weights = [0.1, 0.2, 0.3, 0.4] - version_key = 2 - wait_for_inclusion = True - wait_for_finalization = True - mocked_execute_coroutine = mocker.patch.object(root, "execute_coroutine") - mocked_set_root_weights_extrinsic = mocker.Mock() - root.async_set_root_weights_extrinsic = mocked_set_root_weights_extrinsic +@pytest.mark.parametrize( + "wait_for_inclusion, wait_for_finalization, netuids, weights, expected_success", + [ + (True, False, [1, 2], [0.5, 0.5], True), # Success - weights set + ( + False, + False, + [1, 2], + [0.5, 0.5], + True, + ), # Success - weights set no wait + ( + True, + False, + [1, 2], + [2000, 20], + True, + ), # Success - large value to be normalized + ( + True, + False, + [1, 2], + [2000, 0], + True, + ), # Success - single large value + ( + True, + False, + [1, 2], + [0.5, 0.5], + False, + ), # Failure - setting weights failed + ], + ids=[ + "success-weights-set", + "success-not-wait", + "success-large-value", + "success-single-value", + "failure-setting-weights", + ], +) +def test_set_root_weights_extrinsic( + mock_subtensor, + mock_wallet, + wait_for_inclusion, + wait_for_finalization, + netuids, + weights, + expected_success, + mocker, +): + # Preps + root._do_set_root_weights = mocker.Mock( + return_value=(expected_success, "Mock error") + ) + root._get_limits = mocker.Mock( + return_value=(0, 1), + ) # Call result = root.set_root_weights_extrinsic( - subtensor=fake_subtensor, - wallet=fake_wallet, + subtensor=mock_subtensor, + wallet=mock_wallet, netuids=netuids, weights=weights, - version_key=version_key, + version_key=0, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) # Asserts + assert result == expected_success - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_set_root_weights_extrinsic.return_value, - event_loop=fake_subtensor.event_loop, - ) - mocked_set_root_weights_extrinsic.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - wallet=fake_wallet, - netuids=netuids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + +@pytest.mark.parametrize( + "wait_for_inclusion, wait_for_finalization, netuids, weights, user_response, expected_success", + [ + (True, False, [1, 2], [0.5, 0.5], True, True), # Success - weights set + ( + False, + False, + [1, 2], + [0.5, 0.5], + None, + True, + ), # Success - weights set no wait + ( + True, + False, + [1, 2], + [2000, 20], + True, + True, + ), # Success - large value to be normalized + ( + True, + False, + [1, 2], + [2000, 0], + True, + True, + ), # Success - single large value + ( + True, + False, + [1, 2], + [0.5, 0.5], + None, + False, + ), # Failure - setting weights failed + ], + ids=[ + "success-weights-set", + "success-not-wait", + "success-large-value", + "success-single-value", + "failure-setting-weights", + ], +) +def test_set_root_weights_extrinsic_torch( + mock_subtensor, + mock_wallet, + wait_for_inclusion, + wait_for_finalization, + netuids, + weights, + user_response, + expected_success, + force_legacy_torch_compatible_api, + mocker, +): + test_set_root_weights_extrinsic( + mock_subtensor, + mock_wallet, + wait_for_inclusion, + wait_for_finalization, + netuids, + weights, + expected_success, + mocker, ) - assert result == mocked_execute_coroutine.return_value diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index 27b11ede1f..6d00e97629 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -1,158 +1,376 @@ +# The MIT License (MIT) +# Copyright © 2024 Opentensor Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from unittest.mock import MagicMock, patch + +import pytest +from bittensor_wallet import Wallet + +from bittensor.core.axon import Axon +from bittensor.core.subtensor import Subtensor from bittensor.core.extrinsics import serving -def test_do_serve_axon(mocker): - """Verify that sync `do_serve_axon` method calls proper async method.""" - # Preps - fake_subtensor = mocker.Mock() - fake_wallet = mocker.Mock() - call_params = mocker.Mock() - wait_for_inclusion = True - wait_for_finalization = True - - mocked_execute_coroutine = mocker.patch.object(serving, "execute_coroutine") - mocked_do_serve_axon = mocker.Mock() - serving.async_do_serve_axon = mocked_do_serve_axon - - # Call - result = serving.do_serve_axon( - self=fake_subtensor, - wallet=fake_wallet, - call_params=call_params, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) +@pytest.fixture +def mock_subtensor(mocker): + mock_subtensor = mocker.MagicMock(spec=Subtensor) + mock_subtensor.network = "test_network" + mock_subtensor.substrate = mocker.MagicMock() + return mock_subtensor - # Asserts - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_do_serve_axon.return_value, - event_loop=fake_subtensor.event_loop, - ) - mocked_do_serve_axon.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - wallet=fake_wallet, - call_params=call_params, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - assert result == mocked_execute_coroutine.return_value - - -def test_serve_axon_extrinsic(mocker): - """Verify that sync `serve_axon_extrinsic` method calls proper async method.""" - # Preps - fake_subtensor = mocker.Mock() - netuid = 2 - axon = mocker.Mock() - wait_for_inclusion = True - wait_for_finalization = True - certificate = mocker.Mock() - - mocked_execute_coroutine = mocker.patch.object(serving, "execute_coroutine") - mocked_serve_axon_extrinsic = mocker.Mock() - serving.async_serve_axon_extrinsic = mocked_serve_axon_extrinsic - - # Call - result = serving.serve_axon_extrinsic( - subtensor=fake_subtensor, - netuid=netuid, - axon=axon, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - certificate=certificate, - ) +@pytest.fixture +def mock_wallet(mocker): + wallet = mocker.MagicMock(spec=Wallet) + wallet.hotkey.ss58_address = "hotkey_address" + wallet.coldkeypub.ss58_address = "coldkey_address" + return wallet - # Asserts - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_serve_axon_extrinsic.return_value, - event_loop=fake_subtensor.event_loop, - ) - mocked_serve_axon_extrinsic.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - netuid=netuid, - axon=axon, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - certificate=certificate, - ) - assert result == mocked_execute_coroutine.return_value - - -def test_publish_metadata(mocker): - """Verify that `publish_metadata` calls proper async method.""" - # Preps - fake_subtensor = mocker.Mock() - fake_wallet = mocker.Mock() - netuid = 2 - data_type = "data_type" - data = b"data" - wait_for_inclusion = True - wait_for_finalization = True - - mocked_execute_coroutine = mocker.patch.object(serving, "execute_coroutine") - mocked_publish_metadata = mocker.Mock() - serving.async_publish_metadata = mocked_publish_metadata - - # Call - result = serving.publish_metadata( - self=fake_subtensor, - wallet=fake_wallet, - netuid=netuid, - data_type=data_type, - data=data, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) +@pytest.fixture +def mock_axon(mock_wallet, mocker): + axon = mocker.MagicMock(spec=Axon) + axon.wallet = mock_wallet() + axon.external_port = 9221 + return axon - # Asserts - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_publish_metadata.return_value, - event_loop=fake_subtensor.event_loop, - ) - mocked_publish_metadata.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - wallet=fake_wallet, - netuid=netuid, - data_type=data_type, - data=data, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - assert result == mocked_execute_coroutine.return_value - - -def test_get_metadata(mocker): - """Verify that `get_metadata` calls proper async method.""" - # Preps - fake_subtensor = mocker.Mock() - netuid = 2 - hotkey = "hotkey" - block = 123 - - mocked_execute_coroutine = mocker.patch.object(serving, "execute_coroutine") - mocked_get_metadata = mocker.Mock() - serving.async_get_metadata = mocked_get_metadata - - # Call - result = serving.get_metadata( - self=fake_subtensor, - netuid=netuid, - hotkey=hotkey, - block=block, +@pytest.mark.parametrize( + "ip,port,protocol,netuid,placeholder1,placeholder2,wait_for_inclusion,wait_for_finalization,expected,test_id,", + [ + ( + "192.168.1.1", + 9221, + 1, + 0, + 0, + 0, + False, + True, + True, + "happy-path-no-wait", + ), + ( + "192.168.1.2", + 9222, + 2, + 1, + 1, + 1, + True, + False, + True, + "happy-path-wait-for-inclusion", + ), + ( + "192.168.1.3", + 9223, + 3, + 2, + 2, + 2, + False, + True, + True, + "happy-path-wait-for-finalization", + ), + ], + ids=[ + "happy-path-no-wait", + "happy-path-wait-for-inclusion", + "happy-path-wait-for-finalization", + ], +) +def test_serve_extrinsic_happy_path( + mock_subtensor, + mock_wallet, + ip, + port, + protocol, + netuid, + placeholder1, + placeholder2, + wait_for_inclusion, + wait_for_finalization, + expected, + test_id, + mocker, +): + # Arrange + serving.do_serve_axon = mocker.MagicMock(return_value=(True, "")) + # Act + result = serving.serve_extrinsic( + mock_subtensor, + mock_wallet, + ip, + port, + protocol, + netuid, + placeholder1, + placeholder2, + wait_for_inclusion, + wait_for_finalization, ) - # Asserts - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_get_metadata.return_value, - event_loop=fake_subtensor.event_loop, + # Assert + assert result == expected, f"Test ID: {test_id}" + + +# Various edge cases +@pytest.mark.parametrize( + "ip,port,protocol,netuid,placeholder1,placeholder2,wait_for_inclusion,wait_for_finalization,expected,test_id,", + [ + ( + "192.168.1.4", + 9224, + 4, + 3, + 3, + 3, + True, + True, + True, + "edge_case_max_values", + ), + ], + ids=["edge-case-max-values"], +) +def test_serve_extrinsic_edge_cases( + mock_subtensor, + mock_wallet, + ip, + port, + protocol, + netuid, + placeholder1, + placeholder2, + wait_for_inclusion, + wait_for_finalization, + expected, + test_id, + mocker, +): + # Arrange + serving.do_serve_axon = mocker.MagicMock(return_value=(True, "")) + # Act + result = serving.serve_extrinsic( + mock_subtensor, + mock_wallet, + ip, + port, + protocol, + netuid, + placeholder1, + placeholder2, + wait_for_inclusion, + wait_for_finalization, ) - mocked_get_metadata.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - netuid=netuid, - hotkey=hotkey, - block=block, + + # Assert + assert result == expected, f"Test ID: {test_id}" + + +# Various error cases +@pytest.mark.parametrize( + "ip,port,protocol,netuid,placeholder1,placeholder2,wait_for_inclusion,wait_for_finalization,expected_error_message,test_id,", + [ + ( + "192.168.1.5", + 9225, + 5, + 4, + 4, + 4, + True, + True, + False, + "error-case-failed-serve", + ), + ], + ids=["error-case-failed-serve"], +) +def test_serve_extrinsic_error_cases( + mock_subtensor, + mock_wallet, + ip, + port, + protocol, + netuid, + placeholder1, + placeholder2, + wait_for_inclusion, + wait_for_finalization, + expected_error_message, + test_id, + mocker, +): + # Arrange + serving.do_serve_axon = mocker.MagicMock(return_value=(False, "Error serving axon")) + # Act + result = serving.serve_extrinsic( + mock_subtensor, + mock_wallet, + ip, + port, + protocol, + netuid, + placeholder1, + placeholder2, + wait_for_inclusion, + wait_for_finalization, ) - assert result == mocked_execute_coroutine.return_value + + # Assert + assert result == expected_error_message, f"Test ID: {test_id}" + + +@pytest.mark.parametrize( + "netuid, wait_for_inclusion, wait_for_finalization, external_ip, external_ip_success, serve_success, expected_result, test_id", + [ + # Happy path test + (1, False, True, "192.168.1.1", True, True, True, "happy-ext-ip"), + (1, False, True, None, True, True, True, "happy-net-external-ip"), + # Edge cases + (1, True, True, "192.168.1.1", True, True, True, "edge-case-wait"), + # Error cases + (1, False, True, None, False, True, False, "error-fetching-external-ip"), + ( + 1, + False, + True, + "192.168.1.1", + True, + False, + False, + "error-serving-axon", + ), + ], + ids=[ + "happy-axon-external-ip", + "happy-net-external-ip", + "edge-case-wait", + "error-fetching-external-ip", + "error-serving-axon", + ], +) +def test_serve_axon_extrinsic( + mock_subtensor, + mock_axon, + netuid, + wait_for_inclusion, + wait_for_finalization, + external_ip, + external_ip_success, + serve_success, + expected_result, + test_id, + mocker, +): + mock_axon.external_ip = external_ip + # Arrange + with patch( + "bittensor.utils.networking.get_external_ip", + side_effect=Exception("Failed to fetch IP") + if not external_ip_success + else MagicMock(return_value="192.168.1.1"), + ): + serving.do_serve_axon = mocker.MagicMock(return_value=(serve_success, "")) + # Act + if not external_ip_success: + with pytest.raises(ConnectionError): + serving.serve_axon_extrinsic( + mock_subtensor, + netuid, + mock_axon, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + else: + result = serving.serve_axon_extrinsic( + mock_subtensor, + netuid, + mock_axon, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Assert + assert result == expected_result, f"Test ID: {test_id}" + + +@pytest.mark.parametrize( + "wait_for_inclusion, wait_for_finalization, net_uid, type_u, data, response_success, expected_result, test_id", + [ + ( + True, + True, + 1, + "Sha256", + b"mock_bytes_data", + True, + True, + "happy-path-wait", + ), + ( + False, + False, + 1, + "Sha256", + b"mock_bytes_data", + True, + True, + "happy-path-no-wait", + ), + ], + ids=["happy-path-wait", "happy-path-no-wait"], +) +def test_publish_metadata( + mock_subtensor, + mock_wallet, + wait_for_inclusion, + wait_for_finalization, + net_uid, + type_u, + data, + response_success, + expected_result, + test_id, +): + # Arrange + with patch.object(mock_subtensor.substrate, "compose_call"), patch.object( + mock_subtensor.substrate, "create_signed_extrinsic" + ), patch.object( + mock_subtensor.substrate, + "submit_extrinsic", + return_value=MagicMock( + is_success=response_success, + process_events=MagicMock(), + error_message="error", + ), + ): + # Act + result = serving.publish_metadata( + subtensor=mock_subtensor, + wallet=mock_wallet, + netuid=net_uid, + data_type=type_u, + data=data, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # Assert + assert result == expected_result, f"Test ID: {test_id}" diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index 116065463f..a2aaa4aaab 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -1,47 +1,248 @@ -from bittensor.core.extrinsics import set_weights +from unittest.mock import MagicMock, patch +import pytest +import torch +from bittensor_wallet import Wallet -def test_set_weights_extrinsic(mocker): - """ "Verify that sync `set_weights_extrinsic` method calls proper async method.""" - # Preps - fake_subtensor = mocker.Mock() - fake_wallet = mocker.Mock() - netuid = 2 - uids = [1, 2, 3, 4] - weights = [0.1, 0.2, 0.3, 0.4] - version_key = 2 - wait_for_inclusion = True - wait_for_finalization = True +from bittensor.core import subtensor as subtensor_module +from bittensor.core.extrinsics.set_weights import ( + _do_set_weights, + set_weights_extrinsic, +) +from bittensor.core.settings import version_as_int +from bittensor.core.subtensor import Subtensor - mocked_execute_coroutine = mocker.patch.object(set_weights, "execute_coroutine") - mocked_set_weights_extrinsic = mocker.Mock() - set_weights.async_set_weights_extrinsic = mocked_set_weights_extrinsic + +@pytest.fixture +def mock_subtensor(): + mock = MagicMock(spec=Subtensor) + mock.network = "mock_network" + mock.substrate = MagicMock() + return mock + + +@pytest.fixture +def mock_wallet(): + mock = MagicMock(spec=Wallet) + return mock + + +@pytest.mark.parametrize( + "uids, weights, version_key, wait_for_inclusion, wait_for_finalization, expected_success, expected_message", + [ + ( + [1, 2], + [0.5, 0.5], + 0, + True, + False, + True, + "Successfully set weights and Finalized.", + ), + ( + [1, 2], + [0.5, 0.4], + 0, + False, + False, + True, + "Not waiting for finalization or inclusion.", + ), + ( + [1, 2], + [0.5, 0.5], + 0, + True, + False, + False, + "Mock error message", + ), + ], + ids=[ + "happy-flow", + "not-waiting-finalization-inclusion", + "error-flow", + ], +) +def test_set_weights_extrinsic( + mock_subtensor, + mock_wallet, + uids, + weights, + version_key, + wait_for_inclusion, + wait_for_finalization, + expected_success, + expected_message, +): + uids_tensor = torch.tensor(uids, dtype=torch.int64) + weights_tensor = torch.tensor(weights, dtype=torch.float32) + with patch( + "bittensor.utils.weight_utils.convert_weights_and_uids_for_emit", + return_value=(uids_tensor, weights_tensor), + ), patch( + "bittensor.core.extrinsics.set_weights._do_set_weights", + return_value=(expected_success, "Mock error message"), + ): + result, message = set_weights_extrinsic( + subtensor=mock_subtensor, + wallet=mock_wallet, + netuid=123, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + assert result == expected_success, f"Test {expected_message} failed." + assert message == expected_message, f"Test {expected_message} failed." + + +def test_do_set_weights_is_success(mock_subtensor, mocker): + """Successful _do_set_weights call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_uids = [1, 2, 3] + fake_vals = [4, 5, 6] + fake_netuid = 1 + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + mock_subtensor.substrate.submit_extrinsic.return_value.is_success = True # Call - result = set_weights.set_weights_extrinsic( - subtensor=fake_subtensor, + result = _do_set_weights( + subtensor=mock_subtensor, wallet=fake_wallet, - netuid=netuid, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + uids=fake_uids, + vals=fake_vals, + netuid=fake_netuid, + version_key=version_as_int, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, ) # Asserts - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_set_weights_extrinsic.return_value, - event_loop=fake_subtensor.event_loop, + mock_subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="set_weights", + call_params={ + "dests": fake_uids, + "weights": fake_vals, + "netuid": fake_netuid, + "version_key": version_as_int, + }, ) - mocked_set_weights_extrinsic.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, + + mock_subtensor.substrate.create_signed_extrinsic.assert_called_once() + _, kwargs = mock_subtensor.substrate.create_signed_extrinsic.call_args + assert kwargs["call"] == mock_subtensor.substrate.compose_call.return_value + assert kwargs["keypair"] == fake_wallet.hotkey + assert kwargs["era"] == {"period": 5} + + assert result == (True, "Successfully set weights.") + + +def test_do_set_weights_is_not_success(mock_subtensor, mocker): + """Unsuccessful _do_set_weights call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_uids = [1, 2, 3] + fake_vals = [4, 5, 6] + fake_netuid = 1 + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + mock_subtensor.substrate.submit_extrinsic.return_value.is_success = False + mocked_format_error_message = mocker.MagicMock() + subtensor_module.format_error_message = mocked_format_error_message + + # Call + result = _do_set_weights( + subtensor=mock_subtensor, wallet=fake_wallet, - netuid=netuid, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - assert result == mocked_execute_coroutine.return_value + uids=fake_uids, + vals=fake_vals, + netuid=fake_netuid, + version_key=version_as_int, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mock_subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="set_weights", + call_params={ + "dests": fake_uids, + "weights": fake_vals, + "netuid": fake_netuid, + "version_key": version_as_int, + }, + ) + + mock_subtensor.substrate.create_signed_extrinsic.assert_called_once() + _, kwargs = mock_subtensor.substrate.create_signed_extrinsic.call_args + assert kwargs["call"] == mock_subtensor.substrate.compose_call.return_value + assert kwargs["keypair"] == fake_wallet.hotkey + assert kwargs["era"] == {"period": 5} + + mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( + extrinsic=mock_subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + assert result == ( + False, + "Subtensor returned `UnknownError(UnknownType)` error. This means: `Unknown Description`.", + ) + + +def test_do_set_weights_no_waits(mock_subtensor, mocker): + """Successful _do_set_weights call without wait flags for fake_wait_for_inclusion and fake_wait_for_finalization.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_uids = [1, 2, 3] + fake_vals = [4, 5, 6] + fake_netuid = 1 + fake_wait_for_inclusion = False + fake_wait_for_finalization = False + + # Call + result = _do_set_weights( + subtensor=mock_subtensor, + wallet=fake_wallet, + uids=fake_uids, + vals=fake_vals, + netuid=fake_netuid, + version_key=version_as_int, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mock_subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="set_weights", + call_params={ + "dests": fake_uids, + "weights": fake_vals, + "netuid": fake_netuid, + "version_key": version_as_int, + }, + ) + + mock_subtensor.substrate.create_signed_extrinsic.assert_called_once() + _, kwargs = mock_subtensor.substrate.create_signed_extrinsic.call_args + assert kwargs["call"] == mock_subtensor.substrate.compose_call.return_value + assert kwargs["keypair"] == fake_wallet.hotkey + assert kwargs["era"] == {"period": 5} + + mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( + extrinsic=mock_subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + assert result == (True, "Not waiting for finalization or inclusion.") diff --git a/tests/unit_tests/extrinsics/test_staking.py b/tests/unit_tests/extrinsics/test_staking.py index d30d225ebd..b6fc9cb38f 100644 --- a/tests/unit_tests/extrinsics/test_staking.py +++ b/tests/unit_tests/extrinsics/test_staking.py @@ -1,20 +1,28 @@ from bittensor.core.extrinsics import staking +from bittensor.utils.balance import Balance def test_add_stake_extrinsic(mocker): """Verify that sync `add_stake_extrinsic` method calls proper async method.""" # Preps - fake_subtensor = mocker.Mock() - fake_wallet = mocker.Mock() + fake_subtensor = mocker.Mock( + **{ + "get_balance.return_value": Balance(10), + "get_existential_deposit.return_value": Balance(1), + "get_hotkey_owner.return_value": "hotkey_owner", + "sign_and_send_extrinsic.return_value": (True, ""), + } + ) + fake_wallet = mocker.Mock( + **{ + "coldkeypub.ss58_address": "hotkey_owner", + } + ) hotkey_ss58 = "hotkey" amount = 1.1 wait_for_inclusion = True wait_for_finalization = True - mocked_execute_coroutine = mocker.patch.object(staking, "execute_coroutine") - mocked_add_stake_extrinsic = mocker.Mock() - staking.async_add_stake_extrinsic = mocked_add_stake_extrinsic - # Call result = staking.add_stake_extrinsic( subtensor=fake_subtensor, @@ -26,35 +34,62 @@ def test_add_stake_extrinsic(mocker): ) # Asserts - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_add_stake_extrinsic.return_value, - event_loop=fake_subtensor.event_loop, + assert result is True + + fake_subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="add_stake", + call_params={ + "hotkey": "hotkey", + "amount_staked": 9, + }, ) - mocked_add_stake_extrinsic.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - wallet=fake_wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + fake_subtensor.sign_and_send_extrinsic.assert_called_once_with( + fake_subtensor.substrate.compose_call.return_value, + fake_wallet, + True, + True, ) - assert result == mocked_execute_coroutine.return_value def test_add_stake_multiple_extrinsic(mocker): """Verify that sync `add_stake_multiple_extrinsic` method calls proper async method.""" # Preps - fake_subtensor = mocker.Mock() - fake_wallet = mocker.Mock() + fake_subtensor = mocker.Mock( + **{ + "get_balance.return_value": Balance(10.0), + "sign_and_send_extrinsic.return_value": (True, ""), + "substrate.query_multi.return_value": [ + ( + mocker.Mock( + **{ + "params": ["hotkey1"], + }, + ), + 0, + ), + ( + mocker.Mock( + **{ + "params": ["hotkey2"], + }, + ), + 0, + ), + ], + "substrate.query.return_value": 0, + } + ) + fake_wallet = mocker.Mock( + **{ + "coldkeypub.ss58_address": "hotkey_owner", + } + ) hotkey_ss58s = ["hotkey1", "hotkey2"] amounts = [1.1, 2.2] wait_for_inclusion = True wait_for_finalization = True - mocked_execute_coroutine = mocker.patch.object(staking, "execute_coroutine") - mocked_add_stake_multiple_extrinsic = mocker.Mock() - staking.async_add_stake_multiple_extrinsic = mocked_add_stake_multiple_extrinsic - # Call result = staking.add_stake_multiple_extrinsic( subtensor=fake_subtensor, @@ -66,16 +101,29 @@ def test_add_stake_multiple_extrinsic(mocker): ) # Asserts - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_add_stake_multiple_extrinsic.return_value, - event_loop=fake_subtensor.event_loop, + assert result is True + assert fake_subtensor.substrate.compose_call.call_count == 2 + assert fake_subtensor.sign_and_send_extrinsic.call_count == 2 + + fake_subtensor.substrate.compose_call.assert_any_call( + call_module="SubtensorModule", + call_function="add_stake", + call_params={ + "hotkey": "hotkey1", + "amount_staked": 1099999666, + }, ) - mocked_add_stake_multiple_extrinsic.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - wallet=fake_wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + fake_subtensor.substrate.compose_call.assert_any_call( + call_module="SubtensorModule", + call_function="add_stake", + call_params={ + "hotkey": "hotkey2", + "amount_staked": 2199999333, + }, + ) + fake_subtensor.sign_and_send_extrinsic.assert_called_with( + fake_subtensor.substrate.compose_call.return_value, + fake_wallet, + True, + True, ) - assert result == mocked_execute_coroutine.return_value diff --git a/tests/unit_tests/extrinsics/test_transfer.py b/tests/unit_tests/extrinsics/test_transfer.py index f85e3f267e..607d703758 100644 --- a/tests/unit_tests/extrinsics/test_transfer.py +++ b/tests/unit_tests/extrinsics/test_transfer.py @@ -1,47 +1,147 @@ -from bittensor.core.extrinsics import transfer +import pytest +from bittensor.core import subtensor as subtensor_module +from bittensor.core.extrinsics.transfer import _do_transfer +from bittensor.core.subtensor import Subtensor +from bittensor.utils.balance import Balance -def test_transfer_extrinsic(mocker): - """Verify that sync `transfer_extrinsic` method calls proper async method.""" - # Preps - fake_subtensor = mocker.Mock() - fake_wallet = mocker.Mock() - dest = "hotkey" - amount = 1.1 - transfer_all = True - wait_for_inclusion = True - wait_for_finalization = True - keep_alive = False - mocked_execute_coroutine = mocker.patch.object(transfer, "execute_coroutine") - mocked_transfer_extrinsic = mocker.Mock() - transfer.async_transfer_extrinsic = mocked_transfer_extrinsic +@pytest.fixture +def subtensor(mocker): + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) + return Subtensor() + + +def test_do_transfer_is_success_true(subtensor, mocker): + """Successful do_transfer call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_dest = "SS58PUBLICKEY" + fake_transfer_balance = Balance(1) + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + subtensor.substrate.submit_extrinsic.return_value.is_success = True + + # Call + result = _do_transfer( + subtensor, + fake_wallet, + fake_dest, + fake_transfer_balance, + fake_wait_for_inclusion, + fake_wait_for_finalization, + ) + + # Asserts + subtensor.substrate.compose_call.assert_called_once_with( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, + ) + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey + ) + subtensor.substrate.submit_extrinsic.assert_called_once_with( + extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + # subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == ( + True, + subtensor.substrate.submit_extrinsic.return_value.block_hash, + "Success with response.", + ) + + +def test_do_transfer_is_success_false(subtensor, mocker): + """Successful do_transfer call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_dest = "SS58PUBLICKEY" + fake_transfer_balance = Balance(1) + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + subtensor.substrate.submit_extrinsic.return_value.is_success = False + + mocked_format_error_message = mocker.Mock() + mocker.patch( + "bittensor.core.extrinsics.transfer.format_error_message", + mocked_format_error_message, + ) # Call - result = transfer.transfer_extrinsic( - subtensor=fake_subtensor, - wallet=fake_wallet, - dest=dest, - amount=amount, - transfer_all=transfer_all, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - keep_alive=keep_alive, + result = _do_transfer( + subtensor, + fake_wallet, + fake_dest, + fake_transfer_balance, + fake_wait_for_inclusion, + fake_wait_for_finalization, ) # Asserts - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_transfer_extrinsic.return_value, - event_loop=fake_subtensor.event_loop, - ) - mocked_transfer_extrinsic.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - wallet=fake_wallet, - destination=dest, - amount=amount, - transfer_all=transfer_all, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - keep_alive=keep_alive, - ) - assert result == mocked_execute_coroutine.return_value + subtensor.substrate.compose_call.assert_called_once_with( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, + ) + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey + ) + subtensor.substrate.submit_extrinsic.assert_called_once_with( + extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + mocked_format_error_message.assert_called_once_with( + subtensor.substrate.submit_extrinsic.return_value.error_message + ) + + assert result == ( + False, + "", + mocked_format_error_message.return_value, + ) + + +def test_do_transfer_no_waits(subtensor, mocker): + """Successful do_transfer call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_dest = "SS58PUBLICKEY" + fake_transfer_balance = Balance(1) + fake_wait_for_inclusion = False + fake_wait_for_finalization = False + + # Call + result = _do_transfer( + subtensor, + fake_wallet, + fake_dest, + fake_transfer_balance, + fake_wait_for_inclusion, + fake_wait_for_finalization, + ) + + # Asserts + subtensor.substrate.compose_call.assert_called_once_with( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, + ) + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey + ) + subtensor.substrate.submit_extrinsic.assert_called_once_with( + extrinsic=subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + assert result == (True, "", "Success, extrinsic submitted without waiting.") diff --git a/tests/unit_tests/extrinsics/test_unstaking.py b/tests/unit_tests/extrinsics/test_unstaking.py index afd3c23e76..e8c84c3612 100644 --- a/tests/unit_tests/extrinsics/test_unstaking.py +++ b/tests/unit_tests/extrinsics/test_unstaking.py @@ -1,20 +1,23 @@ from bittensor.core.extrinsics import unstaking +from bittensor.utils.balance import Balance def test_unstake_extrinsic(mocker): - """Verify that sync `unstake_extrinsic` method calls proper async method.""" # Preps - fake_subtensor = mocker.Mock() + fake_subtensor = mocker.Mock( + **{ + "get_hotkey_owner.return_value": "hotkey_owner", + "get_stake_for_coldkey_and_hotkey.return_value": Balance(10.0), + "sign_and_send_extrinsic.return_value": (True, ""), + } + ) fake_wallet = mocker.Mock() + fake_wallet.coldkeypub.ss58_address = "hotkey_owner" hotkey_ss58 = "hotkey" amount = 1.1 wait_for_inclusion = True wait_for_finalization = True - mocked_execute_coroutine = mocker.patch.object(unstaking, "execute_coroutine") - mocked_unstake_extrinsic = mocker.Mock() - unstaking.async_unstake_extrinsic = mocked_unstake_extrinsic - # Call result = unstaking.unstake_extrinsic( subtensor=fake_subtensor, @@ -26,35 +29,42 @@ def test_unstake_extrinsic(mocker): ) # Asserts - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_unstake_extrinsic.return_value, - event_loop=fake_subtensor.event_loop, + assert result is True + + fake_subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": "hotkey", + "amount_unstaked": 1100000000, + }, ) - mocked_unstake_extrinsic.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - wallet=fake_wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + fake_subtensor.sign_and_send_extrinsic.assert_called_once_with( + fake_subtensor.substrate.compose_call.return_value, + fake_wallet, + True, + True, ) - assert result == mocked_execute_coroutine.return_value def test_unstake_multiple_extrinsic(mocker): """Verify that sync `unstake_multiple_extrinsic` method calls proper async method.""" # Preps - fake_subtensor = mocker.Mock() + fake_subtensor = mocker.Mock( + **{ + "get_hotkey_owner.return_value": "hotkey_owner", + "get_stake_for_coldkey_and_hotkey.return_value": Balance(10.0), + "sign_and_send_extrinsic.return_value": (True, ""), + "tx_rate_limit.return_value": 0, + } + ) fake_wallet = mocker.Mock() + fake_wallet.coldkeypub.ss58_address = "hotkey_owner" hotkey_ss58s = ["hotkey1", "hotkey2"] amounts = [1.1, 1.2] wait_for_inclusion = True wait_for_finalization = True - mocked_execute_coroutine = mocker.patch.object(unstaking, "execute_coroutine") - mocked_unstake_multiple_extrinsic = mocker.Mock() - unstaking.async_unstake_multiple_extrinsic = mocked_unstake_multiple_extrinsic - # Call result = unstaking.unstake_multiple_extrinsic( subtensor=fake_subtensor, @@ -66,16 +76,29 @@ def test_unstake_multiple_extrinsic(mocker): ) # Asserts - mocked_execute_coroutine.assert_called_once_with( - coroutine=mocked_unstake_multiple_extrinsic.return_value, - event_loop=fake_subtensor.event_loop, + assert result is True + assert fake_subtensor.substrate.compose_call.call_count == 2 + assert fake_subtensor.sign_and_send_extrinsic.call_count == 2 + + fake_subtensor.substrate.compose_call.assert_any_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": "hotkey1", + "amount_unstaked": 1100000000, + }, ) - mocked_unstake_multiple_extrinsic.assert_called_once_with( - subtensor=fake_subtensor.async_subtensor, - wallet=fake_wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + fake_subtensor.substrate.compose_call.assert_any_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": "hotkey2", + "amount_unstaked": 1200000000, + }, + ) + fake_subtensor.sign_and_send_extrinsic.assert_called_with( + fake_subtensor.substrate.compose_call.return_value, + fake_wallet, + True, + True, ) - assert result == mocked_execute_coroutine.return_value diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index c055cd865b..950d2c7cb6 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -3,7 +3,7 @@ from bittensor.core import async_subtensor from bittensor.core.chain_data import proposal_vote_data -from bittensor.core.subtensor import AsyncSubtensor +from bittensor.core.async_subtensor import AsyncSubtensor @pytest.fixture(autouse=True) @@ -1153,6 +1153,7 @@ async def test_get_neuron_for_pubkey_and_subnet_success(subtensor, mocker): subtensor.substrate.rpc_request.assert_called_once_with( method="neuronInfo_getNeuron", params=[fake_netuid, fake_uid.value], + block_hash=None, reuse_block_hash=False, ) mocked_neuron_info.assert_called_once_with(fake_result) @@ -1230,6 +1231,7 @@ async def test_get_neuron_for_pubkey_and_subnet_rpc_result_empty(subtensor, mock subtensor.substrate.rpc_request.assert_called_once_with( method="neuronInfo_getNeuron", params=[fake_netuid, fake_uid], + block_hash=None, reuse_block_hash=False, ) mocked_get_null_neuron.assert_called_once() @@ -2495,7 +2497,7 @@ async def test_transfer_success(subtensor, mocker): # Call result = await subtensor.transfer( wallet=fake_wallet, - destination=fake_destination, + dest=fake_destination, amount=fake_amount, transfer_all=fake_transfer_all, ) @@ -2504,7 +2506,7 @@ async def test_transfer_success(subtensor, mocker): mocked_transfer_extrinsic.assert_awaited_once_with( subtensor=subtensor, wallet=fake_wallet, - destination=fake_destination, + dest=fake_destination, amount=mocked_balance_from_tao, transfer_all=fake_transfer_all, wait_for_inclusion=True, diff --git a/tests/unit_tests/test_axon.py b/tests/unit_tests/test_axon.py index 868e89ee01..512b4635ae 100644 --- a/tests/unit_tests/test_axon.py +++ b/tests/unit_tests/test_axon.py @@ -283,16 +283,16 @@ async def test_verify_body_integrity_happy_path( @pytest.mark.parametrize( - "body, expected_exception_message", + "body, expected_exception_name", [ - (b"", "Expecting value: line 1 column 1 (char 0)"), # Empty body - (b"not_json", "Expecting value: line 1 column 1 (char 0)"), # Non-JSON body + (b"", "JSONDecodeError"), # Empty body + (b"not_json", "JSONDecodeError"), # Non-JSON body ], ids=["empty_body", "non_json_body"], ) @pytest.mark.asyncio async def test_verify_body_integrity_edge_cases( - mock_request, axon_instance, body, expected_exception_message + mock_request, axon_instance, body, expected_exception_name ): # Arrange mock_request.body.return_value = body @@ -300,9 +300,7 @@ async def test_verify_body_integrity_edge_cases( # Act & Assert with pytest.raises(Exception) as exc_info: await axon_instance.verify_body_integrity(mock_request) - assert expected_exception_message in str( - exc_info.value - ), "Expected specific exception message." + assert exc_info.typename == expected_exception_name, "Expected specific exception" @pytest.mark.parametrize( diff --git a/tests/unit_tests/test_chain_data.py b/tests/unit_tests/test_chain_data.py index ec5c44ef94..63d8a69365 100644 --- a/tests/unit_tests/test_chain_data.py +++ b/tests/unit_tests/test_chain_data.py @@ -1,6 +1,7 @@ import pytest import torch +from async_substrate_interface.utils import json from bittensor.core.chain_data import AxonInfo, DelegateInfo from bittensor.core.chain_data.utils import ChainDataType @@ -102,7 +103,19 @@ def test_eq(other, expected, test_case): hotkey="hot", coldkey="cold", ), - '{"version": 1, "ip": "127.0.0.1", "port": 8080, "ip_type": 4, "hotkey": "hot", "coldkey": "cold", "protocol": 4, "placeholder1": 0, "placeholder2": 0}', + json.dumps( + { + "version": 1, + "ip": "127.0.0.1", + "port": 8080, + "ip_type": 4, + "hotkey": "hot", + "coldkey": "cold", + "protocol": 4, + "placeholder1": 0, + "placeholder2": 0, + } + ), "ID_to_string", ), ], diff --git a/tests/unit_tests/test_metagraph.py b/tests/unit_tests/test_metagraph.py index 0a95bbbbc7..1c8efe4fcc 100644 --- a/tests/unit_tests/test_metagraph.py +++ b/tests/unit_tests/test_metagraph.py @@ -9,7 +9,6 @@ from bittensor.core import settings from bittensor.core.metagraph import Metagraph from bittensor.core.subtensor import Subtensor -from bittensor.utils import execute_coroutine @pytest.fixture @@ -48,7 +47,7 @@ async def test_set_metagraph_attributes(mock_environment): subtensor, neurons = mock_environment metagraph = Metagraph(1, sync=False) metagraph.neurons = neurons - await metagraph._set_metagraph_attributes(block=5, subtensor=subtensor) + metagraph._set_metagraph_attributes(block=5) # Check the attributes are set as expected assert metagraph.n.item() == len(neurons) @@ -123,9 +122,6 @@ def mock_subtensor(mocker): get_current_block=mocker.AsyncMock(return_value=601) ) subtensor.event_loop = asyncio.new_event_loop() - subtensor.execute_coroutine = partial( - execute_coroutine, event_loop=subtensor.event_loop - ) return subtensor @@ -162,6 +158,7 @@ def __contains__(self, item): ], ) def test_sync_warning_cases(block, test_id, metagraph_instance, mock_subtensor, caplog): + mock_subtensor.get_current_block.return_value = 601 metagraph_instance.sync(block=block, lite=True, subtensor=mock_subtensor) expected_message = "Attempting to sync longer than 300 blocks ago on a non-archive node. Please use the 'archive' network for subtensor and retry." diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index bdf6f9720e..e720aa52d7 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1,4 +1,5 @@ from bittensor.core.subtensor import Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor # TODO: It's probably worth adding a test for each corresponding method to check the correctness of the call with arguments @@ -7,29 +8,16 @@ def test_methods_comparable(mocker): """Verifies that methods in sync and async Subtensors are comparable.""" # Preps - mocker.patch( - "async_substrate_interface.substrate_interface.AsyncSubstrateInterface" - ) - subtensor = Subtensor() + subtensor = Subtensor(_mock=True) + async_subtensor = AsyncSubtensor(_mock=True) - # methods which lives in sync subtensor only - excluded_subtensor_methods = ["async_subtensor", "event_loop", "execute_coroutine"] # methods which lives in async subtensor only - excluded_async_subtensor_methods = [ - "determine_block_hash", - "encode_params", - "get_hyperparameter", - "sign_and_send_extrinsic", - ] - subtensor_methods = [ - m - for m in dir(subtensor) - if not m.startswith("_") and m not in excluded_subtensor_methods - ] + excluded_async_subtensor_methods = ["initialize"] + subtensor_methods = [m for m in dir(subtensor) if not m.startswith("_")] async_subtensor_methods = [ m - for m in dir(subtensor.async_subtensor) + for m in dir(async_subtensor) if not m.startswith("_") and m not in excluded_async_subtensor_methods ]