diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 0000000000..900e2ceb85 --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,7 @@ +addReviewers: true + +# A list of team slugs to add as assignees +reviewers: + - opentensor/cortex + +numberOfReviewers: 0 \ No newline at end of file diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml new file mode 100644 index 0000000000..3a952f91b8 --- /dev/null +++ b/.github/workflows/auto-assign.yml @@ -0,0 +1,15 @@ +name: Auto Assign Cortex to Pull Requests + +on: + pull_request: + types: [opened, reopened] + +jobs: + auto-assign: + runs-on: ubuntu-latest + steps: + - name: Auto-assign Cortex Team + uses: kentaro-m/auto-assign-action@v1.2.4 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/auto_assign.yml \ No newline at end of file diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 969423db01..0bc467a94d 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -46,6 +46,7 @@ jobs: run: needs: find-tests runs-on: SubtensorCI + timeout-minutes: 45 strategy: fail-fast: false # Allow other matrix jobs to run even if this job fails max-parallel: 8 # Set the maximum number of parallel jobs diff --git a/CHANGELOG.md b/CHANGELOG.md index 07603362d1..c76ed59957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,83 @@ # Changelog +## 7.4.0 /2024-08-29 + +## What's Changed +* [Fix] Allow unstake below network min by @camfairchild in https://github.com/opentensor/bittensor/pull/2016 +* Tests/e2e tests staging by @open-junius in https://github.com/opentensor/bittensor/pull/1943 +* Chore: Backmerge 7.2 by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2020 +* Fix broken tests and Enforce BTCLI usage by @opendansor in https://github.com/opentensor/bittensor/pull/2027 +* Add time delay to faucet by @opendansor in https://github.com/opentensor/bittensor/pull/2030 +* Skip faucet test by @opendansor in https://github.com/opentensor/bittensor/pull/2031 +* Adds normalization for alpha hyperparams by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2035 +* Revert info logging in processing response by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2043 +* Pin numpy version to 1.26.4 in prod.txt by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/2045 +* Test hot key Swap by @opendansor in https://github.com/opentensor/bittensor/pull/2044 +* Do not run Circle-CI on drafts by @thewhaleking in https://github.com/opentensor/bittensor/pull/1959 +* Enhancement: Detailed nonce information in-case of failures by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2050 +* fix bittensor not installing under Python 3.13 by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/2053 +* Enable Faucet Test by @opendansor in https://github.com/opentensor/bittensor/pull/2056 +* Add back BT_SUBTENSOR_CHAIN_ENDPOINT env variable by @bradleytf in https://github.com/opentensor/bittensor/pull/2034 +* Fix: Logging configs not being set by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2065 +* Feature/gus/liquid alpha params by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2012 +* Test Emissions E2E by @opendansor in https://github.com/opentensor/bittensor/pull/2036 +* Prevent e2e draft by @opendansor in https://github.com/opentensor/bittensor/pull/2072 +* Fix e2e to only run when PR is ready for review by @opendansor in https://github.com/opentensor/bittensor/pull/2077 +* Fix Faucet and fastblocks interaction by @opendansor in https://github.com/opentensor/bittensor/pull/2083 +* Float normalization for child hotkeys by @opendansor in https://github.com/opentensor/bittensor/pull/2093 +* Fix e2e test hanging by @open-junius in https://github.com/opentensor/bittensor/pull/2118 +* Fixes leaked semaphores by @thewhaleking in https://github.com/opentensor/bittensor/pull/2125 +* Backmerge master -> staging by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2136 +* fix: coldkeypub usage instead of coldkey for arbitration_stats by @Rapiiidooo in https://github.com/opentensor/bittensor/pull/2132 +* Removes extra no_prompts in commands by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2140 +* Adds timeout for e2e tests by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2141 +* fix: updates test_axon verify body async tests by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2142 +* test: fix mocksubtensor query previous blocks by @timabilov in https://github.com/opentensor/bittensor/pull/2139 +* Adds E2E for Metagraph command by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2143 +* feat: Enhance dendrite error messaging by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2117 +* Adds E2E Tests for wallet creation commands by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2145 +* [Ledger Integration] [Feature] bump pysub to 1.7.9+ by @camfairchild in https://github.com/opentensor/bittensor/pull/2156 +* Ruff complains about an extra line by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2158 +* support Wallet names with hyphens when passing password through ENV vars by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/1949 +* Fix naming convention of swap hotkey test by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2162 +* Adds E2E test for wallet regenerations + fixes input bug for regen hotkey by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2149 +* Backmerge Master -> Staging (7.4) by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2170 +* ci: auto assigns cortex to opened PRs by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2184 +* CI/E2E test improvements by @mvds00 in https://github.com/opentensor/bittensor/pull/2168 +* Fix multiprocessing POW errors and No Torch logging errors by @thewhaleking in https://github.com/opentensor/bittensor/pull/2186 +* ci: update reviewers by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2189 +* Adds updated type in timeouts dendrite by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2196 +* Bumps setuptools ~=70.0.0 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2150 +* Bump black from 23.7.0 to 24.3.0 in /requirements by @dependabot in https://github.com/opentensor/bittensor/pull/2197 +* btlogging/loggingmachine.py: Fix bw compat API. by @mvds00 in https://github.com/opentensor/bittensor/pull/2155 +* Check for participation before nomination call by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2193 +* test: subnet list e2e by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2198 +* ensure msg is str in _concat_msg by @thewhaleking in https://github.com/opentensor/bittensor/pull/2200 +* Fixes tests depending on explicit line numbers by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2211 +* Merge streaming fix to staging by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2183 +* Multiple bittensor versions e2e workflow by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2212 +* Changes name of workflow file by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2213 +* Enhances e2e tests to contain assertions & logging by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2192 +* Security fix: Bumps ansible and certifi by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2214 +* Wallet List Command e2e test by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2207 +* fix Synapse base performance (more than 10x speed up) by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/2161 +* Child Hotkeys by @opendansor in https://github.com/opentensor/bittensor/pull/2071 +* Improve child hotkeys QOL by @opendansor in https://github.com/opentensor/bittensor/pull/2225 +* Child hotkeys handle excess normalization by @opendansor in https://github.com/opentensor/bittensor/pull/2229 +* Fixes chain compilation timeouts by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2238 +* Update Child Hotkey commands by @opendansor in https://github.com/opentensor/bittensor/pull/2245 +* feat: return error message instead of raising exception by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2244 +* Backmerge master to staging (7.3.1) by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2254 + +## New Contributors +* @bradleytf made their first contribution in https://github.com/opentensor/bittensor/pull/2034 +* @Rapiiidooo made their first contribution in https://github.com/opentensor/bittensor/pull/2132 +* @timabilov made their first contribution in https://github.com/opentensor/bittensor/pull/2139 +* @mvds00 made their first contribution in https://github.com/opentensor/bittensor/pull/2168 +* @dependabot made their first contribution in https://github.com/opentensor/bittensor/pull/2197 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v7.3.1...v7.4.0 + ## 7.3.1 / 2024-08-19 ## What's Changed diff --git a/VERSION b/VERSION index 34a8f745d4..b616717999 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.3.1 \ No newline at end of file +7.4.0 \ No newline at end of file diff --git a/bittensor/__init__.py b/bittensor/__init__.py index e2dc10ae8a..9f4dbf1aa8 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -40,7 +40,7 @@ # Bittensor code and protocol version. -__version__ = "7.3.1" +__version__ = "7.4.0" _version_split = __version__.split(".") __version_info__ = tuple(int(part) for part in _version_split) @@ -125,7 +125,14 @@ def debug(on: bool = True): # Needs to use wss:// __bellagene_entrypoint__ = "wss://parachain.opentensor.ai:443" -__local_entrypoint__ = "ws://127.0.0.1:9944" + +if ( + BT_SUBTENSOR_CHAIN_ENDPOINT := os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") +) is not None: + __local_entrypoint__ = BT_SUBTENSOR_CHAIN_ENDPOINT +else: + __local_entrypoint__ = "ws://127.0.0.1:9944" + __tao_symbol__: str = chr(0x03C4) @@ -200,19 +207,6 @@ def debug(on: bool = True): }, }, }, - "ValidatorIPRuntimeApi": { - "methods": { - "get_associated_validator_ip_info_for_subnet": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - }, - }, - }, "SubnetInfoRuntimeApi": { "methods": { "get_subnet_hyperparams": { @@ -318,7 +312,6 @@ def debug(on: bool = True): strtobool, strtobool_with_default, get_explorer_root_url_by_network_from_map, - get_explorer_root_url_by_network_from_map, get_explorer_url_for_network, ss58_address_to_bytes, U16_NORMALIZED_FLOAT, diff --git a/bittensor/axon.py b/bittensor/axon.py index ca06335307..8cefadfe61 100644 --- a/bittensor/axon.py +++ b/bittensor/axon.py @@ -31,11 +31,12 @@ import traceback import typing import uuid +import warnings from inspect import signature, Signature, Parameter from typing import List, Optional, Tuple, Callable, Any, Dict, Awaitable import uvicorn -from fastapi import FastAPI, APIRouter, Depends +from fastapi import APIRouter, Depends, FastAPI from fastapi.responses import JSONResponse from fastapi.routing import serialize_response from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint @@ -44,18 +45,19 @@ from substrateinterface import Keypair import bittensor +from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds +from bittensor.constants import V_7_2_0 from bittensor.errors import ( + BlacklistedException, InvalidRequestNameError, - SynapseDendriteNoneException, - SynapseParsingError, - UnknownSynapseError, NotVerifiedException, - BlacklistedException, - PriorityException, PostProcessException, + PriorityException, + SynapseDendriteNoneException, SynapseException, + SynapseParsingError, + UnknownSynapseError, ) -from bittensor.constants import ALLOWED_DELTA, V_7_2_0 from bittensor.threadpool import PriorityThreadPoolExecutor from bittensor.utils import networking @@ -484,17 +486,50 @@ def verify_custom(synapse: MyCustomSynapse): async def endpoint(*args, **kwargs): start_time = time.time() - response_synapse = forward_fn(*args, **kwargs) - if isinstance(response_synapse, Awaitable): - response_synapse = await response_synapse - return await self.middleware_cls.synapse_to_response( - synapse=response_synapse, start_time=start_time - ) + response = forward_fn(*args, **kwargs) + if isinstance(response, Awaitable): + response = await response + if isinstance(response, bittensor.Synapse): + return await self.middleware_cls.synapse_to_response( + synapse=response, start_time=start_time + ) + else: + response_synapse = getattr(response, "synapse", None) + if response_synapse is None: + warnings.warn( + "The response synapse is None. The input synapse will be used as the response synapse. " + "Reliance on forward_fn modifying input synapse as a side-effects is deprecated. " + "Explicitly set `synapse` on response object instead.", + DeprecationWarning, + ) + # Replace with `return response` in next major version + response_synapse = args[0] + + return await self.middleware_cls.synapse_to_response( + synapse=response_synapse, + start_time=start_time, + response_override=response, + ) + + return_annotation = forward_sig.return_annotation + + if isinstance(return_annotation, type) and issubclass( + return_annotation, bittensor.Synapse + ): + if issubclass( + return_annotation, + bittensor.StreamingSynapse, + ): + warnings.warn( + "The forward_fn return annotation is a subclass of bittensor.StreamingSynapse. " + "Most likely the correct return annotation would be BTStreamingResponse." + ) + else: + return_annotation = JSONResponse - # replace the endpoint signature, but set return annotation to JSONResponse endpoint.__signature__ = Signature( # type: ignore parameters=list(forward_sig.parameters.values()), - return_annotation=JSONResponse, + return_annotation=return_annotation, ) # Add the endpoint to the router, making it available on both GET and POST methods @@ -847,6 +882,8 @@ async def default_verify(self, synapse: bittensor.Synapse): The method checks for increasing nonce values, which is a vital step in preventing replay attacks. A replay attack involves an adversary reusing or delaying the transmission of a valid data transmission to deceive the receiver. + The first time a nonce is seen, it is checked for freshness by ensuring it is + within an acceptable delta time range. Authenticity and Integrity Checks By verifying that the message's digital signature matches @@ -893,33 +930,43 @@ async def default_verify(self, synapse: bittensor.Synapse): if synapse.dendrite.nonce is None: raise Exception("Missing Nonce") - # If we don't have a nonce stored, ensure that the nonce falls within - # a reasonable delta. - + # Newer nonce structure post v7.2 if ( synapse.dendrite.version is not None and synapse.dendrite.version >= V_7_2_0 ): # If we don't have a nonce stored, ensure that the nonce falls within # a reasonable delta. + current_time_ns = time.time_ns() + allowed_window_ns = allowed_nonce_window_ns( + current_time_ns, synapse.timeout + ) + if ( self.nonces.get(endpoint_key) is None - and synapse.dendrite.nonce - <= time.time_ns() - ALLOWED_DELTA - (synapse.timeout or 0) + and synapse.dendrite.nonce <= allowed_window_ns ): - raise Exception("Nonce is too old") + diff_seconds, allowed_delta_seconds = calculate_diff_seconds( + current_time_ns, synapse.timeout, synapse.dendrite.nonce + ) + raise Exception( + f"Nonce is too old: acceptable delta is {allowed_delta_seconds:.2f} seconds but request was {diff_seconds:.2f} seconds old" + ) + + # If a nonce is stored, ensure the new nonce + # is greater than the previous nonce if ( self.nonces.get(endpoint_key) is not None and synapse.dendrite.nonce <= self.nonces[endpoint_key] ): - raise Exception("Nonce is too old") + raise Exception("Nonce is too old, a newer one was last processed") + # Older nonce structure pre v7.2 else: if ( - endpoint_key in self.nonces.keys() - and self.nonces[endpoint_key] is not None + self.nonces.get(endpoint_key) is not None and synapse.dendrite.nonce <= self.nonces[endpoint_key] ): - raise Exception("Nonce is too small") + raise Exception("Nonce is too old, a newer one was last processed") if not keypair.verify(message, synapse.dendrite.signature): raise Exception( @@ -952,7 +999,7 @@ def log_and_handle_error( exception: Exception, status_code: typing.Optional[int] = None, start_time: typing.Optional[float] = None, -): +) -> bittensor.Synapse: if isinstance(exception, SynapseException): synapse = exception.synapse or synapse @@ -1420,14 +1467,21 @@ async def run( @classmethod async def synapse_to_response( - cls, synapse: bittensor.Synapse, start_time: float - ) -> JSONResponse: + cls, + synapse: bittensor.Synapse, + start_time: float, + *, + response_override: Optional[Response] = None, + ) -> Response: """ Converts the Synapse object into a JSON response with HTTP headers. Args: - synapse (bittensor.Synapse): The Synapse object representing the request. - start_time (float): The timestamp when the request processing started. + synapse: The Synapse object representing the request. + start_time: The timestamp when the request processing started. + response_override: + Instead of serializing the synapse, mutate the provided response object. + This is only really useful for StreamingSynapse responses. Returns: Response: The final HTTP response, with updated headers, ready to be sent back to the client. @@ -1446,11 +1500,14 @@ async def synapse_to_response( synapse.axon.process_time = time.time() - start_time - serialized_synapse = await serialize_response(response_content=synapse) - response = JSONResponse( - status_code=synapse.axon.status_code, - content=serialized_synapse, - ) + if response_override: + response = response_override + else: + serialized_synapse = await serialize_response(response_content=synapse) + response = JSONResponse( + status_code=synapse.axon.status_code, + content=serialized_synapse, + ) try: updated_headers = synapse.to_headers() diff --git a/bittensor/btlogging/loggingmachine.py b/bittensor/btlogging/loggingmachine.py index 1c6aad3bb6..ef58cecdfd 100644 --- a/bittensor/btlogging/loggingmachine.py +++ b/bittensor/btlogging/loggingmachine.py @@ -29,20 +29,21 @@ import os import sys from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler +from logging import Logger from typing import NamedTuple -from statemachine import StateMachine, State +from statemachine import State, StateMachine import bittensor.config from bittensor.btlogging.defines import ( - TRACE_LOG_FORMAT, - DATE_FORMAT, BITTENSOR_LOGGER_NAME, + DATE_FORMAT, + DEFAULT_LOG_BACKUP_COUNT, DEFAULT_LOG_FILE_NAME, DEFAULT_MAX_ROTATING_LOG_FILE_SIZE, - DEFAULT_LOG_BACKUP_COUNT, + TRACE_LOG_FORMAT, ) -from bittensor.btlogging.format import BtStreamFormatter, BtFileFormatter +from bittensor.btlogging.format import BtFileFormatter, BtStreamFormatter from bittensor.btlogging.helpers import all_loggers @@ -55,7 +56,7 @@ class LoggingConfig(NamedTuple): logging_dir: str -class LoggingMachine(StateMachine): +class LoggingMachine(StateMachine, Logger): """Handles logger states for bittensor and 3rd party libraries.""" Default = State(initial=True) @@ -70,19 +71,19 @@ class LoggingMachine(StateMachine): | Default.to(Default) ) - enable_trace: Trace = ( + enable_trace = ( Default.to(Trace) | Debug.to(Trace) | Disabled.to(Trace) | Trace.to(Trace) ) - enable_debug: Debug = ( + enable_debug = ( Default.to(Debug) | Trace.to(Debug) | Disabled.to(Debug) | Debug.to(Debug) ) - disable_trace: Default = Trace.to(Default) + disable_trace = Trace.to(Default) - disable_debug: Default = Debug.to(Default) + disable_debug = Debug.to(Default) - disable_logging: Disabled = ( + disable_logging = ( Trace.to(Disabled) | Debug.to(Disabled) | Default.to(Disabled) @@ -94,7 +95,7 @@ def __init__(self, config: bittensor.config, name: str = BITTENSOR_LOGGER_NAME): super(LoggingMachine, self).__init__() self._queue = mp.Queue(-1) self._primary_loggers = {name} - self._config = config + self._config = self._extract_logging_config(config) # Formatters # @@ -107,7 +108,7 @@ def __init__(self, config: bittensor.config, name: str = BITTENSOR_LOGGER_NAME): # # In the future, we may want to add options to introduce other handlers # for things like log aggregation by external services. - self._handlers = self._configure_handlers(config) + self._handlers = self._configure_handlers(self._config) # configure and start the queue listener self._listener = self._create_and_start_listener(self._handlers) @@ -115,6 +116,23 @@ def __init__(self, config: bittensor.config, name: str = BITTENSOR_LOGGER_NAME): # set up all the loggers self._logger = self._initialize_bt_logger(name) self.disable_third_party_loggers() + self._enable_initial_state(self._config) + + def _enable_initial_state(self, config): + """Set correct state action on initializing""" + if config.trace: + self.enable_trace() + elif config.debug: + self.enable_debug() + else: + self.enable_default() + + def _extract_logging_config(self, config) -> dict: + """Extract btlogging's config from bittensor config""" + if hasattr(config, "logging"): + return config.logging + else: + return config def _configure_handlers(self, config) -> list[stdlogging.Handler]: handlers = list() diff --git a/bittensor/chain_data.py b/bittensor/chain_data.py index 55cdc47b6e..029cb29829 100644 --- a/bittensor/chain_data.py +++ b/bittensor/chain_data.py @@ -1,14 +1,14 @@ # The MIT License (MIT) # Copyright © 2023 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 @@ -1194,7 +1194,9 @@ def list_from_vec_u8(cls, vec_u8: List[int]) -> List["ScheduledColdkeySwapInfo"] @classmethod def decode_account_id_list(cls, vec_u8: List[int]) -> Optional[List[str]]: """Decodes a list of AccountIds from vec_u8.""" - decoded = from_scale_encoding(vec_u8, ChainDataType.AccountId, is_vec=True) + decoded = from_scale_encoding( + vec_u8, ChainDataType.ScheduledColdkeySwapInfo.AccountId, is_vec=True + ) if decoded is None: return None return [ diff --git a/bittensor/cli.py b/bittensor/cli.py index 4a7a47775e..cc87c122e4 100644 --- a/bittensor/cli.py +++ b/bittensor/cli.py @@ -70,6 +70,11 @@ CommitWeightCommand, RevealWeightCommand, CheckColdKeySwapCommand, + SetChildrenCommand, + GetChildrenCommand, + RevokeChildrenCommand, + SetChildKeyTakeCommand, + GetChildKeyTakeCommand, ) # Create a console instance for CLI display. @@ -164,11 +169,16 @@ "stake": { "name": "stake", "aliases": ["st", "stakes"], - "help": "Commands for staking and removing stake from hotkey accounts.", + "help": "Commands for staking and removing stake and setting child hotkey accounts.", "commands": { "show": StakeShow, "add": StakeCommand, "remove": UnStakeCommand, + "get_children": GetChildrenCommand, + "set_children": SetChildrenCommand, + "revoke_children": RevokeChildrenCommand, + "set_childkey_take": SetChildKeyTakeCommand, + "get_childkey_take": GetChildKeyTakeCommand, }, }, "weights": { diff --git a/bittensor/commands/__init__.py b/bittensor/commands/__init__.py index 514a081c41..e1ac8c74cd 100644 --- a/bittensor/commands/__init__.py +++ b/bittensor/commands/__init__.py @@ -62,8 +62,15 @@ } ) -from .stake import StakeCommand, StakeShow -from .unstake import UnStakeCommand +from .stake import ( + StakeCommand, + StakeShow, + SetChildrenCommand, + GetChildrenCommand, + SetChildKeyTakeCommand, + GetChildKeyTakeCommand, +) +from .unstake import UnStakeCommand, RevokeChildrenCommand from .overview import OverviewCommand from .register import ( PowRegisterCommand, diff --git a/bittensor/commands/check_coldkey_swap.py b/bittensor/commands/check_coldkey_swap.py index dce2ca04f9..2b003e8289 100644 --- a/bittensor/commands/check_coldkey_swap.py +++ b/bittensor/commands/check_coldkey_swap.py @@ -1,14 +1,14 @@ # The MIT License (MIT) # Copyright © 2021 Yuma Rao - +# # 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 @@ -30,14 +30,16 @@ def fetch_arbitration_stats(subtensor, wallet): """ Performs a check of the current arbitration data (if any), and displays it through the bittensor console. """ - arbitration_check = len(subtensor.check_in_arbitration(wallet.coldkey.ss58_address)) + arbitration_check = len( + subtensor.check_in_arbitration(wallet.coldkeypub.ss58_address) + ) if arbitration_check == 0: bittensor.__console__.print( "[green]There has been no previous key swap initiated for your coldkey.[/green]" ) if arbitration_check == 1: arbitration_remaining = subtensor.get_remaining_arbitration_period( - wallet.coldkey.ss58_address + wallet.coldkeypub.ss58_address ) hours, minutes, seconds = convert_blocks_to_time(arbitration_remaining) bittensor.__console__.print( diff --git a/bittensor/commands/delegates.py b/bittensor/commands/delegates.py index 4d03b289e4..cfba3526d2 100644 --- a/bittensor/commands/delegates.py +++ b/bittensor/commands/delegates.py @@ -752,7 +752,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Unlock the wallet. wallet.hotkey - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return # Check if the hotkey is already a delegate. if subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): diff --git a/bittensor/commands/identity.py b/bittensor/commands/identity.py index 15232c4440..4f74548495 100644 --- a/bittensor/commands/identity.py +++ b/bittensor/commands/identity.py @@ -115,7 +115,14 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): console.print(":cross_mark: Aborted!") exit(0) - wallet.coldkey # unlock coldkey + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return + with console.status(":satellite: [bold green]Updating identity on-chain..."): try: subtensor.update_identity( diff --git a/bittensor/commands/list.py b/bittensor/commands/list.py index 6079112ed1..b2946efffb 100644 --- a/bittensor/commands/list.py +++ b/bittensor/commands/list.py @@ -56,7 +56,10 @@ def run(cli): except StopIteration: # No wallet files found. wallets = [] + ListCommand._run(cli, wallets) + @staticmethod + def _run(cli: "bittensor.cli", wallets, return_value=False): root = Tree("Wallets") for w_name in wallets: wallet_for_name = bittensor.wallet(path=cli.config.wallet.path, name=w_name) @@ -100,7 +103,10 @@ def run(cli): root.add("[bold red]No wallets found.") # Uses rich print to display the tree. - print(root) + if not return_value: + print(root) + else: + return root @staticmethod def check_config(config: "bittensor.config"): @@ -111,3 +117,12 @@ def add_args(parser: argparse.ArgumentParser): list_parser = parser.add_parser("list", help="""List wallets""") bittensor.wallet.add_args(list_parser) bittensor.subtensor.add_args(list_parser) + + @staticmethod + def get_tree(cli): + try: + wallets = next(os.walk(os.path.expanduser(cli.config.wallet.path)))[1] + except StopIteration: + # No wallet files found. + wallets = [] + return ListCommand._run(cli=cli, wallets=wallets, return_value=True) diff --git a/bittensor/commands/metagraph.py b/bittensor/commands/metagraph.py index 1075f50d31..79fa48b786 100644 --- a/bittensor/commands/metagraph.py +++ b/bittensor/commands/metagraph.py @@ -16,8 +16,11 @@ # DEALINGS IN THE SOFTWARE. import argparse -import bittensor + from rich.table import Table + +import bittensor + from .utils import check_netuid_set console = bittensor.__console__ # type: ignore @@ -261,12 +264,5 @@ def add_args(parser: argparse.ArgumentParser): help="""Set the netuid to get the metagraph of""", default=False, ) - metagraph_parser.add_argument( - "--no_prompt", - dest="no_prompt", - action="store_true", - help="""Set true to avoid prompting the user.""", - default=False, - ) bittensor.subtensor.add_args(metagraph_parser) diff --git a/bittensor/commands/network.py b/bittensor/commands/network.py index b5fada55a9..3564bc534d 100644 --- a/bittensor/commands/network.py +++ b/bittensor/commands/network.py @@ -534,13 +534,6 @@ def add_args(parser: argparse.ArgumentParser): parser.add_argument( "--netuid", dest="netuid", type=int, required=False, default=False ) - parser.add_argument( - "--no_prompt", - dest="no_prompt", - action="store_true", - help="""Set true to avoid prompting the user.""", - default=False, - ) bittensor.subtensor.add_args(parser) @@ -639,13 +632,6 @@ def add_args(parser: argparse.ArgumentParser): parser.add_argument( "--netuid", dest="netuid", type=int, required=False, default=False ) - parser.add_argument( - "--no_prompt", - dest="no_prompt", - action="store_true", - help="""Set true to avoid prompting the user.""", - default=False, - ) bittensor.subtensor.add_args(parser) diff --git a/bittensor/commands/register.py b/bittensor/commands/register.py index 8b21a33304..a5a14773a2 100644 --- a/bittensor/commands/register.py +++ b/bittensor/commands/register.py @@ -523,7 +523,21 @@ def check_config(config: "bittensor.config"): class SwapHotkeyCommand: @staticmethod def run(cli: "bittensor.cli"): - r"""Swap your hotkey for all registered axons on the network.""" + """ + Executes the ``swap_hotkey`` command to swap the hotkeys for a neuron on the network. + + Usage: + The command is used to swap the hotkey of a wallet for another hotkey on that same wallet. + + Optional arguments: + - ``--wallet.name`` (str): Specifies the wallet for which the hotkey is to be swapped. + - ``--wallet.hotkey`` (str): The original hotkey name that is getting swapped out. + - ``--wallet.hotkey_b`` (str): The new hotkey name for which the old is getting swapped out for. + + Example usage:: + + btcli wallet swap_hotkey --wallet.name your_wallet_name --wallet.hotkey original_hotkey --wallet.hotkey_b new_hotkey + """ try: subtensor: "bittensor.subtensor" = bittensor.subtensor( config=cli.config, log_verbose=False diff --git a/bittensor/commands/senate.py b/bittensor/commands/senate.py index 03a73cde5b..37f2d79585 100644 --- a/bittensor/commands/senate.py +++ b/bittensor/commands/senate.py @@ -432,7 +432,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Unlock the wallet. wallet.hotkey - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return # Check if the hotkey is a delegate. if not subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): @@ -514,7 +520,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.cli"): # Unlock the wallet. wallet.hotkey - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return if not subtensor.is_senate_member(hotkey_ss58=wallet.hotkey.ss58_address): console.print( @@ -603,7 +615,15 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Unlock the wallet. wallet.hotkey - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return + + vote_data = subtensor.get_vote_data(proposal_hash) vote_data = subtensor.get_vote_data(proposal_hash) if vote_data == None: diff --git a/bittensor/commands/stake.py b/bittensor/commands/stake.py index 1bc2cf2786..eff415d1a1 100644 --- a/bittensor/commands/stake.py +++ b/bittensor/commands/stake.py @@ -18,10 +18,13 @@ import argparse import os import sys +import re from typing import List, Union, Optional, Dict, Tuple from rich.prompt import Confirm, Prompt from rich.table import Table +from rich.console import Console +from rich.text import Text from tqdm import tqdm import bittensor @@ -31,10 +34,56 @@ get_delegates_details, DelegatesDetails, ) -from . import defaults +from . import defaults # type: ignore +from ..utils import wallet_utils +from ..utils.formatting import u64_to_float, u16_to_float console = bittensor.__console__ +MAX_CHILDREN = 5 + + +def get_netuid( + cli: "bittensor.cli", subtensor: "bittensor.subtensor", prompt: bool = True +) -> Tuple[bool, int]: + """Retrieve and validate the netuid from the user or configuration.""" + console = Console() + if not cli.config.is_set("netuid") and prompt: + cli.config.netuid = Prompt.ask("Enter netuid") + try: + cli.config.netuid = int(cli.config.netuid) + except ValueError: + console.print( + "[red]Invalid input. Please enter a valid integer for netuid.[/red]" + ) + return False, -1 + netuid = cli.config.netuid + if netuid < 0 or netuid > 65535: + console.print( + "[red]Invalid input. Please enter a valid integer for netuid in subnet range.[/red]" + ) + return False, -1 + if not subtensor.subnet_exists(netuid=netuid): + console.print( + "[red]Network with netuid {} does not exist. Please try again.[/red]".format( + netuid + ) + ) + return False, -1 + return True, netuid + + +def get_hotkey(wallet: "bittensor.wallet", config: "bittensor.config") -> str: + """Retrieve the hotkey from the wallet or config.""" + if wallet and wallet.hotkey: + return wallet.hotkey.ss58_address + elif config.is_set("hotkey"): + return config.hotkey + elif config.is_set("ss58"): + return config.ss58 + else: + return Prompt.ask("Enter hotkey (ss58)") + class StakeCommand: """ @@ -566,3 +615,787 @@ def add_args(parser: argparse.ArgumentParser): bittensor.wallet.add_args(list_parser) bittensor.subtensor.add_args(list_parser) + + +class SetChildKeyTakeCommand: + """ + Executes the ``set_childkey_take`` command to modify your childkey take on a specified subnet on the Bittensor network to the caller. + + This command is used to modify your childkey take on a specified subnet on the Bittensor network. + + Usage: + Users can specify the amount or 'take' for their child hotkeys (``SS58`` address), + the user needs to have access to the ss58 hotkey this call, and the take must be between 0 and 18%. + + The command prompts for confirmation before executing the set_childkey_take operation. + + Example usage:: + + btcli stake set_childkey_take --hotkey --netuid 1 --take 0.18 + """ + + @staticmethod + def run(cli: "bittensor.cli"): + """Set childkey take.""" + try: + subtensor: "bittensor.subtensor" = bittensor.subtensor( + config=cli.config, log_verbose=False + ) + SetChildKeyTakeCommand._run(cli, subtensor) + finally: + if "subtensor" in locals(): + subtensor.close() + bittensor.logging.debug("closing subtensor connection") + + @staticmethod + def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + console = Console() + wallet = bittensor.wallet(config=cli.config) + + # Get values if not set. + exists, netuid = get_netuid(cli, subtensor) + if not exists: + return + + # get parent hotkey + hotkey = get_hotkey(wallet, cli.config) + if not wallet_utils.is_valid_ss58_address(hotkey): + console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") + return + + if not cli.config.is_set("take"): + cli.config.take = Prompt.ask( + "Enter the percentage of take for your child hotkey (between 0 and 0.18 representing 0-18%)" + ) + + # extract take from cli input + try: + take = float(cli.config.take) + except ValueError: + print( + ":cross_mark:[red]Take must be a float value using characters between 0 and 9.[/red]" + ) + return + + if take < 0 or take > 0.18: + console.print( + f":cross_mark:[red]Invalid take: Childkey Take must be between 0 and 0.18 (representing 0% to 18%). Proposed take is {take}.[/red]" + ) + return + + success, message = subtensor.set_childkey_take( + wallet=wallet, + netuid=netuid, + hotkey=hotkey, + take=take, + wait_for_inclusion=cli.config.wait_for_inclusion, + wait_for_finalization=cli.config.wait_for_finalization, + prompt=cli.config.prompt, + ) + + # Result + if success: + console.print(":white_heavy_check_mark: [green]Set childkey take.[/green]") + console.print( + f"The childkey take for {hotkey} is now set to {take * 100:.3f}%." + ) + else: + console.print( + f":cross_mark:[red] Unable to set childkey take.[/red] {message}" + ) + + @staticmethod + def check_config(config: "bittensor.config"): + if not config.is_set("wallet.name") and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) + config.wallet.name = str(wallet_name) + if not config.is_set("wallet.hotkey") and not config.no_prompt: + hotkey_or_ss58 = Prompt.ask( + "Enter hotkey name or ss58", default=defaults.wallet.hotkey + ) + if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): + config.ss58 = str(hotkey_or_ss58) + else: + config.wallet.hotkey = str(hotkey_or_ss58) + + @staticmethod + def add_args(parser: argparse.ArgumentParser): + set_childkey_take_parser = parser.add_parser( + "set_childkey_take", help="""Set childkey take.""" + ) + set_childkey_take_parser.add_argument( + "--netuid", dest="netuid", type=int, required=False + ) + set_childkey_take_parser.add_argument( + "--hotkey", dest="hotkey", type=str, required=False + ) + set_childkey_take_parser.add_argument( + "--take", dest="take", type=float, required=False + ) + set_childkey_take_parser.add_argument( + "--wait_for_inclusion", + dest="wait_for_inclusion", + action="store_true", + default=True, + help="""Wait for the transaction to be included in a block.""", + ) + set_childkey_take_parser.add_argument( + "--wait_for_finalization", + dest="wait_for_finalization", + action="store_true", + default=True, + help="""Wait for the transaction to be finalized.""", + ) + set_childkey_take_parser.add_argument( + "--prompt", + dest="prompt", + action="store_true", + default=True, + help="""Prompt for confirmation before proceeding.""", + ) + set_childkey_take_parser.add_argument( + "--y", + "--yes", + "--no_prompt", + dest="prompt", + action="store_false", + help="""Disable prompt for confirmation before proceeding. Defaults to Yes for all prompts.""", + ) + bittensor.wallet.add_args(set_childkey_take_parser) + bittensor.subtensor.add_args(set_childkey_take_parser) + + +class GetChildKeyTakeCommand: + """ + Executes the ``get_childkey_take`` command to get your childkey take on a specified subnet on the Bittensor network to the caller. + + This command is used to get your childkey take on a specified subnet on the Bittensor network. + + Usage: + Users can get the amount or 'take' for their child hotkeys (``SS58`` address) + + Example usage:: + + btcli stake get_childkey_take --hotkey --netuid 1 + """ + + @staticmethod + def run(cli: "bittensor.cli"): + """Get childkey take.""" + try: + subtensor: "bittensor.subtensor" = bittensor.subtensor( + config=cli.config, log_verbose=False + ) + GetChildKeyTakeCommand._run(cli, subtensor) + finally: + if "subtensor" in locals(): + subtensor.close() + bittensor.logging.debug("closing subtensor connection") + + @staticmethod + def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + console = Console() + wallet = bittensor.wallet(config=cli.config) + + # Get values if not set. + exists, netuid = get_netuid(cli, subtensor) + if not exists: + return + + # get parent hotkey + hotkey = get_hotkey(wallet, cli.config) + if not wallet_utils.is_valid_ss58_address(hotkey): + console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") + return + + take_u16 = subtensor.get_childkey_take( + netuid=netuid, + hotkey=hotkey, + ) + + # Result + if take_u16: + take = u16_to_float(take_u16) + console.print(f"The childkey take for {hotkey} is {take * 100:.3f}%.") + else: + console.print(":cross_mark:[red] Unable to get childkey take.[/red]") + + @staticmethod + def check_config(config: "bittensor.config"): + if not config.is_set("wallet.name") and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) + config.wallet.name = str(wallet_name) + if not config.is_set("wallet.hotkey") and not config.no_prompt: + hotkey_or_ss58 = Prompt.ask( + "Enter hotkey name or ss58", default=defaults.wallet.hotkey + ) + if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): + config.ss58 = str(hotkey_or_ss58) + else: + config.wallet.hotkey = str(hotkey_or_ss58) + + @staticmethod + def add_args(parser: argparse.ArgumentParser): + get_childkey_take_parser = parser.add_parser( + "get_childkey_take", help="""Get childkey take.""" + ) + get_childkey_take_parser.add_argument( + "--netuid", dest="netuid", type=int, required=False + ) + get_childkey_take_parser.add_argument( + "--hotkey", dest="hotkey", type=str, required=False + ) + bittensor.wallet.add_args(get_childkey_take_parser) + bittensor.subtensor.add_args(get_childkey_take_parser) + + @staticmethod + def get_take(subtensor, hotkey, netuid) -> float: + """ + Get the take value for a given subtensor, hotkey, and netuid. + + @param subtensor: The subtensor object. + @param hotkey: The hotkey to retrieve the take value for. + @param netuid: The netuid to retrieve the take value for. + + @return: The take value as a float. If the take value is not available, it returns 0. + + """ + take_u16 = subtensor.get_childkey_take( + netuid=netuid, + hotkey=hotkey, + ) + if take_u16: + return u16_to_float(take_u16) + else: + return 0 + + +class SetChildrenCommand: + """ + Executes the ``set_children`` command to add children hotkeys on a specified subnet on the Bittensor network to the caller. + + This command is used to delegate authority to different hotkeys, securing their position and influence on the subnet. + + Usage: + Users can specify the amount or 'proportion' to delegate to child hotkeys (``SS58`` address), + the user needs to have sufficient authority to make this call, and the sum of proportions must equal 1, + representing 100% of the proportion allocation. + + The command prompts for confirmation before executing the set_children operation. + + Example usage:: + + btcli stake set_children --children , --hotkey --netuid 1 --proportions 0.4,0.6 + + Note: + This command is critical for users who wish to delegate children hotkeys among different neurons (hotkeys) on the network. + It allows for a strategic allocation of authority to enhance network participation and influence. + """ + + @staticmethod + def run(cli: "bittensor.cli"): + """Set children hotkeys.""" + try: + subtensor: "bittensor.subtensor" = bittensor.subtensor( + config=cli.config, log_verbose=False + ) + SetChildrenCommand._run(cli, subtensor) + finally: + if "subtensor" in locals(): + subtensor.close() + bittensor.logging.debug("closing subtensor connection") + + @staticmethod + def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + console = Console() + wallet = bittensor.wallet(config=cli.config) + + # Get values if not set. + exists, netuid = get_netuid(cli, subtensor) + if not exists: + return + + # get parent hotkey + hotkey = get_hotkey(wallet, cli.config) + if not wallet_utils.is_valid_ss58_address(hotkey): + console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") + return + + # get current children + curr_children = GetChildrenCommand.retrieve_children( + subtensor=subtensor, + hotkey=hotkey, + netuid=netuid, + render_table=False, + ) + + if curr_children: + # print the table of current children + hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) + GetChildrenCommand.render_table( + subtensor=subtensor, + hotkey=hotkey, + hotkey_stake=hotkey_stake, + children=curr_children, + netuid=netuid, + prompt=False, + ) + + # get new children + if not cli.config.is_set("children"): + cli.config.children = Prompt.ask( + "Enter child hotkeys (ss58) as comma-separated values" + ) + proposed_children = [str(x) for x in re.split(r"[ ,]+", cli.config.children)] + + # Set max 5 children + if len(proposed_children) > MAX_CHILDREN: + console.print( + ":cross_mark:[red] Too many children. Maximum 5 children per hotkey[/red]" + ) + return + + # Validate children SS58 addresses + for child in proposed_children: + if not wallet_utils.is_valid_ss58_address(child): + console.print(f":cross_mark:[red] Invalid SS58 address: {child}[/red]") + return + + # get proportions for new children + if not cli.config.is_set("proportions"): + cli.config.proportions = Prompt.ask( + "Enter the percentage of proportion for each child as comma-separated values (total from all children must be less than or equal to 1)" + ) + + # extract proportions and child addresses from cli input + proportions = [ + float(x) for x in re.split(r"[ ,]+", str(cli.config.proportions)) + ] + total_proposed = sum(proportions) + if total_proposed > 1: + console.print( + f":cross_mark:[red]Invalid proportion: The sum of all proportions must be less or equal to than 1 (representing 100% of the allocation). Proposed sum addition is proportions is {total_proposed}.[/red]" + ) + return + + if len(proportions) != len(proposed_children): + console.print( + ":cross_mark:[red]Invalid proportion and children length: The count of children and number of proportion values entered do not match.[/red]" + ) + return + + # combine proposed and current children + children_with_proportions = list(zip(proportions, proposed_children)) + + SetChildrenCommand.print_current_stake( + subtensor=subtensor, children=proposed_children, hotkey=hotkey + ) + + success, message = subtensor.set_children( + wallet=wallet, + netuid=netuid, + hotkey=hotkey, + children_with_proportions=children_with_proportions, + wait_for_inclusion=cli.config.wait_for_inclusion, + wait_for_finalization=cli.config.wait_for_finalization, + prompt=cli.config.prompt, + ) + + # Result + if success: + if cli.config.wait_for_finalization and cli.config.wait_for_inclusion: + console.print("New Status:") + GetChildrenCommand.retrieve_children( + subtensor=subtensor, + hotkey=hotkey, + netuid=netuid, + render_table=True, + ) + console.print( + ":white_heavy_check_mark: [green]Set children hotkeys.[/green]" + ) + else: + console.print( + f":cross_mark:[red] Unable to set children hotkeys.[/red] {message}" + ) + + @staticmethod + def check_config(config: "bittensor.config"): + if not config.is_set("wallet.name") and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) + config.wallet.name = str(wallet_name) + if not config.is_set("wallet.hotkey") and not config.no_prompt: + hotkey_or_ss58 = Prompt.ask( + "Enter hotkey name or ss58", default=defaults.wallet.hotkey + ) + if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): + config.ss58 = str(hotkey_or_ss58) + else: + config.wallet.hotkey = str(hotkey_or_ss58) + + @staticmethod + def add_args(parser: argparse.ArgumentParser): + set_children_parser = parser.add_parser( + "set_children", help="""Set multiple children hotkeys.""" + ) + set_children_parser.add_argument( + "--netuid", dest="netuid", type=int, required=False + ) + set_children_parser.add_argument( + "--children", dest="children", type=str, required=False + ) + set_children_parser.add_argument( + "--hotkey", dest="hotkey", type=str, required=False + ) + set_children_parser.add_argument( + "--proportions", dest="proportions", type=str, required=False + ) + set_children_parser.add_argument( + "--wait_for_inclusion", + dest="wait_for_inclusion", + action="store_true", + default=True, + help="""Wait for the transaction to be included in a block.""", + ) + set_children_parser.add_argument( + "--wait_for_finalization", + dest="wait_for_finalization", + action="store_true", + default=True, + help="""Wait for the transaction to be finalized.""", + ) + set_children_parser.add_argument( + "--prompt", + dest="prompt", + action="store_true", + default=True, + help="""Prompt for confirmation before proceeding.""", + ) + set_children_parser.add_argument( + "--y", + "--yes", + "--no_prompt", + dest="prompt", + action="store_false", + help="""Disable prompt for confirmation before proceeding. Defaults to Yes for all prompts.""", + ) + bittensor.wallet.add_args(set_children_parser) + bittensor.subtensor.add_args(set_children_parser) + + @staticmethod + def print_current_stake(subtensor, children, hotkey): + console = Console() + parent_stake = subtensor.get_total_stake_for_hotkey(ss58_address=hotkey) + console.print("Current Status:") + console.print(f"My Hotkey: {hotkey} | ", style="cyan", end="", no_wrap=True) + console.print(f"Total Stake: {parent_stake}τ") + for child in children: + child_stake = subtensor.get_total_stake_for_hotkey(child) + console.print( + f"Child Hotkey: {child} | Current Child Stake: {child_stake}τ" + ) + + +class GetChildrenCommand: + """ + Executes the ``get_children_info`` command to get all child hotkeys on a specified subnet on the Bittensor network. + + This command is used to view delegated authority to different hotkeys on the subnet. + + Usage: + Users can specify the subnet and see the children and the proportion that is given to them. + + The command compiles a table showing: + + - ChildHotkey: The hotkey associated with the child. + - ParentHotKey: The hotkey associated with the parent. + - Proportion: The proportion that is assigned to them. + - Expiration: The expiration of the hotkey. + + Example usage:: + + btcli stake get_children --netuid 1 + + Note: + This command is for users who wish to see child hotkeys among different neurons (hotkeys) on the network. + """ + + @staticmethod + def run(cli: "bittensor.cli"): + """Get children hotkeys.""" + try: + subtensor: "bittensor.subtensor" = bittensor.subtensor( + config=cli.config, log_verbose=False + ) + return GetChildrenCommand._run(cli, subtensor) + except Exception as e: + console = Console() + console.print(f":cross_mark:[red] An error occurred: {str(e)}[/red]") + finally: + if "subtensor" in locals(): + subtensor.close() + bittensor.logging.debug("closing subtensor connection") + + @staticmethod + def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + console = Console() + wallet = bittensor.wallet(config=cli.config) + + # check all + if cli.config.is_set("all"): + cli.config.netuid = None + cli.config.all = True + elif cli.config.is_set("netuid"): + if cli.config.netuid == "all": + cli.config.all = True + else: + cli.config.netuid = int(cli.config.netuid) + exists, netuid = get_netuid(cli, subtensor) + if not exists: + return + else: + netuid_input = Prompt.ask("Enter netuid or 'all'", default="all") + if netuid_input == "all": + cli.config.netuid = None + cli.config.all = True + else: + cli.config.netuid = int(netuid_input) + exists, netuid = get_netuid(cli, subtensor, False) + if not exists: + return + + # get parent hotkey + hotkey = get_hotkey(wallet, cli.config) + if not wallet_utils.is_valid_ss58_address(hotkey): + console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") + return + + try: + netuids = subtensor.get_all_subnet_netuids() if cli.config.all else [netuid] + hotkey_stake = GetChildrenCommand.get_parent_stake_info( + console, subtensor, hotkey + ) + for netuid in netuids: + children = subtensor.get_children(hotkey, netuid) + if children: + GetChildrenCommand.render_table( + subtensor, + hotkey, + hotkey_stake, + children, + netuid, + not cli.config.is_set("all"), + ) + except Exception as e: + console.print( + f":cross_mark:[red] An error occurred while retrieving children: {str(e)}[/red]" + ) + return + + return children + + @staticmethod + def get_parent_stake_info(console, subtensor, hotkey): + hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) + console.print( + f"\nYour Hotkey: {hotkey} | ", style="cyan", end="", no_wrap=True + ) + console.print(f"Total Stake: {hotkey_stake}τ") + return hotkey_stake + + @staticmethod + def retrieve_children( + subtensor: "bittensor.subtensor", hotkey: str, netuid: int, render_table: bool + ) -> list[tuple[int, str]]: + """ + + Static method to retrieve children for a given subtensor. + + Args: + subtensor (bittensor.subtensor): The subtensor object used to interact with the Bittensor network. + hotkey (str): The hotkey of the parent. + netuid (int): The network unique identifier of the subtensor. + render_table (bool): Flag indicating whether to render the retrieved children in a table. + + Returns: + List[str]: A list of children hotkeys. + + """ + try: + children = subtensor.get_children(hotkey, netuid) + if render_table: + hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) + GetChildrenCommand.render_table( + subtensor, hotkey, hotkey_stake, children, netuid, False + ) + return children + except Exception as e: + console = Console() + console.print( + f":cross_mark:[red] An error occurred while retrieving children: {str(e)}[/red]" + ) + return [] + + @staticmethod + def check_config(config: "bittensor.config"): + if not config.is_set("wallet.name") and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) + config.wallet.name = str(wallet_name) + if not config.is_set("wallet.hotkey") and not config.no_prompt: + hotkey_or_ss58 = Prompt.ask( + "Enter hotkey name or ss58", default=defaults.wallet.hotkey + ) + if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): + config.ss58 = str(hotkey_or_ss58) + else: + config.wallet.hotkey = str(hotkey_or_ss58) + + @staticmethod + def add_args(parser: argparse.ArgumentParser): + parser = parser.add_parser( + "get_children", help="""Get child hotkeys on subnet.""" + ) + parser.add_argument("--netuid", dest="netuid", type=str, required=False) + parser.add_argument("--hotkey", dest="hotkey", type=str, required=False) + parser.add_argument( + "--all", + dest="all", + action="store_true", + help="Retrieve children from all subnets.", + ) + + bittensor.wallet.add_args(parser) + bittensor.subtensor.add_args(parser) + + @staticmethod + def render_table( + subtensor: "bittensor.subtensor", + hotkey: str, + hotkey_stake: "Balance", + children: list[Tuple[int, str]], + netuid: int, + prompt: bool, + ): + """ + + Render a table displaying information about child hotkeys on a particular subnet. + + Parameters: + - subtensor: An instance of the "bittensor.subtensor" class. + - hotkey: The hotkey of the parent node. + - children: A list of tuples containing information about child hotkeys. Each tuple should contain: + - The proportion of the child's stake relative to the total stake. + - The hotkey of the child node. + - netuid: The ID of the subnet. + - prompt: A boolean indicating whether to display a prompt for adding a child hotkey. + + Returns: + None + + Example Usage: + subtensor = bittensor.subtensor_instance + hotkey = "parent_hotkey" + children = [(0.5, "child1_hotkey"), (0.3, "child2_hotkey"), (0.2, "child3_hotkey")] + netuid = 1234 + prompt = True + render_table(subtensor, hotkey, children, netuid, prompt) + + """ + console = Console() + + # Initialize Rich table for pretty printing + table = Table( + show_header=True, + header_style="bold magenta", + border_style="blue", + style="dim", + ) + + # Add columns to the table with specific styles + table.add_column("Index", style="bold yellow", no_wrap=True, justify="center") + table.add_column("Child Hotkey", style="bold green") + table.add_column("Proportion", style="bold cyan", no_wrap=True, justify="right") + table.add_column( + "Childkey Take", style="bold blue", no_wrap=True, justify="right" + ) + table.add_column( + "Current Stake Weight", style="bold red", no_wrap=True, justify="right" + ) + + if not children: + console.print(table) + console.print( + f"[bold white]There are currently no child hotkeys on subnet {netuid} with Parent HotKey {hotkey}.[/bold white]" + ) + if prompt: + command = f"btcli stake set_children --children --hotkey --netuid {netuid} --proportion " + console.print( + f"[bold cyan]To add a child hotkey you can run the command: [white]{command}[/white][/bold cyan]" + ) + return + + console.print(f"\nChildren for netuid: {netuid} ", style="cyan") + + # calculate totals + total_proportion = 0 + total_stake = 0 + total_stake_weight = 0 + avg_take = 0 + + children_info = [] + for child in children: + proportion = child[0] + child_hotkey = child[1] + child_stake = subtensor.get_total_stake_for_hotkey( + ss58_address=child_hotkey + ) or Balance(0) + + child_take = subtensor.get_childkey_take(child_hotkey, netuid) + child_take = u16_to_float(child_take) + + # add to totals + total_stake += child_stake.tao + avg_take += child_take + + proportion = u64_to_float(proportion) + + children_info.append((proportion, child_hotkey, child_stake, child_take)) + + children_info.sort( + key=lambda x: x[0], reverse=True + ) # sorting by proportion (highest first) + + # add the children info to the table + for i, (proportion, hotkey, stake, child_take) in enumerate(children_info, 1): + proportion_percent = proportion * 100 # Proportion in percent + proportion_tao = hotkey_stake.tao * proportion # Proportion in TAO + + total_proportion += proportion_percent + + # Conditionally format text + proportion_str = f"{proportion_percent:.3f}% ({proportion_tao:.3f}τ)" + stake_weight = stake.tao + proportion_tao + total_stake_weight += stake_weight + take_str = f"{child_take * 100:.3f}%" + + hotkey = Text(hotkey, style="italic red" if proportion == 0 else "") + table.add_row( + str(i), + hotkey, + proportion_str, + take_str, + str(f"{stake_weight:.3f}"), + ) + + avg_take = avg_take / len(children_info) + + # add totals row + table.add_row( + "", + "[dim]Total[/dim]", + f"[dim]{total_proportion:.3f}%[/dim]", + f"[dim](avg) {avg_take * 100:.3f}%[/dim]", + f"[dim]{total_stake_weight:.3f}τ[/dim]", + style="dim", + ) + console.print(table) diff --git a/bittensor/commands/unstake.py b/bittensor/commands/unstake.py index 87d13aab91..291aeb6e9a 100644 --- a/bittensor/commands/unstake.py +++ b/bittensor/commands/unstake.py @@ -16,13 +16,17 @@ # DEALINGS IN THE SOFTWARE. import sys -import bittensor -from tqdm import tqdm +import argparse +from typing import List, Union, Optional, Tuple + from rich.prompt import Confirm, Prompt +from tqdm import tqdm + +import bittensor from bittensor.utils.balance import Balance -from typing import List, Union, Optional, Tuple +from . import defaults, GetChildrenCommand from .utils import get_hotkey_wallets_for_wallet -from . import defaults +from ..utils import wallet_utils console = bittensor.__console__ @@ -294,3 +298,146 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): wait_for_inclusion=True, prompt=False, ) + + +class RevokeChildrenCommand: + """ + Executes the ``revoke_children`` command to remove all children hotkeys on a specified subnet on the Bittensor network. + + This command is used to remove delegated authority from all child hotkeys, removing their position and influence on the subnet. + + Usage: + Users need to specify the parent hotkey and the subnet ID (netuid). + The user needs to have sufficient authority to make this call. + + The command prompts for confirmation before executing the revoke_children operation. + + Example usage:: + + btcli stake revoke_children --hotkey --netuid 1 + + Note: + This command is critical for users who wish to remove children hotkeys on the network. + It allows for a complete removal of delegated authority to enhance network participation and influence. + """ + + @staticmethod + def run(cli: "bittensor.cli"): + """Revokes all children hotkeys.""" + try: + subtensor: "bittensor.subtensor" = bittensor.subtensor( + config=cli.config, log_verbose=False + ) + RevokeChildrenCommand._run(cli, subtensor) + finally: + if "subtensor" in locals(): + subtensor.close() + bittensor.logging.debug("closing subtensor connection") + + @staticmethod + def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + wallet = bittensor.wallet(config=cli.config) + + # Get values if not set. + if not cli.config.is_set("netuid"): + cli.config.netuid = int(Prompt.ask("Enter netuid")) + + netuid = cli.config.netuid + total_subnets = subtensor.get_total_subnets() + if total_subnets is not None and not (0 <= netuid < total_subnets): + console.print("Netuid is outside the current subnet range") + return + + # get parent hotkey + if wallet and wallet.hotkey: + hotkey = wallet.hotkey.ss58_address + elif cli.config.is_set("hotkey"): + hotkey = cli.config.hotkey + elif cli.config.is_set("ss58"): + hotkey = cli.config.ss58 + else: + hotkey = Prompt.ask("Enter parent hotkey (ss58)") + + if not wallet_utils.is_valid_ss58_address(hotkey): + console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") + return + + success, message = subtensor.set_children( + wallet=wallet, + netuid=netuid, + children_with_proportions=[], + hotkey=hotkey, + wait_for_inclusion=cli.config.wait_for_inclusion, + wait_for_finalization=cli.config.wait_for_finalization, + prompt=cli.config.prompt, + ) + + # Result + if success: + if cli.config.wait_for_finalization and cli.config.wait_for_inclusion: + GetChildrenCommand.retrieve_children( + subtensor=subtensor, + hotkey=hotkey, + netuid=netuid, + render_table=True, + ) + console.print( + ":white_heavy_check_mark: [green]Revoked all children hotkeys.[/green]" + ) + else: + console.print( + f":cross_mark:[red] Unable to revoke children hotkeys.[/red] {message}" + ) + + @staticmethod + def check_config(config: "bittensor.config"): + if not config.is_set("wallet.name") and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) + config.wallet.name = str(wallet_name) + if not config.is_set("wallet.hotkey") and not config.no_prompt: + hotkey_or_ss58 = Prompt.ask( + "Enter hotkey name or ss58", default=defaults.wallet.hotkey + ) + if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): + config.ss58 = str(hotkey_or_ss58) + else: + config.wallet.hotkey = str(hotkey_or_ss58) + + @staticmethod + def add_args(parser: argparse.ArgumentParser): + parser = parser.add_parser( + "revoke_children", help="""Revoke all children hotkeys.""" + ) + parser.add_argument("--netuid", dest="netuid", type=int, required=False) + parser.add_argument("--hotkey", dest="hotkey", type=str, required=False) + parser.add_argument( + "--wait_for_inclusion", + dest="wait_for_inclusion", + action="store_true", + default=True, + help="""Wait for the transaction to be included in a block.""", + ) + parser.add_argument( + "--wait_for_finalization", + dest="wait_for_finalization", + action="store_true", + default=True, + help="""Wait for the transaction to be finalized.""", + ) + parser.add_argument( + "--prompt", + dest="prompt", + action="store_true", + default=True, + help="""Prompt for confirmation before proceeding.""", + ) + parser.add_argument( + "--y", + "--yes", + "--no_prompt", + dest="prompt", + action="store_false", + help="""Disable prompt for confirmation before proceeding. Defaults to Yes for all prompts.""", + ) + bittensor.wallet.add_args(parser) + bittensor.subtensor.add_args(parser) diff --git a/bittensor/commands/wallets.py b/bittensor/commands/wallets.py index 0f665db7e4..15819ece7b 100644 --- a/bittensor/commands/wallets.py +++ b/bittensor/commands/wallets.py @@ -16,15 +16,18 @@ # DEALINGS IN THE SOFTWARE. import argparse -import bittensor import os import sys -from rich.prompt import Prompt, Confirm -from rich.table import Table -from typing import Optional, List, Tuple -from . import defaults +from typing import List, Optional, Tuple + import requests +from rich.prompt import Confirm, Prompt +from rich.table import Table + +import bittensor + from ..utils import RAOPERTAO +from . import defaults class RegenColdkeyCommand: @@ -637,7 +640,6 @@ class UpdateWalletCommand: Optional arguments: - ``--all`` (bool): When set, updates all legacy wallets. - - ``--no_prompt`` (bool): Disables user prompting during the update process. Example usage:: diff --git a/bittensor/commands/weights.py b/bittensor/commands/weights.py index ac4d9dfc36..b8844433c3 100644 --- a/bittensor/commands/weights.py +++ b/bittensor/commands/weights.py @@ -70,13 +70,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Get values if not set if not cli.config.is_set("netuid"): - cli.config.netuid = int(Prompt.ask(f"Enter netuid")) + cli.config.netuid = int(Prompt.ask("Enter netuid")) if not cli.config.is_set("uids"): - cli.config.uids = Prompt.ask(f"Enter UIDs (comma-separated)") + cli.config.uids = Prompt.ask("Enter UIDs (comma-separated)") if not cli.config.is_set("weights"): - cli.config.weights = Prompt.ask(f"Enter weights (comma-separated)") + cli.config.weights = Prompt.ask("Enter weights (comma-separated)") # Parse from string netuid = cli.config.netuid @@ -120,7 +120,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Result if success: - bittensor.__console__.print(f"Weights committed successfully") + bittensor.__console__.print("Weights committed successfully") else: bittensor.__console__.print(f"Failed to commit weights: {message}") @@ -201,16 +201,16 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Get values if not set. if not cli.config.is_set("netuid"): - cli.config.netuid = int(Prompt.ask(f"Enter netuid")) + cli.config.netuid = int(Prompt.ask("Enter netuid")) if not cli.config.is_set("uids"): - cli.config.uids = Prompt.ask(f"Enter UIDs (comma-separated)") + cli.config.uids = Prompt.ask("Enter UIDs (comma-separated)") if not cli.config.is_set("weights"): - cli.config.weights = Prompt.ask(f"Enter weights (comma-separated)") + cli.config.weights = Prompt.ask("Enter weights (comma-separated)") if not cli.config.is_set("salt"): - cli.config.salt = Prompt.ask(f"Enter salt (comma-separated)") + cli.config.salt = Prompt.ask("Enter salt (comma-separated)") # Parse from string netuid = cli.config.netuid @@ -245,7 +245,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ) if success: - bittensor.__console__.print(f"Weights revealed successfully") + bittensor.__console__.print("Weights revealed successfully") else: bittensor.__console__.print(f"Failed to reveal weights: {message}") diff --git a/bittensor/constants.py b/bittensor/constants.py index 2b52cfd4bd..74d3dd2e08 100644 --- a/bittensor/constants.py +++ b/bittensor/constants.py @@ -15,6 +15,29 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +# Standard Library +import asyncio +from typing import Dict, Type -ALLOWED_DELTA = 4000000000 # Delta of 4 seconds for nonce validation +# 3rd Party +import aiohttp + + +ALLOWED_DELTA = 4_000_000_000 # Delta of 4 seconds for nonce validation V_7_2_0 = 7002000 +NANOSECONDS_IN_SECOND = 1_000_000_000 + +#### Dendrite #### +DENDRITE_ERROR_MAPPING: Dict[Type[Exception], tuple] = { + aiohttp.ClientConnectorError: ("503", "Service unavailable"), + asyncio.TimeoutError: ("408", "Request timeout"), + aiohttp.ClientResponseError: (None, "Client response error"), + aiohttp.ClientPayloadError: ("400", "Payload error"), + aiohttp.ClientError: ("500", "Client error"), + aiohttp.ServerTimeoutError: ("504", "Server timeout error"), + aiohttp.ServerDisconnectedError: ("503", "Service disconnected"), + aiohttp.ServerConnectionError: ("503", "Service connection error"), +} + +DENDRITE_DEFAULT_ERROR = ("422", "Failed to parse response") +#### End Dendrite #### diff --git a/bittensor/dendrite.py b/bittensor/dendrite.py index 3341d15ddf..683ac595a5 100644 --- a/bittensor/dendrite.py +++ b/bittensor/dendrite.py @@ -17,16 +17,20 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +# Standard Library from __future__ import annotations - import asyncio -import uuid import time +from typing import Optional, List, Union, AsyncGenerator, Any +import uuid + +# 3rd Party import aiohttp from aiohttp import ClientTimeout +# Application import bittensor -from typing import Optional, List, Union, AsyncGenerator, Any +from bittensor.constants import DENDRITE_ERROR_MAPPING, DENDRITE_DEFAULT_ERROR from bittensor.utils.registration import torch, use_torch @@ -226,7 +230,29 @@ def _get_endpoint_url(self, target_axon, request_name): ) return f"http://{endpoint}/{request_name}" - def _handle_request_errors(self, synapse, request_name, exception): + def log_exception(self, exception: Exception): + """ + Logs an exception with a unique identifier. + + This method generates a unique UUID for the error, extracts the error type, + and logs the error message using Bittensor's logging system. + + Args: + exception (Exception): The exception object to be logged. + + Returns: + None + """ + error_id = str(uuid.uuid4()) + error_type = exception.__class__.__name__ + bittensor.logging.error(f"{error_type}#{error_id}: {exception}") + + def process_error_message( + self, + synapse: Union[bittensor.Synapse, bittensor.StreamingSynapse], + request_name: str, + exception: Exception, + ) -> Union[bittensor.Synapse, bittensor.StreamingSynapse]: """ Handles exceptions that occur during network requests, updating the synapse with appropriate status codes and messages. @@ -238,22 +264,32 @@ def _handle_request_errors(self, synapse, request_name, exception): request_name: The name of the request during which the exception occurred. exception: The exception object caught during the request. + Returns: + bittensor.Synapse: The updated synapse object with the error status code and message. + Note: This method updates the synapse object in-place. """ + + self.log_exception(exception) + + error_info = DENDRITE_ERROR_MAPPING.get(type(exception), DENDRITE_DEFAULT_ERROR) + status_code, status_message = error_info + + if status_code: + synapse.dendrite.status_code = status_code # type: ignore + elif isinstance(exception, aiohttp.ClientResponseError): + synapse.dendrite.status_code = str(exception.code) # type: ignore + + message = f"{status_message}: {str(exception)}" if isinstance(exception, aiohttp.ClientConnectorError): - synapse.dendrite.status_code = "503" - synapse.dendrite.status_message = f"Service at {synapse.axon.ip}:{str(synapse.axon.port)}/{request_name} unavailable." + message = f"{status_message} at {synapse.axon.ip}:{synapse.axon.port}/{request_name}" # type: ignore elif isinstance(exception, asyncio.TimeoutError): - synapse.dendrite.status_code = "408" - synapse.dendrite.status_message = ( - f"Timedout after {synapse.timeout} seconds." - ) - else: - synapse.dendrite.status_code = "422" - synapse.dendrite.status_message = ( - f"Failed to parse response object with error: {str(exception)}" - ) + message = f"{status_message} after {synapse.timeout} seconds" + + synapse.dendrite.status_message = message # type: ignore + + return synapse def _log_outgoing_request(self, synapse): """ @@ -533,7 +569,7 @@ async def call( synapse.dendrite.process_time = str(time.time() - start_time) # type: ignore except Exception as e: - self._handle_request_errors(synapse, request_name, e) + synapse = self.process_error_message(synapse, request_name, e) finally: self._log_incoming_response(synapse) @@ -544,10 +580,7 @@ async def call( ) # Return the updated synapse object after deserializing if requested - if deserialize: - return synapse.deserialize() - else: - return synapse + return synapse.deserialize() if deserialize else synapse async def call_stream( self, @@ -618,7 +651,7 @@ async def call_stream( synapse.dendrite.process_time = str(time.time() - start_time) # type: ignore except Exception as e: - self._handle_request_errors(synapse, request_name, e) + synapse = self.process_error_message(synapse, request_name, e) # type: ignore finally: self._log_incoming_response(synapse) @@ -705,10 +738,7 @@ def process_server_response( # Set the attribute in the local synapse from the corresponding # attribute in the server synapse setattr(local_synapse, key, getattr(server_synapse, key)) - except Exception as e: - bittensor.logging.info( - f"Ignoring error when setting attribute: {e}" - ) + except Exception: # Ignore errors during attribute setting pass else: diff --git a/bittensor/extrinsics/delegation.py b/bittensor/extrinsics/delegation.py index 54bdb5273c..e61a97efb4 100644 --- a/bittensor/extrinsics/delegation.py +++ b/bittensor/extrinsics/delegation.py @@ -47,9 +47,17 @@ def nominate_extrinsic( success (bool): ``True`` if the transaction was successful. """ # Unlock the coldkey. - wallet.coldkey - wallet.hotkey + try: + wallet.coldkey + + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False + + wallet.hotkey # Check if the hotkey is already a delegate. if subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): logger.error( @@ -57,6 +65,14 @@ def nominate_extrinsic( ) return False + if not subtensor.is_hotkey_registered_any(wallet.hotkey.ss58_address): + logger.error( + "Hotkey {} is not registered to any network".format( + wallet.hotkey.ss58_address + ) + ) + return False + with bittensor.__console__.status( ":satellite: Sending nominate call on [white]{}[/white] ...".format( subtensor.network @@ -125,7 +141,13 @@ def delegate_extrinsic( NotDelegateError: If the hotkey is not a delegate on the chain. """ # Decrypt keys, - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False if not subtensor.is_hotkey_delegate(delegate_ss58): raise NotDelegateError("Hotkey: {} is not a delegate.".format(delegate_ss58)) @@ -386,7 +408,14 @@ def decrease_take_extrinsic( success (bool): ``True`` if the transaction was successful. """ # Unlock the coldkey. - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False + wallet.hotkey with bittensor.__console__.status( @@ -446,7 +475,14 @@ def increase_take_extrinsic( success (bool): ``True`` if the transaction was successful. """ # Unlock the coldkey. - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False + wallet.hotkey with bittensor.__console__.status( diff --git a/bittensor/extrinsics/network.py b/bittensor/extrinsics/network.py index 16cbc0ed26..5aecaa459a 100644 --- a/bittensor/extrinsics/network.py +++ b/bittensor/extrinsics/network.py @@ -87,7 +87,13 @@ def register_subnetwork_extrinsic( ): return False - wallet.coldkey # unlock coldkey + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False with bittensor.__console__.status(":satellite: Registering subnet..."): with subtensor.substrate as substrate: diff --git a/bittensor/extrinsics/registration.py b/bittensor/extrinsics/registration.py index e82add8383..40bde3fc89 100644 --- a/bittensor/extrinsics/registration.py +++ b/bittensor/extrinsics/registration.py @@ -259,7 +259,13 @@ def burned_register_extrinsic( ) return False - wallet.coldkey # unlock coldkey + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False with bittensor.__console__.status( f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]..." ): @@ -394,7 +400,13 @@ def run_faucet_extrinsic( return False, "Requires torch" # Unlock coldkey - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False, "" # Get previous balance. old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) @@ -497,7 +509,13 @@ def swap_hotkey_extrinsic( wait_for_finalization: bool = True, prompt: bool = False, ) -> bool: - wallet.coldkey # unlock coldkey + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False if prompt: # Prompt user for confirmation. if not Confirm.ask( diff --git a/bittensor/extrinsics/root.py b/bittensor/extrinsics/root.py index 8a7e9e3863..c0a4fcabd1 100644 --- a/bittensor/extrinsics/root.py +++ b/bittensor/extrinsics/root.py @@ -54,7 +54,13 @@ def root_register_extrinsic( Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ - wallet.coldkey # unlock coldkey + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False is_registered = subtensor.is_hotkey_registered( netuid=0, hotkey_ss58=wallet.hotkey.ss58_address @@ -131,7 +137,13 @@ def set_root_weights_extrinsic( Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ - wallet.coldkey # unlock coldkey + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False # First convert types. if isinstance(netuids, list): diff --git a/bittensor/extrinsics/senate.py b/bittensor/extrinsics/senate.py index 043233996c..f586cec399 100644 --- a/bittensor/extrinsics/senate.py +++ b/bittensor/extrinsics/senate.py @@ -46,7 +46,14 @@ def register_senate_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``. """ - wallet.coldkey # unlock coldkey + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False + wallet.hotkey # unlock hotkey if prompt: @@ -121,7 +128,14 @@ def leave_senate_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``. """ - wallet.coldkey # unlock coldkey + try: + wallet.coldkey # unlock coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False + wallet.hotkey # unlock hotkey if prompt: diff --git a/bittensor/extrinsics/serving.py b/bittensor/extrinsics/serving.py index bba5367de1..734561835f 100644 --- a/bittensor/extrinsics/serving.py +++ b/bittensor/extrinsics/serving.py @@ -25,6 +25,7 @@ import bittensor import bittensor.utils.networking as net from bittensor.utils import format_error_message +from bittensor.utils.networking import ensure_connected from ..errors import MetadataError @@ -269,6 +270,7 @@ def publish_metadata( raise MetadataError(format_error_message(response.error_message)) +@ensure_connected def get_metadata(self, netuid: int, hotkey: str, block: Optional[int] = None) -> str: @retry(delay=2, tries=3, backoff=2, max_delay=4) def make_substrate_call_with_retry(): diff --git a/bittensor/extrinsics/set_weights.py b/bittensor/extrinsics/set_weights.py index dc3052d0a0..ea51fab237 100644 --- a/bittensor/extrinsics/set_weights.py +++ b/bittensor/extrinsics/set_weights.py @@ -64,7 +64,7 @@ def set_weights_extrinsic( If ``true``, the call waits for confirmation from the user before proceeding. 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``. + Flag is ``true`` if extrinsic was finalized or included in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ # First convert types. if use_torch(): diff --git a/bittensor/extrinsics/staking.py b/bittensor/extrinsics/staking.py index 298bb1f0d3..b6d5cf5d60 100644 --- a/bittensor/extrinsics/staking.py +++ b/bittensor/extrinsics/staking.py @@ -16,12 +16,17 @@ # 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 bittensor from rich.prompt import Confirm from time import sleep from typing import List, Union, Optional, Tuple + +import bittensor +from ..utils.formatting import float_to_u64, float_to_u16 + from bittensor.utils.balance import Balance +console = bittensor.__console__ + def _check_threshold_amount( subtensor: "bittensor.subtensor", stake_balance: Balance @@ -82,7 +87,13 @@ def add_stake_extrinsic( If the hotkey is not a delegate on the chain. """ # Decrypt keys, - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False # Default to wallet's own hotkey if the value is not passed. if hotkey_ss58 is None: @@ -228,7 +239,7 @@ def add_stake_extrinsic( ) return False - except bittensor.errors.NotRegisteredError as e: + except bittensor.errors.NotRegisteredError: bittensor.__console__.print( ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( wallet.hotkey_str @@ -435,7 +446,7 @@ def add_stake_multiple_extrinsic( ) continue - except bittensor.errors.NotRegisteredError as e: + except bittensor.errors.NotRegisteredError: bittensor.__console__.print( ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( hotkey_ss58 @@ -523,3 +534,227 @@ def __do_add_stake_single( ) return success + + +def set_childkey_take_extrinsic( + subtensor: "bittensor.subtensor", + wallet: "bittensor.wallet", + hotkey: str, + netuid: int, + take: float, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, +) -> Tuple[bool, str]: + """ + Sets childkey take. + + Args: + subtensor (bittensor.subtensor): Subtensor endpoint to use. + wallet (bittensor.wallet): Bittensor wallet object. + hotkey (str): Childkey hotkey. + take (float): Childkey take value. + netuid (int): Unique identifier of for the subnet. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + + Returns: + Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + + Raises: + bittensor.errors.ChildHotkeyError: If the extrinsic fails to be finalized or included in the block. + bittensor.errors.NotRegisteredError: If the hotkey is not registered in any subnets. + + """ + + # Ask before moving on. + if prompt: + if not Confirm.ask( + f"Do you want to set childkey take to: [bold white]{take*100}%[/bold white]?" + ): + return False, "Operation Cancelled" + + # Decrypt coldkey. + wallet.coldkey + + with bittensor.__console__.status( + f":satellite: Setting childkey take on [white]{subtensor.network}[/white] ..." + ): + try: + if 0 < take <= 0.18: + take_u16 = float_to_u16(take) + else: + return False, "Invalid take value" + + success, error_message = subtensor._do_set_childkey_take( + wallet=wallet, + hotkey=hotkey, + netuid=netuid, + take=take_u16, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return ( + True, + "Not waiting for finalization or inclusion. Set childkey take initiated.", + ) + + if success: + bittensor.__console__.print( + ":white_heavy_check_mark: [green]Finalized[/green]" + ) + bittensor.logging.success( + prefix="Setting childkey take", + suffix="Finalized: " + str(success), + ) + return True, "Successfully set childkey take and Finalized." + else: + bittensor.__console__.print( + f":cross_mark: [red]Failed[/red]: {error_message}" + ) + bittensor.logging.warning( + prefix="Setting childkey take", + suffix="Failed: " + str(error_message), + ) + return False, error_message + + except Exception as e: + return False, f"Exception occurred while setting childkey take: {str(e)}" + + +def set_children_extrinsic( + subtensor: "bittensor.subtensor", + wallet: "bittensor.wallet", + hotkey: str, + netuid: int, + children_with_proportions: List[Tuple[float, str]], + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, +) -> Tuple[bool, str]: + """ + Sets children hotkeys with proportions assigned from the parent. + + Args: + subtensor (bittensor.subtensor): Subtensor endpoint to use. + wallet (bittensor.wallet): Bittensor wallet object. + hotkey (str): Parent hotkey. + children_with_proportions (List[str]): Children hotkeys. + netuid (int): Unique identifier of for the subnet. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + + Returns: + Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + + Raises: + bittensor.errors.ChildHotkeyError: If the extrinsic fails to be finalized or included in the block. + bittensor.errors.NotRegisteredError: If the hotkey is not registered in any subnets. + + """ + # Check if all children are being revoked + all_revoked = len(children_with_proportions) == 0 + + operation = "Revoking all child hotkeys" if all_revoked else "Setting child hotkeys" + + # Ask before moving on. + if prompt: + if all_revoked: + if not Confirm.ask( + f"Do you want to revoke all children hotkeys for hotkey {hotkey}?" + ): + return False, "Operation Cancelled" + else: + if not Confirm.ask( + "Do you want to set children hotkeys with proportions:\n[bold white]{}[/bold white]?".format( + "\n".join( + f" {child[1]}: {child[0]}" + for child in children_with_proportions + ) + ) + ): + return False, "Operation Cancelled" + + # Decrypt coldkey. + wallet.coldkey + + user_hotkey_ss58 = wallet.hotkey.ss58_address # Default to wallet's own hotkey. + if hotkey != user_hotkey_ss58: + raise ValueError("Cannot set/revoke child hotkeys for others.") + + with bittensor.__console__.status( + f":satellite: {operation} on [white]{subtensor.network}[/white] ..." + ): + try: + if not all_revoked: + normalized_children = prepare_child_proportions( + children_with_proportions + ) + else: + normalized_children = [] + + success, error_message = subtensor._do_set_children( + wallet=wallet, + hotkey=hotkey, + netuid=netuid, + children=normalized_children, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return ( + True, + f"Not waiting for finalization or inclusion. {operation} initiated.", + ) + + if success: + bittensor.__console__.print( + ":white_heavy_check_mark: [green]Finalized[/green]" + ) + bittensor.logging.success( + prefix=operation, + suffix="Finalized: " + str(success), + ) + return True, f"Successfully {operation.lower()} and Finalized." + else: + bittensor.__console__.print( + f":cross_mark: [red]Failed[/red]: {error_message}" + ) + bittensor.logging.warning( + prefix=operation, + suffix="Failed: " + str(error_message), + ) + return False, error_message + + except Exception as e: + return False, f"Exception occurred while {operation.lower()}: {str(e)}" + + +def prepare_child_proportions(children_with_proportions): + """ + Convert proportions to u64 and normalize, ensuring total does not exceed u64 max. + """ + children_u64 = [ + (float_to_u64(proportion), child) + for proportion, child in children_with_proportions + ] + total = sum(proportion for proportion, _ in children_u64) + + if total > (2**64 - 1): + excess = total - (2**64 - 1) + if excess > (2**64 * 0.01): # Example threshold of 1% of u64 max + raise ValueError("Excess is too great to normalize proportions") + largest_child_index = max( + range(len(children_u64)), key=lambda i: children_u64[i][0] + ) + children_u64[largest_child_index] = ( + children_u64[largest_child_index][0] - excess, + children_u64[largest_child_index][1], + ) + + return children_u64 diff --git a/bittensor/extrinsics/transfer.py b/bittensor/extrinsics/transfer.py index 91ef3237eb..aa340ab406 100644 --- a/bittensor/extrinsics/transfer.py +++ b/bittensor/extrinsics/transfer.py @@ -68,8 +68,15 @@ def transfer_extrinsic( # Convert bytes to hex string. dest = "0x" + dest.hex() - # Unlock wallet coldkey. - wallet.coldkey + try: + # Unlock wallet coldkey. + wallet.coldkey + + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False # Convert to bittensor.Balance if not isinstance(amount, bittensor.Balance): diff --git a/bittensor/extrinsics/unstaking.py b/bittensor/extrinsics/unstaking.py index 105bb145b9..a5de71b7d7 100644 --- a/bittensor/extrinsics/unstaking.py +++ b/bittensor/extrinsics/unstaking.py @@ -58,7 +58,13 @@ def __do_remove_stake_single( """ # Decrypt keys, - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False success = subtensor._do_unstake( wallet=wallet, @@ -126,7 +132,13 @@ def unstake_extrinsic( Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ # Decrypt keys, - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False if hotkey_ss58 is None: hotkey_ss58 = wallet.hotkey.ss58_address # Default to wallet's own hotkey. @@ -168,7 +180,7 @@ def unstake_extrinsic( subtensor=subtensor, stake_balance=(stake_on_uid - unstaking_balance) ): bittensor.__console__.print( - f":warning: [yellow]This action will unstake the entire staked balance![/yellow]" + ":warning: [yellow]This action will unstake the entire staked balance![/yellow]" ) unstaking_balance = stake_on_uid @@ -232,7 +244,7 @@ def unstake_extrinsic( ) return False - except bittensor.errors.NotRegisteredError as e: + except bittensor.errors.NotRegisteredError: bittensor.__console__.print( ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( wallet.hotkey_str @@ -304,7 +316,13 @@ def unstake_multiple_extrinsic( return True # Unlock coldkey. - wallet.coldkey + try: + wallet.coldkey + except bittensor.KeyFileError: + bittensor.__console__.print( + ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" + ) + return False old_stakes = [] own_hotkeys = [] @@ -352,7 +370,7 @@ def unstake_multiple_extrinsic( subtensor=subtensor, stake_balance=(stake_on_uid - unstaking_balance) ): bittensor.__console__.print( - f":warning: [yellow]This action will unstake the entire staked balance![/yellow]" + ":warning: [yellow]This action will unstake the entire staked balance![/yellow]" ) unstaking_balance = stake_on_uid @@ -424,7 +442,7 @@ def unstake_multiple_extrinsic( ) continue - except bittensor.errors.NotRegisteredError as e: + except bittensor.errors.NotRegisteredError: bittensor.__console__.print( ":cross_mark: [red]{} is not registered.[/red]".format(hotkey_ss58) ) diff --git a/bittensor/keyfile.py b/bittensor/keyfile.py index b5157cea4a..d2c75c1041 100644 --- a/bittensor/keyfile.py +++ b/bittensor/keyfile.py @@ -33,6 +33,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from nacl import pwhash, secret +from nacl.exceptions import CryptoError from password_strength import PasswordPolicy from substrateinterface.utils.ss58 import ss58_encode from termcolor import colored @@ -281,7 +282,7 @@ def get_coldkey_password_from_environment(coldkey_name: str) -> Optional[str]: for env_name, env_value in os.environ.items() if (normalized_env_name := env_name.upper()).startswith("BT_COLD_PW_") } - return envs.get(f"BT_COLD_PW_{coldkey_name.upper()}") + return envs.get(f"BT_COLD_PW_{coldkey_name.replace('-', '_').upper()}") def decrypt_keyfile_data( @@ -321,7 +322,10 @@ def decrypt_keyfile_data( memlimit=pwhash.argon2i.MEMLIMIT_SENSITIVE, ) box = secret.SecretBox(key) - decrypted_keyfile_data = box.decrypt(keyfile_data[len("$NACL") :]) + try: + decrypted_keyfile_data = box.decrypt(keyfile_data[len("$NACL") :]) + except CryptoError: + raise bittensor.KeyFileError("Invalid password") # Ansible decrypt. elif keyfile_data_is_encrypted_ansible(keyfile_data): vault = Vault(password) diff --git a/bittensor/mock/subtensor_mock.py b/bittensor/mock/subtensor_mock.py index 30d58f22e0..5c2c3b42d6 100644 --- a/bittensor/mock/subtensor_mock.py +++ b/bittensor/mock/subtensor_mock.py @@ -624,7 +624,7 @@ def query_subtensor( state_at_block = state.get(block, None) while state_at_block is None and block > 0: block -= 1 - state_at_block = self.state.get(block, None) + state_at_block = state.get(block, None) if state_at_block is not None: return SimpleNamespace(value=state_at_block) diff --git a/bittensor/stream.py b/bittensor/stream.py index e0dc17c42c..3a82edc15a 100644 --- a/bittensor/stream.py +++ b/bittensor/stream.py @@ -1,3 +1,5 @@ +import typing + from aiohttp import ClientResponse import bittensor @@ -49,16 +51,24 @@ class BTStreamingResponse(_StreamingResponse): provided by the subclass. """ - def __init__(self, model: BTStreamingResponseModel, **kwargs): + def __init__( + self, + model: BTStreamingResponseModel, + *, + synapse: typing.Optional["StreamingSynapse"] = None, + **kwargs, + ): """ Initializes the BTStreamingResponse with the given token streamer model. Args: model: A BTStreamingResponseModel instance containing the token streamer callable, which is responsible for generating the content of the response. + synapse: The response Synapse to be used to update the response headers etc. **kwargs: Additional keyword arguments passed to the parent StreamingResponse class. """ super().__init__(content=iter(()), **kwargs) self.token_streamer = model.token_streamer + self.synapse = synapse async def stream_response(self, send: Send): """ @@ -139,4 +149,4 @@ def create_streaming_response( """ model_instance = BTStreamingResponseModel(token_streamer=token_streamer) - return self.BTStreamingResponse(model_instance) + return self.BTStreamingResponse(model_instance, synapse=self) diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index 75484fd69f..2c8f1d75c6 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -21,6 +21,8 @@ blockchain, facilitating a range of operations essential for the decentralized machine learning network. """ +from __future__ import annotations + import argparse import copy import socket @@ -55,7 +57,12 @@ IPInfo, custom_rpc_type_registry, ) -from .errors import IdentityError, NominationError, StakeError, TakeError +from .errors import ( + IdentityError, + NominationError, + StakeError, + TakeError, +) from .extrinsics.commit_weights import ( commit_weights_extrinsic, reveal_weights_extrinsic, @@ -91,9 +98,17 @@ get_metadata, ) from .extrinsics.set_weights import set_weights_extrinsic -from .extrinsics.staking import add_stake_extrinsic, add_stake_multiple_extrinsic +from .extrinsics.staking import ( + add_stake_extrinsic, + add_stake_multiple_extrinsic, + set_children_extrinsic, + set_childkey_take_extrinsic, +) from .extrinsics.transfer import transfer_extrinsic -from .extrinsics.unstaking import unstake_extrinsic, unstake_multiple_extrinsic +from .extrinsics.unstaking import ( + unstake_extrinsic, + unstake_multiple_extrinsic, +) from .types import AxonServeCallParams, PrometheusServeCallParams from .utils import ( U16_NORMALIZED_FLOAT, @@ -104,7 +119,7 @@ from .utils.balance import Balance from .utils.registration import POWSolution from .utils.registration import legacy_torch_api_compat -from .utils.subtensor import get_subtensor_errors +from .utils.subtensor import get_subtensor_errors, format_parent, format_children KEY_NONCE: Dict[str, int] = {} @@ -173,6 +188,7 @@ def __init__( config: Optional[bittensor.config] = None, _mock: bool = False, log_verbose: bool = True, + connection_timeout: int = 600, ) -> None: """ Initializes a Subtensor interface for interacting with the Bittensor blockchain. @@ -236,7 +252,25 @@ def __init__( "To get ahead of this change, please run a local subtensor node and point to it." ) - # Attempt to connect to chosen endpoint. Fallback to finney if local unavailable. + self.log_verbose = log_verbose + self._connection_timeout = connection_timeout + self._get_substrate() + + self._subtensor_errors: Dict[str, Dict[str, str]] = {} + + def __str__(self) -> str: + if self.network == self.chain_endpoint: + # Connecting to chain endpoint without network known. + return "subtensor({})".format(self.chain_endpoint) + else: + # Connecting to network with endpoint known. + return "subtensor({}, {})".format(self.network, self.chain_endpoint) + + def __repr__(self) -> str: + return self.__str__() + + def _get_substrate(self): + """Establishes a connection to the Substrate node using configured parameters.""" try: # Set up params. self.substrate = SubstrateInterface( @@ -245,6 +279,11 @@ def __init__( url=self.chain_endpoint, type_registry=bittensor.__type_registry__, ) + if self.log_verbose: + _logger.info( + f"Connected to {self.network} network and {self.chain_endpoint}." + ) + except ConnectionRefusedError: _logger.error( f"Could not connect to {self.network} network with {self.chain_endpoint} chain endpoint. Exiting...", @@ -253,13 +292,10 @@ def __init__( "You can check if you have connectivity by running this command: nc -vz localhost " f"{self.chain_endpoint.split(':')[2]}" ) - exit(1) - # TODO (edu/phil): Advise to run local subtensor and point to dev docs. + return try: - self.substrate.websocket.settimeout(600) - # except: - # bittensor.logging.warning("Could not set websocket timeout.") + self.substrate.websocket.settimeout(self._connection_timeout) except AttributeError as e: _logger.warning(f"AttributeError: {e}") except TypeError as e: @@ -267,24 +303,6 @@ def __init__( except (socket.error, OSError) as e: _logger.warning(f"Socket error: {e}") - if log_verbose: - _logger.info( - f"Connected to {self.network} network and {self.chain_endpoint}." - ) - - self._subtensor_errors: Dict[str, Dict[str, str]] = {} - - def __str__(self) -> str: - if self.network == self.chain_endpoint: - # Connecting to chain endpoint without network known. - return "subtensor({})".format(self.chain_endpoint) - else: - # Connecting to network with endpoint known. - return "subtensor({}, {})".format(self.network, self.chain_endpoint) - - def __repr__(self) -> str: - return self.__str__() - @staticmethod def config() -> "bittensor.config": """ @@ -403,7 +421,7 @@ def determine_chain_endpoint_and_network(network: str): elif "127.0.0.1" in network or "localhost" in network: return "local", network else: - return "unknown", network + return "unknown network", network @staticmethod def setup_config(network: str, config: "bittensor.config"): @@ -655,6 +673,7 @@ def set_take( wait_for_finalization=wait_for_finalization, ) + @networking.ensure_connected def send_extrinsic( self, wallet: "bittensor.wallet", @@ -824,6 +843,7 @@ def set_weights( return success, message + @networking.ensure_connected def _do_set_weights( self, wallet: "bittensor.wallet", @@ -971,6 +991,7 @@ def commit_weights( return success, message + @networking.ensure_connected def _do_commit_weights( self, wallet: "bittensor.wallet", @@ -1095,6 +1116,7 @@ def reveal_weights( return success, message + @networking.ensure_connected def _do_reveal_weights( self, wallet: "bittensor.wallet", @@ -1357,6 +1379,7 @@ def burned_register( prompt=prompt, ) + @networking.ensure_connected def _do_pow_register( self, netuid: int, @@ -1419,6 +1442,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_burned_register( self, netuid: int, @@ -1476,6 +1500,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_swap_hotkey( self, wallet: "bittensor.wallet", @@ -1573,6 +1598,7 @@ def transfer( prompt=prompt, ) + @networking.ensure_connected def get_transfer_fee( self, wallet: "bittensor.wallet", dest: str, value: Union["Balance", float, int] ) -> "Balance": @@ -1630,6 +1656,7 @@ def get_transfer_fee( ) return fee + @networking.ensure_connected def _do_transfer( self, wallet: "bittensor.wallet", @@ -1865,6 +1892,7 @@ def serve_axon( self, netuid, axon, wait_for_inclusion, wait_for_finalization ) + @networking.ensure_connected def _do_serve_axon( self, wallet: "bittensor.wallet", @@ -1932,6 +1960,7 @@ def serve_prometheus( wait_for_finalization=wait_for_finalization, ) + @networking.ensure_connected def _do_serve_prometheus( self, wallet: "bittensor.wallet", @@ -1977,6 +2006,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_associate_ips( self, wallet: "bittensor.wallet", @@ -2107,6 +2137,7 @@ def add_stake_multiple( prompt, ) + @networking.ensure_connected def _do_stake( self, wallet: "bittensor.wallet", @@ -2234,6 +2265,7 @@ def unstake( prompt, ) + @networking.ensure_connected def _do_unstake( self, wallet: "bittensor.wallet", @@ -2283,6 +2315,194 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + ################### + # Child hotkeys # + ################### + + def set_childkey_take( + self, + wallet: "bittensor.wallet", + hotkey: str, + take: float, + netuid: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, + ) -> tuple[bool, str]: + """Sets a childkey take extrinsic on the subnet. + + Args: + wallet (:func:`bittensor.wallet`): Wallet object that can sign the extrinsic. + hotkey: (str): Hotkey ``ss58`` address of the child for which take is getting set. + netuid (int): Unique identifier of for the subnet. + take (float): Value of childhotkey take on subnet. + wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. + wait_for_finalization (bool): If ``true``, waits for finalization before returning. + prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + Returns: + success (bool): ``True`` if the extrinsic was successful. + Raises: + ChildHotkeyError: If the extrinsic failed. + """ + + return set_childkey_take_extrinsic( + self, + wallet=wallet, + hotkey=hotkey, + take=take, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + + @networking.ensure_connected + def _do_set_childkey_take( + self, + wallet: "bittensor.wallet", + hotkey: str, + take: int, + netuid: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> tuple[bool, Optional[str]]: + """Sends a set_children hotkey extrinsic on the chain. + + Args: + wallet (:func:`bittensor.wallet`): Wallet object that can sign the extrinsic. + hotkey: (str): Hotkey ``ss58`` address of the wallet for which take is getting set. + take: (int): The take that this ss58 hotkey will have if assigned as a child hotkey as u16 value. + netuid (int): Unique identifier for the network. + 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. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + def make_substrate_call_with_retry(): + # create extrinsic call + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_childkey_take", + call_params={ + "hotkey": hotkey, + "take": take, + "netuid": netuid, + }, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, None + + response.process_events() + if not response.is_success: + return False, format_error_message(response.error_message) + else: + return True, None + + return make_substrate_call_with_retry() + + def set_children( + self, + wallet: "bittensor.wallet", + hotkey: str, + children_with_proportions: List[Tuple[float, str]], + netuid: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, + ) -> tuple[bool, str]: + """Sets a children hotkeys extrinsic on the subnet. + + Args: + wallet (:func:`bittensor.wallet`): Wallet object that can sign the extrinsic. + hotkey: (str): Hotkey ``ss58`` address of the parent. + netuid (int): Unique identifier of for the subnet. + children_with_proportions (List[Tuple[float, str]]): List of (proportion, child_ss58) pairs. + wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. + wait_for_finalization (bool): If ``true``, waits for finalization before returning. + prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + Returns: + success (bool): ``True`` if the extrinsic was successful. + Raises: + ChildHotkeyError: If the extrinsic failed. + """ + + return set_children_extrinsic( + self, + wallet=wallet, + hotkey=hotkey, + children_with_proportions=children_with_proportions, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + + @networking.ensure_connected + def _do_set_children( + self, + wallet: "bittensor.wallet", + hotkey: str, + children: List[Tuple[int, str]], + netuid: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> tuple[bool, Optional[str]]: + """Sends a set_children hotkey extrinsic on the chain. + + Args: + wallet (:func:`bittensor.wallet`): Wallet object that can sign the extrinsic. + hotkey: (str): Hotkey ``ss58`` address of the parent. + children: (List[Tuple[int, str]]): A list of tuples containing the hotkey ``ss58`` addresses of the children and their proportions as u16 MAX standardized values. + netuid (int): Unique identifier for the network. + 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. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + def make_substrate_call_with_retry(): + # create extrinsic call + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_children", + call_params={ + "hotkey": hotkey, + "children": children, + "netuid": netuid, + }, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, None + + response.process_events() + if not response.is_success: + return False, format_error_message(response.error_message) + else: + return True, None + + return make_substrate_call_with_retry() + ################## # Coldkey Swap # ################## @@ -2605,6 +2825,7 @@ def root_register( prompt=prompt, ) + @networking.ensure_connected def _do_root_register( self, wallet: "bittensor.wallet", @@ -2685,6 +2906,7 @@ def root_set_weights( prompt=prompt, ) + @networking.ensure_connected def _do_set_root_weights( self, wallet: "bittensor.wallet", @@ -2757,6 +2979,7 @@ def make_substrate_call_with_retry(): ################## # Queries subtensor registry named storage with params and block. + @networking.ensure_connected def query_identity( self, key: str, @@ -2799,6 +3022,7 @@ def make_substrate_call_with_retry() -> "ScaleType": identity_info.value["info"] ) + @networking.ensure_connected def update_identity( self, wallet: "bittensor.wallet", @@ -2902,6 +3126,7 @@ def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> ################## # Queries subtensor named storage with params and block. + @networking.ensure_connected def query_subtensor( self, name: str, @@ -2938,6 +3163,7 @@ def make_substrate_call_with_retry() -> "ScaleType": return make_substrate_call_with_retry() # Queries subtensor map storage with params and block. + @networking.ensure_connected def query_map_subtensor( self, name: str, @@ -2974,6 +3200,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def query_constant( self, module_name: str, constant_name: str, block: Optional[int] = None ) -> Optional["ScaleType"]: @@ -3008,6 +3235,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() # Queries any module storage with params and block. + @networking.ensure_connected def query_module( self, module: str, @@ -3047,6 +3275,7 @@ def make_substrate_call_with_retry() -> "ScaleType": return make_substrate_call_with_retry() # Queries any module map storage with params and block. + @networking.ensure_connected def query_map( self, module: str, @@ -3085,6 +3314,7 @@ def make_substrate_call_with_retry() -> "QueryMapResult": return make_substrate_call_with_retry() + @networking.ensure_connected def state_call( self, method: str, @@ -3173,6 +3403,7 @@ def query_runtime_api( return obj.decode() + @networking.ensure_connected def _encode_params( self, call_definition: List["ParamWithTypes"], @@ -4150,6 +4381,7 @@ def get_subnets(self, block: Optional[int] = None) -> List[int]: else [] ) + @networking.ensure_connected def get_all_subnets_info(self, block: Optional[int] = None) -> List[SubnetInfo]: """ Retrieves detailed information about all subnets within the Bittensor network. This function @@ -4181,6 +4413,7 @@ def make_substrate_call_with_retry(): return SubnetInfo.list_from_vec_u8(result) + @networking.ensure_connected def get_subnet_info( self, netuid: int, block: Optional[int] = None ) -> Optional[SubnetInfo]: @@ -4339,6 +4572,7 @@ def get_nominators_for_hotkey( else 0 ) + @networking.ensure_connected def get_delegate_by_hotkey( self, hotkey_ss58: str, block: Optional[int] = None ) -> Optional[DelegateInfo]: @@ -4376,6 +4610,7 @@ def make_substrate_call_with_retry(encoded_hotkey_: List[int]): return DelegateInfo.from_vec_u8(result) + @networking.ensure_connected def get_delegates_lite(self, block: Optional[int] = None) -> List[DelegateInfoLite]: """ Retrieves a lighter list of all delegate neurons within the Bittensor network. This function provides an @@ -4410,6 +4645,7 @@ def make_substrate_call_with_retry(): return [DelegateInfoLite(**d) for d in result] + @networking.ensure_connected def get_delegates(self, block: Optional[int] = None) -> List[DelegateInfo]: """ Retrieves a list of all delegate neurons within the Bittensor network. This function provides an overview of the @@ -4442,6 +4678,7 @@ def make_substrate_call_with_retry(): return DelegateInfo.list_from_vec_u8(result) + @networking.ensure_connected def get_delegated( self, coldkey_ss58: str, block: Optional[int] = None ) -> List[Tuple[DelegateInfo, Balance]]: @@ -4480,6 +4717,100 @@ def make_substrate_call_with_retry(encoded_coldkey_: List[int]): return DelegateInfo.delegated_list_from_vec_u8(result) + ############################ + # Child Hotkey Information # + ############################ + + def get_childkey_take( + self, hotkey: str, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Get the childkey take of a hotkey on a specific network. + Args: + - hotkey (str): The hotkey to search for. + - netuid (int): The netuid to search for. + - block (Optional[int]): Optional parameter specifying the block number. Defaults to None. + + Returns: + - Optional[int]: The value of the "ChildkeyTake" if found, or None if any error occurs. + """ + try: + childkey_take = self.query_subtensor( + name="ChildkeyTake", + block=block, + params=[hotkey, netuid], + ) + if childkey_take: + return int(childkey_take.value) + + except SubstrateRequestException as e: + print(f"Error querying ChildKeys: {e}") + return None + except Exception as e: + print(f"Unexpected error in get_children: {e}") + return None + return None + + @networking.ensure_connected + def get_children(self, hotkey, netuid) -> list[tuple[int, str]] | list[Any] | None: + """ + Get the children of a hotkey on a specific network. + Args: + hotkey (str): The hotkey to query. + netuid (int): The network ID. + Returns: + list or None: List of (proportion, child_address) tuples, or None if an error occurred. + """ + try: + children = self.substrate.query( + module="SubtensorModule", + storage_function="ChildKeys", + params=[hotkey, netuid], + ) + if children: + return format_children(children) + else: + return [] + except SubstrateRequestException as e: + print(f"Error querying ChildKeys: {e}") + return None + except Exception as e: + print(f"Unexpected error in get_children: {e}") + return None + + @networking.ensure_connected + def get_parents(self, child_hotkey, netuid): + """ + Get the parents of a child hotkey on a specific network. + Args: + child_hotkey (str): The child hotkey to query. + netuid (int): The network ID. + Returns: + list or None: List of (proportion, parent_address) tuples, or None if an error occurred. + """ + try: + parents = self.substrate.query( + module="SubtensorModule", + storage_function="ParentKeys", + params=[child_hotkey, netuid], + ) + if not parents: + print("No parents found.") + return [] + + formatted_parents = [ + format_parent(proportion, parent) + for proportion, parent in parents + if proportion != 0 + ] + return formatted_parents + except SubstrateRequestException as e: + print(f"Error querying ParentKeys: {e}") + except Exception as e: + print(f"Unexpected error in get_parents: {e}") + + return None + ##################### # Stake Information # ##################### @@ -4559,6 +4890,7 @@ def get_stake_info_for_coldkeys( return StakeInfo.list_of_tuple_from_vec_u8(bytes_result) # type: ignore + @networking.ensure_connected def get_minimum_required_stake( self, ) -> Balance: @@ -4811,6 +5143,7 @@ def neuron_for_wallet( wallet.hotkey.ss58_address, netuid=netuid, block=block ) + @networking.ensure_connected def neuron_for_uid( self, uid: Optional[int], netuid: int, block: Optional[int] = None ) -> NeuronInfo: @@ -5073,41 +5406,6 @@ def bonds( return b_map - def associated_validator_ip_info( - self, netuid: int, block: Optional[int] = None - ) -> Optional[List["IPInfo"]]: - """ - Retrieves the list of all validator IP addresses associated with a specific subnet in the Bittensor - network. This information is crucial for network communication and the identification of validator nodes. - - Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[List[IPInfo]]: A list of IPInfo objects for validator nodes in the subnet, or ``None`` if no - validators are associated. - - Validator IP information is key for establishing secure and reliable connections within the network, - facilitating consensus and validation processes critical for the network's integrity and performance. - """ - hex_bytes_result = self.query_runtime_api( - runtime_api="ValidatorIPRuntimeApi", - method="get_associated_validator_ip_info_for_subnet", - params=[netuid], # type: ignore - block=block, - ) - - if hex_bytes_result is None: - return None - - if hex_bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - else: - bytes_result = bytes.fromhex(hex_bytes_result) - - return IPInfo.list_from_vec_u8(bytes_result) # type: ignore - def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[str]: """ Retrieves the burn cost for registering a new subnet within the Bittensor network. This cost @@ -5138,6 +5436,7 @@ def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[str]: # Extrinsics # ############## + @networking.ensure_connected def _do_delegation( self, wallet: "bittensor.wallet", @@ -5189,6 +5488,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_undelegation( self, wallet: "bittensor.wallet", @@ -5243,6 +5543,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_nominate( self, wallet: "bittensor.wallet", @@ -5290,6 +5591,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_increase_take( self, wallet: "bittensor.wallet", @@ -5345,6 +5647,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_decrease_take( self, wallet: "bittensor.wallet", @@ -5404,6 +5707,7 @@ def make_substrate_call_with_retry(): # Legacy # ########## + @networking.ensure_connected def get_balance(self, address: str, block: Optional[int] = None) -> Balance: """ Retrieves the token balance of a specific address within the Bittensor network. This function queries @@ -5440,6 +5744,7 @@ def make_substrate_call_with_retry(): return Balance(1000) return Balance(result.value["data"]["free"]) + @networking.ensure_connected def get_current_block(self) -> int: """ Returns the current block number on the Bittensor blockchain. This function provides the latest block @@ -5458,6 +5763,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def get_balances(self, block: Optional[int] = None) -> Dict[str, Balance]: """ Retrieves the token balances of all accounts within the Bittensor network as of a specific blockchain block. @@ -5517,6 +5823,7 @@ def _null_neuron() -> NeuronInfo: ) # type: ignore return neuron + @networking.ensure_connected def get_block_hash(self, block_id: int) -> str: """ Retrieves the hash of a specific block on the Bittensor blockchain. The block hash is a unique diff --git a/bittensor/synapse.py b/bittensor/synapse.py index 80053f7065..f08b5bcb38 100644 --- a/bittensor/synapse.py +++ b/bittensor/synapse.py @@ -369,6 +369,7 @@ class Synapse(BaseModel): """ model_config = ConfigDict(validate_assignment=True) + _model_json_schema: ClassVar[Dict[str, Any]] def deserialize(self) -> "Synapse": """ @@ -580,11 +581,27 @@ def failed_verification(self) -> bool: """ return self.dendrite is not None and self.dendrite.status_code == 401 + @classmethod + def _get_cached_model_json_schema(cls) -> dict: + """ + Returns the JSON schema for the Synapse model. + + This method returns a cached version of the JSON schema for the Synapse model. + The schema is stored in the class variable ``_model_json_schema`` and is only + generated once to improve performance. + + Returns: + dict: The JSON schema for the Synapse model. + """ + if "_model_json_schema" not in cls.__dict__: + cls._model_json_schema = cls.model_json_schema() + return cls._model_json_schema + def get_required_fields(self): """ Get the required fields from the model's JSON schema. """ - schema = self.__class__.model_json_schema() + schema = self._get_cached_model_json_schema() return schema.get("required", []) def to_headers(self) -> dict: @@ -635,16 +652,15 @@ def to_headers(self) -> dict: # Getting the fields of the instance instance_fields = self.model_dump() + required = set(self.get_required_fields()) # Iterating over the fields of the instance for field, value in instance_fields.items(): # If the object is not optional, serializing it, encoding it, and adding it to the headers - required = self.get_required_fields() - # Skipping the field if it's already in the headers or its value is None if field in headers or value is None: continue - elif required and field in required: + elif field in required: try: # create an empty (dummy) instance of type(value) to pass pydantic validation on the axon side serialized_value = json.dumps(value.__class__.__call__()) diff --git a/bittensor/utils/axon_utils.py b/bittensor/utils/axon_utils.py new file mode 100644 index 0000000000..5912f389a4 --- /dev/null +++ b/bittensor/utils/axon_utils.py @@ -0,0 +1,38 @@ +# The MIT License (MIT) +# Copyright © 2021 Yuma Rao +# Copyright © 2022 Opentensor Foundation +# Copyright © 2023 Opentensor Technologies Inc + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + + +from typing import Optional + +from bittensor.constants import ALLOWED_DELTA, NANOSECONDS_IN_SECOND + + +def allowed_nonce_window_ns(current_time_ns: int, synapse_timeout: Optional[float]): + synapse_timeout_ns = (synapse_timeout or 0) * NANOSECONDS_IN_SECOND + allowed_window_ns = current_time_ns - ALLOWED_DELTA - synapse_timeout_ns + return allowed_window_ns + + +def calculate_diff_seconds( + current_time: int, synapse_timeout: Optional[float], synapse_nonce: int +): + synapse_timeout_ns = (synapse_timeout or 0) * NANOSECONDS_IN_SECOND + diff_seconds = (current_time - synapse_nonce) / NANOSECONDS_IN_SECOND + allowed_delta_seconds = (ALLOWED_DELTA + synapse_timeout_ns) / NANOSECONDS_IN_SECOND + return diff_seconds, allowed_delta_seconds diff --git a/bittensor/utils/formatting.py b/bittensor/utils/formatting.py index f0a22d094d..22fbe74c12 100644 --- a/bittensor/utils/formatting.py +++ b/bittensor/utils/formatting.py @@ -1,4 +1,5 @@ import math +from typing import List def get_human_readable(num, suffix="H"): @@ -34,3 +35,89 @@ def convert_blocks_to_time(blocks: int, block_time: int = 12) -> tuple[int, int, minutes = (seconds % 3600) // 60 remaining_seconds = seconds % 60 return hours, minutes, remaining_seconds + + +def float_to_u16(value: float) -> int: + # Ensure the input is within the expected range + if value is None: + return 0 + if not (0 <= value <= 1): + raise ValueError("Input value must be between 0 and 1") + + # Calculate the u16 representation + u16_max = 65535 + return int(value * u16_max) + + +def u16_to_float(value: int) -> float: + # Ensure the input is within the expected range + if value is None: + return 0.0 + if not (0 <= value <= 65535): + raise ValueError("Input value must be between 0 and 65535") + + # Calculate the float representation + u16_max = 65535 + return value / u16_max + + +def float_to_u64(value: float) -> int: + if value == 0.0: + return 0 + # Ensure the input is within the expected range + if not (0 <= value <= 1): + raise ValueError("Input value must be between 0 and 1") + + # Convert the float to a u64 value, take the floor value + return int(math.floor((value * (2**64 - 1)))) + + +def u64_to_float(value: int) -> float: + u64_max = 2**64 - 1 + # Allow for a small margin of error (e.g., 1) to account for potential rounding issues + if not (0 <= value <= u64_max + 1): + raise ValueError( + f"Input value ({value}) must be between 0 and {u64_max} (2^64 - 1)" + ) + return min(value / u64_max, 1.0) # Ensure the result is never greater than 1.0 + + +def normalize_u64_values(values: List[int]) -> List[int]: + """ + Normalize a list of u64 values so that their sum equals u64::MAX (2^64 - 1). + """ + if not values: + raise ValueError("Input list cannot be empty") + + if any(v < 0 for v in values): + raise ValueError("Input values must be non-negative") + + total = sum(values) + if total == 0: + raise ValueError("Sum of input values cannot be zero") + + u64_max = 2**64 - 1 + normalized = [int((v / total) * u64_max) for v in values] + + # Adjust values to ensure sum is exactly u64::MAX + current_sum = sum(normalized) + diff = u64_max - current_sum + + for i in range(abs(diff)): + if diff > 0: + normalized[i % len(normalized)] += 1 + else: + normalized[i % len(normalized)] = max( + 0, normalized[i % len(normalized)] - 1 + ) + + # Final check and adjustment + final_sum = sum(normalized) + if final_sum > u64_max: + normalized[-1] -= final_sum - u64_max + + assert ( + sum(normalized) == u64_max + ), f"Sum of normalized values ({sum(normalized)}) is not equal to u64::MAX ({u64_max})" + + return normalized diff --git a/bittensor/utils/networking.py b/bittensor/utils/networking.py index 4d1af585c3..f4b729fe97 100644 --- a/bittensor/utils/networking.py +++ b/bittensor/utils/networking.py @@ -19,15 +19,17 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -# Standard Lib +import json import os +import socket import urllib -import json -import netaddr +from functools import wraps -# 3rd party +import netaddr import requests +from bittensor.btlogging import logging + def int_to_ip(int_val: int) -> str: r"""Maps an integer to a unique ip-string @@ -171,3 +173,26 @@ def get_formatted_ws_endpoint_url(endpoint_url: str) -> str: endpoint_url = "ws://{}".format(endpoint_url) return endpoint_url + + +def ensure_connected(func): + """Decorator ensuring the function executes with an active substrate connection.""" + + @wraps(func) + def wrapper(self, *args, **kwargs): + # Check the socket state before method execution + if ( + # connection was closed correctly + self.substrate.websocket.sock is None + # connection has a broken pipe + or self.substrate.websocket.sock.getsockopt( + socket.SOL_SOCKET, socket.SO_ERROR + ) + != 0 + ): + logging.info("Reconnection substrate...") + self._get_substrate() + # Execute the method if the connection is active or after reconnecting + return func(self, *args, **kwargs) + + return wrapper diff --git a/bittensor/utils/registration.py b/bittensor/utils/registration.py index 6c504b05f1..d606929dcb 100644 --- a/bittensor/utils/registration.py +++ b/bittensor/utils/registration.py @@ -3,6 +3,7 @@ import hashlib import math import multiprocessing +import multiprocessing.queues # this must be imported separately, or could break type annotations import os import random import time @@ -73,7 +74,7 @@ def _get_real_torch(): def log_no_torch_error(): - bittensor.btlogging.error( + bittensor.logging.error( "This command requires torch. You can install torch for bittensor" ' with `pip install bittensor[torch]` or `pip install ".[torch]"`' " if installing from source, and then run the command with USE_TORCH=1 {command}" @@ -561,7 +562,7 @@ def _solve_for_difficulty_fast( while still updating the block information after a different number of nonces, to increase the transparency of the process while still keeping the speed. """ - if num_processes == None: + if num_processes is None: # get the number of allowed processes for this process num_processes = min(1, get_cpu_count()) @@ -779,7 +780,7 @@ def __init__(self, force: bool = False): def __enter__(self): self._old_start_method = multiprocessing.get_start_method(allow_none=True) - if self._old_start_method == None: + if self._old_start_method is None: self._old_start_method = "spawn" # default to spawn multiprocessing.set_start_method("spawn", force=self._force) @@ -1082,11 +1083,15 @@ def _solve_for_difficulty_fast_cuda( def _terminate_workers_and_wait_for_exit( - workers: List[multiprocessing.Process], + workers: List[Union[multiprocessing.Process, multiprocessing.queues.Queue]], ) -> None: for worker in workers: - worker.terminate() - worker.join() + if isinstance(worker, multiprocessing.queues.Queue): + worker.join_thread() + else: + worker.terminate() + worker.join() + worker.close() def create_pow( diff --git a/bittensor/utils/subtensor.py b/bittensor/utils/subtensor.py index 484184e77f..0df9c64d82 100644 --- a/bittensor/utils/subtensor.py +++ b/bittensor/utils/subtensor.py @@ -22,7 +22,7 @@ import json import logging import os -from typing import Dict, Optional, Union, Any +from typing import Dict, Optional, Union, Any, List, Tuple from substrateinterface.base import SubstrateInterface @@ -137,3 +137,37 @@ def get_subtensor_errors( return subtensor_errors_map else: return cached_errors_map.get("errors", {}) + + +def format_parent(proportion, parent) -> Tuple[str, str]: + """ + Formats raw parent data into a list of tuples. + Args: + parent: The raw parent data. + proportion: proportion of parent data. + Returns: + list: List of (proportion, child_address) tuples. + """ + int_proportion = ( + proportion.value if hasattr(proportion, "value") else int(proportion) + ) + return int_proportion, parent.value + + +def format_children(children) -> List[Tuple[int, str]]: + """ + Formats raw children data into a list of tuples. + Args: + children: The raw children data. + Returns: + list: List of (proportion, child_address) tuples. + """ + formatted_children = [] + for proportion, child in children: + # Convert U64 to int + int_proportion = ( + proportion.value if hasattr(proportion, "value") else int(proportion) + ) + if int_proportion > 0: + formatted_children.append((int_proportion, child.value)) + return formatted_children diff --git a/bittensor/wallet.py b/bittensor/wallet.py index be6aa08c93..28da5d8654 100644 --- a/bittensor/wallet.py +++ b/bittensor/wallet.py @@ -17,13 +17,15 @@ # 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 os -import copy import argparse -import bittensor -from termcolor import colored +import copy +import os +from typing import Dict, Optional, Tuple, Union, overload + from substrateinterface import Keypair -from typing import Optional, Union, Tuple, Dict, overload +from termcolor import colored + +import bittensor from bittensor.utils import is_valid_bittensor_address_or_public_key @@ -138,18 +140,11 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): parser (argparse.ArgumentParser): Argument parser object. prefix (str): Argument prefix. """ - prefix_str = "" if prefix == None else prefix + "." + prefix_str = "" if prefix is None else prefix + "." try: default_name = os.getenv("BT_WALLET_NAME") or "default" default_hotkey = os.getenv("BT_WALLET_NAME") or "default" default_path = os.getenv("BT_WALLET_PATH") or "~/.bittensor/wallets/" - parser.add_argument( - "--no_prompt", - dest="no_prompt", - action="store_true", - help="""Set true to avoid prompting the user.""", - default=False, - ) parser.add_argument( "--" + prefix_str + "wallet.name", required=False, @@ -839,6 +834,8 @@ def regenerate_hotkey( if mnemonic is not None: if isinstance(mnemonic, str): mnemonic = mnemonic.split() + elif isinstance(mnemonic, list) and len(mnemonic) == 1: + mnemonic = mnemonic[0].split() if len(mnemonic) not in [12, 15, 18, 21, 24]: raise ValueError( "Mnemonic has invalid size. This should be 12,15,18,21 or 24 words" diff --git a/requirements/dev.txt b/requirements/dev.txt index 6cc94e2679..a9e1a1bc4e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,4 +1,4 @@ -black==23.7.0 +black==24.3.0 pytest==7.2.0 pytest-asyncio==0.23.7 pytest-mock==3.12.0 diff --git a/requirements/prod.txt b/requirements/prod.txt index e02456f998..8bb6acd0f4 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,8 +1,8 @@ aiohttp~=3.9 -ansible~=6.7 +ansible~=8.5.0 ansible_vault~=2.1 backoff -certifi~=2024.2.2 +certifi~=2024.7.4 colorama~=0.4.6 cryptography~=42.0.5 ddt~=1.6.0 @@ -11,7 +11,7 @@ fuzzywuzzy>=0.18.0 fastapi~=0.110.1 munch~=2.5.0 netaddr -numpy +numpy~=1.26 msgpack-numpy-opentensor~=0.5.0 nest_asyncio packaging @@ -19,16 +19,17 @@ pycryptodome>=3.18.0,<4.0.0 pyyaml password_strength pydantic>=2.3, <3 -PyNaCl>=1.3.0,<=1.5.0 +PyNaCl~=1.3 python-Levenshtein -python-statemachine~=2.1.2 +python-statemachine~=2.1 retry requests rich scalecodec==1.2.11 +setuptools~=70.0.0 shtab~=1.6.5 substrate-interface~=1.7.9 termcolor tqdm -uvicorn<=0.30 +uvicorn wheel diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 7afb6b448f..6f648be130 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -3,6 +3,7 @@ import re import shlex import signal +import socket import subprocess import time @@ -13,67 +14,91 @@ clone_or_update_templates, install_templates, uninstall_templates, - template_path, ) logging.basicConfig(level=logging.INFO) +# Function to check if the process is running by port +def is_chain_running(port): + """Check if a node is running on the given port.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + # Attempt to connect to the given port on localhost + s.connect(("127.0.0.1", port)) + return True + except (ConnectionRefusedError, OSError): + # If the connection is refused or there's an OS error, the node is not running + return False + + # Fixture for setting up and tearing down a localnet.sh chain between tests @pytest.fixture(scope="function") def local_chain(request): param = request.param if hasattr(request, "param") else None - # Get the environment variable for the script path script_path = os.getenv("LOCALNET_SH_PATH") if not script_path: - # Skip the test if the localhost.sh path is not set logging.warning("LOCALNET_SH_PATH env variable is not set, e2e test skipped.") pytest.skip("LOCALNET_SH_PATH environment variable is not set.") - # Check if param is None, and handle it accordingly - args = "" if param is None else f"fast_blocks={param}" - - # compile commands to send to process - cmds = shlex.split(f"{script_path} {args}") - # Start new node process - process = subprocess.Popen( - cmds, stdout=subprocess.PIPE, text=True, preexec_fn=os.setsid - ) - - # Pattern match indicates node is compiled and ready - pattern = re.compile(r"Successfully ran block step\.") + # Determine the port to check based on `param` + port = 9945 # Default port if `param` is None - # install neuron templates - logging.info("downloading and installing neuron templates from github") + # Always perform template installation + logging.info("Downloading and installing neuron templates from GitHub") templates_dir = clone_or_update_templates() install_templates(templates_dir) - def wait_for_node_start(process, pattern): - for line in process.stdout: - print(line.strip()) - if pattern.search(line): - print("Node started!") - break - - wait_for_node_start(process, pattern) + already_running = False + if is_chain_running(port): + already_running = True + logging.info(f"Chain already running on port {port}, skipping start.") + else: + logging.info(f"Starting new chain on port {port}...") + # compile commands to send to process + cmds = shlex.split(f"{script_path} {param}") + # Start new node process + process = subprocess.Popen( + cmds, stdout=subprocess.PIPE, text=True, preexec_fn=os.setsid + ) + + # Wait for the node to start using the existing pattern match + pattern = re.compile(r"Imported #1") + timestamp = int(time.time()) + + def wait_for_node_start(process, pattern): + for line in process.stdout: + print(line.strip()) + if int(time.time()) - timestamp > 20 * 60: + pytest.fail("Subtensor not started in time") + if pattern.search(line): + print("Node started!") + break + + wait_for_node_start(process, pattern) + + # Continue with installing templates + logging.info("Downloading and installing neuron templates from GitHub") + templates_dir = clone_or_update_templates() + install_templates(templates_dir) - # Run the test, passing in substrate interface - yield SubstrateInterface(url="ws://127.0.0.1:9945") + # Run the test, passing in the substrate interface + yield SubstrateInterface(url=f"ws://127.0.0.1:{port}") - # Terminate the process group (includes all child processes) - os.killpg(os.getpgid(process.pid), signal.SIGTERM) + if not already_running: + # Terminate the process group (includes all child processes) + os.killpg(os.getpgid(process.pid), signal.SIGTERM) - # Give some time for the process to terminate - time.sleep(1) + # Give some time for the process to terminate + time.sleep(1) - # If the process is not terminated, send SIGKILL - if process.poll() is None: - os.killpg(os.getpgid(process.pid), signal.SIGKILL) + # If the process is not terminated, send SIGKILL + if process.poll() is None: + os.killpg(os.getpgid(process.pid), signal.SIGKILL) - # Ensure the process has terminated - process.wait() + # Ensure the process has terminated + process.wait() - # uninstall templates - logging.info("uninstalling neuron templates") - uninstall_templates(template_path) + logging.info("Uninstalling neuron templates") + uninstall_templates(templates_dir) diff --git a/tests/e2e_tests/multistep/test_axon.py b/tests/e2e_tests/multistep/test_axon.py index 35514a680a..ebe95587ea 100644 --- a/tests/e2e_tests/multistep/test_axon.py +++ b/tests/e2e_tests/multistep/test_axon.py @@ -4,15 +4,16 @@ import pytest import bittensor -from bittensor.utils import networking +from bittensor import logging from bittensor.commands import ( RegisterCommand, RegisterSubnetworkCommand, ) +from bittensor.utils import networking from tests.e2e_tests.utils import ( setup_wallet, template_path, - repo_name, + templates_repo, ) """ @@ -31,12 +32,16 @@ @pytest.mark.asyncio async def test_axon(local_chain): + logging.info("Testing test_axon") + netuid = 1 # Register root as Alice alice_keypair, exec_command, wallet = setup_wallet("//Alice") exec_command(RegisterSubnetworkCommand, ["s", "create"]) - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + # Verify subnet created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [netuid] + ).serialize(), "Subnet wasn't created successfully" # Register a neuron to the subnet exec_command( @@ -45,16 +50,16 @@ async def test_axon(local_chain): "s", "register", "--netuid", - "1", + str(netuid), ], ) - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") # validate one miner with ip of none old_axon = metagraph.axons[0] - assert len(metagraph.axons) == 1 + assert len(metagraph.axons) == 1, "Expected 1 axon, but got len(metagraph.axons)" assert old_axon.hotkey == alice_keypair.ss58_address assert old_axon.coldkey == alice_keypair.ss58_address assert old_axon.ip == "0.0.0.0" @@ -66,10 +71,10 @@ async def test_axon(local_chain): cmd = " ".join( [ f"{sys.executable}", - f'"{template_path}{repo_name}/neurons/miner.py"', + f'"{template_path}{templates_repo}/neurons/miner.py"', "--no_prompt", "--netuid", - "1", + str(netuid), "--subtensor.network", "local", "--subtensor.chain_endpoint", @@ -88,13 +93,13 @@ async def test_axon(local_chain): stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) - + logging.info("Neuron Alice is now mining") await asyncio.sleep( 5 ) # wait for 5 seconds for the metagraph to refresh with latest data # refresh metagraph - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") updated_axon = metagraph.axons[0] external_ip = networking.get_external_ip() @@ -104,3 +109,4 @@ async def test_axon(local_chain): assert updated_axon.port == 8091 assert updated_axon.hotkey == alice_keypair.ss58_address assert updated_axon.coldkey == alice_keypair.ss58_address + logging.info("Passed test_axon") diff --git a/tests/e2e_tests/multistep/test_dendrite.py b/tests/e2e_tests/multistep/test_dendrite.py index deb37eb133..c68ccda818 100644 --- a/tests/e2e_tests/multistep/test_dendrite.py +++ b/tests/e2e_tests/multistep/test_dendrite.py @@ -1,27 +1,24 @@ import asyncio -import logging import sys import pytest import bittensor +from bittensor import logging from bittensor.commands import ( RegisterCommand, RegisterSubnetworkCommand, - StakeCommand, RootRegisterCommand, RootSetBoostCommand, + StakeCommand, ) from tests.e2e_tests.utils import ( setup_wallet, template_path, - repo_name, + templates_repo, wait_epoch, ) - -logging.basicConfig(level=logging.INFO) - """ Test the dendrites mechanism. @@ -35,12 +32,16 @@ @pytest.mark.asyncio async def test_dendrite(local_chain): + logging.info("Testing test_dendrite") + netuid = 1 # Register root as Alice - the subnet owner alice_keypair, exec_command, wallet = setup_wallet("//Alice") exec_command(RegisterSubnetworkCommand, ["s", "create"]) - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + # Verify subnet created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [netuid] + ).serialize(), "Subnet wasn't created successfully" bob_keypair, exec_command, wallet_path = setup_wallet("//Bob") @@ -51,15 +52,15 @@ async def test_dendrite(local_chain): "s", "register", "--netuid", - "1", + str(netuid), ], ) - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") subtensor = bittensor.subtensor(network="ws://localhost:9945") # assert one neuron is Bob - assert len(subtensor.neurons(netuid=1)) == 1 + assert len(subtensor.neurons(netuid=netuid)) == 1 neuron = metagraph.neurons[0] assert neuron.hotkey == bob_keypair.ss58_address assert neuron.coldkey == bob_keypair.ss58_address @@ -79,10 +80,12 @@ async def test_dendrite(local_chain): ) # refresh metagraph - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") neuron = metagraph.neurons[0] # assert stake is 10000 - assert neuron.stake.tao == 10_000.0 + assert ( + neuron.stake.tao == 10_000.0 + ), f"Expected 10_000.0 staked TAO, but got {neuron.stake.tao}" # assert neuron is not validator assert neuron.active is True @@ -94,10 +97,10 @@ async def test_dendrite(local_chain): cmd = " ".join( [ f"{sys.executable}", - f'"{template_path}{repo_name}/neurons/validator.py"', + f'"{template_path}{templates_repo}/neurons/validator.py"', "--no_prompt", "--netuid", - "1", + str(netuid), "--subtensor.network", "local", "--subtensor.chain_endpoint", @@ -117,7 +120,7 @@ async def test_dendrite(local_chain): stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) - + logging.info("Neuron Alice is now validating") await asyncio.sleep( 5 ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data @@ -129,7 +132,7 @@ async def test_dendrite(local_chain): "root", "register", "--netuid", - "1", + str(netuid), ], ) @@ -139,16 +142,16 @@ async def test_dendrite(local_chain): "root", "boost", "--netuid", - "1", + str(netuid), "--increase", "1", ], ) - # get current block, wait until 360 blocks pass (subnet tempo) - wait_epoch(360, subtensor) + # get current block, wait until next epoch + await wait_epoch(subtensor, netuid=netuid) # refresh metagraph - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") # refresh validator neuron neuron = metagraph.neurons[0] @@ -158,3 +161,4 @@ async def test_dendrite(local_chain): assert neuron.validator_permit is True assert neuron.hotkey == bob_keypair.ss58_address assert neuron.coldkey == bob_keypair.ss58_address + logging.info("Passed test_dendrite") diff --git a/tests/e2e_tests/multistep/test_emissions.py b/tests/e2e_tests/multistep/test_emissions.py new file mode 100644 index 0000000000..a05ff478a4 --- /dev/null +++ b/tests/e2e_tests/multistep/test_emissions.py @@ -0,0 +1,283 @@ +import asyncio +import sys + +import pytest + +import bittensor +from bittensor import logging +from bittensor.commands import ( + RegisterCommand, + RegisterSubnetworkCommand, + RootRegisterCommand, + RootSetBoostCommand, + RootSetWeightsCommand, + SetTakeCommand, + StakeCommand, + SubnetSudoCommand, +) +from tests.e2e_tests.utils import ( + setup_wallet, + template_path, + templates_repo, + wait_epoch, +) + +""" +Test the emissions mechanism. + +Verify that for the miner: +* trust +* rank +* consensus +* incentive +* emission +are updated with proper values after an epoch has passed. + +For the validator verify that: +* validator_permit +* validator_trust +* dividends +* stake +are updated with proper values after an epoch has passed. + +""" + + +@pytest.mark.asyncio +@pytest.mark.skip +async def test_emissions(local_chain): + logging.info("Testing test_emissions") + netuid = 1 + # Register root as Alice - the subnet owner and validator + alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") + alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) + # Verify subnet created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [netuid] + ).serialize(), "Subnet wasn't created successfully" + + # Register Bob as miner + bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") + + # Register Alice as neuron to the subnet + alice_exec_command( + RegisterCommand, + [ + "s", + "register", + "--netuid", + str(netuid), + ], + ) + + # Register Bob as neuron to the subnet + bob_exec_command( + RegisterCommand, + [ + "s", + "register", + "--netuid", + str(netuid), + ], + ) + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + # assert two neurons are in network + assert len(subtensor.neurons(netuid=netuid)) == 2 + + # Alice to stake to become to top neuron after the first epoch + alice_exec_command( + StakeCommand, + [ + "stake", + "add", + "--amount", + "10000", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + # register Alice as validator + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/validator.py"', + "--no_prompt", + "--netuid", + str(netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + alice_wallet.path, + "--wallet.name", + alice_wallet.name, + "--wallet.hotkey", + "default", + "--logging.trace", + ] + ) + # run validator in the background + + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Neuron Alice is now validating") + await asyncio.sleep(5) + + # register validator with root network + alice_exec_command( + RootRegisterCommand, + [ + "root", + "register", + "--netuid", + str(netuid), + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + await wait_epoch(subtensor, netuid=netuid) + + alice_exec_command( + RootSetBoostCommand, + [ + "root", + "boost", + "--netuid", + str(netuid), + "--increase", + "1000", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + # register Bob as miner + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/miner.py"', + "--no_prompt", + "--netuid", + str(netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + bob_wallet.path, + "--wallet.name", + bob_wallet.name, + "--wallet.hotkey", + "default", + "--logging.trace", + ] + ) + + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Neuron Bob is now mining") + await wait_epoch(subtensor) + + logging.warning("Setting root set weights") + alice_exec_command( + RootSetWeightsCommand, + [ + "root", + "weights", + "--netuid", + str(netuid), + "--weights", + "0.3", + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + # Set delegate take for Alice + alice_exec_command(SetTakeCommand, ["r", "set_take", "--take", "0.15"]) + + # Lower the rate limit + alice_exec_command( + SubnetSudoCommand, + [ + "sudo", + "set", + "hyperparameters", + "--netuid", + str(netuid), + "--wallet.name", + alice_wallet.name, + "--param", + "weights_rate_limit", + "--value", + "1", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + # wait epoch until for emissions to get distributed + await wait_epoch(subtensor) + + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data + + # refresh metagraph + subtensor = bittensor.subtensor(network="ws://localhost:9945") + + # get current emissions and validate that Alice has gotten tao + weights = [(0, [(0, 65535), (1, 65535)])] + assert ( + subtensor.weights(netuid=netuid) == weights + ), "Weights set vs weights in subtensor don't match" + + neurons = subtensor.neurons(netuid=netuid) + bob = neurons[1] + alice = neurons[0] + + assert bob.emission > 0 + assert bob.consensus == 1 + assert bob.incentive == 1 + assert bob.rank == 1 + assert bob.trust == 1 + + assert alice.emission > 0 + assert alice.bonds == [(1, 65535)] + assert alice.dividends == 1 + assert alice.stake.tao > 10000 # assert an increase in stake + assert alice.validator_permit is True + assert alice.validator_trust == 1 + + assert alice.weights == [(0, 65535), (1, 65535)] + + assert ( + subtensor.get_emission_value_by_subnet(netuid=netuid) > 0 + ), ( + "Emissions are not greated than 0" + ) # emission on this subnet is strictly greater than 0 + logging.info("Passed test_emissions") diff --git a/tests/e2e_tests/multistep/test_incentive.py b/tests/e2e_tests/multistep/test_incentive.py index d1b4634653..c2f6baa664 100644 --- a/tests/e2e_tests/multistep/test_incentive.py +++ b/tests/e2e_tests/multistep/test_incentive.py @@ -1,26 +1,24 @@ import asyncio -import logging import sys import pytest import bittensor +from bittensor import logging from bittensor.commands import ( RegisterCommand, RegisterSubnetworkCommand, - StakeCommand, RootRegisterCommand, RootSetBoostCommand, + StakeCommand, ) from tests.e2e_tests.utils import ( setup_wallet, template_path, - repo_name, + templates_repo, wait_epoch, ) -logging.basicConfig(level=logging.INFO) - """ Test the incentive mechanism. @@ -43,11 +41,15 @@ @pytest.mark.asyncio async def test_incentive(local_chain): + logging.info("Testing test_incentive") + netuid = 1 # Register root as Alice - the subnet owner and validator alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + # Verify subnet created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [netuid] + ).serialize(), "Subnet wasn't created successfully" # Register Bob as miner bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") @@ -59,7 +61,7 @@ async def test_incentive(local_chain): "s", "register", "--netuid", - "1", + str(netuid), ], ) @@ -70,13 +72,15 @@ async def test_incentive(local_chain): "s", "register", "--netuid", - "1", + str(netuid), ], ) subtensor = bittensor.subtensor(network="ws://localhost:9945") # assert two neurons are in network - assert len(subtensor.neurons(netuid=1)) == 2 + assert ( + len(subtensor.neurons(netuid=netuid)) == 2 + ), "Alice & Bob not registered in the subnet" # Alice to stake to become to top neuron after the first epoch alice_exec_command( @@ -93,10 +97,10 @@ async def test_incentive(local_chain): cmd = " ".join( [ f"{sys.executable}", - f'"{template_path}{repo_name}/neurons/miner.py"', + f'"{template_path}{templates_repo}/neurons/miner.py"', "--no_prompt", "--netuid", - "1", + str(netuid), "--subtensor.network", "local", "--subtensor.chain_endpoint", @@ -116,7 +120,7 @@ async def test_incentive(local_chain): stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) - + logging.info("Neuron Bob is now mining") await asyncio.sleep( 5 ) # wait for 5 seconds for the metagraph to refresh with latest data @@ -125,10 +129,10 @@ async def test_incentive(local_chain): cmd = " ".join( [ f"{sys.executable}", - f'"{template_path}{repo_name}/neurons/validator.py"', + f'"{template_path}{templates_repo}/neurons/validator.py"', "--no_prompt", "--netuid", - "1", + str(netuid), "--subtensor.network", "local", "--subtensor.chain_endpoint", @@ -149,7 +153,7 @@ async def test_incentive(local_chain): stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) - + logging.info("Neuron Alice is now validating") await asyncio.sleep( 5 ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data @@ -161,7 +165,7 @@ async def test_incentive(local_chain): "root", "register", "--netuid", - "1", + str(netuid), "--wallet.name", "default", "--wallet.hotkey", @@ -177,7 +181,7 @@ async def test_incentive(local_chain): "root", "boost", "--netuid", - "1", + str(netuid), "--increase", "100", "--wallet.name", @@ -190,7 +194,7 @@ async def test_incentive(local_chain): ) # get latest metagraph - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") # get current emissions bob_neuron = metagraph.neurons[1] @@ -205,8 +209,8 @@ async def test_incentive(local_chain): assert alice_neuron.stake.tao == 10_000.0 assert alice_neuron.validator_trust == 0 - # wait until 360 blocks pass (subnet tempo) - wait_epoch(360, subtensor) + # wait until next epoch + await wait_epoch(subtensor) # for some reason the weights do not get set through the template. Set weight manually. alice_wallet = bittensor.wallet() @@ -215,17 +219,18 @@ async def test_incentive(local_chain): wallet=alice_wallet, uids=[1], vals=[65535], - netuid=1, + netuid=netuid, version_key=0, wait_for_inclusion=True, wait_for_finalization=True, ) + logging.info("Alice neuron set weights successfully") # wait epoch until weight go into effect - wait_epoch(360, subtensor) + await wait_epoch(subtensor) # refresh metagraph - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") # get current emissions and validate that Alice has gotten tao bob_neuron = metagraph.neurons[1] @@ -239,3 +244,4 @@ async def test_incentive(local_chain): assert alice_neuron.dividends == 1 assert alice_neuron.stake.tao == 10_000.0 assert alice_neuron.validator_trust == 1 + logging.info("Passed test_incentive") diff --git a/tests/e2e_tests/multistep/test_last_tx_block.py b/tests/e2e_tests/multistep/test_last_tx_block.py deleted file mode 100644 index 5bc4759212..0000000000 --- a/tests/e2e_tests/multistep/test_last_tx_block.py +++ /dev/null @@ -1,51 +0,0 @@ -from bittensor.commands.root import RootRegisterCommand -from bittensor.commands.delegates import NominateCommand -from bittensor.commands.network import RegisterSubnetworkCommand -from bittensor.commands.register import RegisterCommand -from ..utils import setup_wallet - - -# Automated testing for take related tests described in -# https://discord.com/channels/799672011265015819/1176889736636407808/1236057424134144152 -def test_takes(local_chain): - # Register root as Alice - keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RootRegisterCommand, ["root", "register"]) - - # Create subnet 1 and verify created successfully - assert not (local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize()) - - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]) - - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - # Register and nominate Bob - keypair, exec_command, wallet = setup_wallet("//Bob") - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlock", [keypair.ss58_address] - ).serialize() - == 0 - ) - - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlockDelegateTake", [keypair.ss58_address] - ).serialize() - == 0 - ) - exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) - exec_command(NominateCommand, ["root", "nominate"]) - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlock", [keypair.ss58_address] - ).serialize() - > 0 - ) - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlockDelegateTake", [keypair.ss58_address] - ).serialize() - > 0 - ) diff --git a/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py b/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py index cefb150f70..ddad1dfcc1 100644 --- a/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py +++ b/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py @@ -1,23 +1,27 @@ -from bittensor.commands.delegates import SetTakeCommand, NominateCommand +from bittensor import logging +from bittensor.commands.delegates import NominateCommand, SetTakeCommand from bittensor.commands.network import RegisterSubnetworkCommand from bittensor.commands.register import RegisterCommand from bittensor.commands.root import RootRegisterCommand - from tests.e2e_tests.utils import setup_wallet def test_set_delegate_increase_take(local_chain): + logging.info("Testing test_set_delegate_increase_take") # Register root as Alice keypair, exec_command, wallet = setup_wallet("//Alice") exec_command(RootRegisterCommand, ["root", "register"]) # Create subnet 1 and verify created successfully - assert not (local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize()) + assert not ( + local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + ), "Subnet is already registered" exec_command(RegisterSubnetworkCommand, ["s", "create"]) - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]) - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [1] + ).serialize(), "Subnet wasn't registered" # Register and nominate Bob keypair, exec_command, wallet = setup_wallet("//Bob") @@ -53,4 +57,5 @@ def test_set_delegate_increase_take(local_chain): exec_command(SetTakeCommand, ["r", "set_take", "--take", "0.15"]) assert local_chain.query( "SubtensorModule", "Delegates", [keypair.ss58_address] - ).value == int(0.15 * 65535) + ).value == int(0.15 * 65535), "Take value set incorrectly" + logging.info("Passed test_set_delegate_increase_take") diff --git a/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py b/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py index cf2522b788..434184cb8d 100644 --- a/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py +++ b/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py @@ -1,8 +1,9 @@ import bittensor +from bittensor import logging from bittensor.commands import ( RegisterCommand, - StakeCommand, RegisterSubnetworkCommand, + StakeCommand, SubnetSudoCommand, ) from tests.e2e_tests.utils import setup_wallet @@ -19,6 +20,7 @@ def test_liquid_alpha_enabled(local_chain, capsys): + logging.info("Testing test_liquid_alpha_enabled") # Register root as Alice keypair, exec_command, wallet = setup_wallet("//Alice") exec_command(RegisterSubnetworkCommand, ["s", "create"]) @@ -273,3 +275,4 @@ def test_liquid_alpha_enabled(local_chain, capsys): "❌ Failed: Subtensor returned `LiquidAlphaDisabled (Module)` error. This means: \n`Attempting to set alpha high/low while disabled`" in output ) + logging.info("Passed test_liquid_alpha_enabled") diff --git a/tests/e2e_tests/subcommands/register/__init__.py b/tests/e2e_tests/subcommands/register/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e_tests/subcommands/register/test_swap_hotkey.py b/tests/e2e_tests/subcommands/register/test_swap_hotkey.py new file mode 100644 index 0000000000..798aafc3f1 --- /dev/null +++ b/tests/e2e_tests/subcommands/register/test_swap_hotkey.py @@ -0,0 +1,566 @@ +import asyncio +import sys +import uuid + +import pytest + +import bittensor +from bittensor import logging +from bittensor.commands import ( + ListCommand, + NewHotkeyCommand, + RegisterCommand, + RegisterSubnetworkCommand, + RootRegisterCommand, + StakeCommand, + SwapHotkeyCommand, +) +from tests.e2e_tests.utils import ( + setup_wallet, + template_path, + templates_repo, + wait_interval, +) + +""" +Test the swap_hotkey mechanism. + +Verify that: +* Alice - neuron is registered on network as a validator +* Bob - neuron is registered on network as a miner +* Swap hotkey of Alice via BTCLI +* verify that the hotkey is swapped +* verify that stake hotkey, delegates hotkey, UIDS and prometheus hotkey is swapped +""" + + +@pytest.mark.asyncio +async def test_swap_hotkey_validator_owner(local_chain): + logging.info("Testing swap hotkey of validator_owner") + # Register root as Alice - the subnet owner and validator + alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") + alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) + # Verify subnet 1 created successfully + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + + # Register Bob as miner + bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") + + alice_old_hotkey_address = alice_wallet.hotkey.ss58_address + + # Register Alice as neuron to the subnet + alice_exec_command( + RegisterCommand, + [ + "s", + "register", + "--netuid", + "1", + ], + ) + + # Register Bob as neuron to the subnet + bob_exec_command( + RegisterCommand, + [ + "s", + "register", + "--netuid", + "1", + ], + ) + + logging.info("Alice and bob registered to the subnet") + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + # assert two neurons are in network + assert ( + len(subtensor.neurons(netuid=1)) == 2 + ), "Alice and Bob neurons not found in the network" + + # register Bob as miner + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/miner.py"', + "--no_prompt", + "--netuid", + "1", + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + bob_wallet.path, + "--wallet.name", + bob_wallet.name, + "--wallet.hotkey", + "default", + "--logging.trace", + ] + ) + + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Bob neuron is now mining") + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph to refresh with latest data + + # register Alice as validator + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/validator.py"', + "--no_prompt", + "--netuid", + "1", + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + alice_wallet.path, + "--wallet.name", + alice_wallet.name, + "--wallet.hotkey", + "default", + "--logging.trace", + ] + ) + # run validator in the background + + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Alice neuron is now validating") + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data + + # register validator with root network + alice_exec_command( + RootRegisterCommand, + [ + "root", + "register", + "--netuid", + "1", + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + ], + ) + + # Alice to stake to become to top neuron after the first epoch + alice_exec_command( + StakeCommand, + [ + "stake", + "add", + "--amount", + "10000", + ], + ) + + # get latest metagraph + metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") + subtensor = bittensor.subtensor(network="ws://localhost:9945") + + # assert alice has old hotkey + alice_neuron = metagraph.neurons[0] + + # get current number of hotkeys + wallet_tree = alice_exec_command(ListCommand, ["w", "list"], "get_tree") + num_hotkeys = len(wallet_tree.children[0].children) + + assert ( + alice_neuron.coldkey == "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + ), "Alice coldkey not as expected" + assert ( + alice_neuron.hotkey == alice_old_hotkey_address + ), "Alice hotkey not as expected" + assert ( + alice_neuron.stake_dict["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"].tao + == 10000.0 + ), "Alice tao not as expected" + assert alice_neuron.hotkey == alice_neuron.coldkey, "Coldkey and hotkey don't match" + assert ( + alice_neuron.hotkey == subtensor.get_all_subnets_info()[1].owner_ss58 + ), "Hotkey doesn't match owner address" + assert alice_neuron.coldkey == subtensor.get_hotkey_owner( + alice_old_hotkey_address + ), "Coldkey doesn't match hotkey owner" + assert ( + subtensor.is_hotkey_delegate(alice_neuron.hotkey) is True + ), "Alice is not a delegate" + assert ( + subtensor.is_hotkey_registered_on_subnet( + hotkey_ss58=alice_neuron.hotkey, netuid=1 + ) + is True + ), "Alice hotkey not registered on subnet" + assert ( + subtensor.get_uid_for_hotkey_on_subnet( + hotkey_ss58=alice_neuron.hotkey, netuid=1 + ) + == alice_neuron.uid + ), "Alice hotkey not regisred on netuid" + if num_hotkeys > 1: + logging.info(f"You have {num_hotkeys} hotkeys for Alice.") + + # generate new guid name for hotkey + new_hotkey_name = str(uuid.uuid4()) + + # create a new hotkey + alice_exec_command( + NewHotkeyCommand, + [ + "w", + "new_hotkey", + "--wallet.name", + alice_wallet.name, + "--wallet.hotkey", + new_hotkey_name, + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + logging.info("New hotkey is created") + # wait rate limit, until we are allowed to change hotkeys + rate_limit = subtensor.tx_rate_limit() + curr_block = subtensor.get_current_block() + await wait_interval(rate_limit + curr_block + 1, subtensor) + + # swap hotkey + alice_exec_command( + SwapHotkeyCommand, + [ + "w", + "swap_hotkey", + "--wallet.name", + alice_wallet.name, + "--wallet.hotkey", + alice_wallet.hotkey_str, + "--wallet.hotkey_b", + new_hotkey_name, + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + # get latest metagraph + metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") + subtensor = bittensor.subtensor(network="ws://localhost:9945") + + # assert Alice has new hotkey + alice_neuron = metagraph.neurons[0] + wallet_tree = alice_exec_command(ListCommand, ["w", "list"], "get_tree") + new_num_hotkeys = len(wallet_tree.children[0].children) + + assert ( + alice_neuron.coldkey == "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + ), "Coldkey was changed" # cold key didnt change + assert ( + alice_neuron.hotkey != alice_old_hotkey_address + ), "Hotkey is not updated w.r.t old_hotkey_address" + assert ( + alice_neuron.hotkey != alice_neuron.coldkey + ), "Hotkey is not updated w.r.t coldkey" + assert ( + alice_neuron.coldkey == subtensor.get_all_subnets_info()[1].owner_ss58 + ) # new hotkey address is subnet owner + assert alice_neuron.coldkey != subtensor.get_hotkey_owner( + alice_old_hotkey_address + ) # old key is NOT owner + assert alice_neuron.coldkey == subtensor.get_hotkey_owner( + alice_neuron.hotkey + ) # new key is owner + assert ( + subtensor.is_hotkey_delegate(alice_neuron.hotkey) is True + ) # new key is delegate + assert ( # new key is registered on subnet + subtensor.is_hotkey_registered_on_subnet( + hotkey_ss58=alice_neuron.hotkey, netuid=1 + ) + is True + ) + assert ( # old key is NOT registered on subnet + subtensor.is_hotkey_registered_on_subnet( + hotkey_ss58=alice_old_hotkey_address, netuid=1 + ) + is False + ) + assert ( # uid is unchanged + subtensor.get_uid_for_hotkey_on_subnet( + hotkey_ss58=alice_neuron.hotkey, netuid=1 + ) + == alice_neuron.uid + ) + assert new_num_hotkeys == num_hotkeys + 1 + logging.info("Finished test_swap_hotkey_validator_owner") + + +""" +Test the swap_hotkey mechanism. + +Verify that: +* Alice - neuron is registered on network as a validator +* Bob - neuron is registered on network as a miner +* Swap hotkey of Bob via BTCLI +* verify that the hotkey is swapped +* verify that stake hotkey, delegates hotkey, UIDS and prometheus hotkey is swapped +""" + + +@pytest.mark.asyncio +async def test_swap_hotkey_miner(local_chain): + logging.info("Testing test_swap_hotkey_miner") + # Register root as Alice - the subnet owner and validator + alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") + alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) + # Verify subnet 1 created successfully + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + + # Register Bob as miner + bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") + + bob_old_hotkey_address = bob_wallet.hotkey.ss58_address + + # Register Alice as neuron to the subnet + alice_exec_command( + RegisterCommand, + [ + "s", + "register", + "--netuid", + "1", + ], + ) + + # Register Bob as neuron to the subnet + bob_exec_command( + RegisterCommand, + [ + "s", + "register", + "--netuid", + "1", + ], + ) + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + # assert two neurons are in network + total_neurons = len(subtensor.neurons(netuid=1)) + assert total_neurons == 2, f"Expected 2 neurons, found {total_neurons}" + + # register Bob as miner + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/miner.py"', + "--no_prompt", + "--netuid", + "1", + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + bob_wallet.path, + "--wallet.name", + bob_wallet.name, + "--wallet.hotkey", + "default", + "--logging.trace", + ] + ) + + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Bob neuron is now mining") + # register Alice as validator + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/validator.py"', + "--no_prompt", + "--netuid", + "1", + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + alice_wallet.path, + "--wallet.name", + alice_wallet.name, + "--wallet.hotkey", + "default", + "--logging.trace", + ] + ) + # run validator in the background + + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Alice neuron is now validating") + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data + + # register validator with root network + alice_exec_command( + RootRegisterCommand, + [ + "root", + "register", + "--netuid", + "1", + ], + ) + + # Alice to stake to become to top neuron after the first epoch + alice_exec_command( + StakeCommand, + [ + "stake", + "add", + "--amount", + "10000", + ], + ) + + # get latest metagraph + metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") + subtensor = bittensor.subtensor(network="ws://localhost:9945") + + # assert bob has old hotkey + bob_neuron = metagraph.neurons[1] + + # get current number of hotkeys + wallet_tree = bob_exec_command(ListCommand, ["w", "list"], "get_tree") + num_hotkeys = len(wallet_tree.children[0].children) + + assert bob_neuron.coldkey == "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + assert bob_neuron.hotkey == bob_old_hotkey_address + assert bob_neuron.hotkey == bob_neuron.coldkey + assert bob_neuron.coldkey == subtensor.get_hotkey_owner(bob_old_hotkey_address) + assert subtensor.is_hotkey_delegate(bob_neuron.hotkey) is False + assert ( + subtensor.is_hotkey_registered_on_subnet( + hotkey_ss58=bob_neuron.hotkey, netuid=1 + ) + is True + ) + assert ( + subtensor.get_uid_for_hotkey_on_subnet(hotkey_ss58=bob_neuron.hotkey, netuid=1) + == bob_neuron.uid + ) + if num_hotkeys > 1: + logging.info(f"You have {num_hotkeys} hotkeys for Bob.") + + # generate new guid name for hotkey + new_hotkey_name = str(uuid.uuid4()) + + # create a new hotkey + bob_exec_command( + NewHotkeyCommand, + [ + "w", + "new_hotkey", + "--wallet.name", + bob_wallet.name, + "--wallet.hotkey", + new_hotkey_name, + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + # wait rate limit, until we are allowed to change hotkeys + rate_limit = subtensor.tx_rate_limit() + curr_block = subtensor.get_current_block() + await wait_interval(rate_limit + curr_block + 1, subtensor) + + # swap hotkey + bob_exec_command( + SwapHotkeyCommand, + [ + "w", + "swap_hotkey", + "--wallet.name", + bob_wallet.name, + "--wallet.hotkey", + bob_wallet.hotkey_str, + "--wallet.hotkey_b", + new_hotkey_name, + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + # get latest metagraph + metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") + subtensor = bittensor.subtensor(network="ws://localhost:9945") + + # assert bob has new hotkey + bob_neuron = metagraph.neurons[1] + wallet_tree = bob_exec_command(ListCommand, ["w", "list"], "get_tree") + new_num_hotkeys = len(wallet_tree.children[0].children) + + assert ( + bob_neuron.coldkey == "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + ) # cold key didn't change + assert ( + bob_neuron.hotkey != bob_old_hotkey_address + ), "Old and New hotkeys are the same" + assert ( + bob_neuron.hotkey != bob_neuron.coldkey + ), "Hotkey is still the same as coldkey" + assert bob_neuron.coldkey == subtensor.get_hotkey_owner( + bob_neuron.hotkey + ), "Coldkey is not the owner of the new hotkey" # new key is owner + assert ( + subtensor.is_hotkey_delegate(bob_neuron.hotkey) is False + ) # new key is delegate ?? + assert ( # new key is registered on subnet + subtensor.is_hotkey_registered_on_subnet( + hotkey_ss58=bob_neuron.hotkey, netuid=1 + ) + is True + ) + assert ( # old key is NOT registered on subnet + subtensor.is_hotkey_registered_on_subnet( + hotkey_ss58=bob_old_hotkey_address, netuid=1 + ) + is False + ) + assert ( # uid is unchanged + subtensor.get_uid_for_hotkey_on_subnet(hotkey_ss58=bob_neuron.hotkey, netuid=1) + == bob_neuron.uid + ), "UID for Bob changed on the subnet" + assert new_num_hotkeys == num_hotkeys + 1, "Total hotkeys are not as expected" + logging.info("Passed test_swap_hotkey_miner") diff --git a/tests/e2e_tests/subcommands/root/__init__.py b/tests/e2e_tests/subcommands/root/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e_tests/subcommands/root/test_root_delegate_list.py b/tests/e2e_tests/subcommands/root/test_root_delegate_list.py new file mode 100644 index 0000000000..998bc90574 --- /dev/null +++ b/tests/e2e_tests/subcommands/root/test_root_delegate_list.py @@ -0,0 +1,24 @@ +from bittensor import logging +from bittensor.commands.delegates import ListDelegatesCommand + +from ...utils import setup_wallet + + +# delegate seems hard code the network config +def test_root_delegate_list(local_chain, capsys): + logging.info("Testing test_root_delegate_list") + alice_keypair, exec_command, wallet = setup_wallet("//Alice") + + # 1200 hardcoded block gap + exec_command( + ListDelegatesCommand, + ["root", "list_delegates"], + ) + + captured = capsys.readouterr() + lines = captured.out.splitlines() + + # the command print too many lines + # To:do - Find a better to validate list delegates + assert len(lines) > 200 + logging.info("Passed test_root_delegate_list") diff --git a/tests/e2e_tests/subcommands/root/test_root_register_add_member_senate.py b/tests/e2e_tests/subcommands/root/test_root_register_add_member_senate.py new file mode 100644 index 0000000000..7d45e5abcb --- /dev/null +++ b/tests/e2e_tests/subcommands/root/test_root_register_add_member_senate.py @@ -0,0 +1,120 @@ +import bittensor +from bittensor import logging +from bittensor.commands import ( + NominateCommand, + RegisterCommand, + RegisterSubnetworkCommand, + RootRegisterCommand, + SetTakeCommand, + StakeCommand, +) +from bittensor.commands.senate import SenateCommand + +from ...utils import setup_wallet + + +def test_root_register_add_member_senate(local_chain, capsys): + logging.info("Testing test_root_register_add_member_senate") + # Register root as Alice - the subnet owner + alice_keypair, exec_command, wallet = setup_wallet("//Alice") + exec_command(RegisterSubnetworkCommand, ["s", "create"]) + + # Register a neuron to the subnet + exec_command( + RegisterCommand, + [ + "s", + "register", + "--netuid", + "1", + ], + ) + + # Stake to become to top neuron after the first epoch + exec_command( + StakeCommand, + [ + "stake", + "add", + "--amount", + "10000", + ], + ) + + exec_command(NominateCommand, ["root", "nominate"]) + + exec_command(SetTakeCommand, ["r", "set_take", "--take", "0.8"]) + + captured = capsys.readouterr() + # Verify subnet 1 created successfully + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + # Query local chain for senate members + members = local_chain.query("SenateMembers", "Members").serialize() + assert len(members) == 3, f"Expected 3 senate members, found {len(members)}" + + # Assert subtensor has 3 senate members + subtensor = bittensor.subtensor(network="ws://localhost:9945") + sub_senate = len(subtensor.get_senate_members()) + assert ( + sub_senate == 3 + ), f"Root senate expected 3 members but found {sub_senate} instead." + + # Execute command and capture output + exec_command( + SenateCommand, + ["root", "senate"], + ) + + captured = capsys.readouterr() + + # assert output is graph Titling "Senate" with names and addresses + assert "Senate" in captured.out + assert "NAME" in captured.out + assert "ADDRESS" in captured.out + assert "5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL" in captured.out + assert "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" in captured.out + assert "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw" in captured.out + + exec_command( + RootRegisterCommand, + [ + "root", + "register", + "--wallet.hotkey", + "default", + "--wallet.name", + "default", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + # sudo_call_add_senate_member(local_chain, wallet) + + members = local_chain.query("SenateMembers", "Members").serialize() + assert len(members) == 4, f"Expected 4 senate members, found {len(members)}" + + # Assert subtensor has 4 senate members + subtensor = bittensor.subtensor(network="ws://localhost:9945") + sub_senate = len(subtensor.get_senate_members()) + assert ( + sub_senate == 4 + ), f"Root senate expected 3 members but found {sub_senate} instead." + + exec_command( + SenateCommand, + ["root", "senate"], + ) + + captured = capsys.readouterr() + + # assert output is graph Titling "Senate" with names and addresses + + assert "Senate" in captured.out + assert "NAME" in captured.out + assert "ADDRESS" in captured.out + assert "5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL" in captured.out + assert "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" in captured.out + assert "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" in captured.out + assert "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw" in captured.out diff --git a/tests/e2e_tests/subcommands/root/test_root_senate_vote.py b/tests/e2e_tests/subcommands/root/test_root_senate_vote.py new file mode 100644 index 0000000000..e08df94072 --- /dev/null +++ b/tests/e2e_tests/subcommands/root/test_root_senate_vote.py @@ -0,0 +1,49 @@ +from bittensor import logging +from bittensor.commands.root import RootRegisterCommand +from bittensor.commands.senate import VoteCommand + +from ...utils import ( + call_add_proposal, + setup_wallet, +) + + +def test_root_senate_vote(local_chain, capsys, monkeypatch): + logging.info("Testing test_root_senate_vote") + keypair, exec_command, wallet = setup_wallet("//Alice") + monkeypatch.setattr("rich.prompt.Confirm.ask", lambda self: True) + + exec_command( + RootRegisterCommand, + ["root", "register"], + ) + + members = local_chain.query("Triumvirate", "Members") + proposals = local_chain.query("Triumvirate", "Proposals").serialize() + + assert len(members) == 3, f"Expected 3 Triumvirate members, found {len(members)}" + assert ( + len(proposals) == 0 + ), f"Expected 0 initial Triumvirate proposals, found {len(proposals)}" + + call_add_proposal(local_chain, wallet) + + proposals = local_chain.query("Triumvirate", "Proposals").serialize() + + assert ( + len(proposals) == 1 + ), f"Expected 1 proposal in the Triumvirate after addition, found {len(proposals)}" + proposal_hash = proposals[0] + + exec_command( + VoteCommand, + ["root", "senate_vote", "--proposal", proposal_hash], + ) + + voting = local_chain.query("Triumvirate", "Voting", [proposal_hash]).serialize() + + assert len(voting["ayes"]) == 1, f"Expected 1 ayes, found {len(voting['ayes'])}" + assert ( + voting["ayes"][0] == wallet.hotkey.ss58_address + ), "wallet hotkey address doesn't match 'ayes' address" + logging.info("Passed test_root_senate_vote") diff --git a/tests/e2e_tests/subcommands/root/test_root_view_proposal.py b/tests/e2e_tests/subcommands/root/test_root_view_proposal.py new file mode 100644 index 0000000000..9de8296e52 --- /dev/null +++ b/tests/e2e_tests/subcommands/root/test_root_view_proposal.py @@ -0,0 +1,45 @@ +import bittensor +from bittensor import logging +from bittensor.commands.senate import ProposalsCommand + +from ...utils import ( + call_add_proposal, + setup_wallet, +) + + +def test_root_view_proposal(local_chain, capsys): + logging.info("Testing test_root_view_proposal") + keypair, exec_command, wallet = setup_wallet("//Alice") + + proposals = local_chain.query("Triumvirate", "Proposals").serialize() + + assert len(proposals) == 0, "Proposals are not 0" + + call_add_proposal(local_chain, wallet) + + proposals = local_chain.query("Triumvirate", "Proposals").serialize() + + assert len(proposals) == 1, "Added proposal not found" + + exec_command( + ProposalsCommand, + ["root", "proposals"], + ) + + simulated_output = [ + "📡 Syncing with chain: local ...", + " Proposals Active Proposals: 1 Senate Size: 3 ", + "HASH C…", + "0x78b8a348690f565efe3730cd8189f7388c0a896b6fd090276639c9130c0eba47 r…", + " \x00) ", + " ", + ] + + captured = capsys.readouterr() + output = captured.out + + for expected_line in simulated_output: + assert ( + expected_line in output + ), f"Expected '{expected_line}' to be in the output" diff --git a/tests/e2e_tests/subcommands/stake/__init__.py b/tests/e2e_tests/subcommands/stake/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e_tests/subcommands/stake/test_childkeys.py b/tests/e2e_tests/subcommands/stake/test_childkeys.py new file mode 100644 index 0000000000..080d01263d --- /dev/null +++ b/tests/e2e_tests/subcommands/stake/test_childkeys.py @@ -0,0 +1,472 @@ +import bittensor +import pytest +from bittensor.commands import ( + RegisterCommand, + StakeCommand, + RegisterSubnetworkCommand, + SetChildrenCommand, + RevokeChildrenCommand, + GetChildrenCommand, +) +from bittensor.commands.stake import SetChildKeyTakeCommand, GetChildKeyTakeCommand +from bittensor.extrinsics.staking import prepare_child_proportions +from tests.e2e_tests.utils import setup_wallet, wait_interval + + +@pytest.mark.asyncio +async def test_set_revoke_children_multiple(local_chain, capsys): + """ + Test the setting and revoking of children hotkeys for staking. + + This test case covers the following scenarios: + 1. Setting multiple children hotkeys with specified proportions + 2. Retrieving children information + 3. Revoking all children hotkeys + 4. Verifying the absence of children after revocation + + The test uses three wallets (Alice, Bob, and Eve) and performs operations + on a local blockchain. + + Args: + local_chain: A fixture providing access to the local blockchain + capsys: A pytest fixture for capturing stdout and stderr + + The test performs the following steps: + - Set up wallets for Alice, Bob, and Eve + - Create a subnet and register wallets + - Add stake to Alice's wallet + - Set Bob and Eve as children of Alice with specific proportions + - Verify the children are set correctly + - Get and verify children information + - Revoke all children + - Verify children are revoked + - Check that no children exist after revocation + + This test ensures the proper functioning of setting children hotkeys, + retrieving children information, and revoking children in the staking system. + """ + # Setup + alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") + bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") + eve_keypair, eve_exec_command, eve_wallet = setup_wallet("//Eve") + + alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + + for exec_command in [alice_exec_command, bob_exec_command, eve_exec_command]: + exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) + + alice_exec_command(StakeCommand, ["stake", "add", "--amount", "100000"]) + + async def wait(): + # wait rate limit, until we are allowed to get children + + rate_limit = ( + subtensor.query_constant( + module_name="SubtensorModule", constant_name="InitialTempo" + ).value + * 2 + ) + curr_block = subtensor.get_current_block() + await wait_interval(rate_limit + curr_block + 1, subtensor) + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + + await wait() + + children_with_proportions = [ + [0.4, bob_keypair.ss58_address], + [0.2, eve_keypair.ss58_address], + ] + + # Test 1: Set multiple children + alice_exec_command( + SetChildrenCommand, + [ + "stake", + "set_children", + "--netuid", + "1", + "--children", + f"{children_with_proportions[0][1]},{children_with_proportions[1][1]}", + "--hotkey", + str(alice_keypair.ss58_address), + "--proportions", + f"{children_with_proportions[0][0]},{children_with_proportions[1][0]}", + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + await wait() + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + children_info = subtensor.get_children(hotkey=alice_keypair.ss58_address, netuid=1) + + assert len(children_info) == 2, "Failed to set children hotkeys" + + normalized_proportions = prepare_child_proportions(children_with_proportions) + assert ( + children_info[0][0] == normalized_proportions[0][0] + and children_info[1][0] == normalized_proportions[1][0] + ), "Incorrect proportions set" + + # Test 2: Get children information + alice_exec_command( + GetChildrenCommand, + [ + "stake", + "get_children", + "--netuid", + "1", + "--hotkey", + str(alice_keypair.ss58_address), + ], + ) + output = capsys.readouterr().out + assert "5FHne… │ 40.000%" in output + assert "5HGjW… │ 20.000%" in output + assert "Total │ 60.000%" in output + + await wait() + + # Test 3: Revoke all children + alice_exec_command( + RevokeChildrenCommand, + [ + "stake", + "revoke_children", + "--netuid", + "1", + "--hotkey", + str(alice_keypair.ss58_address), + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + await wait() + + assert ( + subtensor.get_children(netuid=1, hotkey=alice_keypair.ss58_address) == [] + ), "Failed to revoke children hotkeys" + + await wait() + # Test 4: Get children after revocation + alice_exec_command( + GetChildrenCommand, + [ + "stake", + "get_children", + "--netuid", + "1", + "--hotkey", + str(alice_keypair.ss58_address), + ], + ) + output = capsys.readouterr().out + assert "There are currently no child hotkeys on subnet" in output + + +@pytest.mark.asyncio +async def test_set_revoke_childkey_take(local_chain, capsys): + """ + Test the setting and retrieving of childkey take amounts for staking. + + This test case covers the following scenarios: + 1. Setting a childkey take amount for a specific hotkey + 2. Retrieving the childkey take amount + 3. Verifying the retrieved childkey take amount + + The test uses one wallet (Alice) and performs operations + on a local blockchain. + + Args: + local_chain: A fixture providing access to the local blockchain + capsys: A pytest fixture for capturing stdout and stderr + + The test performs the following steps: + - Set up wallets for Alice, Bob, and Eve + - Create a subnet and register wallets + - Set a childkey take amount for Alice + - Verify the setting operation was successful + - Retrieve the set childkey take amount + - Verify the retrieved amount is correct + + This test ensures the proper functioning of setting and retrieving + childkey take amounts in the staking system. + """ + # Setup + alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") + + alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + + for exec_command in [alice_exec_command]: + exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) + + # Test 1: Set multiple children + alice_exec_command( + SetChildKeyTakeCommand, + [ + "stake", + "set_childkey_take", + "--netuid", + "1", + "--hotkey", + str(alice_keypair.ss58_address), + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + "--take", + "0.12", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + output = capsys.readouterr().out + assert ( + "The childkey take for 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY is now \nset to 12.000%." + in output + ) + + # Test 1: Set multiple children + alice_exec_command( + GetChildKeyTakeCommand, + [ + "stake", + "get_childkey_take", + "--netuid", + "1", + "--hotkey", + str(alice_keypair.ss58_address), + ], + ) + + output = capsys.readouterr().out + assert ( + "The childkey take for 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY is \n12.000%." + in output + ) + + +@pytest.mark.asyncio +async def test_set_revoke_children_singular(local_chain, capsys): + """ + Test the setting and revoking of children hotkeys for staking. + + This test case covers the following scenarios: + 1. Setting multiple children hotkeys with specified proportions (set one at a time) + 2. Retrieving children information + 3. Revoking children hotkeys (one at a time) + 4. Verifying the absence of children after revocation + + The test uses three wallets (Alice, Bob, and Eve) and performs operations + on a local blockchain. + + Args: + local_chain: A fixture providing access to the local blockchain + capsys: A pytest fixture for capturing stdout and stderr + + The test performs the following steps: + - Set up wallets for Alice, Bob, and Eve + - Create a subnet and register wallets + - Add stake to Alice's wallet + - Set Bob and Eve as children of Alice with specific proportions + - Verify the children are set correctly + - Get and verify children information + - Revoke all children + - Verify children are revoked + - Check that no children exist after revocation + + This test ensures the proper functioning of setting children hotkeys, + retrieving children information, and revoking children in the staking system. + """ + # Setup + alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") + bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") + eve_keypair, eve_exec_command, eve_wallet = setup_wallet("//Eve") + + alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + + for exec_command in [alice_exec_command, bob_exec_command, eve_exec_command]: + exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) + + alice_exec_command(StakeCommand, ["stake", "add", "--amount", "100000"]) + + async def wait(): + # wait rate limit, until we are allowed to get children + + rate_limit = ( + subtensor.query_constant( + module_name="SubtensorModule", constant_name="InitialTempo" + ).value + * 2 + ) + curr_block = subtensor.get_current_block() + await wait_interval(rate_limit + curr_block + 1, subtensor) + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + + await wait() + + children_with_proportions = [ + [0.6, bob_keypair.ss58_address], + [0.4, eve_keypair.ss58_address], + ] + + # Test 1: Set first children + alice_exec_command( + SetChildrenCommand, + [ + "stake", + "set_children", + "--netuid", + "1", + "--children", + f"{children_with_proportions[0][1]}", + "--hotkey", + str(alice_keypair.ss58_address), + "--proportions", + f"{children_with_proportions[0][0]}", + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + output = capsys.readouterr().out + assert "5FHne… │ 60.000%" in output + + await wait() + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + children_info = subtensor.get_children(hotkey=alice_keypair.ss58_address, netuid=1) + + assert len(children_info) == 1, "Failed to set child hotkeys" + + # Test 2: Set second child + alice_exec_command( + SetChildrenCommand, + [ + "stake", + "set_children", + "--netuid", + "1", + "--children", + f"{children_with_proportions[1][1]}", + "--hotkey", + str(alice_keypair.ss58_address), + "--proportions", + f"{children_with_proportions[1][0]}", + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + await wait() + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + children_info = subtensor.get_children(hotkey=alice_keypair.ss58_address, netuid=1) + + assert len(children_info) == 1, "Failed to set child hotkey" + + # Test 2: Get children information + alice_exec_command( + GetChildrenCommand, + [ + "stake", + "get_children", + "--netuid", + "1", + "--hotkey", + str(alice_keypair.ss58_address), + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + ], + ) + output = capsys.readouterr().out + assert "5HGjW… │ 40.000%" in output + + await wait() + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + children_info = subtensor.get_children(hotkey=alice_keypair.ss58_address, netuid=1) + assert len(children_info) == 1, "Failed to revoke child hotkey" + + # Test 4: Revoke second child + alice_exec_command( + RevokeChildrenCommand, + [ + "stake", + "revoke_children", + "--netuid", + "1", + "--hotkey", + str(alice_keypair.ss58_address), + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + await wait() + subtensor = bittensor.subtensor(network="ws://localhost:9945") + children_info = subtensor.get_children(hotkey=alice_keypair.ss58_address, netuid=1) + assert len(children_info) == 0, "Failed to revoke child hotkey" + + # Test 4: Get children after revocation + alice_exec_command( + GetChildrenCommand, + [ + "stake", + "get_children", + "--netuid", + "1", + "--hotkey", + str(alice_keypair.ss58_address), + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + ], + ) + output = capsys.readouterr().out + assert ( + "There are currently no child hotkeys on subnet 1 with Parent HotKey \n5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY." + in output + ) diff --git a/tests/e2e_tests/subcommands/stake/test_stake_add_remove.py b/tests/e2e_tests/subcommands/stake/test_stake_add_remove.py new file mode 100644 index 0000000000..2598f1feaa --- /dev/null +++ b/tests/e2e_tests/subcommands/stake/test_stake_add_remove.py @@ -0,0 +1,81 @@ +from bittensor import logging +from bittensor.commands.network import RegisterSubnetworkCommand +from bittensor.commands.register import RegisterCommand +from bittensor.commands.stake import StakeCommand +from bittensor.commands.unstake import UnStakeCommand + +from ...utils import ( + setup_wallet, + sudo_call_set_network_limit, + sudo_call_set_target_stakes_per_interval, +) + + +def test_stake_add(local_chain): + logging.info("Testing test_stake_add") + alice_keypair, exec_command, wallet = setup_wallet("//Alice") + assert sudo_call_set_network_limit( + local_chain, wallet + ), "Unable to set network limit" + assert sudo_call_set_target_stakes_per_interval( + local_chain, wallet + ), "Unable to set target stakes per interval" + + assert not ( + local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + ), "Subnet was found in netuid 1" + + exec_command(RegisterSubnetworkCommand, ["s", "create"]) + + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [1] + ).serialize(), "Subnet 1 was successfully added" + + assert ( + local_chain.query( + "SubtensorModule", "LastTxBlock", [wallet.hotkey.ss58_address] + ).serialize() + == 0 + ), "LastTxBlock is not 0" + + assert ( + local_chain.query( + "SubtensorModule", "LastTxBlockDelegateTake", [wallet.hotkey.ss58_address] + ).serialize() + == 0 + ), "LastTxBlockDelegateTake is not 0" + + exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) + + assert ( + local_chain.query( + "SubtensorModule", "TotalHotkeyStake", [wallet.hotkey.ss58_address] + ).serialize() + == 0 + ), "TotalHotkeyStake is not 0" + + stake_amount = 2 + exec_command(StakeCommand, ["stake", "add", "--amount", str(stake_amount)]) + exact_stake = local_chain.query( + "SubtensorModule", "TotalHotkeyStake", [wallet.hotkey.ss58_address] + ).serialize() + withdraw_loss = 1_000_000 + stake_amount_in_rao = stake_amount * 1_000_000_000 + + assert ( + stake_amount_in_rao - withdraw_loss < exact_stake <= stake_amount_in_rao + ), f"Stake amount mismatch: expected {exact_stake} to be between {stake_amount_in_rao - withdraw_loss} and {stake_amount_in_rao}" + + # we can test remove after set the stake rate limit larger than 1 + remove_amount = 1 + + exec_command(UnStakeCommand, ["stake", "remove", "--amount", str(remove_amount)]) + total_hotkey_stake = local_chain.query( + "SubtensorModule", "TotalHotkeyStake", [wallet.hotkey.ss58_address] + ).serialize() + expected_stake = exact_stake - remove_amount * 1_000_000_000 + assert ( + total_hotkey_stake == expected_stake + ), f"Unstake amount mismatch: expected {expected_stake}, but got {total_hotkey_stake}" + + logging.info("Passed test_stake_add") diff --git a/tests/e2e_tests/subcommands/stake/test_stake_show.py b/tests/e2e_tests/subcommands/stake/test_stake_show.py new file mode 100644 index 0000000000..af155ffc2b --- /dev/null +++ b/tests/e2e_tests/subcommands/stake/test_stake_show.py @@ -0,0 +1,37 @@ +from bittensor import logging +from bittensor.commands.stake import StakeShow + +from ...utils import setup_wallet + + +def test_stake_show(local_chain, capsys): + logging.info("Testing test_stake_show") + keypair, exec_command, wallet = setup_wallet("//Alice") + + # Execute the command + exec_command(StakeShow, ["stake", "show"]) + captured = capsys.readouterr() + output = captured.out + + # Check the header line + assert "Coldkey" in output, "Output missing 'Coldkey'." + assert "Balance" in output, "Output missing 'Balance'." + assert "Account" in output, "Output missing 'Account'." + assert "Stake" in output, "Output missing 'Stake'." + assert "Rate" in output, "Output missing 'Rate'." + + # Check the first line of data + assert "default" in output, "Output missing 'default'." + assert "1000000.000000" in output.replace( + "τ", "" + ), "Output missing '1000000.000000'." + + # Check the second line of data + assert "0.000000" in output.replace("τ", ""), "Output missing '0.000000'." + assert "0/d" in output, "Output missing '0/d'." + + # Check the third line of data + + assert "1000000.00000" in output.replace("τ", ""), "Output missing '1000000.00000'." + assert "0.00000" in output.replace("τ", ""), "Output missing '0.00000'." + assert "0.00000/d" in output.replace("τ", ""), "Output missing '0.00000/d'." diff --git a/tests/e2e_tests/subcommands/subnet/__init__.py b/tests/e2e_tests/subcommands/subnet/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e_tests/subcommands/subnet/test_list.py b/tests/e2e_tests/subcommands/subnet/test_list.py new file mode 100644 index 0000000000..74b79a2dfb --- /dev/null +++ b/tests/e2e_tests/subcommands/subnet/test_list.py @@ -0,0 +1,29 @@ +import bittensor +from bittensor.commands import RegisterSubnetworkCommand +from tests.e2e_tests.utils import setup_wallet + +""" +Test the list command before and after registering subnets. + +Verify that: +* list of subnets gets displayed +------------------------- +* Register a subnets +* Ensure is visible in list cmd +""" + + +def test_list_command(local_chain, capsys): + # Register root as Alice + keypair, exec_command, wallet = setup_wallet("//Alice") + + netuid = 0 + + assert local_chain.query("SubtensorModule", "NetworksAdded", [netuid]).serialize() + + exec_command(RegisterSubnetworkCommand, ["s", "create"]) + + netuid - 1 + + # Verify subnet 1 created successfully + assert local_chain.query("SubtensorModule", "NetworksAdded", [netuid]).serialize() diff --git a/tests/e2e_tests/subcommands/subnet/test_metagraph.py b/tests/e2e_tests/subcommands/subnet/test_metagraph.py new file mode 100644 index 0000000000..e8e18ef617 --- /dev/null +++ b/tests/e2e_tests/subcommands/subnet/test_metagraph.py @@ -0,0 +1,122 @@ +import bittensor +from bittensor import logging +from bittensor.commands import ( + MetagraphCommand, + RegisterCommand, + RegisterSubnetworkCommand, +) +from tests.e2e_tests.utils import setup_wallet + +""" +Test the metagraph command before and after registering neurons. + +Verify that: +* Metagraph gets displayed +* Initially empty +------------------------- +* Register 2 neurons one by one +* Ensure both are visible in metagraph +""" + + +def test_metagraph_command(local_chain, capsys): + logging.info("Testing test_metagraph_command") + # Register root as Alice + keypair, exec_command, wallet = setup_wallet("//Alice") + exec_command(RegisterSubnetworkCommand, ["s", "create"]) + + # Verify subnet 1 created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [1] + ).serialize(), "Subnet wasn't created successfully" + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + + metagraph = subtensor.metagraph(netuid=1) + + # Assert metagraph is empty + assert len(metagraph.uids) == 0, "Metagraph is not empty" + + # Execute btcli metagraph command + exec_command(MetagraphCommand, ["subnet", "metagraph", "--netuid", "1"]) + + captured = capsys.readouterr() + + # Assert metagraph is printed for netuid 1 + + assert ( + "Metagraph: net: local:1" in captured.out + ), "Netuid 1 was not displayed in metagraph" + + # Register Bob as neuron to the subnet + bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") + bob_exec_command( + RegisterCommand, + [ + "s", + "register", + "--netuid", + "1", + ], + ) + + captured = capsys.readouterr() + + # Assert neuron was registered + + assert "✅ Registered" in captured.out, "Neuron was not registered" + + # Refresh the metagraph + metagraph = subtensor.metagraph(netuid=1) + + # Assert metagraph has registered neuron + assert len(metagraph.uids) == 1, "Metagraph doesn't have exactly 1 neuron" + assert ( + metagraph.hotkeys[0] == "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + ), "Neuron's hotkey in metagraph doesn't match" + # Execute btcli metagraph command + exec_command(MetagraphCommand, ["subnet", "metagraph", "--netuid", "1"]) + + captured = capsys.readouterr() + + # Assert the neuron is registered and displayed + assert ( + "Metagraph: net: local:1" and "N: 1/1" in captured.out + ), "Neuron isn't displayed in metagraph" + + # Register Dave as neuron to the subnet + dave_keypair, dave_exec_command, dave_wallet = setup_wallet("//Dave") + dave_exec_command( + RegisterCommand, + [ + "s", + "register", + "--netuid", + "1", + ], + ) + + captured = capsys.readouterr() + + # Assert neuron was registered + + assert "✅ Registered" in captured.out, "Neuron was not registered" + + # Refresh the metagraph + metagraph = subtensor.metagraph(netuid=1) + + # Assert metagraph has registered neuron + assert len(metagraph.uids) == 2 + assert ( + metagraph.hotkeys[1] == "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" + ), "Neuron's hotkey in metagraph doesn't match" + + # Execute btcli metagraph command + exec_command(MetagraphCommand, ["subnet", "metagraph", "--netuid", "1"]) + + captured = capsys.readouterr() + + # Assert the neuron is registered and displayed + assert "Metagraph: net: local:1" and "N: 2/2" in captured.out + + logging.info("Passed test_metagraph_command") diff --git a/tests/e2e_tests/subcommands/wallet/test_faucet.py b/tests/e2e_tests/subcommands/wallet/test_faucet.py index 9f5fc6fc49..64ae2b7f86 100644 --- a/tests/e2e_tests/subcommands/wallet/test_faucet.py +++ b/tests/e2e_tests/subcommands/wallet/test_faucet.py @@ -15,12 +15,15 @@ @pytest.mark.skip @pytest.mark.parametrize("local_chain", [False], indirect=True) def test_faucet(local_chain): + logging.info("Testing test_faucet") # Register root as Alice - keypair, exec_command, wallet_path = setup_wallet("//Alice") + keypair, exec_command, wallet = setup_wallet("//Alice") exec_command(RegisterSubnetworkCommand, ["s", "create"]) # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [1] + ).serialize(), "Subnet wasn't created successfully" # Register a neuron to the subnet exec_command( @@ -46,11 +49,11 @@ def test_faucet(local_chain): # verify current balance wallet_balance = subtensor.get_balance(keypair.ss58_address) - assert wallet_balance.tao == 998999.0 + assert wallet_balance.tao == 998999.0, "Balance wasn't as expected" # run faucet 3 times for i in range(3): - logging.info(f"faucet run #:{i+1}") + logging.info(f"faucet run #:{i + 1}") try: exec_command( RunFaucetCommand, @@ -58,11 +61,13 @@ def test_faucet(local_chain): "wallet", "faucet", "--wallet.name", - "default", + wallet.name, "--wallet.hotkey", "default", - "--subtensor.chain_endpoint", - "ws://localhost:9945", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", ], ) logging.info( @@ -81,7 +86,7 @@ def test_faucet(local_chain): new_wallet_balance = subtensor.get_balance(keypair.ss58_address) # verify balance increase - assert wallet_balance.tao < new_wallet_balance.tao assert ( - new_wallet_balance.tao == 999899.0 - ) # after 3 runs we should see an increase of 900 tao + wallet_balance.tao < new_wallet_balance.tao + ), "Old wallet balance is not less than the new wallet" + logging.info("Passed test_faucet") diff --git a/tests/e2e_tests/subcommands/wallet/test_list.py b/tests/e2e_tests/subcommands/wallet/test_list.py new file mode 100644 index 0000000000..15f34514b0 --- /dev/null +++ b/tests/e2e_tests/subcommands/wallet/test_list.py @@ -0,0 +1,72 @@ +from bittensor.commands.list import ListCommand +from bittensor.commands.wallets import WalletCreateCommand +from bittensor.subtensor import subtensor + +from ...utils import setup_wallet + + +def test_wallet_list(capsys): + """ + Test the listing of wallets in the Bittensor network. + + Steps: + 1. Set up a default wallet + 2. List existing wallets and verify the default setup + 3. Create a new wallet + 4. List wallets again and verify the new wallet is present + + Raises: + AssertionError: If any of the checks or verifications fail + """ + + wallet_path_name = "//Alice" + base_path = f"/tmp/btcli-e2e-wallet-list-{wallet_path_name.strip('/')}" + keypair, exec_command, wallet = setup_wallet(wallet_path_name) + + # List initial wallets + exec_command( + ListCommand, + [ + "wallet", + "list", + ], + ) + + captured = capsys.readouterr() + # Assert the default wallet is present in the display + assert "default" in captured.out + assert "└── default" in captured.out + + # Create a new wallet + exec_command( + WalletCreateCommand, + [ + "wallet", + "create", + "--wallet.name", + "new_wallet", + "--wallet.hotkey", + "new_hotkey", + "--no_password", + "--overwrite_coldkey", + "--overwrite_hotkey", + "--no_prompt", + "--wallet.path", + base_path, + ], + ) + + # List wallets again + exec_command( + ListCommand, + [ + "wallet", + "list", + ], + ) + + captured = capsys.readouterr() + + # Verify the new wallet is displayed + assert "new_wallet" in captured.out + assert "new_hotkey" in captured.out diff --git a/tests/e2e_tests/subcommands/wallet/test_transfer.py b/tests/e2e_tests/subcommands/wallet/test_transfer.py index 5b22b4e778..9d1bd2692c 100644 --- a/tests/e2e_tests/subcommands/wallet/test_transfer.py +++ b/tests/e2e_tests/subcommands/wallet/test_transfer.py @@ -1,10 +1,12 @@ +from bittensor import logging from bittensor.commands.transfer import TransferCommand + from ...utils import setup_wallet -import bittensor # Example test using the local_chain fixture def test_transfer(local_chain): + logging.info("Testing test_transfer") keypair, exec_command, wallet = setup_wallet("//Alice") acc_before = local_chain.query("System", "Account", [keypair.ss58_address]) @@ -30,3 +32,4 @@ def test_transfer(local_chain): assert ( expected_transfer <= actual_difference <= expected_transfer + tolerance ), f"Expected transfer with tolerance: {expected_transfer} <= {actual_difference} <= {expected_transfer + tolerance}" + logging.info("Passed test_transfer") diff --git a/tests/e2e_tests/subcommands/wallet/test_wallet_creations.py b/tests/e2e_tests/subcommands/wallet/test_wallet_creations.py new file mode 100644 index 0000000000..78a235ad25 --- /dev/null +++ b/tests/e2e_tests/subcommands/wallet/test_wallet_creations.py @@ -0,0 +1,505 @@ +import os +import re +import time +from typing import Dict, Optional, Tuple + +from bittensor import logging +from bittensor.commands.list import ListCommand +from bittensor.commands.wallets import ( + NewColdkeyCommand, + NewHotkeyCommand, + RegenColdkeyCommand, + RegenColdkeypubCommand, + RegenHotkeyCommand, + WalletCreateCommand, +) +from bittensor.subtensor import subtensor + +from ...utils import setup_wallet + +""" +Verify commands: + +* btcli w list +* btcli w create +* btcli w new_coldkey +* btcli w new_hotkey +* btcli w regen_coldkey +* btcli w regen_coldkeypub +* btcli w regen_hotkey +""" + + +def verify_wallet_dir( + base_path: str, + wallet_name: str, + hotkey_name: Optional[str] = None, + coldkeypub_name: Optional[str] = None, +) -> Tuple[bool, str]: + """ + Verifies the existence of wallet directory, coldkey, and optionally the hotkey. + + Args: + base_path (str): The base directory path where wallets are stored. + wallet_name (str): The name of the wallet directory to verify. + hotkey_name (str, optional): The name of the hotkey file to verify. If None, + only the wallet and coldkey file are checked. + coldkeypub_name (str, optional): The name of the coldkeypub file to verify. If None + only the wallet and coldkey is checked + + Returns: + tuple: Returns a tuple containing a boolean and a message. The boolean is True if + all checks pass, otherwise False. + """ + wallet_path = os.path.join(base_path, wallet_name) + + # Check if wallet directory exists + if not os.path.isdir(wallet_path): + return False, f"Wallet directory {wallet_name} not found in {base_path}" + + # Check if coldkey file exists + coldkey_path = os.path.join(wallet_path, "coldkey") + if not os.path.isfile(coldkey_path): + return False, f"Coldkey file not found in {wallet_name}" + + # Check if coldkeypub exists + if coldkeypub_name: + coldkeypub_path = os.path.join(wallet_path, coldkeypub_name) + if not os.path.isfile(coldkeypub_path): + return False, f"Coldkeypub file not found in {wallet_name}" + + # Check if hotkey directory and file exists + if hotkey_name: + hotkeys_path = os.path.join(wallet_path, "hotkeys") + if not os.path.isdir(hotkeys_path): + return False, f"Hotkeys directory not found in {wallet_name}" + + hotkey_file_path = os.path.join(hotkeys_path, hotkey_name) + if not os.path.isfile(hotkey_file_path): + return ( + False, + f"Hotkey file {hotkey_name} not found in {wallet_name}/hotkeys", + ) + + return True, f"Wallet {wallet_name} verified successfully" + + +def verify_key_pattern(output: str, wallet_name: str) -> Optional[str]: + """ + Verifies that a specific wallet key pattern exists in the output text. + + Args: + output (str): The string output where the wallet key should be verified. + wallet_name (str): The name of the wallet to search for in the output. + + Raises: + AssertionError: If the wallet key pattern is not found, or if the key does not + start with '5', or if the key is not exactly 48 characters long. + """ + split_output = output.splitlines() + pattern = rf"{wallet_name}\s*\((5[A-Za-z0-9]{{47}})\)" + found = False + + # Traverse each line to find instance of the pattern + for line in split_output: + match = re.search(pattern, line) + if match: + # Assert key starts with '5' + assert match.group(1).startswith( + "5" + ), f"{wallet_name} should start with '5'" + # Assert length of key is 48 characters + assert ( + len(match.group(1)) == 48 + ), f"Key for {wallet_name} should be 48 characters long" + found = True + return match.group(1) + + # If no match is found in any line, raise an assertion error + assert found, f"{wallet_name} not found in wallet list" + return None + + +def extract_ss58_address(output: str, wallet_name: str) -> str: + """ + Extracts the ss58 address from the given output for a specified wallet. + + Args: + output (str): The captured output. + wallet_name (str): The name of the wallet. + + Returns: + str: ss58 address. + """ + pattern = rf"{wallet_name}\s*\((5[A-Za-z0-9]{{47}})\)" + lines = output.splitlines() + for line in lines: + match = re.search(pattern, line) + if match: + return match.group(1) # Return the ss58 address + + raise ValueError(f"ss58 address not found for wallet {wallet_name}") + + +def extract_mnemonics_from_commands(output: str) -> Dict[str, Optional[str]]: + """ + Extracts mnemonics of coldkeys & hotkeys from the given output for a specified wallet. + + Args: + output (str): The captured output. + + Returns: + dict: A dictionary keys 'coldkey' and 'hotkey', each containing their mnemonics. + """ + mnemonics: Dict[str, Optional[str]] = {"coldkey": None, "hotkey": None} + lines = output.splitlines() + + # Regex pattern to capture the mnemonic + pattern = re.compile(r"btcli w regen_(coldkey|hotkey) --mnemonic ([a-z ]+)") + + for line in lines: + line = line.strip().lower() + match = pattern.search(line) + if match: + key_type = match.group(1) # 'coldkey' or 'hotkey' + mnemonic_phrase = match.group(2).strip() + mnemonics[key_type] = mnemonic_phrase + + return mnemonics + + +def test_wallet_creations(local_chain: subtensor, capsys): + """ + Test the creation and verification of wallet keys and directories in the Bittensor network. + + Steps: + 1. List existing wallets and verify the default setup. + 2. Create a new wallet with both coldkey and hotkey, verify their presence in the output, + and check their physical existence. + 3. Create a new coldkey and verify both its display in the command line output and its physical file. + 4. Create a new hotkey for an existing coldkey, verify its display in the command line output, + and check for both coldkey and hotkey files. + + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.info("Testing test_wallet_creations (create, new_hotkey, new_coldkey)") + wallet_path_name = "//Alice" + base_path = f"/tmp/btcli-e2e-wallet-{wallet_path_name.strip('/')}" + keypair, exec_command, wallet = setup_wallet(wallet_path_name) + + exec_command( + ListCommand, + [ + "wallet", + "list", + ], + ) + + captured = capsys.readouterr() + # Assert the coldkey and hotkey are present in the display with keys + assert ( + "default" and "└── default" in captured.out + ), "Default wallet not found in wallet list" + wallet_status, message = verify_wallet_dir( + base_path, "default", hotkey_name="default" + ) + assert wallet_status, message + + # ----------------------------- + # Command 1: + # ----------------------------- + # Create a new wallet (coldkey + hotkey) + logging.info("Testing wallet create command") + exec_command( + WalletCreateCommand, + [ + "wallet", + "create", + "--wallet.name", + "new_wallet", + "--wallet.hotkey", + "new_hotkey", + "--no_password", + "--overwrite_coldkey", + "--overwrite_hotkey", + "--no_prompt", + "--wallet.path", + base_path, + ], + ) + + captured = capsys.readouterr() + + # List the wallets + exec_command( + ListCommand, + [ + "wallet", + "list", + ], + ) + + captured = capsys.readouterr() + + # Verify coldkey "new_wallet" is displayed with key + verify_key_pattern(captured.out, "new_wallet") + + # Verify hotkey "new_hotkey" is displayed with key + verify_key_pattern(captured.out, "new_hotkey") + + # Physically verify "new_wallet" and "new_hotkey" are present + wallet_status, message = verify_wallet_dir( + base_path, "new_wallet", hotkey_name="new_hotkey" + ) + assert wallet_status, message + + # ----------------------------- + # Command 2: + # ----------------------------- + # Create a new wallet (coldkey) + logging.info("Testing wallet new_coldkey command") + exec_command( + NewColdkeyCommand, + [ + "wallet", + "new_coldkey", + "--wallet.name", + "new_coldkey", + "--no_password", + "--no_prompt", + "--overwrite_coldkey", + "--wallet.path", + base_path, + ], + ) + + captured = capsys.readouterr() + + # List the wallets + exec_command( + ListCommand, + [ + "wallet", + "list", + ], + ) + + captured = capsys.readouterr() + + # Verify coldkey "new_coldkey" is displayed with key + verify_key_pattern(captured.out, "new_coldkey") + + # Physically verify "new_coldkey" is present + wallet_status, message = verify_wallet_dir(base_path, "new_coldkey") + assert wallet_status, message + + # ----------------------------- + # Command 3: + # ----------------------------- + # Create a new hotkey for alice_new_coldkey wallet + logging.info("Testing wallet new_hotkey command") + exec_command( + NewHotkeyCommand, + [ + "wallet", + "new_hotkey", + "--wallet.name", + "new_coldkey", + "--wallet.hotkey", + "new_hotkey", + "--no_prompt", + "--overwrite_hotkey", + "--wallet.path", + base_path, + ], + ) + + captured = capsys.readouterr() + + # List the wallets + exec_command( + ListCommand, + [ + "wallet", + "list", + ], + ) + captured = capsys.readouterr() + + # Verify hotkey "alice_new_hotkey" is displyed with key + verify_key_pattern(captured.out, "new_hotkey") + + # Physically verify "alice_new_coldkey" and "alice_new_hotkey" are present + wallet_status, message = verify_wallet_dir( + base_path, "new_coldkey", hotkey_name="new_hotkey" + ) + assert wallet_status, message + logging.info("Passed test_wallet_creations") + + +def test_wallet_regen(local_chain: subtensor, capsys): + """ + Test the regeneration of coldkeys, hotkeys, and coldkeypub files using mnemonics or ss58 address. + + Steps: + 1. List existing wallets and verify the default setup. + 2. Regenerate the coldkey using the mnemonics and verify using mod time. + 3. Regenerate the coldkeypub using ss58 address and verify using mod time + 4. Regenerate the hotkey using mnemonics and verify using mod time. + + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.info( + "Testing test_wallet_regen (regen_coldkey, regen_hotkey, regen_coldkeypub)" + ) + wallet_path_name = "//Bob" + base_path = f"/tmp/btcli-e2e-wallet-{wallet_path_name.strip('/')}" + keypair, exec_command, wallet = setup_wallet(wallet_path_name) + + # Create a new wallet (coldkey + hotkey) + exec_command( + WalletCreateCommand, + [ + "wallet", + "create", + "--wallet.name", + "new_wallet", + "--wallet.hotkey", + "new_hotkey", + "--no_password", + "--overwrite_coldkey", + "--overwrite_hotkey", + "--no_prompt", + "--wallet.path", + base_path, + ], + ) + + captured = capsys.readouterr() + mnemonics = extract_mnemonics_from_commands(captured.out) + + wallet_status, message = verify_wallet_dir( + base_path, + "new_wallet", + hotkey_name="new_hotkey", + coldkeypub_name="coldkeypub.txt", + ) + assert wallet_status, message # Ensure wallet exists + + # ----------------------------- + # Command 1: + # ----------------------------- + + logging.info("Testing w regen_coldkey") + coldkey_path = os.path.join(base_path, "new_wallet", "coldkey") + initial_coldkey_mod_time = os.path.getmtime(coldkey_path) + + exec_command( + RegenColdkeyCommand, + [ + "wallet", + "regen_coldkey", + "--wallet.name", + "new_wallet", + "--wallet.path", + base_path, + "--no_prompt", + "--overwrite_coldkey", + "--mnemonic", + mnemonics["coldkey"], + "--no_password", + ], + ) + + # Wait a bit to ensure file system updates modification time + time.sleep(1) + + new_coldkey_mod_time = os.path.getmtime(coldkey_path) + + assert ( + initial_coldkey_mod_time != new_coldkey_mod_time + ), "Coldkey file was not regenerated as expected" + + # ----------------------------- + # Command 2: + # ----------------------------- + + logging.info("Testing w regen_coldkeypub") + coldkeypub_path = os.path.join(base_path, "new_wallet", "coldkeypub.txt") + initial_coldkeypub_mod_time = os.path.getmtime(coldkeypub_path) + + # List the wallets + exec_command( + ListCommand, + [ + "wallet", + "list", + ], + ) + captured = capsys.readouterr() + ss58_address = extract_ss58_address(captured.out, "new_wallet") + + exec_command( + RegenColdkeypubCommand, + [ + "wallet", + "regen_coldkeypub", + "--wallet.name", + "new_wallet", + "--wallet.path", + base_path, + "--no_prompt", + "--overwrite_coldkeypub", + "--ss58_address", + ss58_address, + ], + ) + + # Wait a bit to ensure file system updates modification time + time.sleep(1) + + new_coldkeypub_mod_time = os.path.getmtime(coldkeypub_path) + + assert ( + initial_coldkeypub_mod_time != new_coldkeypub_mod_time + ), "Coldkeypub file was not regenerated as expected" + + # ----------------------------- + # Command 3: + # ----------------------------- + + logging.info("Testing w regen_hotkey") + hotkey_path = os.path.join(base_path, "new_wallet", "hotkeys", "new_hotkey") + initial_hotkey_mod_time = os.path.getmtime(hotkey_path) + + exec_command( + RegenHotkeyCommand, + [ + "wallet", + "regen_hotkey", + "--no_prompt", + "--overwrite_hotkey", + "--wallet.name", + "new_wallet", + "--wallet.hotkey", + "new_hotkey", + "--wallet.path", + base_path, + "--mnemonic", + mnemonics["hotkey"], + ], + ) + + # Wait a bit to ensure file system updates modification time + time.sleep(1) + + new_hotkey_mod_time = os.path.getmtime(hotkey_path) + + assert ( + initial_hotkey_mod_time != new_hotkey_mod_time + ), "Hotkey file was not regenerated as expected" + + logging.info("Passed test_wallet_regen") diff --git a/tests/e2e_tests/subcommands/weights/test_commit_weights.py b/tests/e2e_tests/subcommands/weights/test_commit_weights.py index f04f4f7ab3..c53746be81 100644 --- a/tests/e2e_tests/subcommands/weights/test_commit_weights.py +++ b/tests/e2e_tests/subcommands/weights/test_commit_weights.py @@ -1,19 +1,21 @@ +import asyncio import re -import time import numpy as np +import pytest import bittensor import bittensor.utils.weight_utils as weight_utils +from bittensor import logging from bittensor.commands import ( + CommitWeightCommand, RegisterCommand, - StakeCommand, RegisterSubnetworkCommand, - CommitWeightCommand, RevealWeightCommand, + StakeCommand, + SubnetSudoCommand, ) -from tests.e2e_tests.utils import setup_wallet - +from tests.e2e_tests.utils import setup_wallet, wait_interval """ Test the Commit/Reveal weights mechanism. @@ -28,7 +30,9 @@ """ -def test_commit_and_reveal_weights(local_chain): +@pytest.mark.asyncio +async def test_commit_and_reveal_weights(local_chain): + logging.info("Testing test_commit_and_reveal_weights") # Register root as Alice keypair, exec_command, wallet = setup_wallet("//Alice") @@ -40,12 +44,19 @@ def test_commit_and_reveal_weights(local_chain): salt = "18, 179, 107, 0, 165, 211, 141, 197" # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [1] + ).serialize(), "Subnet wasn't created successfully" # Register a neuron to the subnet exec_command( RegisterCommand, - ["s", "register", "--netuid", "1", "--wallet.path", "/tmp/btcli-wallet"], + [ + "s", + "register", + "--netuid", + "1", + ], ) # Stake to become to top neuron after the first epoch @@ -54,8 +65,6 @@ def test_commit_and_reveal_weights(local_chain): [ "stake", "add", - "--wallet.path", - "/tmp/btcli-wallet2", "--amount", "100000", ], @@ -64,40 +73,86 @@ def test_commit_and_reveal_weights(local_chain): subtensor = bittensor.subtensor(network="ws://localhost:9945") # Enable Commit Reveal - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="commit_reveal_weights_enabled", - value=True, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=False, + exec_command( + SubnetSudoCommand, + [ + "sudo", + "set", + "hyperparameters", + "--netuid", + "1", + "--wallet.name", + wallet.name, + "--param", + "commit_reveal_weights_enabled", + "--value", + "True", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], ) - assert result, "Failed to enable commit/reveal" + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + assert subtensor.get_subnet_hyperparameters( + netuid=1 + ).commit_reveal_weights_enabled, "Failed to enable commit/reveal" # Lower the interval - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="commit_reveal_weights_interval", - value=370, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=False, + exec_command( + SubnetSudoCommand, + [ + "sudo", + "set", + "hyperparameters", + "--netuid", + "1", + "--wallet.name", + wallet.name, + "--param", + "commit_reveal_weights_interval", + "--value", + "370", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], ) - assert result, "Failed to set commit/reveal interval" - - # Lower the rate lmit - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="weights_rate_limit", - value=0, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=False, + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).commit_reveal_weights_interval + == 370 + ), "Failed to set commit/reveal interval" + + # Lower the rate limit + exec_command( + SubnetSudoCommand, + [ + "sudo", + "set", + "hyperparameters", + "--netuid", + "1", + "--wallet.name", + wallet.name, + "--param", + "weights_rate_limit", + "--value", + "0", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], ) - assert result, "Failed to set weights rate limit" + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).weights_rate_limit == 0 + ), "Failed to set commit/reveal rate limit" # Configure the CLI arguments for the CommitWeightCommand exec_command( @@ -142,13 +197,7 @@ def test_commit_and_reveal_weights(local_chain): assert interval > 0, "Invalid WeightCommitRevealInterval" # Wait until the reveal block range - current_block = subtensor.get_current_block() - reveal_block_start = (commit_block - (commit_block % interval)) + interval - while current_block < reveal_block_start: - time.sleep(1) # Wait for 1 second before checking the block number again - current_block = subtensor.get_current_block() - if current_block % 10 == 0: - print(f"Current Block: {current_block} Revealing at: {reveal_block_start}") + await wait_interval(interval, subtensor) # Configure the CLI arguments for the RevealWeightCommand exec_command( @@ -194,3 +243,4 @@ def test_commit_and_reveal_weights(local_chain): assert ( expected_weights[0] == revealed_weights.value[0][1] ), f"Incorrect revealed weights. Expected: {expected_weights[0]}, Actual: {revealed_weights.value[0][1]}" + logging.info("Passed test_commit_and_reveal_weights") diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index ce7b0bc092..5a4adc6c95 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -1,4 +1,5 @@ import logging +import asyncio import os import shutil import subprocess @@ -6,11 +7,13 @@ import time from typing import List +from substrateinterface import SubstrateInterface + import bittensor from bittensor import Keypair template_path = os.getcwd() + "/neurons/" -repo_name = "templates repository" +templates_repo = "templates repository" def setup_wallet(uri: str): @@ -21,7 +24,7 @@ def setup_wallet(uri: str): wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=True) wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=True) - def exec_command(command, extra_args: List[str]): + def exec_command(command, extra_args: List[str], function: str = "run"): parser = bittensor.cli.__create_parser__() args = extra_args + [ "--no_prompt", @@ -32,20 +35,135 @@ def exec_command(command, extra_args: List[str]): "--wallet.path", wallet_path, ] + logging.info(f'executing command: {command} {" ".join(args)}') config = bittensor.config( parser=parser, args=args, ) cli_instance = bittensor.cli(config) - command.run(cli_instance) + # Dynamically call the specified function on the command + result = getattr(command, function)(cli_instance) + return result return keypair, exec_command, wallet +def sudo_call_set_network_limit( + substrate: SubstrateInterface, wallet: bittensor.wallet +) -> bool: + inner_call = substrate.compose_call( + call_module="AdminUtils", + call_function="sudo_set_network_rate_limit", + call_params={"rate_limit": 1}, + ) + call = substrate.compose_call( + call_module="Sudo", + call_function="sudo", + call_params={"call": inner_call}, + ) + + extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) + response = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + response.process_events() + return response.is_success + + +def sudo_call_set_target_stakes_per_interval( + substrate: SubstrateInterface, wallet: bittensor.wallet +) -> bool: + inner_call = substrate.compose_call( + call_module="AdminUtils", + call_function="sudo_set_target_stakes_per_interval", + call_params={"target_stakes_per_interval": 100}, + ) + call = substrate.compose_call( + call_module="Sudo", + call_function="sudo", + call_params={"call": inner_call}, + ) + + extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) + response = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + response.process_events() + return response.is_success + + +def call_add_proposal(substrate: SubstrateInterface, wallet: bittensor.wallet) -> bool: + proposal_call = substrate.compose_call( + call_module="System", + call_function="remark", + call_params={"remark": [0]}, + ) + call = substrate.compose_call( + call_module="Triumvirate", + call_function="propose", + call_params={ + "proposal": proposal_call, + "length_bound": 100_000, + "duration": 100_000_000, + }, + ) + + extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) + response = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + response.process_events() + return response.is_success + + +async def wait_epoch(subtensor, netuid=1): + q_tempo = [ + v.value + for [k, v] in subtensor.query_map_subtensor("Tempo") + if k.value == netuid + ] + if len(q_tempo) == 0: + raise Exception("could not determine tempo") + tempo = q_tempo[0] + logging.info(f"tempo = {tempo}") + await wait_interval(tempo, subtensor, netuid) + + +async def wait_interval(tempo, subtensor, netuid=1): + interval = tempo + 1 + current_block = subtensor.get_current_block() + last_epoch = current_block - 1 - (current_block + netuid + 1) % interval + next_tempo_block_start = last_epoch + interval + last_reported = None + while current_block < next_tempo_block_start: + await asyncio.sleep( + 1 + ) # Wait for 1 second before checking the block number again + current_block = subtensor.get_current_block() + if last_reported is None or current_block - last_reported >= 10: + last_reported = current_block + print( + f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" + ) + logging.info( + f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" + ) + + def clone_or_update_templates(): + specific_commit = None install_dir = template_path repo_mapping = { - repo_name: "https://github.com/opentensor/bittensor-subnet-template.git", + templates_repo: "https://github.com/opentensor/bittensor-subnet-template.git", } os.makedirs(install_dir, exist_ok=True) os.chdir(install_dir) @@ -60,7 +178,16 @@ def clone_or_update_templates(): subprocess.run(["git", "pull"], check=True) os.chdir("..") - return install_dir + repo_name + "/" + # here for pulling specific commit versions of repo + if specific_commit: + os.chdir(templates_repo) + print( + f"\033[94mChecking out commit {specific_commit} in {templates_repo}...\033[0m" + ) + subprocess.run(["git", "checkout", specific_commit], check=True) + os.chdir("..") + + return install_dir + templates_repo + "/" def install_templates(install_dir): @@ -76,16 +203,12 @@ def uninstall_templates(install_dir): shutil.rmtree(install_dir) -def wait_epoch(interval, subtensor): - current_block = subtensor.get_current_block() - next_tempo_block_start = (current_block - (current_block % interval)) + interval - while current_block < next_tempo_block_start: - time.sleep(1) # Wait for 1 second before checking the block number again - current_block = subtensor.get_current_block() - if current_block % 10 == 0: - print( - f"Current Block: {current_block} Next tempo at: {next_tempo_block_start}" - ) - logging.info( - f"Current Block: {current_block} Next tempo at: {next_tempo_block_start}" - ) +async def write_output_log_to_file(name, stream): + log_file = f"{name}.log" + with open(log_file, "a") as f: + while True: + line = await stream.readline() + if not line: + break + f.write(line.decode()) + f.flush() diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index e3661210bc..407dee848c 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -115,6 +115,16 @@ def test_get_current_block(self): block = self.subtensor.get_current_block() assert type(block) == int + def test_do_block_step(self): + self.subtensor.do_block_step() + block = self.subtensor.get_current_block() + assert type(block) == int + + def test_do_block_step_query_previous_block(self): + self.subtensor.do_block_step() + block = self.subtensor.get_current_block() + self.subtensor.query_subtensor("NetworksAdded", block) + def test_unstake(self): self.subtensor._do_unstake = MagicMock(return_value=True) diff --git a/tests/unit_tests/test_axon.py b/tests/unit_tests/test_axon.py index cfb46c32c2..7ba433a151 100644 --- a/tests/unit_tests/test_axon.py +++ b/tests/unit_tests/test_axon.py @@ -20,24 +20,28 @@ # Standard Lib import re +import time from dataclasses import dataclass -from typing import Any +from typing import Any, Optional from unittest import IsolatedAsyncioTestCase from unittest.mock import AsyncMock, MagicMock, patch # Third Party +import fastapi import netaddr - +import pydantic import pytest from starlette.requests import Request from fastapi.testclient import TestClient # Bittensor import bittensor -from bittensor import Synapse, RunException +from bittensor import Synapse, RunException, StreamingSynapse from bittensor.axon import AxonMiddleware from bittensor.axon import axon as Axon +from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds +from bittensor.constants import ALLOWED_DELTA, NANOSECONDS_IN_SECOND def test_attach(): @@ -282,6 +286,7 @@ async def test_priority_pass(middleware): ), ], ) +@pytest.mark.asyncio async def test_verify_body_integrity_happy_path( mock_request, axon_instance, body, expected ): @@ -298,11 +303,12 @@ async def test_verify_body_integrity_happy_path( @pytest.mark.parametrize( "body, expected_exception_message", [ - (b"", "EOFError"), # Empty body - (b"not_json", "JSONDecodeError"), # Non-JSON body + (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 ], 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 ): @@ -323,6 +329,7 @@ async def test_verify_body_integrity_edge_cases( ("incorrect_hash", ValueError), ], ) +@pytest.mark.asyncio async def test_verify_body_integrity_error_cases( mock_request, axon_instance, computed_hash, expected_error ): @@ -532,6 +539,39 @@ def http_client(self, axon): async def no_verify_fn(self, synapse): return + class NonDeterministicHeaders(pydantic.BaseModel): + """ + Helper class to verify headers. + + Size headers are non-determistic as for example, header_size depends on non-deterministic + processing-time value. + """ + + bt_header_axon_process_time: float = pydantic.Field(gt=0, lt=30) + timeout: float = pydantic.Field(gt=0, lt=30) + header_size: int = pydantic.Field(None, gt=10, lt=400) + total_size: int = pydantic.Field(gt=100, lt=10000) + content_length: Optional[int] = pydantic.Field( + None, alias="content-length", gt=100, lt=10000 + ) + + def assert_headers(self, response, expected_headers): + expected_headers = { + "bt_header_axon_status_code": "200", + "bt_header_axon_status_message": "Success", + **expected_headers, + } + headers = dict(response.headers) + non_deterministic_headers_names = { + field.alias or field_name + for field_name, field in self.NonDeterministicHeaders.model_fields.items() + } + non_deterministic_headers = { + field: headers.pop(field, None) for field in non_deterministic_headers_names + } + assert headers == expected_headers + self.NonDeterministicHeaders.model_validate(non_deterministic_headers) + async def test_unknown_path(self, http_client): response = http_client.get("/no_such_path") assert (response.status_code, response.json()) == ( @@ -557,6 +597,14 @@ async def test_ping__without_verification(self, http_client, axon): assert response.status_code == 200 response_synapse = Synapse(**response.json()) assert response_synapse.axon.status_code == 200 + self.assert_headers( + response, + { + "computed_body_hash": "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", + "content-type": "application/json", + "name": "Synapse", + }, + ) @pytest.fixture def custom_synapse_cls(self): @@ -565,6 +613,17 @@ class CustomSynapse(Synapse): return CustomSynapse + @pytest.fixture + def streaming_synapse_cls(self): + class CustomStreamingSynapse(StreamingSynapse): + async def process_streaming_response(self, response): + pass + + def extract_response_json(self, response) -> dict: + return {} + + return CustomStreamingSynapse + async def test_synapse__explicitly_set_status_code( self, http_client, axon, custom_synapse_cls, no_verify_axon ): @@ -613,3 +672,110 @@ async def forward_fn(synapse: custom_synapse_cls): response_data = response.json() assert sorted(response_data.keys()) == ["message"] assert re.match(r"Internal Server Error #[\da-f\-]+", response_data["message"]) + + +def test_allowed_nonce_window_ns(): + mock_synapse = SynapseMock() + current_time = time.time_ns() + allowed_window_ns = allowed_nonce_window_ns(current_time, mock_synapse.timeout) + expected_window_ns = ( + current_time - ALLOWED_DELTA - (mock_synapse.timeout * NANOSECONDS_IN_SECOND) + ) + assert ( + allowed_window_ns < current_time + ), "Allowed window should be less than the current time" + assert ( + allowed_window_ns == expected_window_ns + ), f"Expected {expected_window_ns} but got {allowed_window_ns}" + + +@pytest.mark.parametrize("nonce_offset_seconds", [1, 3, 5, 10]) +def test_nonce_diff_seconds(nonce_offset_seconds): + mock_synapse = SynapseMock() + current_time_ns = time.time_ns() + synapse_nonce = current_time_ns - (nonce_offset_seconds * NANOSECONDS_IN_SECOND) + diff_seconds, allowed_delta_seconds = calculate_diff_seconds( + current_time_ns, mock_synapse.timeout, synapse_nonce + ) + + expected_diff_seconds = nonce_offset_seconds # Because we subtracted nonce_offset_seconds from current_time_ns + expected_allowed_delta_seconds = ( + ALLOWED_DELTA + (mock_synapse.timeout * NANOSECONDS_IN_SECOND) + ) / NANOSECONDS_IN_SECOND + + assert ( + diff_seconds == expected_diff_seconds + ), f"Expected {expected_diff_seconds} but got {diff_seconds}" + assert ( + allowed_delta_seconds == expected_allowed_delta_seconds + ), f"Expected {expected_allowed_delta_seconds} but got {allowed_delta_seconds}" + + +# Mimicking axon default_verify nonce verification +# True: Nonce is fresh, False: Nonce is old +def is_nonce_within_allowed_window(synapse_nonce, allowed_window_ns): + return not (synapse_nonce <= allowed_window_ns) + + +# Test assuming synapse timeout is the default 12 seconds +@pytest.mark.parametrize( + "nonce_offset_seconds, expected_result", + [(1, True), (3, True), (5, True), (15, True), (18, False), (19, False)], +) +def test_nonce_within_allowed_window(nonce_offset_seconds, expected_result): + mock_synapse = SynapseMock() + current_time_ns = time.time_ns() + synapse_nonce = current_time_ns - (nonce_offset_seconds * NANOSECONDS_IN_SECOND) + allowed_window_ns = allowed_nonce_window_ns(current_time_ns, mock_synapse.timeout) + + result = is_nonce_within_allowed_window(synapse_nonce, allowed_window_ns) + + assert result == expected_result, f"Expected {expected_result} but got {result}" + + @pytest.mark.parametrize( + "forward_fn_return_annotation", + [ + None, + fastapi.Response, + bittensor.StreamingSynapse, + ], + ) + async def test_streaming_synapse( + self, + http_client, + axon, + streaming_synapse_cls, + no_verify_axon, + forward_fn_return_annotation, + ): + tokens = [f"data{i}\n" for i in range(10)] + + async def streamer(send): + for token in tokens: + await send( + { + "type": "http.response.body", + "body": token.encode(), + "more_body": True, + } + ) + await send({"type": "http.response.body", "body": b"", "more_body": False}) + + async def forward_fn(synapse: streaming_synapse_cls): + return synapse.create_streaming_response(token_streamer=streamer) + + if forward_fn_return_annotation is not None: + forward_fn.__annotations__["return"] = forward_fn_return_annotation + + axon.attach(forward_fn) + + response = http_client.post_synapse(streaming_synapse_cls()) + assert (response.status_code, response.text) == (200, "".join(tokens)) + self.assert_headers( + response, + { + "content-type": "text/event-stream", + "name": "CustomStreamingSynapse", + "computed_body_hash": "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", + }, + ) diff --git a/tests/unit_tests/test_dendrite.py b/tests/unit_tests/test_dendrite.py index 0505247728..0146bb7782 100644 --- a/tests/unit_tests/test_dendrite.py +++ b/tests/unit_tests/test_dendrite.py @@ -17,14 +17,21 @@ # 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 pydantic import ValidationError -import pytest +# Standard Lib +import asyncio import typing -import bittensor from unittest.mock import MagicMock, Mock -from tests.helpers import _get_mock_wallet +# Third Party +import aiohttp +import pytest + +# Application +import bittensor +from bittensor.constants import DENDRITE_ERROR_MAPPING, DENDRITE_DEFAULT_ERROR +from bittensor.dendrite import dendrite as Dendrite from bittensor.synapse import TerminalInfo +from tests.helpers import _get_mock_wallet class SynapseDummy(bittensor.Synapse): @@ -334,3 +341,75 @@ async def test_dendrite__call__handles_http_error_response( assert synapse.axon.status_code == synapse.dendrite.status_code == status_code assert synapse.axon.status_message == synapse.dendrite.status_message == message + + +@pytest.mark.parametrize( + "exception, expected_status_code, expected_message, synapse_timeout, synapse_ip, synapse_port, request_name", + [ + ( + aiohttp.ClientConnectorError(Mock(), Mock()), + DENDRITE_ERROR_MAPPING[aiohttp.ClientConnectorError][0], + f"{DENDRITE_ERROR_MAPPING[aiohttp.ClientConnectorError][1]} at 127.0.0.1:8080/test_request", + None, + "127.0.0.1", + "8080", + "test_request_client_connector_error", + ), + ( + asyncio.TimeoutError(), + DENDRITE_ERROR_MAPPING[asyncio.TimeoutError][0], + f"{DENDRITE_ERROR_MAPPING[asyncio.TimeoutError][1]} after 5 seconds", + 5, + None, + None, + "test_request_timeout", + ), + ( + aiohttp.ClientResponseError(Mock(), Mock(), status=404), + "404", + f"{DENDRITE_ERROR_MAPPING[aiohttp.ClientResponseError][1]}: 404, message=''", + None, + None, + None, + "test_request_client_response_error", + ), + ( + Exception("Unknown error"), + DENDRITE_DEFAULT_ERROR[0], + f"{DENDRITE_DEFAULT_ERROR[1]}: Unknown error", + None, + None, + None, + "test_request_unknown_error", + ), + ], + ids=[ + "ClientConnectorError", + "TimeoutError", + "ClientResponseError", + "GenericException", + ], +) +def test_process_error_message( + exception, + expected_status_code, + expected_message, + synapse_timeout, + synapse_ip, + synapse_port, + request_name, +): + # Arrange + dendrite = Dendrite() + synapse = Mock() + + synapse.timeout = synapse_timeout + synapse.axon.ip = synapse_ip + synapse.axon.port = synapse_port + + # Act + result = dendrite.process_error_message(synapse, request_name, exception) + + # Assert + assert result.dendrite.status_code == expected_status_code + assert expected_message in result.dendrite.status_message diff --git a/tests/unit_tests/test_keyfile.py b/tests/unit_tests/test_keyfile.py index d20af809f9..0f3b69cacf 100644 --- a/tests/unit_tests/test_keyfile.py +++ b/tests/unit_tests/test_keyfile.py @@ -613,7 +613,8 @@ def test_deserialize_keypair_from_keyfile_data(keyfile_setup_teardown): def test_get_coldkey_password_from_environment(monkeypatch): password_by_wallet = { "WALLET": "password", - "my_wallet": "password", + "my_wallet": "password2", + "my-wallet": "password2", } monkeypatch.setenv("bt_cold_pw_wallet", password_by_wallet["WALLET"]) @@ -623,3 +624,20 @@ def test_get_coldkey_password_from_environment(monkeypatch): assert get_coldkey_password_from_environment(wallet) == password assert get_coldkey_password_from_environment("non_existent_wallet") is None + + +def test_keyfile_error_incorrect_password(keyfile_setup_teardown): + """ + Test case for attempting to decrypt a keyfile with an incorrect password. + """ + root_path = keyfile_setup_teardown + keyfile = bittensor.keyfile(path=os.path.join(root_path, "keyfile")) + + # Ensure the keyfile is encrypted + assert keyfile.is_encrypted() + + # Attempt to decrypt with an incorrect password + with pytest.raises(bittensor.KeyFileError) as excinfo: + keyfile.get_keypair(password="incorrect_password") + + assert "Invalid password" in str(excinfo.value) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 731285c225..b8dfc3e81b 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -269,7 +269,7 @@ def mock_add_argument(*args, **kwargs): ("localhost", "local", "localhost"), # Edge cases (None, None, None), - ("unknown", "unknown", "unknown"), + ("unknown", "unknown network", "unknown"), ], ) def test_determine_chain_endpoint_and_network( @@ -2315,3 +2315,39 @@ def test_get_remaining_arbitration_period_happy(subtensor, mocker): ) # if we change the methods logic in the future we have to be make sure the returned type is correct assert result == 1800 # 2000 - 200 + + +def test_connect_without_substrate(mocker): + """Ensure re-connection is called when using an alive substrate.""" + # Prep + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 1 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) + fake_subtensor = Subtensor() + spy_get_substrate = mocker.spy(Subtensor, "_get_substrate") + + # Call + _ = fake_subtensor.block + + # Assertions + assert spy_get_substrate.call_count == 1 + + +def test_connect_with_substrate(mocker): + """Ensure re-connection is non called when using an alive substrate.""" + # Prep + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) + fake_subtensor = Subtensor() + spy_get_substrate = mocker.spy(Subtensor, "_get_substrate") + + # Call + _ = fake_subtensor.block + + # Assertions + assert spy_get_substrate.call_count == 0 diff --git a/tests/unit_tests/utils/test_registration.py b/tests/unit_tests/utils/test_registration.py index a6861783a4..d0c4fc743b 100644 --- a/tests/unit_tests/utils/test_registration.py +++ b/tests/unit_tests/utils/test_registration.py @@ -14,7 +14,7 @@ def error(self, message): @pytest.fixture def mock_bittensor_logging(monkeypatch): mock_logger = MockBittensorLogging() - monkeypatch.setattr("bittensor.btlogging", mock_logger) + monkeypatch.setattr("bittensor.logging", mock_logger) return mock_logger