diff --git a/.circleci/config.yml b/.circleci/config.yml index fbe39a6645..74c29215b3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,7 +39,7 @@ jobs: python -m venv .venv . .venv/bin/activate python -m pip install --upgrade uv - uv pip install ruff -c requirements/dev.txt + uv pip install ruff - save_cache: name: Save cached ruff venv @@ -68,9 +68,7 @@ jobs: name: Install dependencies and Check compatibility command: | if [ "$REQUIREMENTS_CHANGED" == "true" ]; then - sudo apt-get update - sudo apt-get install -y jq curl - ./scripts/check_compatibility.sh << parameters.python_version >> + python -m pip install ".[dev,cli]" --dry-run --python-version << parameters.python_version >> --no-deps else echo "Skipping compatibility checks..." fi @@ -87,31 +85,19 @@ jobs: steps: - checkout - - restore_cache: - name: Restore cached venv - keys: - - v2-pypi-py<< parameters.python-version >>-{{ checksum "requirements/prod.txt" }}+{{ checksum "requirements/dev.txt" }} - - v2-pypi-py<< parameters.python-version >> - - run: name: Update & Activate venv command: | python -m venv .venv . .venv/bin/activate python -m pip install --upgrade uv - uv sync --all-extras --dev - - - save_cache: - name: Save cached venv - paths: - - "venv/" - key: v2-pypi-py<< parameters.python-version >>-{{ checksum "requirements/prod.txt" }}+{{ checksum "requirements/dev.txt" }} + uv sync --extra dev --dev - run: name: Install Bittensor command: | . .venv/bin/activate - uv sync --all-extras --dev + uv sync --extra dev --dev - run: name: Instantiate Mock Wallet @@ -178,32 +164,20 @@ jobs: steps: - checkout - - restore_cache: - name: Restore cached venv - keys: - - v2-pypi-py<< parameters.python-version >>-{{ checksum "requirements/prod.txt" }}+{{ checksum "requirements/dev.txt" }} - - v2-pypi-py<< parameters.python-version >> - - run: name: Update & Activate venv command: | python -m venv .venv . .venv/bin/activate python -m pip install --upgrade uv - uv sync --all-extras --dev + uv sync --extra dev --dev uv pip install flake8 - - save_cache: - name: Save cached venv - paths: - - "env/" - key: v2-pypi-py<< parameters.python-version >>-{{ checksum "requirements/prod.txt" }}+{{ checksum "requirements/dev.txt" }} - - run: name: Install Bittensor command: | . .venv/bin/activate - uv sync --all-extras --dev + uv sync --extra dev --dev - run: name: Lint with flake8 @@ -235,18 +209,6 @@ jobs: uv pip install --upgrade coveralls coveralls --finish --rcfile .coveragerc || echo "Failed to upload coverage" - check-version-updated: - docker: - - image: cimg/python:3.10 - steps: - - checkout - - - run: - name: Version is updated - command: | - [[ $(git diff-tree --no-commit-id --name-only -r HEAD..master | grep bittensor/__init__.py | wc -l) == 1 ]] && echo "bittensor/__init__.py has changed" - [[ $(git diff-tree --no-commit-id --name-only -r HEAD..master | grep VERSION | wc -l) == 1 ]] && echo "VERSION has changed" - check-changelog-updated: docker: - image: cimg/python:3.10 @@ -323,11 +285,6 @@ workflows: release-branches-requirements: jobs: - - check-version-updated: - filters: - branches: - only: - - /^(release|hotfix)/.*/ - check-changelog-updated: filters: branches: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index adff4d0aab..9ffce36ee7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,8 +1,7 @@ version: 2 updates: - package-ecosystem: "pip" - directory: "" - file: "requirements/prod.txt" + directory: "/" schedule: interval: "daily" open-pull-requests-limit: 0 # Only security updates will be opened as PRs diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index c74c5fadfe..0adcc5ffbd 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -39,6 +39,8 @@ jobs: id: get-tests run: | test_files=$(find tests/e2e_tests -name "test*.py" | jq -R -s -c 'split("\n") | map(select(. != ""))') + # keep it here for future debug + # test_files=$(find tests/e2e_tests -type f -name "test*.py" | grep -E 'test_(incentive|commit_weights|set_weights)\.py$' | jq -R -s -c 'split("\n") | map(select(. != ""))') echo "::set-output name=test-files::$test_files" shell: bash @@ -61,7 +63,7 @@ jobs: path: subtensor-localnet.tar # Job to run tests in parallel - run: + run-e2e-test: name: ${{ matrix.test-file }} / Python ${{ matrix.python-version }} needs: - find-tests @@ -70,7 +72,7 @@ jobs: timeout-minutes: 45 strategy: fail-fast: false # Allow other matrix jobs to run even if this job fails - max-parallel: 32 # Set the maximum number of parallel jobs (same as we have cores in SubtensorCI runner) + max-parallel: 32 # Set the maximum number of parallel jobs (same as we have cores in ubuntu-latest runner) matrix: os: - ubuntu-latest @@ -89,7 +91,7 @@ jobs: uses: astral-sh/setup-uv@v4 - name: install dependencies - run: uv sync --all-extras --dev + run: uv sync --extra dev --dev - name: Download Cached Docker Image uses: actions/download-artifact@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 84d7c10774..54c4b8fbdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 9.3.1a1 /2025-04-14 + +## What's Changed +* Release/9.3.0 by @ibraheem-abe in https://github.com/opentensor/bittensor/pull/2805 +* Fix for flaky behavior of `test_incentive`, `test_commit_weights` and `test_set_weights` by @basfroman in https://github.com/opentensor/bittensor/pull/2795 +* Add `get_next_epoch_start_block` method to Async/Subtensor by @basfroman in https://github.com/opentensor/bittensor/pull/2808 +* Adds compatibility for torch 2.6.0+ by @thewhaleking in https://github.com/opentensor/bittensor/pull/2811 +* f Update CONTRIBUTING.md by @Hack666r in https://github.com/opentensor/bittensor/pull/2813 +* docs: replaced discord link with documentation by @sashaphmn in https://github.com/opentensor/bittensor/pull/2809 +* sometimes it's still flaky because the chain returns data with time offset by @basfroman in https://github.com/opentensor/bittensor/pull/2816 +* Remove requirements directory by @thewhaleking in https://github.com/opentensor/bittensor/pull/2812 +* version in one place by @thewhaleking in https://github.com/opentensor/bittensor/pull/2806 +* Update CONTRIBUTING hyperlinks by @thewhaleking in https://github.com/opentensor/bittensor/pull/2820 + +## New Contributors +* @Hack666r made their first contribution in https://github.com/opentensor/bittensor/pull/2813 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.3.0...v9.3.1a1 + ## 9.3.0 /2025-04-09 ## What's Changed diff --git a/README.md b/README.md index 7dbfcbc260..8c5165a769 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## Internet-scale Neural Networks -[Discord](https://discord.gg/qasY3HA9F9) • [Network](https://taostats.io/) • [Research](https://bittensor.com/whitepaper) +[Discord](https://discord.gg/qasY3HA9F9) • [Network](https://taostats.io/) • [Research](https://bittensor.com/whitepaper) • [Documentation](https://docs.bittensor.com) diff --git a/VERSION b/VERSION deleted file mode 100644 index 4d0ffae7b5..0000000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -9.3.0 \ No newline at end of file diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 9a132f4258..7bd2a9b881 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1687,6 +1687,38 @@ async def get_neuron_for_pubkey_and_subnet( reuse_block=reuse_block, ) + async def get_next_epoch_start_block( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """ + Calculates the first block number of the next epoch for the given subnet. + + If `block` is not provided, the current chain block will be used. Epochs are + determined based on the subnet's tempo (i.e., blocks per epoch). The result + is the block number at which the next epoch will begin. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int], optional): The reference block to calculate from. + If None, uses the current chain block height. + block_hash (Optional[int]): The blockchain block number at which to perform the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. + + + Returns: + int: The block number at which the next epoch will start. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + if not block_hash and reuse_block: + block_hash = self.substrate.last_block_hash + block = await self.substrate.get_block_number(block_hash=block_hash) + tempo = await self.tempo(netuid=netuid, block_hash=block_hash) + return (((block // tempo) + 1) * tempo) + 1 if tempo else None + async def get_owned_hotkeys( self, coldkey_ss58: str, @@ -3813,6 +3845,7 @@ async def set_weights( wait_for_finalization: bool = False, max_retries: int = 5, block_time: float = 12.0, + period: int = 5, ): """ Sets the inter-neuronal weights for the specified neuron. This process involves specifying the influence or @@ -3833,6 +3866,7 @@ async def set_weights( ``False``. max_retries (int): The number of maximum attempts to set weights. Default is ``5``. block_time (float): The amount of seconds for block duration. Default is 12.0 seconds. + period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. Returns: tuple[bool, str]: ``True`` if the setting of weights is successful, False otherwise. And `msg`, a string @@ -3907,6 +3941,7 @@ async def _blocks_weight_limit() -> bool: version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) except Exception as e: logging.error(f"Error setting weights: {e}") diff --git a/bittensor/core/extrinsics/asyncex/weights.py b/bittensor/core/extrinsics/asyncex/weights.py index b2221a5263..6e07b90adb 100644 --- a/bittensor/core/extrinsics/asyncex/weights.py +++ b/bittensor/core/extrinsics/asyncex/weights.py @@ -287,6 +287,7 @@ async def set_weights_extrinsic( version_key: int = 0, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, + period: int = 5, ) -> tuple[bool, str]: """Sets the given weights and values on chain for wallet hotkey account. @@ -302,6 +303,7 @@ async def set_weights_extrinsic( 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. + period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for @@ -331,6 +333,7 @@ async def set_weights_extrinsic( version_key=version_key, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, + period=period, ) if not wait_for_finalization and not wait_for_inclusion: diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 4c1c194708..908fb2b2a9 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -91,6 +91,7 @@ def set_weights_extrinsic( version_key: int = 0, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, + period: int = 5, ) -> tuple[bool, str]: """Sets the given weights and values on chain for wallet hotkey account. @@ -106,6 +107,7 @@ def set_weights_extrinsic( 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. + period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for @@ -135,6 +137,7 @@ def set_weights_extrinsic( version_key=version_key, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, + period=period, ) if not wait_for_finalization and not wait_for_inclusion: diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 5554b32a3d..af758939ea 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -1,4 +1,5 @@ import asyncio +import contextlib import copy import os import pickle @@ -11,6 +12,7 @@ import numpy as np from async_substrate_interface.errors import SubstrateRequestException from numpy.typing import NDArray +from packaging import version from bittensor.core import settings from bittensor.core.chain_data import ( @@ -143,6 +145,27 @@ def latest_block_path(dir_path: str) -> str: return latest_file_full_path +def safe_globals(): + """ + Context manager to load torch files for version 2.6+ + """ + if version.parse(torch.__version__).release < version.parse("2.6").release: + return contextlib.nullcontext() + + np_core = ( + np._core if version.parse(np.__version__) >= version.parse("2.0.0") else np.core + ) + allow_list = [ + np_core.multiarray._reconstruct, + np.ndarray, + np.dtype, + type(np.dtype(np.uint32)), + np.dtypes.Float32DType, + bytes, + ] + return torch.serialization.safe_globals(allow_list) + + class MetagraphMixin(ABC): """ The metagraph class is a core component of the Bittensor network, representing the neural graph that forms the @@ -1124,7 +1147,8 @@ def load_from_path(self, dir_path: str) -> "MetagraphMixin": """ graph_file = latest_block_path(dir_path) - state_dict = torch.load(graph_file) + with safe_globals(): + state_dict = torch.load(graph_file) self.n = torch.nn.Parameter(state_dict["n"], requires_grad=False) self.block = torch.nn.Parameter(state_dict["block"], requires_grad=False) self.uids = torch.nn.Parameter(state_dict["uids"], requires_grad=False) @@ -1256,7 +1280,8 @@ def load_from_path(self, dir_path: str) -> "MetagraphMixin": try: import torch as real_torch - state_dict = real_torch.load(graph_filename) + with safe_globals(): + state_dict = real_torch.load(graph_filename) for key in METAGRAPH_STATE_DICT_NDARRAY_KEYS: state_dict[key] = state_dict[key].detach().numpy() del real_torch diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index d3b68c04d3..cf2c07b38d 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -1,6 +1,5 @@ -__version__ = "9.3.0" - import os +import importlib.metadata import re from pathlib import Path @@ -15,6 +14,8 @@ WALLETS_DIR = USER_BITTENSOR_DIR / "wallets" MINERS_DIR = USER_BITTENSOR_DIR / "miners" +__version__ = importlib.metadata.version("bittensor") + if not READ_ONLY: # Create dirs if they don't exist diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 19b14d09c0..4870cf5e2e 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1300,6 +1300,28 @@ def get_neuron_for_pubkey_and_subnet( return NeuronInfo.from_dict(result) + def get_next_epoch_start_block( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Calculates the first block number of the next epoch for the given subnet. + + If `block` is not provided, the current chain block will be used. Epochs are + determined based on the subnet's tempo (i.e., blocks per epoch). The result + is the block number at which the next epoch will begin. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int], optional): The reference block to calculate from. + If None, uses the current chain block height. + + Returns: + int: The block number at which the next epoch will start. + """ + block = block or self.block + tempo = self.tempo(netuid=netuid, block=block) + return (((block // tempo) + 1) * tempo) + 1 if tempo else None + def get_owned_hotkeys( self, coldkey_ss58: str, @@ -3100,6 +3122,7 @@ def set_weights( wait_for_finalization: bool = False, max_retries: int = 5, block_time: float = 12.0, + period: int = 5, ) -> tuple[bool, str]: """ Sets the inter-neuronal weights for the specified neuron. This process involves specifying the influence or @@ -3120,6 +3143,7 @@ def set_weights( ``False``. max_retries (int): The number of maximum attempts to set weights. Default is ``5``. block_time (float): The amount of seconds for block duration. Default is 12.0 seconds. + period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. Returns: tuple[bool, str]: ``True`` if the setting of weights is successful, False otherwise. And `msg`, a string @@ -3183,6 +3207,7 @@ def _blocks_weight_limit() -> bool: version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) except Exception as e: logging.error(f"Error setting weights: {e}") diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index e0da1d9c99..3d2c82803b 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -250,6 +250,16 @@ def format_error_message(error_message: Union[dict, Exception]) -> str: err_docs = error_message.get("docs", [err_description]) err_description = err_docs[0] if err_docs else err_description + elif error_message.get("code") and error_message.get("message"): + err_type = error_message.get("code", err_name) + err_name = "Custom type" + err_description = error_message.get("message", err_description) + + else: + logging.error( + f"String representation of real error_message: {str(error_message)}" + ) + return f"Subtensor returned `{err_name}({err_type})` error. This means: `{err_description}`." diff --git a/contrib/CONTRIBUTING.md b/contrib/CONTRIBUTING.md index b27b426a09..c4ac594543 100644 --- a/contrib/CONTRIBUTING.md +++ b/contrib/CONTRIBUTING.md @@ -18,7 +18,7 @@ The following is a set of guidelines for contributing to Bittensor, which are ho 1. [Refactoring](#refactoring) 1. [Peer Review](#peer-review) 1. [Reporting Bugs](#reporting-bugs) - 1. [Suggesting Features](#suggesting-enhancements) + 1. [Suggesting Features](#suggesting-enhancements-and-features) ## I don't want to read this whole thing I just have a question! @@ -70,7 +70,7 @@ And also here. You can contribute to Bittensor in one of two main ways (as well as many others): 1. [Bug](#reporting-bugs) reporting and fixes -2. New features and Bittensor [enhancements](#suggesting-enhancements) +2. New features and Bittensor [enhancements](#suggesting-enhancements-and-features) > Please follow the Bittensor [style guide](./STYLE.md) regardless of your contribution type. @@ -252,7 +252,7 @@ Explain the problem and include additional details to help maintainers reproduce * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. * **Explain which behavior you expected to see instead and why.** * **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. -* **If you're reporting that Bittensor crashed**, include a crash report with a stack trace from the operating system. On macOS, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". Include the crash report in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist. +* **If you're reporting that Bittensor crashed**, include a crash report with a stack trace from the operating system. On macOS, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". Include the crash report in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://docs.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist. * **If the problem is related to performance or memory**, include a CPU profile capture with your report, if you're using a GPU then include a GPU profile capture as well. Look into the [PyTorch Profiler](https://pytorch.org/tutorials/recipes/recipes/profiler_recipe.html) to look at memory usage of your model. * **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below. diff --git a/pyproject.toml b/pyproject.toml index 2232e33960..b9e3541575 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["setuptools~=70.0.0", "wheel"] +requires = ["setuptools>=70.0.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "bittensor" -version = "9.3.0" +version = "9.3.1a1" description = "Bittensor" readme = "README.md" authors = [ @@ -13,6 +13,7 @@ authors = [ license = { file = "LICENSE" } requires-python = ">=3.9,<3.14" dependencies = [ + "wheel", "setuptools~=70.0.0", "aiohttp~=3.9", @@ -40,11 +41,11 @@ dependencies = [ [project.optional-dependencies] dev = [ - "pytest==7.2.0", - "pytest-asyncio==0.23.7", - "pytest-mock==3.12.0", - "pytest-split==0.8.0", - "pytest-xdist==3.0.2", + "pytest==8.3.5", + "pytest-asyncio==0.26.0", + "pytest-mock==3.14.0", + "pytest-split==0.10.0", + "pytest-xdist==3.6.1", "pytest-rerunfailures==10.2", "coveralls==3.3.1", "pytest-cov==4.0.0", @@ -59,14 +60,19 @@ dev = [ "aioresponses==0.7.6", "factory-boy==3.3.0", "types-requests", - "torch>=1.13.1,<2.6.0" + "torch>=1.13.1,<3.0" ] torch = [ - "torch>=1.13.1,<2.6.0" + "torch>=1.13.1,<3.0" ] cli = [ "bittensor-cli>=9.0.2" ] +cubit = [ + "torch>=1.13.1,<3.0", + "cubit @ git+https://github.com/opentensor/cubit.git@v1.1.2" +] + [project.urls] # more details can be found here diff --git a/requirements/cli.txt b/requirements/cli.txt deleted file mode 100644 index e395b2b9c1..0000000000 --- a/requirements/cli.txt +++ /dev/null @@ -1 +0,0 @@ -bittensor-cli>=9.0.2 \ No newline at end of file diff --git a/requirements/cubit.txt b/requirements/cubit.txt deleted file mode 100644 index 5af1316836..0000000000 --- a/requirements/cubit.txt +++ /dev/null @@ -1,3 +0,0 @@ -torch>=1.13.1 -cubit>=1.1.0 -cubit @ git+https://github.com/opentensor/cubit.git diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index 77e21b0eeb..0000000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,19 +0,0 @@ -pytest==7.2.0 -pytest-asyncio==0.23.7 -pytest-mock==3.12.0 -pytest-split==0.8.0 -pytest-xdist==3.0.2 -pytest-rerunfailures==10.2 -coveralls==3.3.1 -pytest-cov==4.0.0 -ddt==1.6.0 -hypothesis==6.81.1 -flake8==7.0.0 -mypy==1.8.0 -types-retry==0.9.9.4 -freezegun==1.5.0 -httpx==0.27.0 -ruff==0.4.7 -aioresponses==0.7.6 -factory-boy==3.3.0 -types-requests \ No newline at end of file diff --git a/requirements/prod.txt b/requirements/prod.txt deleted file mode 100644 index 893d925ce9..0000000000 --- a/requirements/prod.txt +++ /dev/null @@ -1,26 +0,0 @@ -wheel -setuptools~=70.0.0 -aiohttp~=3.9 -asyncstdlib~=3.13.0 -colorama~=0.4.6 -fastapi~=0.110.1 -munch~=2.5.0 -numpy~=2.0.1 -msgpack-numpy-opentensor~=0.5.0 -nest_asyncio -netaddr -packaging -python-statemachine~=2.1 -pycryptodome>=3.18.0,<4.0.0 -pyyaml -retry -requests -rich -pydantic>=2.3, <3 -python-Levenshtein -scalecodec==1.2.11 -uvicorn -websockets>=14.1 -bittensor-commit-reveal>=0.3.1 -bittensor-wallet>=3.0.7 -async-substrate-interface>=1.0.4 diff --git a/requirements/torch.txt b/requirements/torch.txt deleted file mode 100644 index 1abaa00adc..0000000000 --- a/requirements/torch.txt +++ /dev/null @@ -1 +0,0 @@ -torch>=1.13.1,<2.6.0 diff --git a/scripts/check_compatibility.sh b/scripts/check_compatibility.sh deleted file mode 100755 index b9c89c24dd..0000000000 --- a/scripts/check_compatibility.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash - -if [ -z "$1" ]; then - echo "Please provide a Python version as an argument." - exit 1 -fi - -python_version="$1" -all_passed=true - -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -check_compatibility() { - all_supported=0 - - while read -r requirement; do - # Skip lines starting with git+ - if [[ "$requirement" == git+* ]]; then - continue - fi - - package_name=$(echo "$requirement" | awk -F'[!=<>~]' '{print $1}' | awk -F'[' '{print $1}') # Strip off brackets - echo -n "Checking $package_name... " - - url="https://pypi.org/pypi/$package_name/json" - response=$(curl -s $url) - status_code=$(curl -s -o /dev/null -w "%{http_code}" $url) - - if [ "$status_code" != "200" ]; then - echo -e "${RED}Information not available for $package_name. Failure.${NC}" - all_supported=1 - continue - fi - - classifiers=$(echo "$response" | jq -r '.info.classifiers[]') - requires_python=$(echo "$response" | jq -r '.info.requires_python') - - base_version="Programming Language :: Python :: ${python_version%%.*}" - specific_version="Programming Language :: Python :: $python_version" - - if echo "$classifiers" | grep -q "$specific_version" || echo "$classifiers" | grep -q "$base_version"; then - echo -e "${GREEN}Supported${NC}" - elif [ "$requires_python" != "null" ]; then - if echo "$requires_python" | grep -Eq "==$python_version|>=$python_version|<=$python_version"; then - echo -e "${GREEN}Supported${NC}" - else - echo -e "${RED}Not compatible with Python $python_version due to constraint $requires_python.${NC}" - all_supported=1 - fi - else - echo -e "${YELLOW}Warning: Specific version not listed, assuming compatibility${NC}" - fi - done < requirements/prod.txt - - return $all_supported -} - -echo "Checking compatibility for Python $python_version..." -check_compatibility -if [ $? -eq 0 ]; then - echo -e "${GREEN}All requirements are compatible with Python $python_version.${NC}" -else - echo -e "${RED}All requirements are NOT compatible with Python $python_version.${NC}" - all_passed=false -fi - -echo "" -if $all_passed; then - echo -e "${GREEN}All tests passed.${NC}" -else - echo -e "${RED}All tests did not pass.${NC}" - exit 1 -fi diff --git a/scripts/check_requirements_changes.sh b/scripts/check_requirements_changes.sh index 5fcd27ea3f..5b41f463c1 100755 --- a/scripts/check_requirements_changes.sh +++ b/scripts/check_requirements_changes.sh @@ -1,8 +1,8 @@ #!/bin/bash # Check if requirements files have changed in the last commit -if git diff --name-only HEAD~1 | grep -E 'requirements/prod.txt|requirements/dev.txt'; then - echo "Requirements files have changed. Running compatibility checks..." +if git diff --name-only HEAD~1 | grep -E 'pyproject.toml'; then + echo "Requirements files may have changed. Running compatibility checks..." echo 'export REQUIREMENTS_CHANGED="true"' >> $BASH_ENV else echo "Requirements files have not changed. Skipping compatibility checks..." diff --git a/tests/e2e_tests/test_commit_reveal_v3.py b/tests/e2e_tests/test_commit_reveal_v3.py index 6d45cbd94d..460c943ad1 100644 --- a/tests/e2e_tests/test_commit_reveal_v3.py +++ b/tests/e2e_tests/test_commit_reveal_v3.py @@ -96,7 +96,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle # Fetch current block and calculate next tempo for the subnet current_block = subtensor.get_current_block() - upcoming_tempo = next_tempo(current_block, tempo, netuid) + upcoming_tempo = next_tempo(current_block, tempo) logging.console.info( f"Checking if window is too low with Current block: {current_block}, next tempo: {upcoming_tempo}" ) @@ -114,7 +114,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle ) current_block = subtensor.get_current_block() latest_drand_round = subtensor.last_drand_round() - upcoming_tempo = next_tempo(current_block, tempo, netuid) + upcoming_tempo = next_tempo(current_block, tempo) logging.console.info( f"Post first wait_interval (to ensure window isnt too low): {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" ) @@ -142,7 +142,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle current_block = subtensor.get_current_block() latest_drand_round = subtensor.last_drand_round() - upcoming_tempo = next_tempo(current_block, tempo, netuid) + upcoming_tempo = next_tempo(current_block, tempo) logging.console.info( f"After setting weights: Current block: {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" ) @@ -177,6 +177,9 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle f"Latest drand round after waiting for tempo: {latest_drand_round}" ) + # for fast-block 6 seconds (drand round period) is 12 fast blocks. Let's make sure this round passed. + subtensor.wait_for_block(subtensor.block + 24) + # Fetch weights on the chain as they should be revealed now revealed_weights_ = subtensor.weights(netuid=netuid) diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index c4701ad71e..1a9f4c6016 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -1,13 +1,13 @@ -import asyncio - import numpy as np import pytest +import retry +from bittensor.utils.btlogging import logging from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit from tests.e2e_tests.utils.chain_interactions import ( sudo_set_admin_utils, sudo_set_hyperparameter_bool, - use_and_wait_for_next_nonce, + execute_and_wait_for_next_nonce, wait_epoch, ) @@ -165,10 +165,12 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall Raises: AssertionError: If any of the checks or verifications fail """ + subnet_tempo = 50 + netuid = 2 + # Wait for 2 tempos to pass as CR3 only reveals weights after 2 tempos - subtensor.wait_for_block(20) + subtensor.wait_for_block(subnet_tempo * 2 + 1) - netuid = 2 print("Testing test_commit_and_reveal_weights") # Register root as Alice assert subtensor.register_subnet(alice_wallet), "Unable to register the subnet" @@ -176,6 +178,17 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall # Verify subnet 1 created successfully assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" + # weights sensitive to epoch changes + assert sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tempo", + call_params={ + "netuid": netuid, + "tempo": subnet_tempo, + }, + ) + # Enable commit_reveal on the subnet assert sudo_set_hyperparameter_bool( local_chain, @@ -203,72 +216,67 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, ) - assert error is None - assert status is True + assert error is None and status is True, f"Failed to set rate limit: {error}" assert ( subtensor.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit == 0 ), "Failed to set weights_rate_limit" assert subtensor.weights_rate_limit(netuid=netuid) == 0 - # Commit-reveal values - uids = np.array([0], dtype=np.int64) - weights = np.array([0.1], dtype=np.float32) - salt = [18, 179, 107, 0, 165, 211, 141, 197] - weight_uids, weight_vals = convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) - - # Make a second salt - salt2 = salt.copy() - salt2[0] += 1 # Increment the first byte to produce a different commit hash - - # Make a third salt - salt3 = salt.copy() - salt3[0] += 2 # Increment the first byte to produce a different commit hash - - # Commit all three salts - async with use_and_wait_for_next_nonce(subtensor, alice_wallet): - success, message = subtensor.commit_weights( - alice_wallet, - netuid, - salt=salt, - uids=weight_uids, - weights=weight_vals, - wait_for_inclusion=False, # Don't wait for inclusion, we are testing the nonce when there is a tx in the pool - wait_for_finalization=False, + # wait while weights_rate_limit changes applied. + subtensor.wait_for_block(subnet_tempo + 1) + + # create different commited data to avoid coming into pool black list with the error + # Failed to commit weights: Subtensor returned `Custom type(1012)` error. This means: `Transaction is temporarily + # banned`.Failed to commit weights: Subtensor returned `Custom type(1012)` error. This means: `Transaction is + # temporarily banned`.` + def get_weights_and_salt(counter: int): + # Commit-reveal values + salt_ = [18, 179, 107, counter, 165, 211, 141, 197] + uids_ = np.array([0], dtype=np.int64) + weights_ = np.array([counter / 10], dtype=np.float32) + weight_uids_, weight_vals_ = convert_weights_and_uids_for_emit( + uids=uids_, weights=weights_ ) + return salt_, weight_uids_, weight_vals_ - assert success is True + logging.console.info( + f"[orange]Nonce before first commit_weights: " + f"{subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" + ) - async with use_and_wait_for_next_nonce(subtensor, alice_wallet): + # 3 time doing call if nonce wasn't updated, then raise error + @retry.retry(exceptions=Exception, tries=3, delay=1) + @execute_and_wait_for_next_nonce(subtensor=subtensor, wallet=alice_wallet) + def send_commit(salt_, weight_uids_, weight_vals_): success, message = subtensor.commit_weights( - alice_wallet, - netuid, - salt=salt2, - uids=weight_uids, - weights=weight_vals, - wait_for_inclusion=False, - wait_for_finalization=False, + wallet=alice_wallet, + netuid=netuid, + salt=salt_, + uids=weight_uids_, + weights=weight_vals_, + wait_for_inclusion=True, + wait_for_finalization=True, ) + assert success is True, message - assert success is True + # send some amount of commit weights + AMOUNT_OF_COMMIT_WEIGHTS = 3 + for call in range(AMOUNT_OF_COMMIT_WEIGHTS): + weight_uids, weight_vals, salt = get_weights_and_salt(call) - async with use_and_wait_for_next_nonce(subtensor, alice_wallet): - success, message = subtensor.commit_weights( - alice_wallet, - netuid, - salt=salt3, - uids=weight_uids, - weights=weight_vals, - wait_for_inclusion=False, - wait_for_finalization=False, - ) + send_commit(salt, weight_uids, weight_vals) - assert success is True + # let's wait for 3 (12 fast blocks) seconds between transactions + subtensor.wait_for_block(subtensor.block + 12) + + logging.console.info( + f"[orange]Nonce after third commit_weights: " + f"{subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" + ) # Wait a few blocks - await asyncio.sleep(10) # Wait for the txs to be included in the chain + subtensor.wait_for_block(subtensor.block + subtensor.tempo(netuid) * 2) # Query the WeightCommits storage map for all three salts weight_commits = subtensor.query_module( @@ -282,4 +290,6 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall assert commit_block > 0, f"Invalid block number: {commit_block}" # Check for three commits in the WeightCommits storage map - assert len(weight_commits.value) == 3, "Expected 3 weight commits" + assert ( + len(weight_commits.value) == AMOUNT_OF_COMMIT_WEIGHTS + ), "Expected exact list of weight commits" diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index 56f23ddd52..ff040b6f35 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -2,6 +2,8 @@ import pytest +from bittensor.utils.btlogging import logging + from tests.e2e_tests.utils.chain_interactions import ( root_set_subtensor_hyperparameter_values, sudo_set_admin_utils, @@ -48,11 +50,8 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa # Wait for the first epoch to pass await wait_epoch(subtensor, netuid) - # Get latest metagraph - metagraph = subtensor.metagraph(netuid) - # Get current miner/validator stats - alice_neuron = metagraph.neurons[0] + alice_neuron = subtensor.neurons(netuid=netuid)[0] assert alice_neuron.validator_permit is True assert alice_neuron.dividends == 0 @@ -62,7 +61,7 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa assert alice_neuron.consensus == 0 assert alice_neuron.rank == 0 - bob_neuron = metagraph.neurons[1] + bob_neuron = subtensor.neurons(netuid=netuid)[1] assert bob_neuron.incentive == 0 assert bob_neuron.consensus == 0 @@ -98,49 +97,67 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa assert error is None assert status is True - async with templates.miner(bob_wallet, netuid): - async with templates.validator(alice_wallet, netuid) as validator: - # wait for the Validator to process and set_weights - await asyncio.wait_for(validator.set_weights.wait(), 60) - - # Wait till new epoch - await wait_interval(tempo, subtensor, netuid) - - # Refresh metagraph - metagraph = subtensor.metagraph(netuid) - - # Get current emissions and validate that Alice has gotten tao - alice_neuron = metagraph.neurons[0] - - assert alice_neuron.validator_permit is True - assert alice_neuron.dividends == 1.0 - assert alice_neuron.stake.tao > 0 - assert alice_neuron.validator_trust > 0.99 - assert alice_neuron.incentive < 0.5 - assert alice_neuron.consensus < 0.5 - assert alice_neuron.rank < 0.5 - - bob_neuron = metagraph.neurons[1] - - assert bob_neuron.incentive > 0.5 - assert bob_neuron.consensus > 0.5 - assert bob_neuron.rank > 0.5 - assert bob_neuron.trust == 1 - - bonds = subtensor.bonds(netuid) - - assert bonds == [ - ( - 0, - [ - (0, 65535), - (1, 65535), - ], - ), - ( - 1, - [], - ), - ] - - print("✅ Passed test_incentive") + # max attempts to run miner and validator + max_attempt = 3 + while True: + try: + async with templates.miner(bob_wallet, netuid) as miner: + await asyncio.wait_for(miner.started.wait(), 60) + + async with templates.validator(alice_wallet, netuid) as validator: + # wait for the Validator to process and set_weights + await asyncio.wait_for(validator.set_weights.wait(), 60) + break + except asyncio.TimeoutError: + if max_attempt > 0: + max_attempt -= 1 + continue + raise + + # wait one tempo (fast block + subtensor.wait_for_block(subtensor.block + subtensor.tempo(netuid)) + + while True: + try: + neurons = subtensor.neurons(netuid=netuid) + logging.info(f"neurons: {neurons}") + + # Get current emissions and validate that Alice has gotten tao + alice_neuron = neurons[0] + + assert alice_neuron.validator_permit is True + assert alice_neuron.dividends == 1.0 + assert alice_neuron.stake.tao > 0 + assert alice_neuron.validator_trust > 0.99 + assert alice_neuron.incentive < 0.5 + assert alice_neuron.consensus < 0.5 + assert alice_neuron.rank < 0.5 + + bob_neuron = neurons[1] + + assert bob_neuron.incentive > 0.5 + assert bob_neuron.consensus > 0.5 + assert bob_neuron.rank > 0.5 + assert bob_neuron.trust == 1 + + bonds = subtensor.bonds(netuid) + + assert bonds == [ + ( + 0, + [ + (0, 65535), + (1, 65535), + ], + ), + ( + 1, + [], + ), + ] + + print("✅ Passed test_incentive") + break + except Exception: + subtensor.wait_for_block(subtensor.block) + continue diff --git a/tests/e2e_tests/test_set_weights.py b/tests/e2e_tests/test_set_weights.py index 8211e62aa9..2ea6305099 100644 --- a/tests/e2e_tests/test_set_weights.py +++ b/tests/e2e_tests/test_set_weights.py @@ -1,13 +1,14 @@ import numpy as np import pytest +import retry from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit from tests.e2e_tests.utils.chain_interactions import ( sudo_set_hyperparameter_bool, sudo_set_admin_utils, - use_and_wait_for_next_nonce, - wait_epoch, + execute_and_wait_for_next_nonce, ) @@ -26,7 +27,11 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) Raises: AssertionError: If any of the checks or verifications fail """ + netuids = [2, 3] + subnet_tempo = 50 + BLOCK_TIME = 0.25 # 12 for non-fast-block, 0.25 for fast block + print("Testing test_set_weights_uses_next_nonce") # Lower the network registration rate limit and cost @@ -56,6 +61,20 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) for netuid in netuids: assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" + # weights sensitive to epoch changes + assert sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tempo", + call_params={ + "netuid": netuid, + "tempo": subnet_tempo, + }, + ) + + # make sure 2 epochs are passed + subtensor.wait_for_block(subnet_tempo * 2 + 1) + # Stake to become to top neuron after the first epoch for netuid in netuids: subtensor.add_stake( @@ -83,7 +102,7 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) subtensor.weights_rate_limit(netuid=netuid) > 0 ), "Weights rate limit is below 0" - # Lower the rate limit + # Lower set weights rate limit status, error = sudo_set_admin_utils( local_chain, alice_wallet, @@ -102,35 +121,50 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) # Weights values uids = np.array([0], dtype=np.int64) - weights = np.array([0.1], dtype=np.float32) + weights = np.array([0.5], dtype=np.float32) weight_uids, weight_vals = convert_weights_and_uids_for_emit( uids=uids, weights=weights ) - # Set weights for each subnet - for netuid in netuids: - async with use_and_wait_for_next_nonce(subtensor, alice_wallet): - success, message = subtensor.set_weights( - alice_wallet, - netuid, - uids=weight_uids, - weights=weight_vals, - wait_for_inclusion=False, # Don't wait for inclusion, we are testing the nonce when there is a tx in the pool - wait_for_finalization=False, - ) + logging.console.info( + f"[orange]Nonce before first set_weights: " + f"{subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" + ) - assert success is True, message + # 3 time doing call if nonce wasn't updated, then raise error + @retry.retry(exceptions=Exception, tries=3, delay=1) + @execute_and_wait_for_next_nonce(subtensor=subtensor, wallet=alice_wallet) + def set_weights(netuid_): + success, message = subtensor.set_weights( + wallet=alice_wallet, + netuid=netuid_, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=False, + period=subnet_tempo, + ) + assert success is True, message - # Wait for the txs to be included in the chain - await wait_epoch(subtensor, netuid=netuids[-1], times=4) + logging.console.info( + f"[orange]Nonce after second set_weights: " + f"{subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" + ) + + # Set weights for each subnet + for netuid in netuids: + set_weights(netuid) for netuid in netuids: # Query the Weights storage map for all three subnets - weights = subtensor.query_module( + query = subtensor.query_module( module="SubtensorModule", name="Weights", params=[netuid, 0], # Alice should be the only UID - ).value + ) + + weights = query.value + logging.console.info(f"Weights for subnet {netuid}: {weights}") assert weights is not None, f"Weights not found for subnet {netuid}" assert weights == list( diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index d571f7ff80..c5871d3155 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -4,7 +4,8 @@ """ import asyncio -import contextlib +import functools +import time from typing import Union, Optional, TYPE_CHECKING from bittensor.utils.balance import Balance @@ -93,19 +94,18 @@ async def wait_epoch(subtensor: "Subtensor", netuid: int = 1, **kwargs): await wait_interval(tempo, subtensor, netuid, **kwargs) -def next_tempo(current_block: int, tempo: int, netuid: int) -> int: +def next_tempo(current_block: int, tempo: int) -> int: """ Calculates the next tempo block for a specific subnet. Args: current_block (int): The current block number. tempo (int): The tempo value for the subnet. - netuid (int): The unique identifier of the subnet. Returns: int: The next tempo block number. """ - return (((current_block + netuid) // tempo) + 1) * tempo + 1 + return ((current_block // tempo) + 1) * tempo + 1 async def wait_interval( @@ -127,7 +127,7 @@ async def wait_interval( next_tempo_block_start = current_block for _ in range(times): - next_tempo_block_start = next_tempo(next_tempo_block_start, tempo, netuid) + next_tempo_block_start = next_tempo(next_tempo_block_start, tempo) last_reported = None @@ -146,28 +146,49 @@ async def wait_interval( ) -@contextlib.asynccontextmanager -async def use_and_wait_for_next_nonce( - subtensor: "Subtensor", - wallet: "Wallet", - sleep: float = 0.25, - timeout: float = 15.0, +def execute_and_wait_for_next_nonce( + subtensor, wallet, sleep=0.25, timeout=60.0, max_retries=3 ): """ - ContextManager that makes sure the Nonce has been consumed after sending Extrinsic. + Decorator that ensures the nonce has been consumed after a blockchain extrinsic call. """ - nonce = subtensor.substrate.get_account_next_index(wallet.hotkey.ss58_address) + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + for attempt in range(max_retries): + start_nonce = subtensor.substrate.get_account_next_index( + wallet.hotkey.ss58_address + ) + + result = func(*args, **kwargs) + + start_time = time.time() + + while time.time() - start_time < timeout: + current_nonce = subtensor.substrate.get_account_next_index( + wallet.hotkey.ss58_address + ) + + if current_nonce != start_nonce: + logging.console.info( + f"✅ Nonce changed from {start_nonce} to {current_nonce}" + ) + return result + + logging.console.info( + f"⏳ Waiting for nonce increment. Current: {current_nonce}" + ) + time.sleep(sleep) - yield + logging.warning( + f"⚠️ Attempt {attempt + 1}/{max_retries}: Nonce did not increment." + ) + raise TimeoutError(f"❌ Nonce did not change after {max_retries} attempts.") - async def wait_for_new_nonce(): - while nonce == subtensor.substrate.get_account_next_index( - wallet.hotkey.ss58_address - ): - await asyncio.sleep(sleep) + return wrapper - await asyncio.wait_for(wait_for_new_nonce(), timeout) + return decorator # Helper to execute sudo wrapped calls on the chain diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index e10cbfa6d8..1336c66cb4 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -93,6 +93,8 @@ def __init__(self, dir, wallet, netuid): self.started = asyncio.Event() async def __aenter__(self): + env = os.environ.copy() + env["BT_LOGGING_INFO"] = "1" self.process = await asyncio.create_subprocess_exec( sys.executable, f"{self.dir}/miner.py", @@ -108,15 +110,19 @@ async def __aenter__(self): self.wallet.name, "--wallet.hotkey", "default", - env={ - "BT_LOGGING_INFO": "1", - }, + env=env, stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, ) self.__reader_task = asyncio.create_task(self._reader()) - await asyncio.wait_for(self.started.wait(), 30) + try: + await asyncio.wait_for(self.started.wait(), 60) + except asyncio.TimeoutError: + self.process.kill() + await self.process.wait() + raise RuntimeError("Miner failed to start within timeout") return self @@ -128,6 +134,14 @@ async def __aexit__(self, exc_type, exc_value, traceback): async def _reader(self): async for line in self.process.stdout: + try: + bittensor.logging.console.info( + f"[green]MINER LOG: {line.split(b'|')[-1].strip().decode()}[/blue]" + ) + except BaseException: + # skipp empty lines + pass + if b"Starting main loop" in line: self.started.set() @@ -142,6 +156,8 @@ def __init__(self, dir, wallet, netuid): self.set_weights = asyncio.Event() async def __aenter__(self): + env = os.environ.copy() + env["BT_LOGGING_INFO"] = "1" self.process = await asyncio.create_subprocess_exec( sys.executable, f"{self.dir}/validator.py", @@ -157,15 +173,19 @@ async def __aenter__(self): self.wallet.name, "--wallet.hotkey", "default", - env={ - "BT_LOGGING_INFO": "1", - }, + env=env, stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, ) self.__reader_task = asyncio.create_task(self._reader()) - await asyncio.wait_for(self.started.wait(), 30) + try: + await asyncio.wait_for(self.started.wait(), 60) + except asyncio.TimeoutError: + self.process.kill() + await self.process.wait() + raise RuntimeError("Validator failed to start within timeout") return self @@ -177,9 +197,19 @@ async def __aexit__(self, exc_type, exc_value, traceback): async def _reader(self): async for line in self.process.stdout: + try: + bittensor.logging.console.info( + f"[orange]VALIDATOR LOG: {line.split(b'|')[-1].strip().decode()}[/orange]" + ) + except BaseException: + # skipp empty lines + pass + if b"Starting validator loop." in line: + bittensor.logging.console.info("Validator started.") self.started.set() elif b"Successfully set weights and Finalized." in line: + bittensor.logging.console.info("Validator is setting weights.") self.set_weights.set() def __init__(self): diff --git a/tests/unit_tests/extrinsics/asyncex/test_weights.py b/tests/unit_tests/extrinsics/asyncex/test_weights.py index 8a297602f1..c01490055d 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_weights.py +++ b/tests/unit_tests/extrinsics/asyncex/test_weights.py @@ -169,6 +169,7 @@ async def test_set_weights_extrinsic_success_with_finalization( version_key=0, wait_for_finalization=True, wait_for_inclusion=True, + period=5, ) assert result is True assert message == "Successfully set weights and Finalized." diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 3acab650e9..37c6efc7ff 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -2688,6 +2688,7 @@ async def test_set_weights_success(subtensor, fake_wallet, mocker): wait_for_finalization=False, wait_for_inclusion=False, weights=fake_weights, + period=5, ) mocked_weights_rate_limit.assert_called_once_with(fake_netuid) assert result is True diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 513ce7ab3b..c03e158f79 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1220,6 +1220,7 @@ def test_set_weights(subtensor, mocker, fake_wallet): version_key=settings.version_as_int, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, + period=5, ) assert result == expected_result