diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index cd967da879..a32971a918 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -33,7 +33,7 @@ jobs: test-files: ${{ steps.get-tests.outputs.test-files }} steps: - name: Check-out repository under $GITHUB_WORKSPACE - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Find test files id: get-tests @@ -66,7 +66,7 @@ jobs: TARGET: ${{ matrix.rust-target }} steps: - name: Check-out repository under $GITHUB_WORKSPACE - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install dependencies run: | @@ -92,14 +92,18 @@ jobs: working-directory: ${{ github.workspace }}/subtensor run: git checkout devnet-ready + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: install dependencies + run: uv sync --all-extras --dev + - name: Run tests run: | - python3 -m pip install -e .[dev] pytest - LOCALNET_SH_PATH="${{ github.workspace }}/subtensor/scripts/localnet.sh" pytest ${{ matrix.test-file }} -s + LOCALNET_SH_PATH="${{ github.workspace }}/subtensor/scripts/localnet.sh" uv run pytest ${{ matrix.test-file }} -s - name: Retry failed tests if: failure() run: | sleep 10 - python3 -m pip install -e .[dev] pytest - LOCALNET_SH_PATH="${{ github.workspace }}/subtensor/scripts/localnet.sh" pytest ${{ matrix.test-file }} -s + LOCALNET_SH_PATH="${{ github.workspace }}/subtensor/scripts/localnet.sh" uv run pytest ${{ matrix.test-file }} -s diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8cffa9632d..3d282dbb2d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,14 +23,14 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install build wheel twine + pip install setuptools wheel twine build toml - name: Build package - run: python setup.py sdist bdist_wheel + run: python -m build --sdist --wheel --outdir dist/ - name: Check if package version already exists run: | - PACKAGE_NAME=$(python setup.py --name) + PACKAGE_NAME=$(python -c "import toml; print(toml.load('pyproject.toml')['project']['name'])") PACKAGE_VERSION=${{ github.event.inputs.version }} if twine check dist/*; then if pip install $PACKAGE_NAME==$PACKAGE_VERSION; then diff --git a/CHANGELOG.md b/CHANGELOG.md index eb66e0fa12..e28ba24627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 9.0.4 /2025-03-06 + +## What's Changed +* Release/9.0.3 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2712 +* improve `wait_for_node_start` until 20 mins by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2714 +* More E2E tests by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2678 +* fix(2715): use ChainIdentity for identities by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2718 +* Metagraph use block correctly in `_get_all_stakes_from_chain` by @thewhaleking in https://github.com/opentensor/bittensor/pull/2719 +* Integration tests for async-substrate-interface 1.0.4 compatibility by @thewhaleking in https://github.com/opentensor/bittensor/pull/2720 +* Backmerge main staging 904 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2721 +* Skip E2E test_children by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2723 +* More Subtensor unnitests by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2713 +* Change to pyproject.toml by @thewhaleking in https://github.com/opentensor/bittensor/pull/2504 +* Updates test_incentive by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2722 +* Use uv for gh actions by @thewhaleking in https://github.com/opentensor/bittensor/pull/2503 +* Bumps async substrate interface by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2725 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.0.3...v9.0.4 + ## 9.0.3 /2025-02-26 ## What's Changed diff --git a/VERSION b/VERSION index 8e055f7721..93c8cbd8ab 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -9.0.3 \ No newline at end of file +9.0.4 \ No newline at end of file diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 81304bbd29..02cc910f2a 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -5,7 +5,6 @@ from functools import partial from typing import Optional, Any, Union, Iterable, TYPE_CHECKING -import aiohttp import asyncstdlib as a import numpy as np import scalecodec @@ -28,6 +27,7 @@ decode_account_id, DynamicInfo, ) +from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.delegate_info import DelegatedInfo from bittensor.core.chain_data.utils import decode_metadata from bittensor.core.config import Config @@ -68,15 +68,14 @@ reveal_weights_extrinsic, ) from bittensor.core.metagraph import AsyncMetagraph -from bittensor.core.settings import version_as_int, TYPE_REGISTRY, DELEGATES_DETAILS_URL +from bittensor.core.settings import version_as_int, TYPE_REGISTRY from bittensor.core.types import ParamWithTypes, SubtensorMixin from bittensor.utils import ( + Certificate, decode_hex_identity_dict, format_error_message, torch, u16_normalized_float, - _decode_hex_identity_dict, - Certificate, u64_normalized_float, ) from bittensor.utils.balance import ( @@ -85,7 +84,6 @@ check_and_convert_to_balance, ) from bittensor.utils.btlogging import logging -from bittensor.utils.delegates_details import DelegatesDetails from bittensor.utils.weight_utils import generate_weight_hash if TYPE_CHECKING: @@ -1079,11 +1077,9 @@ async def get_delegate_identities( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict[str, "DelegatesDetails"]: + ) -> dict[str, ChainIdentity]: """ - Fetches delegates identities from the chain and GitHub. Preference is given to chain data, and missing info is - filled-in by the info from GitHub. At some point, we want to totally move away from fetching this info from - GitHub, but chain data is still limited in that regard. + Fetches delegates identities from the chain. Arguments: block (Optional[int]): The blockchain block number for the query. @@ -1091,63 +1087,23 @@ async def get_delegate_identities( reuse_block (bool): Whether to reuse the last-used blockchain block hash. Returns: - Dict {ss58: DelegatesDetails, ...} + Dict {ss58: ChainIdentity, ...} """ block_hash = await self.determine_block_hash(block, block_hash, reuse_block) - timeout = aiohttp.ClientTimeout(10.0) - async with aiohttp.ClientSession(timeout=timeout) as session: - identities_info, response = await asyncio.gather( - self.substrate.query_map( - module="Registry", - storage_function="IdentityOf", - block_hash=block_hash, - reuse_block_hash=reuse_block, - ), - session.get(DELEGATES_DETAILS_URL), - ) - - all_delegates_details = {} - async for ss58_address, identity in identities_info: - all_delegates_details.update( - { - decode_account_id( - ss58_address[0] - ): DelegatesDetails.from_chain_data( - decode_hex_identity_dict(identity.value["info"]) - ) - } - ) - - if response.ok: - all_delegates: dict[str, Any] = await response.json(content_type=None) - - for delegate_hotkey, delegate_details in all_delegates.items(): - delegate_info = all_delegates_details.setdefault( - delegate_hotkey, - DelegatesDetails( - display=delegate_details.get("name", ""), - web=delegate_details.get("url", ""), - additional=delegate_details.get("description", ""), - pgp_fingerprint=delegate_details.get("fingerprint", ""), - ), - ) - delegate_info.display = ( - delegate_info.display or delegate_details.get("name", "") - ) - delegate_info.web = delegate_info.web or delegate_details.get( - "url", "" - ) - delegate_info.additional = ( - delegate_info.additional - or delegate_details.get("description", "") - ) - delegate_info.pgp_fingerprint = ( - delegate_info.pgp_fingerprint - or delegate_details.get("fingerprint", "") - ) + identities = await self.substrate.query_map( + module="SubtensorModule", + storage_function="IdentitiesV2", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) - return all_delegates_details + return { + decode_account_id(ss58_address[0]): ChainIdentity.from_dict( + decode_hex_identity_dict(identity.value), + ) + async for ss58_address, identity in identities + } async def get_delegate_take( self, @@ -2424,7 +2380,7 @@ async def query_identity( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict: + ) -> Optional[ChainIdentity]: """ Queries the identity of a neuron on the Bittensor blockchain using the given key. This function retrieves detailed identity information about a specific neuron, which is a crucial aspect of the network's @@ -2455,12 +2411,16 @@ async def query_identity( block_hash=block_hash, reuse_block_hash=reuse_block, ) + if not identity_info: - return {} + return None + try: - return _decode_hex_identity_dict(identity_info) + return ChainIdentity.from_dict( + decode_hex_identity_dict(identity_info), + ) except TypeError: - return {} + return None async def recycle( self, diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 06b9a66bb2..dad65986c9 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -1400,7 +1400,7 @@ async def sync( await self._set_weights_and_bonds(subtensor=subtensor) # Fills in the stake associated attributes of a class instance from a chain response. - await self._get_all_stakes_from_chain() + await self._get_all_stakes_from_chain(block=block) # apply MetagraphInfo data to instance await self._apply_metagraph_info() @@ -1566,13 +1566,14 @@ async def _process_root_weights( ) return tensor_param - async def _get_all_stakes_from_chain(self): + async def _get_all_stakes_from_chain(self, block: int): """Fills in the stake associated attributes of a class instance from a chain response.""" try: result = await self.subtensor.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", method="get_subnet_state", params=[self.netuid], + block=block, ) if result is None: @@ -1713,7 +1714,7 @@ def sync( self._set_weights_and_bonds(subtensor=subtensor) # Fills in the stake associated attributes of a class instance from a chain response. - self._get_all_stakes_from_chain() + self._get_all_stakes_from_chain(block=block) # apply MetagraphInfo data to instance self._apply_metagraph_info() @@ -1873,13 +1874,14 @@ def _process_root_weights( ) return tensor_param - def _get_all_stakes_from_chain(self): + def _get_all_stakes_from_chain(self, block: int): """Fills in the stake associated attributes of a class instance from a chain response.""" try: result = self.subtensor.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", method="get_subnet_state", params=[self.netuid], + block=block, ) if result is None: diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 853c390d7d..53eb32266a 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -1,4 +1,4 @@ -__version__ = "9.0.3" +__version__ = "9.0.4" import os import re @@ -72,9 +72,6 @@ # Wallet ss58 address length SS58_ADDRESS_LENGTH = 48 -# Raw GitHub url for delegates registry file -DELEGATES_DETAILS_URL = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" - # Block Explorers map network to explorer url # Must all be polkadotjs explorer urls NETWORK_EXPLORER_MAP = { diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index ab7e2b5d53..406074bb12 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -5,12 +5,10 @@ from typing import TYPE_CHECKING, Any, Iterable, Optional, Union, cast import numpy as np -import requests import scalecodec from async_substrate_interface.errors import SubstrateRequestException from async_substrate_interface.types import ScaleObj from async_substrate_interface.sync_substrate import SubstrateInterface -from async_substrate_interface.utils import json from numpy.typing import NDArray from bittensor.core.async_subtensor import ProposalVoteData @@ -29,6 +27,7 @@ DelegatedInfo, decode_account_id, ) +from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.utils import decode_metadata from bittensor.core.config import Config from bittensor.core.extrinsics.commit_reveal import commit_reveal_v3_extrinsic @@ -71,16 +70,14 @@ version_as_int, SS58_FORMAT, TYPE_REGISTRY, - DELEGATES_DETAILS_URL, ) from bittensor.core.types import ParamWithTypes, SubtensorMixin from bittensor.utils import ( - torch, - format_error_message, + Certificate, decode_hex_identity_dict, + format_error_message, + torch, u16_normalized_float, - _decode_hex_identity_dict, - Certificate, u64_normalized_float, ) from bittensor.utils.balance import ( @@ -90,7 +87,6 @@ check_and_convert_to_balance, ) from bittensor.utils.btlogging import logging -from bittensor.utils.delegates_details import DelegatesDetails from bittensor.utils.weight_utils import generate_weight_hash if TYPE_CHECKING: @@ -815,62 +811,29 @@ def get_delegate_by_hotkey( def get_delegate_identities( self, block: Optional[int] = None - ) -> dict[str, "DelegatesDetails"]: + ) -> dict[str, ChainIdentity]: """ - Fetches delegates identities from the chain and GitHub. Preference is given to chain data, and missing info is - filled-in by the info from GitHub. At some point, we want to totally move away from fetching this info from - GitHub, but chain data is still limited in that regard. + Fetches delegates identities from the chain. Arguments: block (Optional[int]): The blockchain block number for the query. Returns: - Dict {ss58: DelegatesDetails, ...} + Dict {ss58: ChainIdentity, ...} """ - block_hash = self.determine_block_hash(block) - response = requests.get(DELEGATES_DETAILS_URL) - identities_info = self.substrate.query_map( - module="Registry", storage_function="IdentityOf", block_hash=block_hash - ) - - all_delegates_details = {} - for ss58_address, identity in identities_info: - all_delegates_details.update( - { - decode_account_id( - ss58_address[0] - ): DelegatesDetails.from_chain_data( - decode_hex_identity_dict(identity.value["info"]) - ) - } - ) - if response.ok: - all_delegates: dict[str, Any] = json.loads(response.content) - - for delegate_hotkey, delegate_details in all_delegates.items(): - delegate_info = all_delegates_details.setdefault( - delegate_hotkey, - DelegatesDetails( - display=delegate_details.get("name", ""), - web=delegate_details.get("url", ""), - additional=delegate_details.get("description", ""), - pgp_fingerprint=delegate_details.get("fingerprint", ""), - ), - ) - delegate_info.display = delegate_info.display or delegate_details.get( - "name", "" - ) - delegate_info.web = delegate_info.web or delegate_details.get("url", "") - delegate_info.additional = ( - delegate_info.additional or delegate_details.get("description", "") - ) - delegate_info.pgp_fingerprint = ( - delegate_info.pgp_fingerprint - or delegate_details.get("fingerprint", "") - ) + identities = self.substrate.query_map( + module="SubtensorModule", + storage_function="IdentitiesV2", + block_hash=self.determine_block_hash(block), + ) - return all_delegates_details + return { + decode_account_id(ss58_address[0]): ChainIdentity.from_dict( + decode_hex_identity_dict(identity.value), + ) + for ss58_address, identity in identities + } def get_delegate_take( self, hotkey_ss58: str, block: Optional[int] = None @@ -1843,7 +1806,9 @@ def neurons_lite( return NeuronInfoLite.list_from_dicts(result) - def query_identity(self, coldkey_ss58: str, block: Optional[int] = None) -> dict: + def query_identity( + self, coldkey_ss58: str, block: Optional[int] = None + ) -> Optional[ChainIdentity]: """ Queries the identity of a neuron on the Bittensor blockchain using the given key. This function retrieves detailed identity information about a specific neuron, which is a crucial aspect of the network's @@ -1870,12 +1835,16 @@ def query_identity(self, coldkey_ss58: str, block: Optional[int] = None) -> dict params=[coldkey_ss58], block_hash=self.determine_block_hash(block), ) + if not identity_info: - return {} + return None + try: - return _decode_hex_identity_dict(identity_info) + return ChainIdentity.from_dict( + decode_hex_identity_dict(identity_info), + ) except TypeError: - return {} + return None def recycle(self, netuid: int, block: Optional[int] = None) -> Optional[Balance]: """ diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 5fef4cf172..c116cc0f84 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -50,8 +50,7 @@ def __new__(cls, data: Union[str, dict]): return str.__new__(cls, string) -def _decode_hex_identity_dict(info_dictionary: dict[str, Any]) -> dict[str, Any]: - # TODO why does this exist alongside `decode_hex_identity_dict`? +def decode_hex_identity_dict(info_dictionary: dict[str, Any]) -> dict[str, Any]: """Decodes a dictionary of hexadecimal identities.""" decoded_info = {} for k, v in info_dictionary.items(): @@ -317,61 +316,6 @@ def is_valid_bittensor_address_or_public_key(address: Union[str, bytes]) -> bool return False -def decode_hex_identity_dict(info_dictionary) -> dict[str, Any]: - """ - Decodes hex-encoded strings in a dictionary. - - This function traverses the given dictionary, identifies hex-encoded strings, and decodes them into readable - strings. It handles nested dictionaries and lists within the dictionary. - - Args: - info_dictionary (dict): The dictionary containing hex-encoded strings to decode. - - Returns: - dict: The dictionary with decoded strings. - - Examples: - input_dict = { - ... "name": {"value": "0x6a6f686e"}, - ... "additional": [ - ... [{"data": "0x64617461"}] - ... ] - ... } - decode_hex_identity_dict(input_dict) - {'name': 'john', 'additional': [('data', 'data')]} - """ - - def get_decoded(data: str) -> Optional[str]: - """Decodes a hex-encoded string.""" - try: - return bytes.fromhex(data[2:]).decode() - except UnicodeDecodeError: - print(f"Could not decode: {key}: {item}") - - for key, value in info_dictionary.items(): - if isinstance(value, dict): - item = list(value.values())[0] - if isinstance(item, str) and item.startswith("0x"): - try: - info_dictionary[key] = get_decoded(item) - except UnicodeDecodeError: - print(f"Could not decode: {key}: {item}") - else: - info_dictionary[key] = item - if key == "additional": - additional = [] - for item in value: - additional.append( - tuple( - get_decoded(data=next(iter(sub_item.values()))) - for sub_item in item - ) - ) - info_dictionary[key] = additional - - return info_dictionary - - def validate_chain_endpoint(endpoint_url: str) -> tuple[bool, str]: """Validates if the provided endpoint URL is a valid WebSocket URL.""" parsed = urlparse(endpoint_url) diff --git a/bittensor/utils/delegates_details.py b/bittensor/utils/delegates_details.py deleted file mode 100644 index 92f3872dd2..0000000000 --- a/bittensor/utils/delegates_details.py +++ /dev/null @@ -1,44 +0,0 @@ -from dataclasses import dataclass -from typing import Any, Optional, Union - - -# TODO: consider move it to `bittensor.core.chain_data` -@dataclass -class DelegatesDetails: - display: str - additional: list[tuple[str, str]] - web: str - legal: Optional[str] = None - riot: Optional[str] = None - email: Optional[str] = None - pgp_fingerprint: Optional[str] = None - image: Optional[str] = None - twitter: Optional[str] = None - - @classmethod - def from_chain_data(cls, data: dict[str, Any]) -> "DelegatesDetails": - def decode(key: str, default: Union[Optional[str], list] = ""): - try: - if isinstance(data.get(key), dict): - value = next(data.get(key).values()) - return bytes(value[0]).decode("utf-8") - elif isinstance(data.get(key), int): - return data.get(key) - elif isinstance(data.get(key), tuple): - return bytes(data.get(key)[0]).decode("utf-8") - else: - return default - except (UnicodeDecodeError, TypeError): - return default - - return cls( - display=decode("display"), - additional=decode("additional", []), - web=decode("web"), - legal=decode("legal"), - riot=decode("riot"), - email=decode("email"), - pgp_fingerprint=decode("pgp_fingerprint", None), - image=decode("image"), - twitter=decode("twitter"), - ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..e618850dd7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,96 @@ +[build-system] +requires = ["setuptools~=70.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "bittensor" +version = "9.0.4" +description = "Bittensor" +readme = "README.md" +authors = [ + {name = "bittensor.com"} +] +license = { file = "LICENSE" } +requires-python = ">=3.9,<3.13" +dependencies = [ + "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.2.0", + "bittensor-wallet>=3.0.4", + "async-substrate-interface>=1.0.5" +] + +[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-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", + "torch>=1.13.1,<2.6.0" +] +torch = [ + "torch>=1.13.1,<2.6.0" +] + +[project.urls] +# more details can be found here +homepage = "https://github.com/opentensor/bittensor" +Repository = "https://github.com/opentensor/bittensor" + +[tool.flit.metadata] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +[tool.setuptools] +package-dir = {"bittensor" = "bittensor"} +script-files = ["bittensor/utils/certifi.sh"] \ No newline at end of file diff --git a/requirements/prod.txt b/requirements/prod.txt index 9062893efe..bcd699d8b7 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -23,4 +23,4 @@ uvicorn websockets>=14.1 bittensor-commit-reveal>=0.2.0 bittensor-wallet>=3.0.4 -async-substrate-interface>=1.0.3 +async-substrate-interface>=1.0.4 diff --git a/setup.py b/setup.py deleted file mode 100644 index 480a3f66bd..0000000000 --- a/setup.py +++ /dev/null @@ -1,85 +0,0 @@ -import codecs -import os -import pathlib -import re -from io import open -from os import path - -from setuptools import setup, find_packages - - -def read_requirements(path_): - requirements_ = [] - - with pathlib.Path(path_).open() as requirements_txt: - for line in requirements_txt: - if line.startswith("git+"): - pkg_name = re.search(r"egg=([a-zA-Z0-9_-]+)", line.strip()).group(1) - requirements_.append(pkg_name + " @ " + line.strip()) - else: - requirements_.append(line.strip()) - - return requirements_ - - -requirements = read_requirements("requirements/prod.txt") -extra_requirements_dev = read_requirements("requirements/dev.txt") -extra_requirements_cubit = read_requirements("requirements/cubit.txt") -extra_requirements_torch = read_requirements("requirements/torch.txt") -extra_requirements_cli = read_requirements("requirements/cli.txt") - -here = path.abspath(path.dirname(__file__)) - -with open(path.join(here, "README.md"), encoding="utf-8") as f: - long_description = f.read() - - -# loading version from setup.py -with codecs.open( - os.path.join(here, "bittensor/core/settings.py"), encoding="utf-8" -) as init_file: - version_match = re.search( - r"^__version__ = ['\"]([^'\"]*)['\"]", init_file.read(), re.M - ) - version_string = version_match.group(1) - -setup( - name="bittensor", - version=version_string, - description="bittensor", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/opentensor/bittensor", - author="bittensor.com", - packages=find_packages(exclude=["tests", "tests.*"]), - include_package_data=True, - package_data={ - "bittensor": ["utils/certifi.sh"], - }, - author_email="", - license="MIT", - python_requires=">=3.9", - install_requires=requirements, - extras_require={ - "dev": extra_requirements_dev + extra_requirements_torch, - "torch": extra_requirements_torch, - "cli": extra_requirements_cli, - }, - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Topic :: Software Development :: Build Tools", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Mathematics", - "Topic :: Scientific/Engineering :: Artificial Intelligence", - "Topic :: Software Development", - "Topic :: Software Development :: Libraries", - "Topic :: Software Development :: Libraries :: Python Modules", - ], -) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 9bcdf903fd..b4770ed053 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -48,7 +48,7 @@ def wait_for_node_start(process, pattern): print(line.strip()) # 10 min as timeout - if int(time.time()) - timestamp > 10 * 60: + if int(time.time()) - timestamp > 20 * 60: print("Subtensor not started in time") raise TimeoutError if pattern.search(line): diff --git a/tests/e2e_tests/test_commit_reveal_v3.py b/tests/e2e_tests/test_commit_reveal_v3.py index 473c866f87..a31063cd47 100644 --- a/tests/e2e_tests/test_commit_reveal_v3.py +++ b/tests/e2e_tests/test_commit_reveal_v3.py @@ -53,9 +53,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle ), "Unable to enable commit reveal on the subnet" # Verify commit_reveal was enabled - assert subtensor.get_subnet_hyperparameters( - netuid=netuid, - ).commit_reveal_weights_enabled, "Failed to enable commit/reveal" + assert subtensor.commit_reveal_enabled(netuid), "Failed to enable commit/reveal" logging.console.info("Commit reveal enabled") # Change the weights rate limit on the subnet diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index e94baf3d6c..cb6b7fd885 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -45,9 +45,7 @@ async def test_commit_and_reveal_weights_legacy(local_chain, subtensor, alice_wa netuid, ), "Unable to enable commit reveal on the subnet" - assert subtensor.get_subnet_hyperparameters( - netuid=netuid, - ).commit_reveal_weights_enabled, "Failed to enable commit/reveal" + assert subtensor.commit_reveal_enabled(netuid), "Failed to enable commit/reveal" assert ( subtensor.get_subnet_hyperparameters(netuid=netuid).commit_reveal_period == 1 @@ -114,11 +112,9 @@ async def test_commit_and_reveal_weights_legacy(local_chain, subtensor, alice_wa assert commit_block > 0, f"Invalid block number: {commit_block}" # Query the WeightCommitRevealInterval storage map - reveal_periods = subtensor.query_module( - module="SubtensorModule", name="RevealPeriodEpochs", params=[netuid] - ) - periods = reveal_periods - assert periods > 0, "Invalid RevealPeriodEpochs" + assert ( + subtensor.get_subnet_reveal_period_epochs(netuid) > 0 + ), "Invalid RevealPeriodEpochs" # Wait until the reveal block range await wait_epoch(subtensor, netuid) @@ -187,9 +183,7 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall netuid, ), "Unable to enable commit reveal on the subnet" - assert subtensor.get_subnet_hyperparameters( - netuid=netuid, - ).commit_reveal_weights_enabled, "Failed to enable commit/reveal" + assert subtensor.commit_reveal_enabled(netuid), "Failed to enable commit/reveal" assert ( subtensor.get_subnet_hyperparameters(netuid=netuid).commit_reveal_period == 1 diff --git a/tests/e2e_tests/test_delegate.py b/tests/e2e_tests/test_delegate.py new file mode 100644 index 0000000000..ae9c6b9cac --- /dev/null +++ b/tests/e2e_tests/test_delegate.py @@ -0,0 +1,336 @@ +import pytest + +from bittensor.core.chain_data.chain_identity import ChainIdentity +from bittensor.core.chain_data.delegate_info import DelegateInfo, DelegatedInfo +from bittensor.utils.balance import Balance +from tests.e2e_tests.utils.chain_interactions import ( + decrease_take, + increase_take, + set_identity, + sudo_set_admin_utils, +) + + +DEFAULT_DELEGATE_TAKE = 0.179995422293431 + + +def test_identity(subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Check Delegate's default identity + - Update Delegate's identity + """ + + identity = subtensor.query_identity(alice_wallet.coldkeypub.ss58_address) + + assert identity is None + + identities = subtensor.get_delegate_identities() + + assert alice_wallet.coldkey.ss58_address not in identities + + subtensor.root_register( + alice_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + identities = subtensor.get_delegate_identities() + + assert alice_wallet.coldkey.ss58_address not in identities + + success, error = set_identity( + subtensor, + alice_wallet, + name="Alice", + url="https://www.example.com", + github_repo="https://github.com/opentensor/bittensor", + description="Local Chain", + ) + + assert error == "" + assert success is True + + identity = subtensor.query_identity(alice_wallet.coldkeypub.ss58_address) + + assert identity == ChainIdentity( + additional="", + description="Local Chain", + discord="", + github="https://github.com/opentensor/bittensor", + image="", + name="Alice", + url="https://www.example.com", + ) + + identities = subtensor.get_delegate_identities() + + assert alice_wallet.coldkey.ss58_address in identities + + identity = identities[alice_wallet.coldkey.ss58_address] + + assert identity == ChainIdentity( + additional="", + description="Local Chain", + discord="", + github="https://github.com/opentensor/bittensor", + image="", + name="Alice", + url="https://www.example.com", + ) + + +def test_change_take(local_chain, subtensor, alice_wallet): + """ + Tests: + - Get default Delegate's take once registered in root subnet + - Increase and decreased Delegate's take + - Try corner cases (increase/decrease beyond allowed min/max) + """ + + success, error = decrease_take( + subtensor, + alice_wallet, + 0.1, + ) + + assert success is False + assert "`HotKeyAccountNotExists(Module)`" in error + + subtensor.root_register( + alice_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert ( + subtensor.get_delegate_take(alice_wallet.hotkey.ss58_address) + == DEFAULT_DELEGATE_TAKE + ) + + success, error = increase_take( + subtensor, + alice_wallet, + 0.5, + ) + + assert success is False + assert "`DelegateTakeTooHigh(Module)`" in error + + # increase_take but try to change from 0.18 to 0.1 + success, error = increase_take( + subtensor, + alice_wallet, + 0.1, + ) + + assert "`DelegateTakeTooLow(Module)`" in error + assert success is False + + success, error = decrease_take( + subtensor, + alice_wallet, + 0.1, + ) + + assert success is True + assert error == "" + + take = subtensor.get_delegate_take(alice_wallet.hotkey.ss58_address) + + assert take == 0.09999237048905166 + + success, error = increase_take( + subtensor, + alice_wallet, + 0.15, + ) + + assert success is False + assert "`DelegateTxRateLimitExceeded(Module)`" in error + + take = subtensor.get_delegate_take(alice_wallet.hotkey.ss58_address) + + assert take == 0.09999237048905166 + + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tx_delegate_take_rate_limit", + call_params={ + "tx_rate_limit": 0, + }, + ) + + success, error = increase_take( + subtensor, + alice_wallet, + 0.15, + ) + + assert success is True + assert error == "" + + take = subtensor.get_delegate_take(alice_wallet.hotkey.ss58_address) + + assert take == 0.14999618524452582 + + +@pytest.mark.asyncio +async def test_delegates(subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Check default Delegates + - Register Delegates + - Check if Hotkey is a Delegate + - Nominator Staking + """ + + assert subtensor.get_delegates() == [] + assert subtensor.get_delegated(alice_wallet.coldkey.ss58_address) == [] + assert subtensor.get_delegate_by_hotkey(alice_wallet.hotkey.ss58_address) is None + assert subtensor.get_delegate_by_hotkey(bob_wallet.hotkey.ss58_address) is None + + assert subtensor.is_hotkey_delegate(alice_wallet.hotkey.ss58_address) is False + assert subtensor.is_hotkey_delegate(bob_wallet.hotkey.ss58_address) is False + + subtensor.root_register( + alice_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + subtensor.root_register( + bob_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert subtensor.is_hotkey_delegate(alice_wallet.hotkey.ss58_address) is True + assert subtensor.is_hotkey_delegate(bob_wallet.hotkey.ss58_address) is True + + alice_delegate = subtensor.get_delegate_by_hotkey(alice_wallet.hotkey.ss58_address) + + assert alice_delegate == DelegateInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + owner_ss58=alice_wallet.coldkey.ss58_address, + take=DEFAULT_DELEGATE_TAKE, + validator_permits=[], + registrations=[0], + return_per_1000=Balance(0), + total_daily_return=Balance(0), + total_stake={}, + nominators={}, + ) + + bob_delegate = subtensor.get_delegate_by_hotkey(bob_wallet.hotkey.ss58_address) + + assert bob_delegate == DelegateInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + owner_ss58=bob_wallet.coldkey.ss58_address, + take=DEFAULT_DELEGATE_TAKE, + validator_permits=[], + registrations=[0], + return_per_1000=Balance(0), + total_daily_return=Balance(0), + total_stake={}, + nominators={}, + ) + + delegates = subtensor.get_delegates() + + assert delegates == [ + bob_delegate, + alice_delegate, + ] + + assert subtensor.get_delegated(bob_wallet.coldkey.ss58_address) == [] + + subtensor.add_stake( + bob_wallet, + alice_wallet.hotkey.ss58_address, + netuid=0, + amount=Balance.from_tao(10_000), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert subtensor.get_delegated(bob_wallet.coldkey.ss58_address) == [ + DelegatedInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + owner_ss58=alice_wallet.coldkey.ss58_address, + take=DEFAULT_DELEGATE_TAKE, + validator_permits=[], + registrations=[0], + return_per_1000=Balance(0), + total_daily_return=Balance(0), + netuid=0, + stake=Balance.from_tao(9_999.99995), + ), + ] + + +def test_nominator_min_required_stake(local_chain, subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Check default NominatorMinRequiredStake + - Add Stake to Nominate + - Update NominatorMinRequiredStake + - Check Nominator is removed + """ + + minimum_required_stake = subtensor.get_minimum_required_stake() + + assert minimum_required_stake == Balance(0) + + subtensor.root_register( + alice_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + subtensor.root_register( + bob_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + success = subtensor.add_stake( + alice_wallet, + bob_wallet.hotkey.ss58_address, + netuid=0, + amount=Balance.from_tao(10_000), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert success is True + + stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=0, + ) + + assert stake == Balance.from_tao(9_999.99995) + + # this will trigger clear_small_nominations + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_nominator_min_required_stake", + call_params={ + "min_stake": "100000000000000", + }, + return_error_message=True, + ) + + minimum_required_stake = subtensor.get_minimum_required_stake() + + assert minimum_required_stake == Balance.from_tao(100_000) + + stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=0, + ) + + assert stake == Balance(0) diff --git a/tests/e2e_tests/test_hotkeys.py b/tests/e2e_tests/test_hotkeys.py new file mode 100644 index 0000000000..c6b4445e41 --- /dev/null +++ b/tests/e2e_tests/test_hotkeys.py @@ -0,0 +1,152 @@ +import pytest + +from tests.e2e_tests.utils.chain_interactions import ( + set_children, + wait_epoch, +) + + +SET_CHILDREN_RATE_LIMIT = 150 + + +def test_hotkeys(subtensor, alice_wallet): + """ + Tests: + - Check if Hotkey exists + - Check if Hotkey is registered + """ + + coldkey = alice_wallet.coldkeypub.ss58_address + hotkey = alice_wallet.hotkey.ss58_address + + with pytest.raises(ValueError, match="Invalid checksum"): + subtensor.does_hotkey_exist("fake") + + assert subtensor.does_hotkey_exist(hotkey) is False + assert subtensor.get_hotkey_owner(hotkey) is None + + assert subtensor.is_hotkey_registered(hotkey) is False + assert subtensor.is_hotkey_registered_any(hotkey) is False + assert ( + subtensor.is_hotkey_registered_on_subnet( + hotkey, + netuid=1, + ) + is False + ) + + subtensor.burned_register( + alice_wallet, + netuid=1, + ) + + assert subtensor.does_hotkey_exist(hotkey) is True + assert subtensor.get_hotkey_owner(hotkey) == coldkey + + assert subtensor.is_hotkey_registered(hotkey) is True + assert subtensor.is_hotkey_registered_any(hotkey) is True + assert ( + subtensor.is_hotkey_registered_on_subnet( + hotkey, + netuid=1, + ) + is True + ) + + +@pytest.mark.skip( + reason="""The behavior of set_children changes: Instead of setting children immediately, the children will be set in the subnet epoch after a cool down period (7200 blocks). +https://github.com/opentensor/subtensor/pull/1050 +""", +) +@pytest.mark.asyncio +async def test_children(subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Get default children (empty list) + - Update children list + - Trigger rate limit + - Clear children list + """ + + subtensor.burned_register( + alice_wallet, + netuid=1, + ) + subtensor.burned_register( + bob_wallet, + netuid=1, + ) + + success, children, error = subtensor.get_children( + alice_wallet.hotkey.ss58_address, + netuid=1, + ) + + assert error == "" + assert success is True + assert children == [] + + success, error = set_children( + subtensor, + alice_wallet, + netuid=1, + children=[ + ( + 2**64 - 1, + bob_wallet.hotkey.ss58_address, + ), + ], + ) + + assert error == "" + assert success is True + + await wait_epoch(subtensor, netuid=1) + + success, children, error = subtensor.get_children( + alice_wallet.hotkey.ss58_address, + netuid=1, + ) + + assert error == "" + assert success is True + assert children == [ + ( + 1.0, + bob_wallet.hotkey.ss58_address, + ) + ] + + success, error = set_children( + subtensor, + alice_wallet, + netuid=1, + children=[], + ) + + assert "`TxRateLimitExceeded(Module)`" in error + assert success is False + + subtensor.wait_for_block(subtensor.block + SET_CHILDREN_RATE_LIMIT) + + success, error = set_children( + subtensor, + alice_wallet, + netuid=1, + children=[], + ) + + assert error == "" + assert success is True + + await wait_epoch(subtensor, netuid=1) + + success, children, error = subtensor.get_children( + alice_wallet.hotkey.ss58_address, + netuid=1, + ) + + assert error == "" + assert success is True + assert children == [] diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index 9352ddbc8e..5a404316f6 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -2,13 +2,17 @@ import pytest +from bittensor import Balance + from tests.e2e_tests.utils.chain_interactions import ( sudo_set_hyperparameter_values, wait_epoch, + sudo_set_admin_utils, ) @pytest.mark.asyncio +@pytest.mark.parametrize("local_chain", [False], indirect=True) async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wallet): """ Test the incentive mechanism and interaction of miners/validators @@ -31,6 +35,21 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa # Verify subnet created successfully assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" + # Change tempo to 10 + tempo_set = 10 + assert ( + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": netuid, "tempo": tempo_set}, + return_error_message=True, + )[0] + is True + ) + tempo = subtensor.get_subnet_hyperparameters(netuid=netuid).tempo + assert tempo_set == tempo + # Register Bob as a neuron on the subnet assert subtensor.burned_register( bob_wallet, netuid @@ -41,6 +60,27 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa len(subtensor.neurons(netuid=netuid)) == 2 ), "Alice & Bob not registered in the subnet" + # Add stake for Alice + assert subtensor.add_stake( + alice_wallet, + netuid=netuid, + amount=Balance.from_tao(1_000), + wait_for_inclusion=True, + wait_for_finalization=True, + ), "Failed to add stake for Alice" + + # Wait for the first epoch to pass + await wait_epoch(subtensor, netuid) + + # Add further stake so validator permit is activated + assert subtensor.add_stake( + alice_wallet, + netuid=netuid, + amount=Balance.from_tao(1_000), + wait_for_inclusion=True, + wait_for_finalization=True, + ), "Failed to add stake for Alice" + # Get latest metagraph metagraph = subtensor.metagraph(netuid) @@ -70,11 +110,11 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa async with templates.miner(bob_wallet, netuid): async with templates.validator(alice_wallet, netuid): - # wait for the Validator to process and set_weights + # Wait for the Validator to process and set_weights await asyncio.sleep(5) # Wait few epochs - await wait_epoch(subtensor, netuid, times=4) + await wait_epoch(subtensor, netuid, times=2) # Refresh metagraph metagraph = subtensor.metagraph(netuid) diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index cb07f699da..69da571573 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -1,9 +1,14 @@ import os.path +import re import shutil import time +from bittensor.core.chain_data.metagraph_info import MetagraphInfo from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils.chain_interactions import ANY_BALANCE + +NULL_KEY = tuple(bytearray(32)) def neuron_to_dict(neuron): @@ -171,3 +176,254 @@ def test_metagraph(subtensor, alice_wallet, bob_wallet, dave_wallet): ), "Neurons don't match after save and load" logging.console.info("✅ Passed test_metagraph") + + +def test_metagraph_info(subtensor, alice_wallet): + """ + Tests: + - Check MetagraphInfo + - Register Neuron + - Register Subnet + - Check MetagraphInfo is updated + """ + + metagraph_info = subtensor.get_metagraph_info(netuid=1, block=1) + + assert metagraph_info == MetagraphInfo( + netuid=1, + name="apex", + symbol="α", + identity=None, + network_registered_at=0, + owner_hotkey=(NULL_KEY,), + owner_coldkey=(NULL_KEY,), + block=1, + tempo=100, + last_step=0, + blocks_since_last_step=1, + subnet_emission=Balance(0), + alpha_in=Balance.from_tao(10), + alpha_out=Balance.from_tao(2), + tao_in=ANY_BALANCE, + alpha_out_emission=Balance.from_tao(1), + alpha_in_emission=Balance(0), + tao_in_emission=Balance(0), + pending_alpha_emission=Balance.from_tao(0.820004577), + pending_root_emission=Balance(0), + subnet_volume=Balance(0), + moving_price=Balance.from_tao(0.000003000), + rho=10, + kappa=32767, + min_allowed_weights=0.0, + max_weights_limit=1.0, + weights_version=0, + weights_rate_limit=100, + activity_cutoff=5000, + max_validators=64, + num_uids=1, + max_uids=256, + burn=Balance.from_tao(1), + difficulty=5.421010862427522e-13, + registration_allowed=True, + pow_registration_allowed=False, + immunity_period=4096, + min_difficulty=5.421010862427522e-13, + max_difficulty=0.25, + min_burn=Balance.from_tao(0.0005), + max_burn=Balance.from_tao(100), + adjustment_alpha=0.0, + adjustment_interval=100, + target_regs_per_interval=2, + max_regs_per_block=1, + serving_rate_limit=50, + commit_reveal_weights_enabled=False, + commit_reveal_period=1, + liquid_alpha_enabled=False, + alpha_high=0.9000076295109484, + alpha_low=0.7000076295109483, + bonds_moving_avg=4.87890977618477e-14, + hotkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], + coldkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], + identities={}, + axons=( + { + "block": 0, + "version": 0, + "ip": 0, + "port": 0, + "ip_type": 0, + "protocol": 0, + "placeholder1": 0, + "placeholder2": 0, + }, + ), + active=(True,), + validator_permit=(False,), + pruning_score=[0.0], + last_update=(0,), + emission=[Balance(0)], + dividends=[0.0], + incentives=[0.0], + consensus=[0.0], + trust=[0.0], + rank=[0.0], + block_at_registration=(0,), + alpha_stake=[ANY_BALANCE], + tao_stake=[Balance(0)], + total_stake=[ANY_BALANCE], + tao_dividends_per_hotkey=[ + ("5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", Balance(0)) + ], + alpha_dividends_per_hotkey=[ + ("5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", Balance(0)) + ], + ) + + metagraph_infos = subtensor.get_all_metagraphs_info(block=1) + + assert metagraph_infos == [ + MetagraphInfo( + netuid=0, + name="root", + symbol="Τ", + identity=None, + network_registered_at=0, + owner_hotkey=(NULL_KEY,), + owner_coldkey=(NULL_KEY,), + block=1, + tempo=100, + last_step=0, + blocks_since_last_step=1, + subnet_emission=Balance(0), + alpha_in=Balance(0), + alpha_out=Balance(0), + tao_in=Balance(0), + alpha_out_emission=Balance(0), + alpha_in_emission=Balance(0), + tao_in_emission=Balance(0), + pending_alpha_emission=Balance(0), + pending_root_emission=Balance(0), + subnet_volume=Balance(0), + moving_price=Balance(0), + rho=10, + kappa=32767, + min_allowed_weights=0.0, + max_weights_limit=1.0, + weights_version=0, + weights_rate_limit=100, + activity_cutoff=5000, + max_validators=64, + num_uids=0, + max_uids=64, + burn=Balance.from_tao(1), + difficulty=5.421010862427522e-13, + registration_allowed=True, + pow_registration_allowed=False, + immunity_period=4096, + min_difficulty=5.421010862427522e-13, + max_difficulty=0.25, + min_burn=Balance.from_tao(0.0005), + max_burn=Balance.from_tao(100), + adjustment_alpha=0.0, + adjustment_interval=100, + target_regs_per_interval=1, + max_regs_per_block=1, + serving_rate_limit=50, + commit_reveal_weights_enabled=False, + commit_reveal_period=1, + liquid_alpha_enabled=False, + alpha_high=0.9000076295109484, + alpha_low=0.7000076295109483, + bonds_moving_avg=4.87890977618477e-14, + hotkeys=[], + coldkeys=[], + identities={}, + axons=(), + active=(), + validator_permit=(), + pruning_score=[], + last_update=(), + emission=[], + dividends=[], + incentives=[], + consensus=[], + trust=[], + rank=[], + block_at_registration=(), + alpha_stake=[], + tao_stake=[], + total_stake=[], + tao_dividends_per_hotkey=[], + alpha_dividends_per_hotkey=[], + ), + metagraph_info, + ] + + subtensor.burned_register( + alice_wallet, + netuid=1, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + metagraph_info = subtensor.get_metagraph_info(netuid=1) + + assert metagraph_info.num_uids == 2 + assert metagraph_info.hotkeys == [ + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + alice_wallet.hotkey.ss58_address, + ] + assert metagraph_info.coldkeys == [ + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + alice_wallet.coldkey.ss58_address, + ] + assert metagraph_info.tao_dividends_per_hotkey == [ + ("5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", Balance(0)), + (alice_wallet.hotkey.ss58_address, Balance(0)), + ] + assert metagraph_info.alpha_dividends_per_hotkey == [ + ("5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", Balance(0)), + (alice_wallet.hotkey.ss58_address, Balance(0)), + ] + + subtensor.register_subnet( + alice_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + block = subtensor.get_current_block() + metagraph_info = subtensor.get_metagraph_info(netuid=2, block=block) + + assert metagraph_info.owner_coldkey == (tuple(alice_wallet.hotkey.public_key),) + assert metagraph_info.owner_hotkey == (tuple(alice_wallet.coldkey.public_key),) + + metagraph_infos = subtensor.get_all_metagraphs_info(block) + + assert len(metagraph_infos) == 3 + assert metagraph_infos[-1] == metagraph_info + + metagraph_info = subtensor.get_metagraph_info(netuid=3) + + assert metagraph_info is None + + +def test_blocks(subtensor): + """ + Tests: + - Get current block + - Get block hash + - Wait for block + """ + + block = subtensor.get_current_block() + + assert block == subtensor.block + + block_hash = subtensor.get_block_hash(block) + + assert re.match("0x[a-z0-9]{64}", block_hash) + + subtensor.wait_for_block(block + 10) + + assert subtensor.get_current_block() == block + 10 diff --git a/tests/e2e_tests/test_neuron_certificate.py b/tests/e2e_tests/test_neuron_certificate.py index 674a4ce27c..8ada77dd35 100644 --- a/tests/e2e_tests/test_neuron_certificate.py +++ b/tests/e2e_tests/test_neuron_certificate.py @@ -1,9 +1,6 @@ import pytest from bittensor.core.axon import Axon from bittensor.utils.btlogging import logging -from tests.e2e_tests.utils.chain_interactions import ( - wait_interval, -) @pytest.mark.asyncio @@ -35,9 +32,13 @@ async def test_neuron_certificate(subtensor, alice_wallet): # Serve Alice's axon with a certificate axon = Axon(wallet=alice_wallet) encoded_certificate = "?FAKE_ALICE_CERT" - axon.serve(netuid=netuid, subtensor=subtensor, certificate=encoded_certificate) - - await wait_interval(tempo=1, subtensor=subtensor, netuid=netuid) + subtensor.serve_axon( + netuid, + axon, + certificate=encoded_certificate, + wait_for_inclusion=True, + wait_for_finalization=True, + ) # Verify we are getting the correct certificate assert ( diff --git a/tests/e2e_tests/test_set_weights.py b/tests/e2e_tests/test_set_weights.py index 61145b5fc2..d124238b5f 100644 --- a/tests/e2e_tests/test_set_weights.py +++ b/tests/e2e_tests/test_set_weights.py @@ -77,9 +77,9 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) netuid, ), "Unable to enable commit reveal on the subnet" - assert not subtensor.get_subnet_hyperparameters( - netuid=netuid, - ).commit_reveal_weights_enabled, "Failed to enable commit/reveal" + assert not subtensor.commit_reveal_enabled( + netuid, + ), "Failed to enable commit/reveal" assert ( subtensor.weights_rate_limit(netuid=netuid) > 0 @@ -96,6 +96,7 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) assert ( subtensor.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit == 0 ), "Failed to set weights_rate_limit" + assert subtensor.get_hyperparameter("WeightsSetRateLimit", netuid) == 0 assert subtensor.weights_rate_limit(netuid=netuid) == 0 # Weights values diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py new file mode 100644 index 0000000000..64d61f466d --- /dev/null +++ b/tests/e2e_tests/test_staking.py @@ -0,0 +1,247 @@ +import pytest + +from bittensor.core.chain_data.stake_info import StakeInfo +from bittensor.utils.balance import Balance +from tests.e2e_tests.utils.chain_interactions import ANY_BALANCE + + +def test_single_operation(subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Staking using `add_stake` + - Unstaking using `unstake` + - Checks StakeInfo + """ + + subtensor.burned_register( + alice_wallet, + netuid=1, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + subtensor.burned_register( + bob_wallet, + netuid=1, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=1, + ) + + assert stake == Balance(0) + + success = subtensor.add_stake( + alice_wallet, + bob_wallet.hotkey.ss58_address, + netuid=1, + amount=Balance.from_tao(10_000), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert success is True + + stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=1, + ) + + assert stake > Balance(0) + + stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) + + assert stakes == [ + StakeInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=1, + stake=stake, + locked=Balance(0), + emission=Balance(0), + drain=0, + is_registered=True, + ), + ] + + stakes = subtensor.get_stake_info_for_coldkey(alice_wallet.coldkey.ss58_address) + + assert stakes == [ + StakeInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=1, + stake=stake, + locked=Balance(0), + emission=Balance(0), + drain=0, + is_registered=True, + ), + ] + + stakes = subtensor.get_stake_for_coldkey_and_hotkey( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + ) + + assert stakes == { + 0: StakeInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=0, + stake=Balance(0), + locked=Balance(0), + emission=Balance(0), + drain=0, + is_registered=False, + ), + 1: StakeInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=1, + stake=stake, + locked=Balance.from_tao(0, netuid=1), + emission=Balance.from_tao(0, netuid=1), + drain=0, + is_registered=True, + ), + } + + success = subtensor.unstake( + alice_wallet, + bob_wallet.hotkey.ss58_address, + netuid=1, + amount=stake, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert success is True + + stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=1, + ) + + assert stake == Balance(0) + + +@pytest.mark.skip( + reason="add_stake_multiple and unstake_multiple doesn't return (just hangs)", +) +def test_batch_operations(subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Staking using `add_stake_multiple` + - Unstaking using `unstake_multiple` + - Checks StakeInfo + - Checks Accounts Balance + """ + + netuids = [ + 2, + 3, + ] + + for _ in netuids: + subtensor.register_subnet( + alice_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + for netuid in netuids: + subtensor.burned_register( + bob_wallet, + netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + for netuid in netuids: + stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + + assert stake == Balance(0), f"netuid={netuid} stake={stake}" + + balances = subtensor.get_balances( + alice_wallet.coldkey.ss58_address, + bob_wallet.coldkey.ss58_address, + ) + + assert balances == { + alice_wallet.coldkey.ss58_address: ANY_BALANCE, + bob_wallet.coldkey.ss58_address: Balance.from_tao(999_998), + } + + alice_balance = balances[alice_wallet.coldkey.ss58_address] + + success = subtensor.add_stake_multiple( + alice_wallet, + hotkey_ss58s=[bob_wallet.hotkey.ss58_address for _ in netuids], + netuids=netuids, + amounts=[Balance.from_tao(10_000) for _ in netuids], + ) + + assert success is True + + stakes = [ + subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + for netuid in netuids + ] + + for netuid, stake in zip(netuids, stakes): + assert stake > Balance(0), f"netuid={netuid} stake={stake}" + + alice_balance -= len(netuids) * Balance.from_tao(10_000) + + balances = subtensor.get_balances( + alice_wallet.coldkey.ss58_address, + bob_wallet.coldkey.ss58_address, + ) + + assert balances == { + alice_wallet.coldkey.ss58_address: alice_balance, + bob_wallet.coldkey.ss58_address: Balance.from_tao(999_998), + } + + success = subtensor.unstake_multiple( + alice_wallet, + hotkey_ss58s=[bob_wallet.hotkey.ss58_address for _ in netuids], + netuids=netuids, + amounts=[Balance.from_tao(100) for _ in netuids], + ) + + assert success is True + + for netuid, old_stake in zip(netuids, stakes): + stake = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + + assert stake < old_stake, f"netuid={netuid} stake={stake}" + + balances = subtensor.get_balances( + alice_wallet.coldkey.ss58_address, + bob_wallet.coldkey.ss58_address, + ) + + assert balances == { + alice_wallet.coldkey.ss58_address: ANY_BALANCE, + bob_wallet.coldkey.ss58_address: Balance.from_tao(999_998), + } + assert balances[alice_wallet.coldkey.ss58_address] > alice_balance diff --git a/tests/e2e_tests/test_subnets.py b/tests/e2e_tests/test_subnets.py new file mode 100644 index 0000000000..e9031d56c5 --- /dev/null +++ b/tests/e2e_tests/test_subnets.py @@ -0,0 +1,34 @@ +def test_subnets(subtensor, alice_wallet): + """ + Tests: + - Querying subnets + - Filtering subnets + - Checks default TxRateLimit + """ + + subnets = subtensor.all_subnets() + + assert len(subnets) == 2 + + subtensor.register_subnet( + alice_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + subnets = subtensor.all_subnets() + + assert len(subnets) == 3 + + netuids = subtensor.filter_netuids_by_registered_hotkeys( + all_netuids=[0, 1, 2], + filter_for_netuids=[2], + all_hotkeys=[alice_wallet], + block=subtensor.block, + ) + + assert netuids == [2] + + tx_rate_limit = subtensor.tx_rate_limit() + + assert tx_rate_limit == 1000 diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 6cf1d50bd6..b80548041b 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -4,6 +4,7 @@ """ import asyncio +import unittest.mock from typing import Union, Optional, TYPE_CHECKING from bittensor.utils.btlogging import logging @@ -15,6 +16,12 @@ from async_substrate_interface import SubstrateInterface, ExtrinsicReceipt +ANY_BALANCE = unittest.mock.Mock( + rao=unittest.mock.ANY, + unit=unittest.mock.ANY, +) + + def sudo_set_hyperparameter_bool( substrate: "SubstrateInterface", wallet: "Wallet", @@ -211,3 +218,83 @@ async def root_set_subtensor_hyperparameter_values( return response.is_success, response.error_message return response.is_success, "" + + +def set_children(subtensor, wallet, netuid, children): + return subtensor.sign_and_send_extrinsic( + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_children", + call_params={ + "children": children, + "hotkey": wallet.hotkey.ss58_address, + "netuid": netuid, + }, + ), + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +def increase_take(subtensor, wallet, take): + return subtensor.sign_and_send_extrinsic( + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="increase_take", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "take": int(take * 0xFFFF), # u16 representation of the take + }, + ), + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +def decrease_take(subtensor, wallet, take): + return subtensor.sign_and_send_extrinsic( + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="decrease_take", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "take": int(take * 0xFFFF), # u16 representation of the take + }, + ), + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +def set_identity( + subtensor, + wallet, + name="", + url="", + github_repo="", + image="", + discord="", + description="", + additional="", +): + return subtensor.sign_and_send_extrinsic( + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_identity", + call_params={ + "name": name, + "url": url, + "github_repo": github_repo, + "image": image, + "discord": discord, + "description": description, + "additional": additional, + }, + ), + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) diff --git a/tests/helpers/registry b/tests/helpers/registry deleted file mode 100644 index fe9ece3b0a..0000000000 Binary files a/tests/helpers/registry and /dev/null differ diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index 7ed3c6efa8..46cb382671 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -1,7 +1,4 @@ -import os.path - import pytest -from bt_decode import PortableRegistry, MetadataV15 from bittensor import NeuronInfo from bittensor.core.chain_data.axon_info import AxonInfo @@ -24,18 +21,11 @@ async def prepare_test(mocker, seed): """ Helper function: sets up the test environment. """ - with open( - os.path.join(os.path.dirname(__file__), "..", "helpers", "registry"), "rb" - ) as f: - metadata_v15 = MetadataV15.decode_from_metadata_option(f.read()) - registry = PortableRegistry.from_metadata_v15(metadata_v15) mocker.patch( "async_substrate_interface.sync_substrate.connect", mocker.Mock(return_value=FakeWebsocket(seed=seed)), ) subtensor = Subtensor("unknown", _mock=True) - subtensor.substrate.metadata_v15 = metadata_v15 - mocker.patch.object(subtensor.substrate, "registry", registry) return subtensor diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py index 21395735fc..34788a2768 100644 --- a/tests/unit_tests/extrinsics/test_root.py +++ b/tests/unit_tests/extrinsics/test_root.py @@ -152,10 +152,12 @@ def test_set_root_weights_extrinsic( mocker, ): # Preps - root._do_set_root_weights = mocker.Mock( - return_value=(expected_success, "Mock error") + mocker.patch.object( + root, "_do_set_root_weights", return_value=(expected_success, "Mock error") ) - root._get_limits = mocker.Mock( + mocker.patch.object( + root, + "_get_limits", return_value=(0, 1), ) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index becc9776b8..be95739324 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -8,6 +8,7 @@ from bittensor import u64_normalized_float from bittensor.core import async_subtensor from bittensor.core.async_subtensor import AsyncSubtensor +from bittensor.core.chain_data.chain_identity import ChainIdentity from bittensor.core.chain_data.stake_info import StakeInfo from bittensor.core.chain_data import proposal_vote_data from bittensor.utils.balance import Balance @@ -56,7 +57,7 @@ def test_decode_ss58_tuples_in_proposal_vote_data(mocker): def test_decode_hex_identity_dict_with_non_tuple_value(): """Tests _decode_hex_identity_dict when value is not a tuple.""" info_dict = {"info": "regular_string"} - result = async_subtensor._decode_hex_identity_dict(info_dict) + result = async_subtensor.decode_hex_identity_dict(info_dict) assert result["info"] == "regular_string" @@ -1346,17 +1347,19 @@ async def test_query_identity_successful(subtensor, mocker): # Preps fake_coldkey_ss58 = "test_key" fake_block_hash = "block_hash" - fake_identity_info = {"info": {"stake": (b"\x01\x02",)}} + fake_identity_info = { + "additional": "Additional", + "description": "Description", + "discord": "", + "github_repo": "https://github.com/opentensor/bittensor", + "image": "", + "name": "Name", + "url": "https://www.example.com", + } mocked_query = mocker.AsyncMock(return_value=fake_identity_info) subtensor.substrate.query = mocked_query - mocker.patch.object( - async_subtensor, - "_decode_hex_identity_dict", - return_value={"stake": "01 02"}, - ) - # Call result = await subtensor.query_identity( coldkey_ss58=fake_coldkey_ss58, block_hash=fake_block_hash @@ -1370,7 +1373,15 @@ async def test_query_identity_successful(subtensor, mocker): block_hash=fake_block_hash, reuse_block_hash=False, ) - assert result == {"stake": "01 02"} + assert result == ChainIdentity( + additional="Additional", + description="Description", + discord="", + github="https://github.com/opentensor/bittensor", + image="", + name="Name", + url="https://www.example.com", + ) @pytest.mark.asyncio @@ -1393,7 +1404,7 @@ async def test_query_identity_no_info(subtensor, mocker): block_hash=None, reuse_block_hash=False, ) - assert result == {} + assert result is None @pytest.mark.asyncio @@ -1408,7 +1419,7 @@ async def test_query_identity_type_error(subtensor, mocker): mocker.patch.object( async_subtensor, - "_decode_hex_identity_dict", + "decode_hex_identity_dict", side_effect=TypeError, ) @@ -1423,7 +1434,7 @@ async def test_query_identity_type_error(subtensor, mocker): block_hash=None, reuse_block_hash=False, ) - assert result == {} + assert result is None @pytest.mark.asyncio @@ -2012,34 +2023,40 @@ async def test_get_delegate_identities(subtensor, mocker): """Tests get_delegate_identities with successful data retrieval from both chain and GitHub.""" # Preps fake_block_hash = "block_hash" - fake_chain_data = mocker.AsyncMock( - return_value=[ - ( - ["delegate1_ss58"], - mocker.Mock(value={"info": {"name": "Chain Delegate 1"}}), + fake_chain_data = [ + ( + ["delegate1_ss58"], + mocker.Mock( + value={ + "additional": "", + "description": "", + "discord": "", + "github_repo": "", + "image": "", + "name": "Chain Delegate 1", + "url": "", + }, ), - ( - ["delegate2_ss58"], - mocker.Mock(value={"info": {"name": "Chain Delegate 2"}}), + ), + ( + ["delegate2_ss58"], + mocker.Mock( + value={ + "additional": "", + "description": "", + "discord": "", + "github_repo": "", + "image": "", + "name": "Chain Delegate 2", + "url": "", + }, ), - ] - ) - fake_github_data = { - "delegate1_ss58": { - "name": "GitHub Delegate 1", - "url": "https://delegate1.com", - "description": "GitHub description 1", - "fingerprint": "fingerprint1", - }, - "delegate3_ss58": { - "name": "GitHub Delegate 3", - "url": "https://delegate3.com", - "description": "GitHub description 3", - "fingerprint": "fingerprint3", - }, - } + ), + ] - mocked_query_map = mocker.AsyncMock(return_value=fake_chain_data) + mocked_query_map = mocker.AsyncMock( + **{"return_value.__aiter__.return_value": iter(fake_chain_data)}, + ) subtensor.substrate.query_map = mocked_query_map mocked_decode_account_id = mocker.Mock(side_effect=lambda ss58: ss58) @@ -2050,27 +2067,19 @@ async def test_get_delegate_identities(subtensor, mocker): async_subtensor, "decode_hex_identity_dict", mocked_decode_hex_identity_dict ) - mock_response = mocker.Mock() - mock_response.ok = True - mock_response.json = mocker.AsyncMock(return_value=fake_github_data) - - mock_session_get = mocker.AsyncMock(return_value=mock_response) - mocker.patch("aiohttp.ClientSession.get", mock_session_get) - # Call result = await subtensor.get_delegate_identities(block_hash=fake_block_hash) # Asserts mocked_query_map.assert_called_once_with( - module="Registry", - storage_function="IdentityOf", + module="SubtensorModule", + storage_function="IdentitiesV2", block_hash=fake_block_hash, reuse_block_hash=False, ) - mock_session_get.assert_called_once_with(async_subtensor.DELEGATES_DETAILS_URL) - assert result["delegate1_ss58"].display == "GitHub Delegate 1" - assert result["delegate3_ss58"].display == "GitHub Delegate 3" + assert result["delegate1_ss58"].name == "Chain Delegate 1" + assert result["delegate2_ss58"].name == "Chain Delegate 2" @pytest.mark.asyncio diff --git a/tests/unit_tests/test_subtensor_extended.py b/tests/unit_tests/test_subtensor_extended.py new file mode 100644 index 0000000000..ce33db130c --- /dev/null +++ b/tests/unit_tests/test_subtensor_extended.py @@ -0,0 +1,1533 @@ +import unittest.mock +from typing import Optional + +import pytest + +import bittensor.core.subtensor +from bittensor.core.chain_data.axon_info import AxonInfo +from bittensor.core.chain_data.chain_identity import ChainIdentity +from bittensor.core.chain_data.delegate_info import DelegatedInfo, DelegateInfo +from bittensor.core.chain_data.dynamic_info import DynamicInfo +from bittensor.core.chain_data.neuron_info import NeuronInfo +from bittensor.core.chain_data.neuron_info_lite import NeuronInfoLite +from bittensor.core.chain_data.prometheus_info import PrometheusInfo +from bittensor.core.chain_data.stake_info import StakeInfo +from bittensor.utils.balance import Balance + + +def assert_submit_signed_extrinsic( + substrate, + keypair, + call_module, + call_function, + call_params: Optional[dict] = None, + era: Optional[dict] = None, + nonce: Optional[int] = None, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, +): + substrate.compose_call.assert_called_with( + call_module, + call_function, + call_params, + ) + + extrinsic = { + "call": substrate.compose_call.return_value, + "keypair": keypair, + } + + if era: + extrinsic["era"] = era + + if nonce: + extrinsic["nonce"] = nonce + + substrate.create_signed_extrinsic.assert_called_with( + **extrinsic, + ) + + substrate.submit_extrinsic.assert_called_with( + substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + +@pytest.fixture +def mock_substrate(): + with unittest.mock.patch( + "bittensor.core.subtensor.SubstrateInterface", + autospec=True, + ) as mocked: + yield mocked.return_value + + +@pytest.fixture +def subtensor(mock_substrate): + return bittensor.core.subtensor.Subtensor() + + +@pytest.fixture +def wallet(): + return unittest.mock.Mock() + + +@pytest.fixture +def mock_delegate_info(): + return { + "delegate_ss58": tuple(bytearray(32)), + "total_stake": {}, + "nominators": [], + "owner_ss58": tuple(bytearray(32)), + "take": 2**16 - 1, + "validator_permits": [], + "registrations": [], + "return_per_1000": 2, + "total_daily_return": 3, + } + + +@pytest.fixture +def mock_dynamic_info(): + return { + "netuid": 0, + "owner_hotkey": tuple(bytearray(32)), + "owner_coldkey": tuple(bytearray(32)), + "subnet_name": (114, 111, 111, 116), + "token_symbol": (206, 164), + "tempo": 100, + "last_step": 4919910, + "blocks_since_last_step": 84234, + "emission": 0, + "alpha_in": 14723086336554, + "alpha_out": 6035890271491007, + "tao_in": 6035892206947246, + "alpha_out_emission": 0, + "alpha_in_emission": 0, + "tao_in_emission": 0, + "pending_alpha_emission": 0, + "pending_root_emission": 0, + "subnet_volume": 2240411565906691, + "network_registered_at": 0, + "subnet_identity": None, + "moving_price": {"bits": 0}, + } + + +@pytest.fixture +def mock_neuron_info(): + return { + "active": 0, + "axon_info": { + "ip_type": 4, + "ip": 2130706433, + "placeholder1": 0, + "placeholder2": 0, + "port": 8080, + "protocol": 0, + "version": 1, + }, + "bonds": [], + "coldkey": tuple(bytearray(32)), + "consensus": 0.0, + "dividends": 0.0, + "emission": 0.0, + "hotkey": tuple(bytearray(32)), + "incentive": 0.0, + "is_null": False, + "last_update": 0, + "netuid": 1, + "prometheus_info": { + "block": 0, + "ip_type": 0, + "ip": 0, + "port": 0, + "version": 1, + }, + "pruning_score": 0.0, + "rank": 0.0, + "stake_dict": {}, + "stake": [], + "total_stake": 1e12, + "trust": 0.0, + "uid": 1, + "validator_permit": True, + "validator_trust": 0.0, + "weights": [], + } + + +def test_all_subnets(mock_substrate, subtensor, mock_dynamic_info): + mock_substrate.runtime_call.return_value.decode.return_value = [ + mock_dynamic_info, + ] + + result = subtensor.all_subnets() + + assert result == [ + DynamicInfo( + netuid=0, + owner_hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + owner_coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + subnet_name="root", + symbol="Τ", + tempo=100, + last_step=4919910, + blocks_since_last_step=84234, + emission=Balance(0), + alpha_in=Balance(14723086336554), + alpha_out=Balance(6035890271491007), + tao_in=Balance(6035892206947246), + price=Balance.from_tao(1), + k=88866962081017766138079430284, + is_dynamic=False, + alpha_out_emission=Balance(0), + alpha_in_emission=Balance(0), + tao_in_emission=Balance(0), + pending_alpha_emission=Balance(0), + pending_root_emission=Balance(0), + network_registered_at=0, + subnet_volume=Balance(2240411565906691), + subnet_identity=None, + moving_price=0.0, + ), + ] + + mock_substrate.runtime_call.assert_called_once_with( + "SubnetInfoRuntimeApi", + "get_all_dynamic_info", + block_hash=None, + ) + + +def test_bonds(mock_substrate, subtensor, mocker): + mock_substrate.query_map.return_value = [ + (0, mocker.Mock(value=[(1, 100), (2, 200)])), + (1, mocker.Mock(value=[(0, 150), (2, 250)])), + (2, mocker.Mock(value=None)), + ] + + result = subtensor.bonds(netuid=1) + + assert result == [ + (0, [(1, 100), (2, 200)]), + (1, [(0, 150), (2, 250)]), + ] + + mock_substrate.query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="Bonds", + params=[1], + block_hash=None, + ) + + +def test_burned_register(mock_substrate, subtensor, wallet, mocker): + mocker.patch.object( + subtensor, + "get_neuron_for_pubkey_and_subnet", + return_value=NeuronInfo.get_null_neuron(), + ) + mocker.patch.object(subtensor, "get_balance") + + success = subtensor.burned_register( + wallet, + netuid=1, + ) + + assert success is True + + subtensor.get_neuron_for_pubkey_and_subnet.assert_called_once_with( + wallet.hotkey.ss58_address, + netuid=1, + block=mock_substrate.get_block_number.return_value, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + wallet.coldkey, + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": 1, + "hotkey": wallet.hotkey.ss58_address, + }, + wait_for_finalization=True, + wait_for_inclusion=False, + ) + + +def test_get_all_commitments(mock_substrate, subtensor, mocker): + mock_substrate.query_map.return_value = [ + ( + (tuple(bytearray(32)),), + { + "info": { + "fields": [ + ( + { + "Raw4": (tuple(b"Test"),), + }, + ), + ], + }, + }, + ), + ] + + result = subtensor.get_all_commitments(netuid=1) + + assert result == { + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM": "Test", + } + + mock_substrate.query_map.assert_called_once_with( + module="Commitments", + storage_function="CommitmentOf", + params=[1], + block_hash=None, + ) + + +def test_get_balance(mock_substrate, subtensor): + mock_substrate.query.return_value = { + "data": { + "free": 123, + }, + } + + result = subtensor.get_balance( + "hotkey_ss58", + ) + + assert result == Balance(123) + + mock_substrate.query.assert_called_once_with( + module="System", + storage_function="Account", + params=["hotkey_ss58"], + block_hash=None, + ) + + +def test_get_balances(mock_substrate, subtensor, mocker): + create_storage_keys = [ + mocker.Mock(), + mocker.Mock(), + ] + + mock_substrate.create_storage_key.side_effect = create_storage_keys + mock_substrate.query_multi.return_value = [ + ( + mocker.Mock( + params=["hotkey1_ss58"], + ), + { + "data": { + "free": 1, + }, + }, + ), + ( + mocker.Mock( + params=["hotkey2_ss58"], + ), + { + "data": { + "free": 2, + }, + }, + ), + ] + + result = subtensor.get_balances( + "hotkey1_ss58", + "hotkey2_ss58", + ) + + assert result == { + "hotkey1_ss58": Balance(1), + "hotkey2_ss58": Balance(2), + } + + mock_substrate.query_multi.assert_called_once_with( + create_storage_keys, + block_hash=mock_substrate.get_chain_head.return_value, + ) + mock_substrate.create_storage_key.assert_has_calls( + [ + mocker.call( + "System", + "Account", + ["hotkey1_ss58"], + block_hash=mock_substrate.get_chain_head.return_value, + ), + mocker.call( + "System", + "Account", + ["hotkey2_ss58"], + block_hash=mock_substrate.get_chain_head.return_value, + ), + ] + ) + + +def test_get_block_hash_none(mock_substrate, subtensor): + result = subtensor.get_block_hash(block=None) + + assert result == mock_substrate.get_chain_head.return_value + + mock_substrate.get_chain_head.assert_called_once() + + +def test_get_children(mock_substrate, subtensor, wallet): + mock_substrate.query.return_value.value = [ + ( + 2**64 - 1, + (tuple(bytearray(32)),), + ), + ] + + success, children, error = subtensor.get_children( + "hotkey_ss58", + netuid=1, + ) + + assert success is True + assert children == [ + ( + 1.0, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + ] + assert error == "" + + mock_substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="ChildKeys", + params=["hotkey_ss58", 1], + block_hash=None, + ) + + +def test_get_current_weight_commit_info(mock_substrate, subtensor, wallet, mocker): + mock_substrate.query_map.return_value.records = [ + ( + mocker.ANY, + [ + ( + bytearray(32), + b"data", + 123, + ), + ], + ), + ] + + result = subtensor.get_current_weight_commit_info( + netuid=1, + ) + + assert result == [ + ( + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + "0x64617461", + 123, + ), + ] + + mock_substrate.query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="CRV3WeightCommits", + params=[1], + block_hash=None, + ) + + +def test_get_delegate_by_hotkey(mock_substrate, subtensor, mock_delegate_info): + mock_substrate.runtime_call.return_value.value = mock_delegate_info + + result = subtensor.get_delegate_by_hotkey( + "hotkey_ss58", + ) + + assert result == DelegateInfo( + hotkey_ss58="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + owner_ss58="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + take=1.0, + validator_permits=[], + registrations=[], + return_per_1000=Balance(2), + total_daily_return=Balance(3), + total_stake={}, + nominators={}, + ) + + mock_substrate.runtime_call.assert_called_once_with( + "DelegateInfoRuntimeApi", + "get_delegate", + ["hotkey_ss58"], + None, + ) + + +def test_get_delegate_identities(mock_substrate, subtensor, mocker): + mock_substrate.query_map.return_value = [ + ( + (tuple(bytearray(32)),), + mocker.Mock( + value={ + "additional": "Additional", + "description": "Description", + "discord": "", + "github_repo": "https://github.com/opentensor/bittensor", + "image": "", + "name": "Chain Delegate", + "url": "https://www.example.com", + }, + ), + ), + ] + + result = subtensor.get_delegate_identities() + + assert result == { + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM": ChainIdentity( + additional="Additional", + description="Description", + discord="", + github="https://github.com/opentensor/bittensor", + image="", + name="Chain Delegate", + url="https://www.example.com", + ), + } + + +def test_get_delegated(mock_substrate, subtensor, mock_delegate_info): + mock_substrate.runtime_call.return_value.value = [ + ( + mock_delegate_info, + ( + 0, + 999, + ), + ), + ] + + result = subtensor.get_delegated( + "coldkey_ss58", + ) + + assert result == [ + DelegatedInfo( + hotkey_ss58="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + owner_ss58="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + take=1.0, + validator_permits=[], + registrations=[], + return_per_1000=Balance(2), + total_daily_return=Balance(3), + netuid=0, + stake=Balance(999), + ), + ] + + mock_substrate.runtime_call.assert_called_once_with( + "DelegateInfoRuntimeApi", + "get_delegated", + ["coldkey_ss58"], + None, + ) + + +def test_get_neuron_certificate(mock_substrate, subtensor): + mock_substrate.query.return_value = { + "public_key": (tuple(b"CERTDATA"),), + "algorithm": 63, + } + + result = subtensor.get_neuron_certificate( + "hotkey_ss58", + netuid=1, + ) + + assert result == "?CERTDATA" + + mock_substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="NeuronCertificates", + params=[1, "hotkey_ss58"], + block_hash=None, + ) + + +def test_get_stake_for_coldkey(mock_substrate, subtensor, mocker): + mock_substrate.runtime_call.return_value.value = [ + { + "coldkey": tuple(bytearray(32)), + "drain": 0, + "emission": 3, + "hotkey": tuple(bytearray(32)), + "is_registered": True, + "locked": 2, + "netuid": 1, + "stake": 999, + }, + # filter out (stake=0): + { + "coldkey": tuple(bytearray(32)), + "drain": 1000, + "emission": 1000, + "hotkey": tuple(bytearray(32)), + "is_registered": True, + "locked": 1000, + "netuid": 2, + "stake": 0, + }, + ] + + result = subtensor.get_stake_for_coldkey( + "coldkey_ss58", + ) + + assert result == [ + StakeInfo( + coldkey_ss58="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + drain=0, + emission=Balance(3), + hotkey_ss58="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + is_registered=True, + locked=Balance(2), + netuid=1, + stake=Balance(999), + ), + ] + + mock_substrate.runtime_call.assert_called_once_with( + "StakeInfoRuntimeApi", + "get_stake_info_for_coldkey", + ["coldkey_ss58"], + None, + ) + + +def test_filter_netuids_by_registered_hotkeys( + mock_substrate, subtensor, wallet, mocker +): + mock_substrate.query_map.return_value = mocker.MagicMock( + **{ + "__iter__.return_value": iter( + [ + ( + 2, + mocker.Mock( + value=1, + ), + ), + ( + 3, + mocker.Mock( + value=1, + ), + ), + ] + ), + }, + ) + + result = subtensor.filter_netuids_by_registered_hotkeys( + all_netuids=[0, 1, 2], + filter_for_netuids=[2], + all_hotkeys=[wallet], + block=10, + ) + + assert result == [2] + + mock_substrate.get_block_hash.assert_called_once_with(10) + mock_substrate.query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="IsNetworkMember", + params=[wallet.hotkey.ss58_address], + block_hash=mock_substrate.get_block_hash.return_value, + ) + + +def test_last_drand_round(mock_substrate, subtensor): + mock_substrate.query.return_value.value = 123 + + result = subtensor.last_drand_round() + + assert result == 123 + + mock_substrate.query.assert_called_once_with( + module="Drand", + storage_function="LastStoredRound", + ) + + +@pytest.mark.parametrize( + "wait", + ( + True, + False, + ), +) +def test_move_stake(mock_substrate, subtensor, wallet, wait): + success = subtensor.move_stake( + wallet, + origin_hotkey="origin_hotkey", + origin_netuid=1, + destination_hotkey="destination_hotkey", + destination_netuid=2, + amount=Balance(1), + wait_for_finalization=wait, + wait_for_inclusion=wait, + ) + + assert success is True + + assert_submit_signed_extrinsic( + mock_substrate, + wallet.coldkey, + call_module="SubtensorModule", + call_function="move_stake", + call_params={ + "origin_hotkey": "origin_hotkey", + "origin_netuid": 1, + "destination_hotkey": "destination_hotkey", + "destination_netuid": 2, + "alpha_amount": 1, + }, + wait_for_finalization=wait, + wait_for_inclusion=wait, + ) + + +def test_move_stake_insufficient_stake(mock_substrate, subtensor, wallet, mocker): + mocker.patch.object(subtensor, "get_stake", return_value=Balance(0)) + + success = subtensor.move_stake( + wallet, + origin_hotkey="origin_hotkey", + origin_netuid=1, + destination_hotkey="destination_hotkey", + destination_netuid=2, + amount=Balance(1), + ) + + assert success is False + + mock_substrate.submit_extrinsic.assert_not_called() + + +def test_move_stake_error(mock_substrate, subtensor, wallet, mocker): + mock_substrate.submit_extrinsic.return_value = mocker.Mock( + error_message="ERROR", + is_success=False, + ) + + success = subtensor.move_stake( + wallet, + origin_hotkey="origin_hotkey", + origin_netuid=1, + destination_hotkey="destination_hotkey", + destination_netuid=2, + amount=Balance(1), + ) + + assert success is False + + assert_submit_signed_extrinsic( + mock_substrate, + wallet.coldkey, + call_module="SubtensorModule", + call_function="move_stake", + call_params={ + "origin_hotkey": "origin_hotkey", + "origin_netuid": 1, + "destination_hotkey": "destination_hotkey", + "destination_netuid": 2, + "alpha_amount": 1, + }, + wait_for_finalization=False, + wait_for_inclusion=True, + ) + + +def test_move_stake_exception(mock_substrate, subtensor, wallet): + mock_substrate.submit_extrinsic.side_effect = RuntimeError + + success = subtensor.move_stake( + wallet, + origin_hotkey="origin_hotkey", + origin_netuid=1, + destination_hotkey="destination_hotkey", + destination_netuid=2, + amount=Balance(1), + ) + + assert success is False + + assert_submit_signed_extrinsic( + mock_substrate, + wallet.coldkey, + call_module="SubtensorModule", + call_function="move_stake", + call_params={ + "origin_hotkey": "origin_hotkey", + "origin_netuid": 1, + "destination_hotkey": "destination_hotkey", + "destination_netuid": 2, + "alpha_amount": 1, + }, + wait_for_finalization=False, + wait_for_inclusion=True, + ) + + +def test_neurons(mock_substrate, subtensor, mock_neuron_info): + mock_substrate.runtime_call.return_value.value = [ + mock_neuron_info, + ] + + neurons = subtensor.neurons(netuid=1) + + assert neurons == [ + NeuronInfo( + axon_info=AxonInfo( + version=1, + ip="127.0.0.1", + port=8080, + ip_type=4, + hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + active=0, + bonds=[], + coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + consensus=0.0, + dividends=0.0, + emission=0.0, + hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + incentive=0.0, + is_null=False, + last_update=0, + netuid=1, + prometheus_info=PrometheusInfo( + block=0, + version=1, + ip="0.0.0.0", + port=0, + ip_type=0, + ), + pruning_score=0.0, + rank=0.0, + stake_dict={}, + stake=Balance(0), + total_stake=Balance(0), + trust=0.0, + uid=1, + validator_permit=True, + validator_trust=0.0, + weights=[], + ), + ] + + mock_substrate.runtime_call.assert_called_once_with( + "NeuronInfoRuntimeApi", + "get_neurons", + [1], + None, + ) + + +def test_neurons_lite(mock_substrate, subtensor, mock_neuron_info): + mock_substrate.runtime_call.return_value.value = [ + mock_neuron_info, + ] + + result = subtensor.neurons_lite(netuid=1) + + assert result == [ + NeuronInfoLite( + axon_info=AxonInfo( + version=1, + ip="127.0.0.1", + port=8080, + ip_type=4, + hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + active=0, + coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + consensus=0.0, + dividends=0.0, + emission=0.0, + hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + incentive=0.0, + is_null=False, + last_update=0, + netuid=1, + prometheus_info=PrometheusInfo( + block=0, + version=1, + ip="0.0.0.0", + port=0, + ip_type=0, + ), + pruning_score=0.0, + rank=0.0, + stake_dict={}, + stake=Balance(0), + total_stake=Balance(0), + trust=0.0, + uid=1, + validator_permit=True, + validator_trust=0.0, + ), + ] + + mock_substrate.runtime_call.assert_called_once_with( + "NeuronInfoRuntimeApi", + "get_neurons_lite", + [1], + None, + ) + + +def test_subnet(mock_substrate, subtensor, mock_dynamic_info): + mock_substrate.runtime_call.return_value.decode.return_value = mock_dynamic_info + + subtensor = bittensor.core.subtensor.Subtensor() + result = subtensor.subnet(netuid=0) + + assert result == DynamicInfo( + netuid=0, + owner_hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + owner_coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + subnet_name="root", + symbol="Τ", + tempo=100, + last_step=4919910, + blocks_since_last_step=84234, + emission=Balance(0), + alpha_in=Balance(14723086336554), + alpha_out=Balance(6035890271491007), + tao_in=Balance(6035892206947246), + price=Balance.from_tao(1), + k=88866962081017766138079430284, + is_dynamic=False, + alpha_out_emission=Balance(0), + alpha_in_emission=Balance(0), + tao_in_emission=Balance(0), + pending_alpha_emission=Balance(0), + pending_root_emission=Balance(0), + network_registered_at=0, + subnet_volume=Balance(2240411565906691), + subnet_identity=None, + moving_price=0.0, + ) + + mock_substrate.runtime_call.assert_called_once_with( + "SubnetInfoRuntimeApi", + "get_dynamic_info", + params=[0], + block_hash=None, + ) + + +def test_subtensor_contextmanager(mock_substrate, subtensor): + with subtensor: + pass + + mock_substrate.close.assert_called_once() + + +def test_swap_stake(mock_substrate, subtensor, wallet, mocker): + mocker.patch.object(subtensor, "get_stake", return_value=Balance(1000)) + mocker.patch.object( + subtensor, + "get_hotkey_owner", + autospec=True, + return_value=wallet.coldkeypub.ss58_address, + ) + + result = subtensor.swap_stake( + wallet, + wallet.hotkey.ss58_address, + origin_netuid=1, + destination_netuid=2, + amount=Balance(999), + ) + + assert result is True + + assert_submit_signed_extrinsic( + mock_substrate, + wallet.coldkey, + call_module="SubtensorModule", + call_function="swap_stake", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "origin_netuid": 1, + "destination_netuid": 2, + "alpha_amount": 999, + }, + wait_for_inclusion=True, + wait_for_finalization=False, + ) + + +@pytest.mark.parametrize( + "query,result", + ( + ( + None, + None, + ), + ( + { + "additional": "Additional", + "description": "Description", + "discord": "", + "github_repo": "https://github.com/opentensor/bittensor", + "image": "", + "name": "Chain Delegate", + "url": "https://www.example.com", + }, + ChainIdentity( + additional="Additional", + description="Description", + discord="", + github="https://github.com/opentensor/bittensor", + image="", + name="Chain Delegate", + url="https://www.example.com", + ), + ), + ), +) +def test_query_identity(mock_substrate, subtensor, query, result): + mock_substrate.query.return_value = query + + identity = subtensor.query_identity( + "coldkey_ss58", + ) + + assert identity == result + + mock_substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="IdentitiesV2", + params=["coldkey_ss58"], + block_hash=None, + ) + + +def test_register(mock_substrate, subtensor, wallet, mocker): + create_pow = mocker.patch( + "bittensor.core.extrinsics.registration.create_pow", + return_value=mocker.Mock( + **{ + "is_stale.return_value": False, + "seal": b"\1\2\3", + }, + ), + ) + mocker.patch.object( + subtensor, + "get_neuron_for_pubkey_and_subnet", + return_value=NeuronInfo.get_null_neuron(), + ) + + result = subtensor.register( + wallet, + netuid=1, + ) + + assert result is True + + subtensor.get_neuron_for_pubkey_and_subnet.assert_called_once_with( + hotkey_ss58=wallet.hotkey.ss58_address, + netuid=1, + block=mock_substrate.get_block_number.return_value, + ) + create_pow.assert_called_once_with( + subtensor=subtensor, + wallet=wallet, + netuid=1, + output_in_place=True, + cuda=False, + num_processes=None, + update_interval=None, + log_verbose=False, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + wallet.coldkey, + call_module="SubtensorModule", + call_function="register", + call_params={ + "block_number": create_pow.return_value.block_number, + "coldkey": wallet.coldkeypub.ss58_address, + "hotkey": wallet.hotkey.ss58_address, + "netuid": 1, + "nonce": create_pow.return_value.nonce, + "work": [1, 2, 3], + }, + ) + + +@pytest.mark.parametrize( + "success", + [ + True, + False, + ], +) +def test_register_subnet(mock_substrate, subtensor, wallet, mocker, success): + mocker.patch.object(subtensor, "get_balance", return_value=Balance(100)) + mocker.patch.object(subtensor, "get_subnet_burn_cost", return_value=Balance(10)) + + mock_substrate.submit_extrinsic.return_value = mocker.Mock( + is_success=success, + ) + + result = subtensor.register_subnet( + wallet, + ) + + assert result is success + + assert_submit_signed_extrinsic( + mock_substrate, + wallet.coldkey, + call_module="SubtensorModule", + call_function="register_network", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "mechid": 1, + }, + ) + + +def test_register_subnet_insufficient_funds(mock_substrate, subtensor, wallet, mocker): + mocker.patch.object(subtensor, "get_balance", return_value=Balance(0)) + mocker.patch.object(subtensor, "get_subnet_burn_cost", return_value=Balance(10)) + + success = subtensor.register_subnet( + wallet, + ) + + assert success is False + + mock_substrate.submit_extrinsic.assert_not_called() + + +def test_root_register(mock_substrate, subtensor, wallet, mocker): + mocker.patch.object( + subtensor, "get_balance", autospec=True, return_value=Balance(100) + ) + mocker.patch.object(subtensor, "get_hyperparameter", autospec=True, return_value=10) + mocker.patch.object( + subtensor, "is_hotkey_registered_on_subnet", autospec=True, return_value=False + ) + + success = subtensor.root_register(wallet) + + assert success is True + + subtensor.get_balance.assert_called_once_with( + wallet.coldkeypub.ss58_address, + block=mock_substrate.get_block_number.return_value, + ) + subtensor.get_hyperparameter.assert_called_once() + subtensor.is_hotkey_registered_on_subnet.assert_called_once_with( + wallet.hotkey.ss58_address, + 0, + None, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + wallet.coldkey, + call_module="SubtensorModule", + call_function="root_register", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + }, + ) + + +def test_root_register_is_already_registered(mock_substrate, subtensor, wallet, mocker): + mocker.patch.object( + subtensor, "get_balance", autospec=True, return_value=Balance(100) + ) + mocker.patch.object(subtensor, "get_hyperparameter", autospec=True, return_value=10) + mocker.patch.object( + subtensor, "is_hotkey_registered_on_subnet", autospec=True, return_value=True + ) + + success = subtensor.root_register(wallet) + + assert success is True + + subtensor.is_hotkey_registered_on_subnet.assert_called_once_with( + wallet.hotkey.ss58_address, + 0, + None, + ) + mock_substrate.submit_extrinsic.assert_not_called() + + +def test_root_register_insufficient_balance(mock_substrate, subtensor, wallet, mocker): + mocker.patch.object( + subtensor, "get_balance", autospec=True, return_value=Balance(1) + ) + mocker.patch.object(subtensor, "get_hyperparameter", autospec=True, return_value=10) + + success = subtensor.root_register(wallet) + + assert success is False + + subtensor.get_balance.assert_called_once_with( + wallet.coldkeypub.ss58_address, + block=mock_substrate.get_block_number.return_value, + ) + mock_substrate.submit_extrinsic.assert_not_called() + + +def test_root_set_weights(mock_substrate, subtensor, wallet, mocker): + MIN_ALLOWED_WEIGHTS = 0 + MAX_WEIGHTS_LIMIT = 1 + + mock_substrate.query.return_value = 1 + mocker.patch.object( + subtensor, + "get_hyperparameter", + autospec=True, + side_effect=[ + MIN_ALLOWED_WEIGHTS, + MAX_WEIGHTS_LIMIT, + ], + ) + + subtensor.root_set_weights( + wallet, + netuids=[1, 2], + weights=[0.5, 0.5], + ) + + subtensor.get_hyperparameter.assert_has_calls( + [ + mocker.call("MinAllowedWeights", netuid=0), + mocker.call("MaxWeightsLimit", netuid=0), + ] + ) + mock_substrate.query.assert_called_once_with( + "SubtensorModule", + "Uids", + [0, wallet.hotkey.ss58_address], + ) + + assert_submit_signed_extrinsic( + mock_substrate, + wallet.coldkey, + call_module="SubtensorModule", + call_function="set_root_weights", + call_params={ + "dests": [1, 2], + "weights": [65535, 65535], + "netuid": 0, + "version_key": 0, + "hotkey": wallet.hotkey.ss58_address, + }, + era={ + "period": 5, + }, + nonce=mock_substrate.get_account_next_index.return_value, + wait_for_finalization=False, + ) + + +def test_root_set_weights_no_uid(mock_substrate, subtensor, wallet, mocker): + mock_substrate.query.return_value = None + + success = subtensor.root_set_weights( + wallet, + netuids=[1, 2], + weights=[0.5, 0.5], + ) + + assert success is False + + mock_substrate.query.assert_called_once_with( + "SubtensorModule", + "Uids", + [0, wallet.hotkey.ss58_address], + ) + mock_substrate.submit_extrinsic.assert_not_called() + + +def test_root_set_weights_min_allowed_weights( + mock_substrate, subtensor, wallet, mocker +): + mocker.patch.object( + subtensor, + "get_hyperparameter", + autospec=True, + return_value=5, + ) + mock_substrate.query.return_value = 1 + + with pytest.raises( + ValueError, + match="The minimum number of weights required to set weights is 5, got 2", + ): + subtensor.root_set_weights( + wallet, + netuids=[1, 2], + weights=[0.5, 0.5], + ) + + subtensor.get_hyperparameter.assert_any_call("MinAllowedWeights", netuid=0) + mock_substrate.submit_extrinsic.assert_not_called() + + +def test_sign_and_send_extrinsic(mock_substrate, subtensor, wallet, mocker): + call = mocker.Mock() + + subtensor.sign_and_send_extrinsic( + call, + wallet, + use_nonce=True, + period=10, + ) + + mock_substrate.get_account_next_index.assert_called_once_with( + wallet.hotkey.ss58_address, + ) + mock_substrate.create_signed_extrinsic.assert_called_once_with( + call=call, + era={ + "period": 10, + }, + keypair=wallet.coldkey, + nonce=mock_substrate.get_account_next_index.return_value, + ) + mock_substrate.submit_extrinsic.assert_called_once_with( + mock_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=True, + wait_for_finalization=False, + ) + + +@pytest.mark.parametrize( + "wait", + ( + True, + False, + ), +) +def test_transfer_stake(mock_substrate, subtensor, wallet, mocker, wait): + mocker.patch.object( + subtensor, + "get_hotkey_owner", + autospec=True, + return_value=wallet.coldkeypub.ss58_address, + ) + + success = subtensor.transfer_stake( + wallet, + "dest", + "hotkey_ss58", + origin_netuid=1, + destination_netuid=1, + amount=Balance(1), + wait_for_finalization=wait, + wait_for_inclusion=wait, + ) + + assert success is True + + assert_submit_signed_extrinsic( + mock_substrate, + wallet.coldkey, + call_module="SubtensorModule", + call_function="transfer_stake", + call_params={ + "destination_coldkey": "dest", + "hotkey": "hotkey_ss58", + "origin_netuid": 1, + "destination_netuid": 1, + "alpha_amount": 1, + }, + wait_for_finalization=wait, + wait_for_inclusion=wait, + ) + + +@pytest.mark.parametrize( + "side_effect", + ( + ( + unittest.mock.Mock( + error_message="ERROR", + is_success=False, + ), + ), + RuntimeError, + ), +) +def test_transfer_stake_error(mock_substrate, subtensor, wallet, mocker, side_effect): + mocker.patch.object( + subtensor, + "get_hotkey_owner", + autospec=True, + return_value=wallet.coldkeypub.ss58_address, + ) + mock_substrate.submit_extrinsic.return_value = side_effect + + success = subtensor.transfer_stake( + wallet, + "dest", + "hotkey_ss58", + origin_netuid=1, + destination_netuid=1, + amount=Balance(1), + ) + + assert success is False + + assert_submit_signed_extrinsic( + mock_substrate, + wallet.coldkey, + call_module="SubtensorModule", + call_function="transfer_stake", + call_params={ + "destination_coldkey": "dest", + "hotkey": "hotkey_ss58", + "origin_netuid": 1, + "destination_netuid": 1, + "alpha_amount": 1, + }, + wait_for_finalization=False, + wait_for_inclusion=True, + ) + + +def test_transfer_stake_non_owner(mock_substrate, subtensor, wallet, mocker): + mocker.patch.object( + subtensor, + "get_hotkey_owner", + autospec=True, + return_value="owner2_ss58", + ) + + success = subtensor.transfer_stake( + wallet, + "dest", + "hotkey_ss58", + origin_netuid=1, + destination_netuid=1, + amount=Balance(1), + ) + + assert success is False + + subtensor.get_hotkey_owner.assert_called_once_with( + "hotkey_ss58", + ) + mock_substrate.submit_extrinsic.assert_not_called() + + +def test_transfer_stake_insufficient_stake(mock_substrate, subtensor, wallet, mocker): + mocker.patch.object( + subtensor, + "get_hotkey_owner", + autospec=True, + return_value=wallet.coldkeypub.ss58_address, + ) + + with unittest.mock.patch.object( + subtensor, + "get_stake", + return_value=Balance(0), + ): + success = subtensor.transfer_stake( + wallet, + "dest", + "hotkey_ss58", + origin_netuid=1, + destination_netuid=1, + amount=Balance(1), + ) + + assert success is False + + mock_substrate.submit_extrinsic.assert_not_called() + + +def test_wait_for_block(mock_substrate, subtensor, mocker): + mock_subscription_handler = None + + def get_block_handler( + current_block_hash, + header_only, + subscription_handler, + ): + nonlocal mock_subscription_handler + mock_subscription_handler = mocker.Mock(wraps=subscription_handler) + + for block in range(1, 20): + if mock_subscription_handler( + { + "header": { + "number": block, + }, + } + ): + return + + assert False + + mock_substrate.get_block.side_effect = [ + { + "header": { + "number": 1, + }, + }, + ] + mock_substrate._get_block_handler.side_effect = get_block_handler + + subtensor.wait_for_block(block=9) + + assert mock_subscription_handler.call_count == 9 + + +def test_weights(mock_substrate, subtensor): + mock_substrate.query_map.return_value = [ + (1, unittest.mock.Mock(value=0.5)), + ] + + results = subtensor.weights( + netuid=1, + ) + + assert results == [ + ( + 1, + 0.5, + ), + ] + + mock_substrate.query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="Weights", + params=[1], + block_hash=None, + )