diff --git a/.circleci/config.yml b/.circleci/config.yml index fbe39a6645..2c5d843e09 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,7 +39,7 @@ jobs: python -m venv .venv . .venv/bin/activate python -m pip install --upgrade uv - uv pip install ruff -c requirements/dev.txt + uv pip install ruff==0.11.5 - save_cache: name: Save cached ruff venv @@ -68,9 +68,7 @@ jobs: name: Install dependencies and Check compatibility command: | if [ "$REQUIREMENTS_CHANGED" == "true" ]; then - sudo apt-get update - sudo apt-get install -y jq curl - ./scripts/check_compatibility.sh << parameters.python_version >> + python -m pip install ".[dev,cli]" --dry-run --python-version << parameters.python_version >> --no-deps else echo "Skipping compatibility checks..." fi @@ -87,31 +85,19 @@ jobs: steps: - checkout - - restore_cache: - name: Restore cached venv - keys: - - v2-pypi-py<< parameters.python-version >>-{{ checksum "requirements/prod.txt" }}+{{ checksum "requirements/dev.txt" }} - - v2-pypi-py<< parameters.python-version >> - - run: name: Update & Activate venv command: | python -m venv .venv . .venv/bin/activate python -m pip install --upgrade uv - uv sync --all-extras --dev - - - save_cache: - name: Save cached venv - paths: - - "venv/" - key: v2-pypi-py<< parameters.python-version >>-{{ checksum "requirements/prod.txt" }}+{{ checksum "requirements/dev.txt" }} + uv sync --extra dev --dev - run: name: Install Bittensor command: | . .venv/bin/activate - uv sync --all-extras --dev + uv sync --extra dev --dev - run: name: Instantiate Mock Wallet @@ -178,32 +164,20 @@ jobs: steps: - checkout - - restore_cache: - name: Restore cached venv - keys: - - v2-pypi-py<< parameters.python-version >>-{{ checksum "requirements/prod.txt" }}+{{ checksum "requirements/dev.txt" }} - - v2-pypi-py<< parameters.python-version >> - - run: name: Update & Activate venv command: | python -m venv .venv . .venv/bin/activate python -m pip install --upgrade uv - uv sync --all-extras --dev + uv sync --extra dev --dev uv pip install flake8 - - save_cache: - name: Save cached venv - paths: - - "env/" - key: v2-pypi-py<< parameters.python-version >>-{{ checksum "requirements/prod.txt" }}+{{ checksum "requirements/dev.txt" }} - - run: name: Install Bittensor command: | . .venv/bin/activate - uv sync --all-extras --dev + uv sync --extra dev --dev - run: name: Lint with flake8 @@ -235,18 +209,6 @@ jobs: uv pip install --upgrade coveralls coveralls --finish --rcfile .coveragerc || echo "Failed to upload coverage" - check-version-updated: - docker: - - image: cimg/python:3.10 - steps: - - checkout - - - run: - name: Version is updated - command: | - [[ $(git diff-tree --no-commit-id --name-only -r HEAD..master | grep bittensor/__init__.py | wc -l) == 1 ]] && echo "bittensor/__init__.py has changed" - [[ $(git diff-tree --no-commit-id --name-only -r HEAD..master | grep VERSION | wc -l) == 1 ]] && echo "VERSION has changed" - check-changelog-updated: docker: - image: cimg/python:3.10 @@ -323,11 +285,6 @@ workflows: release-branches-requirements: jobs: - - check-version-updated: - filters: - branches: - only: - - /^(release|hotfix)/.*/ - check-changelog-updated: filters: branches: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index adff4d0aab..9ffce36ee7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,8 +1,7 @@ version: 2 updates: - package-ecosystem: "pip" - directory: "" - file: "requirements/prod.txt" + directory: "/" schedule: interval: "daily" open-pull-requests-limit: 0 # Only security updates will be opened as PRs diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index c74c5fadfe..08e8c2606f 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -39,7 +39,9 @@ jobs: id: get-tests run: | test_files=$(find tests/e2e_tests -name "test*.py" | jq -R -s -c 'split("\n") | map(select(. != ""))') - echo "::set-output name=test-files::$test_files" + # keep it here for future debug + # test_files=$(find tests/e2e_tests -type f -name "test*.py" | grep -E 'test_(incentive|commit_weights|set_weights)\.py$' | jq -R -s -c 'split("\n") | map(select(. != ""))') + echo "test-files=$test_files" >> "$GITHUB_OUTPUT" shell: bash pull-docker-image: @@ -61,7 +63,7 @@ jobs: path: subtensor-localnet.tar # Job to run tests in parallel - run: + run-e2e-test: name: ${{ matrix.test-file }} / Python ${{ matrix.python-version }} needs: - find-tests @@ -70,7 +72,7 @@ jobs: timeout-minutes: 45 strategy: fail-fast: false # Allow other matrix jobs to run even if this job fails - max-parallel: 32 # Set the maximum number of parallel jobs (same as we have cores in SubtensorCI runner) + max-parallel: 32 # Set the maximum number of parallel jobs (same as we have cores in ubuntu-latest runner) matrix: os: - ubuntu-latest @@ -89,7 +91,7 @@ jobs: uses: astral-sh/setup-uv@v4 - name: install dependencies - run: uv sync --all-extras --dev + run: uv sync --extra dev --dev - name: Download Cached Docker Image uses: actions/download-artifact@v4 @@ -99,5 +101,26 @@ jobs: - name: Load Docker Image run: docker load -i subtensor-localnet.tar - - name: Run tests - run: uv run pytest ${{ matrix.test-file }} -s +# - name: Run tests +# run: uv run pytest ${{ matrix.test-file }} -s + + - name: Run tests with retry + run: | + set +e + for i in 1 2; do + echo "🔁 Attempt $i: Running tests" + uv run pytest ${{ matrix.test-file }} -s + status=$? + if [ $status -eq 0 ]; then + echo "✅ Tests passed on attempt $i" + break + else + echo "❌ Tests failed on attempt $i" + if [ $i -eq 2 ]; then + echo "Tests failed after 2 attempts" + exit 1 + fi + echo "Retrying..." + sleep 5 + fi + done diff --git a/CHANGELOG.md b/CHANGELOG.md index 84d7c10774..5d4ca830d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # Changelog +## 9.4.0 /2025-04-17 + +## What's Changed +* Release/9.3.0 by @ibraheem-abe in https://github.com/opentensor/bittensor/pull/2805 +* Fix for flaky behavior of `test_incentive`, `test_commit_weights` and `test_set_weights` by @basfroman in https://github.com/opentensor/bittensor/pull/2795 +* Add `get_next_epoch_start_block` method to Async/Subtensor by @basfroman in https://github.com/opentensor/bittensor/pull/2808 +* Adds compatibility for torch 2.6.0+ by @thewhaleking in https://github.com/opentensor/bittensor/pull/2811 +* f Update CONTRIBUTING.md by @Hack666r in https://github.com/opentensor/bittensor/pull/2813 +* docs: replaced discord link with documentation by @sashaphmn in https://github.com/opentensor/bittensor/pull/2809 +* sometimes it's still flaky because the chain returns data with time offset by @basfroman in https://github.com/opentensor/bittensor/pull/2816 +* Remove requirements directory by @thewhaleking in https://github.com/opentensor/bittensor/pull/2812 +* version in one place by @thewhaleking in https://github.com/opentensor/bittensor/pull/2806 +* Update CONTRIBUTING hyperlinks by @thewhaleking in https://github.com/opentensor/bittensor/pull/2820 +* 9.3.1a1: Updates changelog by @ibraheem-abe in https://github.com/opentensor/bittensor/pull/2821 +* Remove cubit as extra by @thewhaleking in https://github.com/opentensor/bittensor/pull/2823 +* fix/roman/get-metagraph-identities by @basfroman in https://github.com/opentensor/bittensor/pull/2826 +* Replace waiting logic for `test_commit_reveal_v3.py` by @basfroman in https://github.com/opentensor/bittensor/pull/2827 +* Add `start_call` extrinsic to the sdk by @basfroman in https://github.com/opentensor/bittensor/pull/2828 +* Add `timelock` module by @basfroman in https://github.com/opentensor/bittensor/pull/2824 +* Fix: raise_error=True in AsyncSubtensor fails to get error_message by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2771 +* Fix: async set_subnet_identity_extrinsic doesn't sign the extrinsic by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2772 +* Update settings.py by @petryshkaCODE in https://github.com/opentensor/bittensor/pull/2814 +* Bumping ruff version by @basfroman in https://github.com/opentensor/bittensor/pull/2831 +* Fix and improve e2e tests after `start call` new limit in subtensor by @basfroman in https://github.com/opentensor/bittensor/pull/2830 +* Add `bittensor.timelock` module description by @basfroman in https://github.com/opentensor/bittensor/pull/2833 +* fix: Update broken link in `CONTRIBUTING.md` by @cypherpepe in https://github.com/opentensor/bittensor/pull/2818 +* docs: added a new badge by @sashaphmn in https://github.com/opentensor/bittensor/pull/2819 +* Fix AxonInfo initialization in get_mock_neuron function by @VolodymyrBg in https://github.com/opentensor/bittensor/pull/2803 +* Dendrite: log ClientOSError as Debug by @Thykof in https://github.com/opentensor/bittensor/pull/2770 +* remove `rao` endpoint from settings by @basfroman in https://github.com/opentensor/bittensor/pull/2834 + +## New Contributors +* @Hack666r made their first contribution in https://github.com/opentensor/bittensor/pull/2813 +* @petryshkaCODE made their first contribution in https://github.com/opentensor/bittensor/pull/2814 +* @cypherpepe made their first contribution in https://github.com/opentensor/bittensor/pull/2818 +* @VolodymyrBg made their first contribution in https://github.com/opentensor/bittensor/pull/2803 +* @Thykof made their first contribution in https://github.com/opentensor/bittensor/pull/2770 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.3.0...v9.4.0 + ## 9.3.0 /2025-04-09 ## What's Changed diff --git a/Makefile b/Makefile index 344c3e4184..154d6a1f2f 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,3 @@ install: install-dev: python3 -m pip install '.[dev]' - -install-cubit: - python3 -m pip install '.[cubit]' \ No newline at end of file diff --git a/README.md b/README.md index 7dbfcbc260..953ee9e5e8 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,14 @@ [![Discord Chat](https://img.shields.io/discord/308323056592486420.svg)](https://discord.gg/bittensor) [![CodeQL](https://github.com/opentensor/bittensor/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/opentensor/bittensor/actions) [![PyPI version](https://badge.fury.io/py/bittensor.svg)](https://badge.fury.io/py/bittensor) +[![Codecov](https://codecov.io/gh/opentensor/bittensor/graph/badge.svg)](https://app.codecov.io/gh/opentensor/bittensor) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) --- ## Internet-scale Neural Networks -[Discord](https://discord.gg/qasY3HA9F9) • [Network](https://taostats.io/) • [Research](https://bittensor.com/whitepaper) +[Discord](https://discord.gg/qasY3HA9F9) • [Network](https://taostats.io/) • [Research](https://bittensor.com/whitepaper) • [Documentation](https://docs.bittensor.com) diff --git a/VERSION b/VERSION deleted file mode 100644 index 4d0ffae7b5..0000000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -9.3.0 \ No newline at end of file diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 9a132f4258..1bd3ce9820 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -57,6 +57,7 @@ publish_metadata, get_metadata, ) +from bittensor.core.extrinsics.asyncex.start_call import start_call_extrinsic from bittensor.core.extrinsics.asyncex.serving import serve_axon_extrinsic from bittensor.core.extrinsics.asyncex.staking import ( add_stake_extrinsic, @@ -752,6 +753,7 @@ async def does_hotkey_exist( return_val = ( False if result is None + # not the default key (0x0) else result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" ) return return_val @@ -1687,6 +1689,38 @@ async def get_neuron_for_pubkey_and_subnet( reuse_block=reuse_block, ) + async def get_next_epoch_start_block( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """ + Calculates the first block number of the next epoch for the given subnet. + + If `block` is not provided, the current chain block will be used. Epochs are + determined based on the subnet's tempo (i.e., blocks per epoch). The result + is the block number at which the next epoch will begin. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int], optional): The reference block to calculate from. + If None, uses the current chain block height. + block_hash (Optional[int]): The blockchain block number at which to perform the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. + + + Returns: + int: The block number at which the next epoch will start. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + if not block_hash and reuse_block: + block_hash = self.substrate.last_block_hash + block = await self.substrate.get_block_number(block_hash=block_hash) + tempo = await self.tempo(netuid=netuid, block_hash=block_hash) + return (((block // tempo) + 1) * tempo) + 1 if tempo else None + async def get_owned_hotkeys( self, coldkey_ss58: str, @@ -2972,7 +3006,7 @@ async def wait_for_block(self, block: Optional[int] = None): async def handler(block_data: dict): logging.debug( - f'reached block {block_data["header"]["number"]}. Waiting for block {target_block}' + f"reached block {block_data['header']['number']}. Waiting for block {target_block}" ) if block_data["header"]["number"] >= target_block: return True @@ -3149,7 +3183,7 @@ async def sign_and_send_extrinsic( return True, "" if raise_error: - raise ChainError.from_error(response.error_message) + raise ChainError.from_error(await response.error_message) return False, format_error_message(await response.error_message) @@ -3813,6 +3847,7 @@ async def set_weights( wait_for_finalization: bool = False, max_retries: int = 5, block_time: float = 12.0, + period: int = 5, ): """ Sets the inter-neuronal weights for the specified neuron. This process involves specifying the influence or @@ -3833,6 +3868,7 @@ async def set_weights( ``False``. max_retries (int): The number of maximum attempts to set weights. Default is ``5``. block_time (float): The amount of seconds for block duration. Default is 12.0 seconds. + period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. Returns: tuple[bool, str]: ``True`` if the setting of weights is successful, False otherwise. And `msg`, a string @@ -3907,6 +3943,7 @@ async def _blocks_weight_limit() -> bool: version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) except Exception as e: logging.error(f"Error setting weights: {e}") @@ -3951,6 +3988,36 @@ async def serve_axon( certificate=certificate, ) + async def start_call( + self, + wallet: "Wallet", + netuid: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> tuple[bool, str]: + """ + Submits a start_call extrinsic to the blockchain, to trigger the start call process for a subnet (used to start a + new subnet's emission mechanism). + + Args: + wallet (Wallet): The wallet used to sign the extrinsic (must be unlocked). + netuid (int): The UID of the target subnet for which the call is being initiated. + wait_for_inclusion (bool, optional): Whether to wait for the extrinsic to be included in a block. Defaults to True. + wait_for_finalization (bool, optional): Whether to wait for finalization of the extrinsic. Defaults to False. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + return await start_call_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + async def swap_stake( self, wallet: "Wallet", diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 54817ccdfd..56b55e60eb 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -504,9 +504,9 @@ def verify_custom(synapse: MyCustomSynapse): ) param_class = first_param.annotation - assert issubclass( - param_class, Synapse - ), "The first argument of forward_fn must inherit from bittensor.Synapse" + assert issubclass(param_class, Synapse), ( + "The first argument of forward_fn must inherit from bittensor.Synapse" + ) request_name = param_class.__name__ async def endpoint(*args, **kwargs): @@ -580,19 +580,19 @@ async def endpoint(*args, **kwargs): blacklist_sig = Signature( expected_params, return_annotation=Tuple[bool, str] ) - assert ( - signature(blacklist_fn) == blacklist_sig - ), f"The blacklist_fn function must have the signature: blacklist( synapse: {request_name} ) -> tuple[bool, str]" + assert signature(blacklist_fn) == blacklist_sig, ( + f"The blacklist_fn function must have the signature: blacklist( synapse: {request_name} ) -> tuple[bool, str]" + ) if priority_fn: priority_sig = Signature(expected_params, return_annotation=float) - assert ( - signature(priority_fn) == priority_sig - ), f"The priority_fn function must have the signature: priority( synapse: {request_name} ) -> float" + assert signature(priority_fn) == priority_sig, ( + f"The priority_fn function must have the signature: priority( synapse: {request_name} ) -> float" + ) if verify_fn: verify_sig = Signature(expected_params, return_annotation=None) - assert ( - signature(verify_fn) == verify_sig - ), f"The verify_fn function must have the signature: verify( synapse: {request_name} ) -> None" + assert signature(verify_fn) == verify_sig, ( + f"The verify_fn function must have the signature: verify( synapse: {request_name} ) -> None" + ) # Store functions in appropriate attribute dictionaries self.forward_class_types[request_name] = param_class @@ -747,9 +747,9 @@ def check_config(cls, config: "Config"): Raises: AssertionError: If the axon or external ports are not in range [1024, 65535] """ - assert ( - 1024 < config.axon.port < 65535 - ), "Axon port must be in range [1024, 65535]" + assert 1024 < config.axon.port < 65535, ( + "Axon port must be in range [1024, 65535]" + ) assert config.axon.external_port is None or ( 1024 < config.axon.external_port < 65535 diff --git a/bittensor/core/chain_data/metagraph_info.py b/bittensor/core/chain_data/metagraph_info.py index 1ed2733124..422a0607b3 100644 --- a/bittensor/core/chain_data/metagraph_info.py +++ b/bittensor/core/chain_data/metagraph_info.py @@ -25,7 +25,7 @@ def _chr_str(codes: tuple[int]) -> str: def process_nested(data: Union[tuple, dict], chr_transform): """Processes nested data structures by applying a transformation function to their elements.""" if isinstance(data, (list, tuple)): - if len(data) > 0 and isinstance(data[0], dict): + if len(data) > 0: return [ {k: chr_transform(v) for k, v in item.items()} if item is not None diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index d8a2fb08fa..5dcc509e16 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -258,7 +258,7 @@ def log_exception(self, exception: Exception): """ error_id = str(uuid.uuid4()) error_type = exception.__class__.__name__ - if isinstance(exception, (aiohttp.ClientConnectorError, asyncio.TimeoutError)): + if isinstance(exception, (aiohttp.ClientOSError, asyncio.TimeoutError)): logging.debug(f"{error_type}#{error_id}: {exception}") else: logging.error(f"{error_type}#{error_id}: {exception}") diff --git a/bittensor/core/extrinsics/asyncex/registration.py b/bittensor/core/extrinsics/asyncex/registration.py index 070b55e904..cfdd78e6f5 100644 --- a/bittensor/core/extrinsics/asyncex/registration.py +++ b/bittensor/core/extrinsics/asyncex/registration.py @@ -498,7 +498,7 @@ async def set_subnet_identity_extrinsic( }, ) - response = await subtensor.substrate.submit_extrinsic( + success, error_message = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, wait_for_inclusion=wait_for_inclusion, @@ -508,14 +508,13 @@ async def set_subnet_identity_extrinsic( if not wait_for_finalization and not wait_for_inclusion: return True, f"Identities for subnet {netuid} are sent to the chain." - if await response.is_success: + if success: logging.success( f":white_heavy_check_mark: [green]Identities for subnet[/green] [blue]{netuid}[/blue] [green]are set.[/green]" ) return True, f"Identities for subnet {netuid} are set." - else: - error_message = await response.error_message - logging.error( - f":cross_mark: Failed to set identity for subnet [blue]{netuid}[/blue]: {error_message}" - ) - return False, f"Failed to set identity for subnet {netuid}: {error_message}" + + logging.error( + f":cross_mark: Failed to set identity for subnet [blue]{netuid}[/blue]: {error_message}" + ) + return False, f"Failed to set identity for subnet {netuid}: {error_message}" diff --git a/bittensor/core/extrinsics/asyncex/staking.py b/bittensor/core/extrinsics/asyncex/staking.py index d3e22f1905..ddaa1bd240 100644 --- a/bittensor/core/extrinsics/asyncex/staking.py +++ b/bittensor/core/extrinsics/asyncex/staking.py @@ -127,7 +127,7 @@ async def add_stake_extrinsic( logging.info( f":satellite: [magenta]Safe Staking to:[/magenta] " f"[blue]netuid: [green]{netuid}[/green], amount: [green]{staking_balance}[/green], " - f"tolerance percentage: [green]{rate_tolerance*100}%[/green], " + f"tolerance percentage: [green]{rate_tolerance * 100}%[/green], " f"price limit: [green]{rate_with_tolerance}[/green], " f"original price: [green]{base_rate}[/green], " f"with partial stake: [green]{allow_partial_stake}[/green] " diff --git a/bittensor/core/extrinsics/asyncex/start_call.py b/bittensor/core/extrinsics/asyncex/start_call.py new file mode 100644 index 0000000000..bc0089ddb9 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/start_call.py @@ -0,0 +1,61 @@ +from typing import TYPE_CHECKING + +from bittensor.utils import unlock_key, format_error_message +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def start_call_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Submits a start_call extrinsic to the blockchain, to trigger the start call process for a subnet (used to start a + new subnet's emission mechanism). + + Args: + subtensor (Subtensor): The Subtensor client instance used for blockchain interaction. + wallet (Wallet): The wallet used to sign the extrinsic (must be unlocked). + netuid (int): The UID of the target subnet for which the call is being initiated. + wait_for_inclusion (bool, optional): Whether to wait for the extrinsic to be included in a block. Defaults to True. + wait_for_finalization (bool, optional): Whether to wait for finalization of the extrinsic. Defaults to False. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + async with subtensor.substrate as substrate: + start_call = await substrate.compose_call( + call_module="SubtensorModule", + call_function="start_call", + call_params={"netuid": netuid}, + ) + signed_ext = await substrate.create_signed_extrinsic( + call=start_call, + keypair=wallet.coldkey, + ) + + response = await substrate.submit_extrinsic( + extrinsic=signed_ext, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, "Not waiting for finalization or inclusion." + + if await response.is_success: + return True, "Success with `start_call` response." + + return False, format_error_message(await response.error_message) diff --git a/bittensor/core/extrinsics/asyncex/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index 031859c043..54fb43c79d 100644 --- a/bittensor/core/extrinsics/asyncex/unstaking.py +++ b/bittensor/core/extrinsics/asyncex/unstaking.py @@ -106,7 +106,7 @@ async def unstake_extrinsic( logging.info( f":satellite: [magenta]Safe Unstaking from:[/magenta] " f"netuid: [green]{netuid}[/green], amount: [green]{unstaking_balance}[/green], " - f"tolerance percentage: [green]{rate_tolerance*100}%[/green], " + f"tolerance percentage: [green]{rate_tolerance * 100}%[/green], " f"price limit: [green]{rate_with_tolerance}[/green], " f"original price: [green]{base_rate}[/green], " f"with partial unstake: [green]{allow_partial_stake}[/green] " diff --git a/bittensor/core/extrinsics/asyncex/weights.py b/bittensor/core/extrinsics/asyncex/weights.py index b2221a5263..6e07b90adb 100644 --- a/bittensor/core/extrinsics/asyncex/weights.py +++ b/bittensor/core/extrinsics/asyncex/weights.py @@ -287,6 +287,7 @@ async def set_weights_extrinsic( version_key: int = 0, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, + period: int = 5, ) -> tuple[bool, str]: """Sets the given weights and values on chain for wallet hotkey account. @@ -302,6 +303,7 @@ async def set_weights_extrinsic( returns ``False`` if the extrinsic fails to enter the block within the timeout. wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for @@ -331,6 +333,7 @@ async def set_weights_extrinsic( version_key=version_key, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, + period=period, ) if not wait_for_finalization and not wait_for_inclusion: diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 4c1c194708..908fb2b2a9 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -91,6 +91,7 @@ def set_weights_extrinsic( version_key: int = 0, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, + period: int = 5, ) -> tuple[bool, str]: """Sets the given weights and values on chain for wallet hotkey account. @@ -106,6 +107,7 @@ def set_weights_extrinsic( returns ``False`` if the extrinsic fails to enter the block within the timeout. wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``True``, or returns ``False`` if the extrinsic fails to be finalized within the timeout. + period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for @@ -135,6 +137,7 @@ def set_weights_extrinsic( version_key=version_key, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, + period=period, ) if not wait_for_finalization and not wait_for_inclusion: diff --git a/bittensor/core/extrinsics/staking.py b/bittensor/core/extrinsics/staking.py index 3e0b30e130..3a011bdf9b 100644 --- a/bittensor/core/extrinsics/staking.py +++ b/bittensor/core/extrinsics/staking.py @@ -115,7 +115,7 @@ def add_stake_extrinsic( logging.info( f":satellite: [magenta]Safe Staking to:[/magenta] " f"[blue]netuid: [green]{netuid}[/green], amount: [green]{staking_balance}[/green], " - f"tolerance percentage: [green]{rate_tolerance*100}%[/green], " + f"tolerance percentage: [green]{rate_tolerance * 100}%[/green], " f"price limit: [green]{rate_with_tolerance}[/green], " f"original price: [green]{base_rate}[/green], " f"with partial stake: [green]{allow_partial_stake}[/green] " diff --git a/bittensor/core/extrinsics/start_call.py b/bittensor/core/extrinsics/start_call.py new file mode 100644 index 0000000000..d3a1d423ce --- /dev/null +++ b/bittensor/core/extrinsics/start_call.py @@ -0,0 +1,62 @@ +from typing import TYPE_CHECKING + +from bittensor.utils import unlock_key, format_error_message +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +def start_call_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Submits a start_call extrinsic to the blockchain, to trigger the start call process for a subnet (used to start a + new subnet's emission mechanism). + + Args: + subtensor (Subtensor): The Subtensor client instance used for blockchain interaction. + wallet (Wallet): The wallet used to sign the extrinsic (must be unlocked). + netuid (int): The UID of the target subnet for which the call is being initiated. + wait_for_inclusion (bool, optional): Whether to wait for the extrinsic to be included in a block. Defaults to True. + wait_for_finalization (bool, optional): Whether to wait for finalization of the extrinsic. Defaults to False. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + with subtensor.substrate as substrate: + start_call = substrate.compose_call( + call_module="SubtensorModule", + call_function="start_call", + call_params={"netuid": netuid}, + ) + + signed_ext = substrate.create_signed_extrinsic( + call=start_call, + keypair=wallet.coldkey, + ) + + response = substrate.submit_extrinsic( + extrinsic=signed_ext, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, "Not waiting for finalization or inclusion." + + if response.is_success: + return True, "Success with `start_call` response." + + return False, format_error_message(response.error_message) diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index edc2538902..f49362a4db 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -103,7 +103,7 @@ def unstake_extrinsic( logging.info( f":satellite: [magenta]Safe Unstaking from:[/magenta] " f"netuid: [green]{netuid}[/green], amount: [green]{unstaking_balance}[/green], " - f"tolerance percentage: [green]{rate_tolerance*100}%[/green], " + f"tolerance percentage: [green]{rate_tolerance * 100}%[/green], " f"price limit: [green]{rate_with_tolerance}[/green], " f"original price: [green]{base_rate}[/green], " f"with partial unstake: [green]{allow_partial_stake}[/green] " diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 5554b32a3d..af758939ea 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -1,4 +1,5 @@ import asyncio +import contextlib import copy import os import pickle @@ -11,6 +12,7 @@ import numpy as np from async_substrate_interface.errors import SubstrateRequestException from numpy.typing import NDArray +from packaging import version from bittensor.core import settings from bittensor.core.chain_data import ( @@ -143,6 +145,27 @@ def latest_block_path(dir_path: str) -> str: return latest_file_full_path +def safe_globals(): + """ + Context manager to load torch files for version 2.6+ + """ + if version.parse(torch.__version__).release < version.parse("2.6").release: + return contextlib.nullcontext() + + np_core = ( + np._core if version.parse(np.__version__) >= version.parse("2.0.0") else np.core + ) + allow_list = [ + np_core.multiarray._reconstruct, + np.ndarray, + np.dtype, + type(np.dtype(np.uint32)), + np.dtypes.Float32DType, + bytes, + ] + return torch.serialization.safe_globals(allow_list) + + class MetagraphMixin(ABC): """ The metagraph class is a core component of the Bittensor network, representing the neural graph that forms the @@ -1124,7 +1147,8 @@ def load_from_path(self, dir_path: str) -> "MetagraphMixin": """ graph_file = latest_block_path(dir_path) - state_dict = torch.load(graph_file) + with safe_globals(): + state_dict = torch.load(graph_file) self.n = torch.nn.Parameter(state_dict["n"], requires_grad=False) self.block = torch.nn.Parameter(state_dict["block"], requires_grad=False) self.uids = torch.nn.Parameter(state_dict["uids"], requires_grad=False) @@ -1256,7 +1280,8 @@ def load_from_path(self, dir_path: str) -> "MetagraphMixin": try: import torch as real_torch - state_dict = real_torch.load(graph_filename) + with safe_globals(): + state_dict = real_torch.load(graph_filename) for key in METAGRAPH_STATE_DICT_NDARRAY_KEYS: state_dict[key] = state_dict[key].detach().numpy() del real_torch diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index d3b68c04d3..39798ed295 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -1,6 +1,5 @@ -__version__ = "9.3.0" - import os +import importlib.metadata import re from pathlib import Path @@ -15,6 +14,8 @@ WALLETS_DIR = USER_BITTENSOR_DIR / "wallets" MINERS_DIR = USER_BITTENSOR_DIR / "miners" +__version__ = importlib.metadata.version("bittensor") + if not READ_ONLY: # Create dirs if they don't exist @@ -22,7 +23,7 @@ MINERS_DIR.mkdir(parents=True, exist_ok=True) # Bittensor networks name -NETWORKS = ["finney", "test", "archive", "local", "subvortex", "rao", "latent-lite"] +NETWORKS = ["finney", "test", "archive", "local", "subvortex", "latent-lite"] # Bittensor endpoints (Needs to use wss://) FINNEY_ENTRYPOINT = "wss://entrypoint-finney.opentensor.ai:443" @@ -30,7 +31,6 @@ ARCHIVE_ENTRYPOINT = "wss://archive.chain.opentensor.ai:443" LOCAL_ENTRYPOINT = os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") or "ws://127.0.0.1:9944" SUBVORTEX_ENTRYPOINT = "ws://subvortex.info:9944" -RAO_ENTRYPOINT = "wss://rao.chain.opentensor.ai:443" LATENT_LITE_ENTRYPOINT = "wss://lite.sub.latent.to:443" NETWORK_MAP = { @@ -39,8 +39,7 @@ NETWORKS[2]: ARCHIVE_ENTRYPOINT, NETWORKS[3]: LOCAL_ENTRYPOINT, NETWORKS[4]: SUBVORTEX_ENTRYPOINT, - NETWORKS[5]: RAO_ENTRYPOINT, - NETWORKS[6]: LATENT_LITE_ENTRYPOINT, + NETWORKS[5]: LATENT_LITE_ENTRYPOINT, } REVERSE_NETWORK_MAP = { @@ -49,8 +48,7 @@ ARCHIVE_ENTRYPOINT: NETWORKS[2], LOCAL_ENTRYPOINT: NETWORKS[3], SUBVORTEX_ENTRYPOINT: NETWORKS[4], - RAO_ENTRYPOINT: NETWORKS[5], - LATENT_LITE_ENTRYPOINT: NETWORKS[6], + LATENT_LITE_ENTRYPOINT: NETWORKS[5], } DEFAULT_NETWORK = NETWORKS[0] @@ -81,9 +79,9 @@ "finney": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", }, "taostats": { - "local": "https://x.taostats.io", - "endpoint": "https://x.taostats.io", - "finney": "https://x.taostats.io", + "local": "https://taostats.io", + "endpoint": "https://taostats.io", + "finney": "https://taostats.io", }, } diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 19b14d09c0..f7626297ca 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -60,6 +60,7 @@ get_metadata, serve_axon_extrinsic, ) +from bittensor.core.extrinsics.start_call import start_call_extrinsic from bittensor.core.extrinsics.set_weights import set_weights_extrinsic from bittensor.core.extrinsics.staking import ( add_stake_extrinsic, @@ -516,6 +517,7 @@ def does_hotkey_exist(self, hotkey_ss58: str, block: Optional[int] = None) -> bo return_val = ( False if result is None + # not the default key (0x0) else result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" ) return return_val @@ -1300,6 +1302,28 @@ def get_neuron_for_pubkey_and_subnet( return NeuronInfo.from_dict(result) + def get_next_epoch_start_block( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Calculates the first block number of the next epoch for the given subnet. + + If `block` is not provided, the current chain block will be used. Epochs are + determined based on the subnet's tempo (i.e., blocks per epoch). The result + is the block number at which the next epoch will begin. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int], optional): The reference block to calculate from. + If None, uses the current chain block height. + + Returns: + int: The block number at which the next epoch will start. + """ + block = block or self.block + tempo = self.tempo(netuid=netuid, block=block) + return (((block // tempo) + 1) * tempo) + 1 if tempo else None + def get_owned_hotkeys( self, coldkey_ss58: str, @@ -2444,7 +2468,7 @@ def wait_for_block(self, block: Optional[int] = None): def handler(block_data: dict): logging.debug( - f'reached block {block_data["header"]["number"]}. Waiting for block {target_block}' + f"reached block {block_data['header']['number']}. Waiting for block {target_block}" ) if block_data["header"]["number"] >= target_block: return True @@ -3100,6 +3124,7 @@ def set_weights( wait_for_finalization: bool = False, max_retries: int = 5, block_time: float = 12.0, + period: int = 5, ) -> tuple[bool, str]: """ Sets the inter-neuronal weights for the specified neuron. This process involves specifying the influence or @@ -3120,6 +3145,7 @@ def set_weights( ``False``. max_retries (int): The number of maximum attempts to set weights. Default is ``5``. block_time (float): The amount of seconds for block duration. Default is 12.0 seconds. + period (int, optional): The period in seconds to wait for extrinsic inclusion or finalization. Defaults to 5. Returns: tuple[bool, str]: ``True`` if the setting of weights is successful, False otherwise. And `msg`, a string @@ -3183,6 +3209,7 @@ def _blocks_weight_limit() -> bool: version_key=version_key, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + period=period, ) except Exception as e: logging.error(f"Error setting weights: {e}") @@ -3227,6 +3254,36 @@ def serve_axon( certificate=certificate, ) + def start_call( + self, + wallet: "Wallet", + netuid: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> tuple[bool, str]: + """ + Submits a start_call extrinsic to the blockchain, to trigger the start call process for a subnet (used to start a + new subnet's emission mechanism). + + Args: + wallet (Wallet): The wallet used to sign the extrinsic (must be unlocked). + netuid (int): The UID of the target subnet for which the call is being initiated. + wait_for_inclusion (bool, optional): Whether to wait for the extrinsic to be included in a block. Defaults to True. + wait_for_finalization (bool, optional): Whether to wait for finalization of the extrinsic. Defaults to False. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + return start_call_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + def swap_stake( self, wallet: "Wallet", diff --git a/bittensor/core/threadpool.py b/bittensor/core/threadpool.py index 868abf8452..bca3ad014c 100644 --- a/bittensor/core/threadpool.py +++ b/bittensor/core/threadpool.py @@ -219,7 +219,7 @@ def submit(self, fn: Callable, *args, **kwargs) -> _base.Future: raise RuntimeError("cannot schedule new futures after shutdown") if _shutdown: raise RuntimeError( - "cannot schedule new futures after " "interpreter shutdown" + "cannot schedule new futures after interpreter shutdown" ) priority = kwargs.get("priority", random.randint(0, 1000000)) @@ -269,7 +269,7 @@ def weakref_cb(_, q=self._work_queue): def _initializer_failed(self): with self._shutdown_lock: self._broken = ( - "A thread initializer failed, the thread pool " "is not usable anymore" + "A thread initializer failed, the thread pool is not usable anymore" ) # Drain work queue and mark pending futures failed while True: diff --git a/bittensor/core/timelock.py b/bittensor/core/timelock.py new file mode 100644 index 0000000000..24a18405f4 --- /dev/null +++ b/bittensor/core/timelock.py @@ -0,0 +1,184 @@ +""" +This module provides functionality for TimeLock Encryption (TLE), a mechanism that encrypts data such that it can +only be decrypted after a specific amount of time (expressed in the form of Drand rounds). It includes functions +for encryption, decryption, and handling the decryption process by waiting for the reveal round. The logic is based on +Drand QuickNet. + +Main Functions: + - encrypt: Encrypts data and returns the encrypted data along with the reveal round. + - decrypt: Decrypts the provided encrypted data when the reveal round is reached. + - wait_reveal_and_decrypt: Waits for the reveal round and decrypts the encrypted data. + +Usage Example: + ```python + from bittensor import timelock + data = "From Cortex to Bittensor" + encrypted_data, reveal_round = timelock.encrypt(data, n_blocks=5) + decrypted_data = timelock.wait_reveal_and_decrypt(encrypted_data) + ``` + +Usage Example with custom data: + ```python + import pickle + from dataclasses import dataclass + + from bittensor import timelock + + + @dataclass + class Person: + name: str + age: int + + # get instance of your data + x_person = Person("X Lynch", 123) + + # get bytes of your instance + byte_data = pickle.dumps(x_person) + + # get TLE encoded bytes + encrypted, reveal_round = timelock.encrypt(byte_data, 1) + + # wait when reveal round appears in Drand QuickNet and get decrypted data + decrypted = timelock.wait_reveal_and_decrypt(encrypted_data=encrypted) + + # convert bytes into your instance back + x_person_2 = pickle.loads(decrypted) + + # make sure initial and decoded instances are the same + assert x_person == x_person_2 + ``` + +Note: +For handling fast-block nodes, set the `block_time` parameter to 0.25 seconds during encryption. +""" + +import struct +import time +from typing import Optional, Union + +from bittensor_commit_reveal import ( + encrypt as _btr_encrypt, + decrypt as _btr_decrypt, + get_latest_round, +) + +TLE_ENCRYPTED_DATA_SUFFIX = b"AES_GCM_" + + +def encrypt( + data: Union[bytes, str], n_blocks: int, block_time: Union[int, float] = 12.0 +) -> tuple[bytes, int]: + """Encrypts data using TimeLock Encryption + + Arguments: + data: Any bytes data to be encrypted. + n_blocks: Number of blocks to encrypt. + block_time: Time in seconds for each block. Default is `12.0` seconds. + + Returns: + tuple: A tuple containing the encrypted data and reveal TimeLock reveal round. + + Raises: + PyValueError: If failed to encrypt data. + + Usage: + data = "From Cortex to Bittensor" + + # default usage + encrypted_data, reveal_round = encrypt(data, 10) + + # passing block_time for fast-blocks node + encrypted_data, reveal_round = encrypt(data, 15, block_time=0.25) + + encrypted_data, reveal_round = encrypt(data, 5) + + + Note: + For using this function with fast-blocks node you need to set block_time to 0.25 seconds. + data, round = encrypt(data, n_blocks, block_time=0.25) + """ + if isinstance(data, str): + data = data.encode() + return _btr_encrypt(data, n_blocks, block_time) + + +def decrypt( + encrypted_data: bytes, no_errors: bool = True, return_str: bool = False +) -> Optional[Union[bytes, str]]: + """Decrypts encrypted data using TimeLock Decryption + + Arguments: + encrypted_data: Encrypted data to be decrypted. + no_errors: If True, no errors will be raised during decryption. + return_str: convert decrypted data to string if `True`. Default is `False`. + + Returns: + decrypted_data: Decrypted data, when reveled round is reached. + + Usage: + # default usage + decrypted_data = decrypt(encrypted_data) + + # passing no_errors=False for raising errors during decryption + decrypted_data = decrypt(encrypted_data, no_errors=False) + + # passing return_str=True for returning decrypted data as string + decrypted_data = decrypt(encrypted_data, return_str=True) + """ + result = _btr_decrypt(encrypted_data, no_errors) + if result is None: + return None + if return_str: + return result.decode() + return result + + +def wait_reveal_and_decrypt( + encrypted_data: bytes, + reveal_round: Optional[int] = None, + no_errors: bool = True, + return_str: bool = False, +) -> bytes: + """ + Waits for reveal round and decrypts data using TimeLock Decryption. + + Arguments: + encrypted_data: Encrypted data to be decrypted. + reveal_round: Reveal round to wait for. If None, will be parsed from encrypted data. + no_errors: If True, no errors will be raised during decryption. + return_str: convert decrypted data to string if `True`. Default is `False`. + + Raises: + struct.error: If failed to parse reveal round from encrypted data. + TypeError: If reveal_round is None or wrong type. + IndexError: If provided encrypted_data does not contain reveal round. + + Returns: + bytes: Decrypted data. + + Usage: + import bittensor as bt + encrypted, reveal_round = bt.timelock.encrypt("Cortex is power", 3) + """ + if reveal_round is None: + try: + reveal_round = struct.unpack( + " str: err_docs = error_message.get("docs", [err_description]) err_description = err_docs[0] if err_docs else err_description + elif error_message.get("code") and error_message.get("message"): + err_type = error_message.get("code", err_name) + err_name = "Custom type" + err_description = error_message.get("message", err_description) + + else: + logging.error( + f"String representation of real error_message: {str(error_message)}" + ) + return f"Subtensor returned `{err_name}({err_type})` error. This means: `{err_description}`." diff --git a/bittensor/utils/easy_imports.py b/bittensor/utils/easy_imports.py index 59ebeda7ba..cc81efe3d2 100644 --- a/bittensor/utils/easy_imports.py +++ b/bittensor/utils/easy_imports.py @@ -27,7 +27,7 @@ ) from bittensor_wallet.wallet import display_mnemonic_msg, Wallet # noqa: F401 -from bittensor.core import settings +from bittensor.core import settings, timelock # noqa: F401 from bittensor.core.async_subtensor import AsyncSubtensor from bittensor.core.axon import Axon from bittensor.core.chain_data import ( # noqa: F401 diff --git a/contrib/CONTRIBUTING.md b/contrib/CONTRIBUTING.md index b27b426a09..e0a0a75cee 100644 --- a/contrib/CONTRIBUTING.md +++ b/contrib/CONTRIBUTING.md @@ -18,7 +18,7 @@ The following is a set of guidelines for contributing to Bittensor, which are ho 1. [Refactoring](#refactoring) 1. [Peer Review](#peer-review) 1. [Reporting Bugs](#reporting-bugs) - 1. [Suggesting Features](#suggesting-enhancements) + 1. [Suggesting Features](#suggesting-enhancements-and-features) ## I don't want to read this whole thing I just have a question! @@ -70,7 +70,7 @@ And also here. You can contribute to Bittensor in one of two main ways (as well as many others): 1. [Bug](#reporting-bugs) reporting and fixes -2. New features and Bittensor [enhancements](#suggesting-enhancements) +2. New features and Bittensor [enhancements](#suggesting-enhancements-and-features) > Please follow the Bittensor [style guide](./STYLE.md) regardless of your contribution type. @@ -93,7 +93,7 @@ Here is a high-level summary: > Review the Bittensor [style guide](./STYLE.md) and [development workflow](./DEVELOPMENT_WORKFLOW.md) before contributing. -If you're looking to contribute to Bittensor but unsure where to start, please join our community [discord](https://discord.gg/bittensor), a developer-friendly Bittensor town square. Start with [#development](https://discord.com/channels/799672011265015819/799678806159392768) and [#bounties](https://discord.com/channels/799672011265015819/1095684873810890883) to see what issues are currently posted. For a greater understanding of Bittensor's usage and development, check the [Bittensor Documentation](https://bittensor.com/docs). +If you're looking to contribute to Bittensor but unsure where to start, please join our community [discord](https://discord.gg/bittensor), a developer-friendly Bittensor town square. Start with [#development](https://discord.com/channels/799672011265015819/799678806159392768) and [#bounties](https://discord.com/channels/799672011265015819/1095684873810890883) to see what issues are currently posted. For a greater understanding of Bittensor's usage and development, check the [Bittensor Documentation](https://docs.bittensor.com). #### Pull Request Philosophy @@ -252,7 +252,7 @@ Explain the problem and include additional details to help maintainers reproduce * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. * **Explain which behavior you expected to see instead and why.** * **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. -* **If you're reporting that Bittensor crashed**, include a crash report with a stack trace from the operating system. On macOS, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". Include the crash report in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist. +* **If you're reporting that Bittensor crashed**, include a crash report with a stack trace from the operating system. On macOS, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". Include the crash report in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://docs.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist. * **If the problem is related to performance or memory**, include a CPU profile capture with your report, if you're using a GPU then include a GPU profile capture as well. Look into the [PyTorch Profiler](https://pytorch.org/tutorials/recipes/recipes/profiler_recipe.html) to look at memory usage of your model. * **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below. diff --git a/pyproject.toml b/pyproject.toml index 2232e33960..18d80f6c44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["setuptools~=70.0.0", "wheel"] +requires = ["setuptools>=70.0.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "bittensor" -version = "9.3.0" +version = "9.4.0" description = "Bittensor" readme = "README.md" authors = [ @@ -13,6 +13,7 @@ authors = [ license = { file = "LICENSE" } requires-python = ">=3.9,<3.14" dependencies = [ + "wheel", "setuptools~=70.0.0", "aiohttp~=3.9", @@ -33,18 +34,18 @@ dependencies = [ "pydantic>=2.3, <3", "scalecodec==1.2.11", "uvicorn", - "bittensor-commit-reveal>=0.3.1", + "bittensor-commit-reveal>=0.4.0", "bittensor-wallet>=3.0.8", "async-substrate-interface>=1.1.0" ] [project.optional-dependencies] dev = [ - "pytest==7.2.0", - "pytest-asyncio==0.23.7", - "pytest-mock==3.12.0", - "pytest-split==0.8.0", - "pytest-xdist==3.0.2", + "pytest==8.3.5", + "pytest-asyncio==0.26.0", + "pytest-mock==3.14.0", + "pytest-split==0.10.0", + "pytest-xdist==3.6.1", "pytest-rerunfailures==10.2", "coveralls==3.3.1", "pytest-cov==4.0.0", @@ -55,19 +56,20 @@ dev = [ "types-retry==0.9.9.4", "freezegun==1.5.0", "httpx==0.27.0", - "ruff==0.4.7", + "ruff==0.11.5", "aioresponses==0.7.6", "factory-boy==3.3.0", "types-requests", - "torch>=1.13.1,<2.6.0" + "torch>=1.13.1,<3.0" ] torch = [ - "torch>=1.13.1,<2.6.0" + "torch>=1.13.1,<3.0" ] cli = [ "bittensor-cli>=9.0.2" ] + [project.urls] # more details can be found here homepage = "https://github.com/opentensor/bittensor" diff --git a/requirements/cli.txt b/requirements/cli.txt deleted file mode 100644 index e395b2b9c1..0000000000 --- a/requirements/cli.txt +++ /dev/null @@ -1 +0,0 @@ -bittensor-cli>=9.0.2 \ No newline at end of file diff --git a/requirements/cubit.txt b/requirements/cubit.txt deleted file mode 100644 index 5af1316836..0000000000 --- a/requirements/cubit.txt +++ /dev/null @@ -1,3 +0,0 @@ -torch>=1.13.1 -cubit>=1.1.0 -cubit @ git+https://github.com/opentensor/cubit.git diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index 77e21b0eeb..0000000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,19 +0,0 @@ -pytest==7.2.0 -pytest-asyncio==0.23.7 -pytest-mock==3.12.0 -pytest-split==0.8.0 -pytest-xdist==3.0.2 -pytest-rerunfailures==10.2 -coveralls==3.3.1 -pytest-cov==4.0.0 -ddt==1.6.0 -hypothesis==6.81.1 -flake8==7.0.0 -mypy==1.8.0 -types-retry==0.9.9.4 -freezegun==1.5.0 -httpx==0.27.0 -ruff==0.4.7 -aioresponses==0.7.6 -factory-boy==3.3.0 -types-requests \ No newline at end of file diff --git a/requirements/prod.txt b/requirements/prod.txt deleted file mode 100644 index 893d925ce9..0000000000 --- a/requirements/prod.txt +++ /dev/null @@ -1,26 +0,0 @@ -wheel -setuptools~=70.0.0 -aiohttp~=3.9 -asyncstdlib~=3.13.0 -colorama~=0.4.6 -fastapi~=0.110.1 -munch~=2.5.0 -numpy~=2.0.1 -msgpack-numpy-opentensor~=0.5.0 -nest_asyncio -netaddr -packaging -python-statemachine~=2.1 -pycryptodome>=3.18.0,<4.0.0 -pyyaml -retry -requests -rich -pydantic>=2.3, <3 -python-Levenshtein -scalecodec==1.2.11 -uvicorn -websockets>=14.1 -bittensor-commit-reveal>=0.3.1 -bittensor-wallet>=3.0.7 -async-substrate-interface>=1.0.4 diff --git a/requirements/torch.txt b/requirements/torch.txt deleted file mode 100644 index 1abaa00adc..0000000000 --- a/requirements/torch.txt +++ /dev/null @@ -1 +0,0 @@ -torch>=1.13.1,<2.6.0 diff --git a/scripts/check_compatibility.sh b/scripts/check_compatibility.sh deleted file mode 100755 index b9c89c24dd..0000000000 --- a/scripts/check_compatibility.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash - -if [ -z "$1" ]; then - echo "Please provide a Python version as an argument." - exit 1 -fi - -python_version="$1" -all_passed=true - -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -check_compatibility() { - all_supported=0 - - while read -r requirement; do - # Skip lines starting with git+ - if [[ "$requirement" == git+* ]]; then - continue - fi - - package_name=$(echo "$requirement" | awk -F'[!=<>~]' '{print $1}' | awk -F'[' '{print $1}') # Strip off brackets - echo -n "Checking $package_name... " - - url="https://pypi.org/pypi/$package_name/json" - response=$(curl -s $url) - status_code=$(curl -s -o /dev/null -w "%{http_code}" $url) - - if [ "$status_code" != "200" ]; then - echo -e "${RED}Information not available for $package_name. Failure.${NC}" - all_supported=1 - continue - fi - - classifiers=$(echo "$response" | jq -r '.info.classifiers[]') - requires_python=$(echo "$response" | jq -r '.info.requires_python') - - base_version="Programming Language :: Python :: ${python_version%%.*}" - specific_version="Programming Language :: Python :: $python_version" - - if echo "$classifiers" | grep -q "$specific_version" || echo "$classifiers" | grep -q "$base_version"; then - echo -e "${GREEN}Supported${NC}" - elif [ "$requires_python" != "null" ]; then - if echo "$requires_python" | grep -Eq "==$python_version|>=$python_version|<=$python_version"; then - echo -e "${GREEN}Supported${NC}" - else - echo -e "${RED}Not compatible with Python $python_version due to constraint $requires_python.${NC}" - all_supported=1 - fi - else - echo -e "${YELLOW}Warning: Specific version not listed, assuming compatibility${NC}" - fi - done < requirements/prod.txt - - return $all_supported -} - -echo "Checking compatibility for Python $python_version..." -check_compatibility -if [ $? -eq 0 ]; then - echo -e "${GREEN}All requirements are compatible with Python $python_version.${NC}" -else - echo -e "${RED}All requirements are NOT compatible with Python $python_version.${NC}" - all_passed=false -fi - -echo "" -if $all_passed; then - echo -e "${GREEN}All tests passed.${NC}" -else - echo -e "${RED}All tests did not pass.${NC}" - exit 1 -fi diff --git a/scripts/check_requirements_changes.sh b/scripts/check_requirements_changes.sh index 5fcd27ea3f..5b41f463c1 100755 --- a/scripts/check_requirements_changes.sh +++ b/scripts/check_requirements_changes.sh @@ -1,8 +1,8 @@ #!/bin/bash # Check if requirements files have changed in the last commit -if git diff --name-only HEAD~1 | grep -E 'requirements/prod.txt|requirements/dev.txt'; then - echo "Requirements files have changed. Running compatibility checks..." +if git diff --name-only HEAD~1 | grep -E 'pyproject.toml'; then + echo "Requirements files may have changed. Running compatibility checks..." echo 'export REQUIREMENTS_CHANGED="true"' >> $BASH_ENV else echo "Requirements files have not changed. Skipping compatibility checks..." diff --git a/tests/e2e_tests/test_axon.py b/tests/e2e_tests/test_axon.py index 53dff3646e..0a139eb457 100644 --- a/tests/e2e_tests/test_axon.py +++ b/tests/e2e_tests/test_axon.py @@ -34,12 +34,12 @@ async def test_axon(subtensor, templates, alice_wallet): # Validate current metagraph stats old_axon = metagraph.axons[0] assert len(metagraph.axons) == 1, f"Expected 1 axon, but got {len(metagraph.axons)}" - assert ( - old_axon.hotkey == alice_wallet.hotkey.ss58_address - ), "Hotkey mismatch for the axon" - assert ( - old_axon.coldkey == alice_wallet.coldkey.ss58_address - ), "Coldkey mismatch for the axon" + assert old_axon.hotkey == alice_wallet.hotkey.ss58_address, ( + "Hotkey mismatch for the axon" + ) + assert old_axon.coldkey == alice_wallet.coldkey.ss58_address, ( + "Coldkey mismatch for the axon" + ) assert old_axon.ip == "0.0.0.0", f"Expected IP 0.0.0.0, but got {old_axon.ip}" assert old_axon.port == 0, f"Expected port 0, but got {old_axon.port}" assert old_axon.ip_type == 0, f"Expected IP type 0, but got {old_axon.ip_type}" @@ -54,30 +54,30 @@ async def test_axon(subtensor, templates, alice_wallet): external_ip = networking.get_external_ip() # Assert updated attributes - assert ( - len(metagraph.axons) == 1 - ), f"Expected 1 axon, but got {len(metagraph.axons)} after mining" + assert len(metagraph.axons) == 1, ( + f"Expected 1 axon, but got {len(metagraph.axons)} after mining" + ) - assert ( - len(metagraph.neurons) == 1 - ), f"Expected 1 neuron, but got {len(metagraph.neurons)}" + assert len(metagraph.neurons) == 1, ( + f"Expected 1 neuron, but got {len(metagraph.neurons)}" + ) - assert ( - updated_axon.ip == external_ip - ), f"Expected IP {external_ip}, but got {updated_axon.ip}" + assert updated_axon.ip == external_ip, ( + f"Expected IP {external_ip}, but got {updated_axon.ip}" + ) - assert ( - updated_axon.ip_type == networking.ip_version(external_ip) - ), f"Expected IP type {networking.ip_version(external_ip)}, but got {updated_axon.ip_type}" + assert updated_axon.ip_type == networking.ip_version(external_ip), ( + f"Expected IP type {networking.ip_version(external_ip)}, but got {updated_axon.ip_type}" + ) assert updated_axon.port == 8091, f"Expected port 8091, but got {updated_axon.port}" - assert ( - updated_axon.hotkey == alice_wallet.hotkey.ss58_address - ), "Hotkey mismatch after mining" + assert updated_axon.hotkey == alice_wallet.hotkey.ss58_address, ( + "Hotkey mismatch after mining" + ) - assert ( - updated_axon.coldkey == alice_wallet.coldkey.ss58_address - ), "Coldkey mismatch after mining" + assert updated_axon.coldkey == alice_wallet.coldkey.ss58_address, ( + "Coldkey mismatch after mining" + ) print("✅ Passed test_axon") diff --git a/tests/e2e_tests/test_commit_reveal_v3.py b/tests/e2e_tests/test_commit_reveal_v3.py index 6d45cbd94d..106af50a96 100644 --- a/tests/e2e_tests/test_commit_reveal_v3.py +++ b/tests/e2e_tests/test_commit_reveal_v3.py @@ -1,4 +1,5 @@ import re +import time import numpy as np import pytest @@ -12,7 +13,7 @@ ) -@pytest.mark.parametrize("local_chain", [True], indirect=True) +# @pytest.mark.parametrize("local_chain", [True], indirect=True) @pytest.mark.asyncio async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_wallet): """ @@ -96,7 +97,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle # Fetch current block and calculate next tempo for the subnet current_block = subtensor.get_current_block() - upcoming_tempo = next_tempo(current_block, tempo, netuid) + upcoming_tempo = next_tempo(current_block, tempo) logging.console.info( f"Checking if window is too low with Current block: {current_block}, next tempo: {upcoming_tempo}" ) @@ -114,7 +115,7 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle ) current_block = subtensor.get_current_block() latest_drand_round = subtensor.last_drand_round() - upcoming_tempo = next_tempo(current_block, tempo, netuid) + upcoming_tempo = next_tempo(current_block, tempo) logging.console.info( f"Post first wait_interval (to ensure window isnt too low): {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" ) @@ -142,15 +143,15 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle current_block = subtensor.get_current_block() latest_drand_round = subtensor.last_drand_round() - upcoming_tempo = next_tempo(current_block, tempo, netuid) + upcoming_tempo = next_tempo(current_block, tempo) logging.console.info( f"After setting weights: Current block: {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" ) # Ensure the expected drand round is well in the future - assert ( - expected_reveal_round >= latest_drand_round - ), "Revealed drand pulse is older than the drand pulse right after setting weights" + assert expected_reveal_round >= latest_drand_round + 1, ( + "Revealed drand pulse is older than the drand pulse right after setting weights" + ) # Fetch current commits pending on the chain commits_on_chain = subtensor.get_current_weight_commit_info(netuid=netuid) @@ -177,6 +178,10 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle f"Latest drand round after waiting for tempo: {latest_drand_round}" ) + # wait until last_drand_round is the same or greeter than expected_reveal_round with sleep 3 second (as Drand round period) + while expected_reveal_round >= subtensor.last_drand_round(): + time.sleep(3) + # Fetch weights on the chain as they should be revealed now revealed_weights_ = subtensor.weights(netuid=netuid) @@ -190,8 +195,8 @@ async def test_commit_and_reveal_weights_cr3(local_chain, subtensor, alice_walle assert subtensor.get_current_weight_commit_info(netuid=netuid) == [] # Ensure the drand_round is always in the positive w.r.t expected when revealed - assert ( - latest_drand_round - expected_reveal_round >= 0 - ), f"latest_drand_round ({latest_drand_round}) is less than expected_reveal_round ({expected_reveal_round})" + assert latest_drand_round - expected_reveal_round >= 0, ( + f"latest_drand_round ({latest_drand_round}) is less than expected_reveal_round ({expected_reveal_round})" + ) logging.console.info("✅ Passed commit_reveal v3") diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index c4701ad71e..1b39dd411e 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -1,13 +1,13 @@ -import asyncio - import numpy as np import pytest +import retry +from bittensor.utils.btlogging import logging from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit from tests.e2e_tests.utils.chain_interactions import ( sudo_set_admin_utils, sudo_set_hyperparameter_bool, - use_and_wait_for_next_nonce, + execute_and_wait_for_next_nonce, wait_epoch, ) @@ -51,9 +51,9 @@ async def test_commit_and_reveal_weights_legacy(local_chain, subtensor, alice_wa subtensor.get_subnet_hyperparameters(netuid=netuid).commit_reveal_period == 1 ), "Failed to set commit/reveal periods" - assert ( - subtensor.weights_rate_limit(netuid=netuid) > 0 - ), "Weights rate limit is below 0" + assert subtensor.weights_rate_limit(netuid=netuid) > 0, ( + "Weights rate limit is below 0" + ) # Lower the rate limit status, error = sudo_set_admin_utils( @@ -114,9 +114,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 - assert ( - subtensor.get_subnet_reveal_period_epochs(netuid) > 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) @@ -144,9 +144,9 @@ async def test_commit_and_reveal_weights_legacy(local_chain, subtensor, alice_wa # Assert that the revealed weights are set correctly assert revealed_weights is not None, "Weight reveal not found in storage" - assert ( - weight_vals[0] == revealed_weights[0][1] - ), f"Incorrect revealed weights. Expected: {weights[0]}, Actual: {revealed_weights[0][1]}" + assert weight_vals[0] == revealed_weights[0][1], ( + f"Incorrect revealed weights. Expected: {weights[0]}, Actual: {revealed_weights[0][1]}" + ) print("✅ Passed test_commit_and_reveal_weights") @@ -165,10 +165,12 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall Raises: AssertionError: If any of the checks or verifications fail """ + subnet_tempo = 50 + netuid = 2 + # Wait for 2 tempos to pass as CR3 only reveals weights after 2 tempos - subtensor.wait_for_block(20) + subtensor.wait_for_block(subnet_tempo * 2 + 1) - netuid = 2 print("Testing test_commit_and_reveal_weights") # Register root as Alice assert subtensor.register_subnet(alice_wallet), "Unable to register the subnet" @@ -176,6 +178,17 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall # Verify subnet 1 created successfully assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" + # weights sensitive to epoch changes + assert sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tempo", + call_params={ + "netuid": netuid, + "tempo": subnet_tempo, + }, + ) + # Enable commit_reveal on the subnet assert sudo_set_hyperparameter_bool( local_chain, @@ -191,9 +204,9 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall subtensor.get_subnet_hyperparameters(netuid=netuid).commit_reveal_period == 1 ), "Failed to set commit/reveal periods" - assert ( - subtensor.weights_rate_limit(netuid=netuid) > 0 - ), "Weights rate limit is below 0" + assert subtensor.weights_rate_limit(netuid=netuid) > 0, ( + "Weights rate limit is below 0" + ) # Lower the rate limit status, error = sudo_set_admin_utils( @@ -203,72 +216,67 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, ) - assert error is None - assert status is True + assert error is None and status is True, f"Failed to set rate limit: {error}" assert ( subtensor.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit == 0 ), "Failed to set weights_rate_limit" assert subtensor.weights_rate_limit(netuid=netuid) == 0 - # Commit-reveal values - uids = np.array([0], dtype=np.int64) - weights = np.array([0.1], dtype=np.float32) - salt = [18, 179, 107, 0, 165, 211, 141, 197] - weight_uids, weight_vals = convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) - - # Make a second salt - salt2 = salt.copy() - salt2[0] += 1 # Increment the first byte to produce a different commit hash - - # Make a third salt - salt3 = salt.copy() - salt3[0] += 2 # Increment the first byte to produce a different commit hash - - # Commit all three salts - async with use_and_wait_for_next_nonce(subtensor, alice_wallet): - success, message = subtensor.commit_weights( - alice_wallet, - netuid, - salt=salt, - uids=weight_uids, - weights=weight_vals, - wait_for_inclusion=False, # Don't wait for inclusion, we are testing the nonce when there is a tx in the pool - wait_for_finalization=False, + # wait while weights_rate_limit changes applied. + subtensor.wait_for_block(subnet_tempo + 1) + + # create different commited data to avoid coming into pool black list with the error + # Failed to commit weights: Subtensor returned `Custom type(1012)` error. This means: `Transaction is temporarily + # banned`.Failed to commit weights: Subtensor returned `Custom type(1012)` error. This means: `Transaction is + # temporarily banned`.` + def get_weights_and_salt(counter: int): + # Commit-reveal values + salt_ = [18, 179, 107, counter, 165, 211, 141, 197] + uids_ = np.array([0], dtype=np.int64) + weights_ = np.array([counter / 10], dtype=np.float32) + weight_uids_, weight_vals_ = convert_weights_and_uids_for_emit( + uids=uids_, weights=weights_ ) + return salt_, weight_uids_, weight_vals_ - assert success is True + logging.console.info( + f"[orange]Nonce before first commit_weights: " + f"{subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" + ) - async with use_and_wait_for_next_nonce(subtensor, alice_wallet): + # 3 time doing call if nonce wasn't updated, then raise error + @retry.retry(exceptions=Exception, tries=3, delay=1) + @execute_and_wait_for_next_nonce(subtensor=subtensor, wallet=alice_wallet) + def send_commit(salt_, weight_uids_, weight_vals_): success, message = subtensor.commit_weights( - alice_wallet, - netuid, - salt=salt2, - uids=weight_uids, - weights=weight_vals, - wait_for_inclusion=False, - wait_for_finalization=False, + wallet=alice_wallet, + netuid=netuid, + salt=salt_, + uids=weight_uids_, + weights=weight_vals_, + wait_for_inclusion=True, + wait_for_finalization=True, ) + assert success is True, message - assert success is True + # send some amount of commit weights + AMOUNT_OF_COMMIT_WEIGHTS = 3 + for call in range(AMOUNT_OF_COMMIT_WEIGHTS): + weight_uids, weight_vals, salt = get_weights_and_salt(call) - async with use_and_wait_for_next_nonce(subtensor, alice_wallet): - success, message = subtensor.commit_weights( - alice_wallet, - netuid, - salt=salt3, - uids=weight_uids, - weights=weight_vals, - wait_for_inclusion=False, - wait_for_finalization=False, - ) + send_commit(salt, weight_uids, weight_vals) + + # let's wait for 3 (12 fast blocks) seconds between transactions + subtensor.wait_for_block(subtensor.block + 12) - assert success is True + logging.console.info( + f"[orange]Nonce after third commit_weights: " + f"{subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" + ) # Wait a few blocks - await asyncio.sleep(10) # Wait for the txs to be included in the chain + subtensor.wait_for_block(subtensor.block + subtensor.tempo(netuid) * 2) # Query the WeightCommits storage map for all three salts weight_commits = subtensor.query_module( @@ -282,4 +290,6 @@ async def test_commit_weights_uses_next_nonce(local_chain, subtensor, alice_wall assert commit_block > 0, f"Invalid block number: {commit_block}" # Check for three commits in the WeightCommits storage map - assert len(weight_commits.value) == 3, "Expected 3 weight commits" + assert len(weight_commits.value) == AMOUNT_OF_COMMIT_WEIGHTS, ( + "Expected exact list of weight commits" + ) diff --git a/tests/e2e_tests/test_commitment.py b/tests/e2e_tests/test_commitment.py index 0df20688ea..e4704c8d8f 100644 --- a/tests/e2e_tests/test_commitment.py +++ b/tests/e2e_tests/test_commitment.py @@ -3,38 +3,47 @@ from bittensor import logging from tests.e2e_tests.utils.chain_interactions import sudo_set_admin_utils +from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call logging.set_trace() -def test_commitment(local_chain, subtensor, alice_wallet): +def test_commitment(local_chain, subtensor, alice_wallet, dave_wallet): + dave_subnet_netuid = 2 + assert subtensor.register_subnet(dave_wallet, True, True) + assert subtensor.subnet_exists(dave_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, dave_wallet, dave_subnet_netuid, 10) + with pytest.raises(SubstrateRequestException, match="AccountNotAllowedCommit"): subtensor.set_commitment( alice_wallet, - netuid=1, + netuid=dave_subnet_netuid, data="Hello World!", ) assert subtensor.burned_register( alice_wallet, - netuid=1, + netuid=dave_subnet_netuid, ) uid = subtensor.get_uid_for_hotkey_on_subnet( alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, ) assert uid is not None assert "" == subtensor.get_commitment( - netuid=1, + netuid=dave_subnet_netuid, uid=uid, ) assert subtensor.set_commitment( alice_wallet, - netuid=1, + netuid=dave_subnet_netuid, data="Hello World!", ) @@ -44,7 +53,7 @@ def test_commitment(local_chain, subtensor, alice_wallet): call_module="Commitments", call_function="set_max_space", call_params={ - "netuid": 1, + "netuid": dave_subnet_netuid, "new_limit": len("Hello World!"), }, ) @@ -57,51 +66,67 @@ def test_commitment(local_chain, subtensor, alice_wallet): ): subtensor.set_commitment( alice_wallet, - netuid=1, + netuid=dave_subnet_netuid, data="Hello World!1", ) assert "Hello World!" == subtensor.get_commitment( - netuid=1, + netuid=dave_subnet_netuid, uid=uid, ) assert ( - subtensor.get_all_commitments(netuid=1)[alice_wallet.hotkey.ss58_address] + subtensor.get_all_commitments(netuid=dave_subnet_netuid)[ + alice_wallet.hotkey.ss58_address + ] == "Hello World!" ) @pytest.mark.asyncio -async def test_commitment_async(local_chain, async_subtensor, alice_wallet): +async def test_commitment_async( + local_chain, async_subtensor, alice_wallet, dave_wallet +): + dave_subnet_netuid = 2 + assert await async_subtensor.register_subnet(dave_wallet) + assert await async_subtensor.subnet_exists(dave_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + await async_subtensor.wait_for_block(await async_subtensor.block + 20) + status, message = await async_subtensor.start_call( + dave_wallet, dave_subnet_netuid, True, True + ) + assert status, message + async with async_subtensor as sub: with pytest.raises(SubstrateRequestException, match="AccountNotAllowedCommit"): await sub.set_commitment( alice_wallet, - netuid=1, + netuid=dave_subnet_netuid, data="Hello World!", ) assert await sub.burned_register( alice_wallet, - netuid=1, + netuid=dave_subnet_netuid, ) uid = await sub.get_uid_for_hotkey_on_subnet( alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, ) assert uid is not None assert "" == await sub.get_commitment( - netuid=1, + netuid=dave_subnet_netuid, uid=uid, ) assert await sub.set_commitment( alice_wallet, - netuid=1, + netuid=dave_subnet_netuid, data="Hello World!", ) @@ -111,7 +136,7 @@ async def test_commitment_async(local_chain, async_subtensor, alice_wallet): call_module="Commitments", call_function="set_max_space", call_params={ - "netuid": 1, + "netuid": dave_subnet_netuid, "new_limit": len("Hello World!"), }, ) @@ -124,15 +149,15 @@ async def test_commitment_async(local_chain, async_subtensor, alice_wallet): ): await sub.set_commitment( alice_wallet, - netuid=1, + netuid=dave_subnet_netuid, data="Hello World!1", ) assert "Hello World!" == await sub.get_commitment( - netuid=1, + netuid=dave_subnet_netuid, uid=uid, ) - assert (await sub.get_all_commitments(netuid=1))[ + assert (await sub.get_all_commitments(netuid=dave_subnet_netuid))[ alice_wallet.hotkey.ss58_address ] == "Hello World!" diff --git a/tests/e2e_tests/test_delegate.py b/tests/e2e_tests/test_delegate.py index 59f7bbbb75..c66691351d 100644 --- a/tests/e2e_tests/test_delegate.py +++ b/tests/e2e_tests/test_delegate.py @@ -6,11 +6,13 @@ from bittensor.core.chain_data.proposal_vote_data import ProposalVoteData from bittensor.utils.balance import Balance from tests.e2e_tests.utils.chain_interactions import ( + get_dynamic_balance, propose, set_identity, sudo_set_admin_utils, vote, ) +from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call from tests.helpers.helpers import CLOSE_IN_VALUE DEFAULT_DELEGATE_TAKE = 0.179995422293431 @@ -237,26 +239,40 @@ async def test_delegates(subtensor, alice_wallet, bob_wallet): assert subtensor.get_delegated(bob_wallet.coldkey.ss58_address) == [] + alice_subnet_netuid = subtensor.get_total_subnets() # 2 + # Register a subnet, netuid 2 + assert subtensor.register_subnet(alice_wallet), "Subnet wasn't created" + + # Verify subnet created successfully + assert subtensor.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + subtensor.add_stake( bob_wallet, alice_wallet.hotkey.ss58_address, - netuid=0, + netuid=alice_subnet_netuid, amount=Balance.from_tao(10_000), wait_for_inclusion=True, wait_for_finalization=True, ) - assert subtensor.get_delegated(bob_wallet.coldkey.ss58_address) == [ + bob_delegated = subtensor.get_delegated(bob_wallet.coldkey.ss58_address) + assert bob_delegated == [ DelegatedInfo( hotkey_ss58=alice_wallet.hotkey.ss58_address, owner_ss58=alice_wallet.coldkey.ss58_address, take=DEFAULT_DELEGATE_TAKE, - validator_permits=[], - registrations=[0], + validator_permits=[alice_subnet_netuid], + registrations=[0, alice_subnet_netuid], return_per_1000=Balance(0), - total_daily_return=Balance(0), - netuid=0, - stake=Balance.from_tao(9_999.99995), + total_daily_return=get_dynamic_balance( + bob_delegated[0].total_daily_return.rao + ), + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(bob_delegated[0].stake.rao), ), ] @@ -270,17 +286,29 @@ def test_nominator_min_required_stake(local_chain, subtensor, alice_wallet, bob_ - Check Nominator is removed """ - minimum_required_stake = subtensor.get_minimum_required_stake() - - assert minimum_required_stake == Balance(0) + alice_subnet_netuid = subtensor.get_total_subnets() # 2 - subtensor.root_register( + # Register a subnet, netuid 2 + assert subtensor.register_subnet( alice_wallet, wait_for_inclusion=True, wait_for_finalization=True, + ), "Subnet wasn't created" + + # Verify subnet created successfully + assert subtensor.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" ) - subtensor.root_register( + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + minimum_required_stake = subtensor.get_minimum_required_stake() + + assert minimum_required_stake == Balance(0) + + subtensor.burned_register( bob_wallet, + alice_subnet_netuid, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -288,7 +316,7 @@ def test_nominator_min_required_stake(local_chain, subtensor, alice_wallet, bob_ success = subtensor.add_stake( alice_wallet, bob_wallet.hotkey.ss58_address, - netuid=0, + netuid=alice_subnet_netuid, amount=Balance.from_tao(10_000), wait_for_inclusion=True, wait_for_finalization=True, @@ -299,10 +327,10 @@ def test_nominator_min_required_stake(local_chain, subtensor, alice_wallet, bob_ stake = subtensor.get_stake( alice_wallet.coldkey.ss58_address, bob_wallet.hotkey.ss58_address, - netuid=0, + netuid=alice_subnet_netuid, ) - assert stake == Balance.from_tao(9_999.99995) + assert stake > 0 # this will trigger clear_small_nominations sudo_set_admin_utils( @@ -321,7 +349,7 @@ def test_nominator_min_required_stake(local_chain, subtensor, alice_wallet, bob_ stake = subtensor.get_stake( alice_wallet.coldkey.ss58_address, bob_wallet.hotkey.ss58_address, - netuid=0, + netuid=alice_subnet_netuid, ) assert stake == Balance(0) diff --git a/tests/e2e_tests/test_dendrite.py b/tests/e2e_tests/test_dendrite.py index 229a7f40b9..bc439d2da3 100644 --- a/tests/e2e_tests/test_dendrite.py +++ b/tests/e2e_tests/test_dendrite.py @@ -8,6 +8,7 @@ sudo_set_admin_utils, wait_epoch, ) +from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call @pytest.mark.asyncio @@ -25,19 +26,23 @@ async def test_dendrite(local_chain, subtensor, templates, alice_wallet, bob_wal AssertionError: If any of the checks or verifications fail """ + alice_subnet_netuid = subtensor.get_total_subnets() # 2 logging.console.info("Testing test_dendrite") - netuid = 2 # Register a subnet, netuid 2 - assert subtensor.register_subnet(alice_wallet), "Subnet wasn't created" + assert subtensor.register_subnet(alice_wallet, True, True), "Subnet wasn't created" # Verify subnet created successfully - assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" + assert subtensor.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) # Make sure Alice is Top Validator assert subtensor.add_stake( alice_wallet, - netuid=netuid, + netuid=alice_subnet_netuid, amount=Balance.from_tao(1), ) @@ -47,7 +52,7 @@ async def test_dendrite(local_chain, subtensor, templates, alice_wallet, bob_wal alice_wallet, call_function="sudo_set_max_allowed_validators", call_params={ - "netuid": netuid, + "netuid": alice_subnet_netuid, "max_allowed_validators": 1, }, ) @@ -58,7 +63,7 @@ async def test_dendrite(local_chain, subtensor, templates, alice_wallet, bob_wal alice_wallet, call_function="sudo_set_weights_set_rate_limit", call_params={ - "netuid": netuid, + "netuid": alice_subnet_netuid, "weights_set_rate_limit": 10, }, ) @@ -67,11 +72,11 @@ async def test_dendrite(local_chain, subtensor, templates, alice_wallet, bob_wal assert status is True # Register Bob to the network - assert subtensor.burned_register( - bob_wallet, netuid - ), "Unable to register Bob as a neuron" + assert subtensor.burned_register(bob_wallet, alice_subnet_netuid), ( + "Unable to register Bob as a neuron" + ) - metagraph = subtensor.metagraph(netuid) + metagraph = subtensor.metagraph(alice_subnet_netuid) # Assert neurons are Alice and Bob assert len(metagraph.neurons) == 2 @@ -89,16 +94,16 @@ async def test_dendrite(local_chain, subtensor, templates, alice_wallet, bob_wal # Stake to become to top neuron after the first epoch tao = Balance.from_tao(10_000) - alpha, _ = subtensor.subnet(netuid).tao_to_alpha_with_slippage(tao) + alpha, _ = subtensor.subnet(alice_subnet_netuid).tao_to_alpha_with_slippage(tao) assert subtensor.add_stake( bob_wallet, - netuid=netuid, + netuid=alice_subnet_netuid, amount=tao, ) # Refresh metagraph - metagraph = subtensor.metagraph(netuid) + metagraph = subtensor.metagraph(alice_subnet_netuid) bob_neuron = metagraph.neurons[1] # Assert alpha is close to stake equivalent @@ -110,13 +115,13 @@ async def test_dendrite(local_chain, subtensor, templates, alice_wallet, bob_wal assert bob_neuron.validator_trust == 0.0 assert bob_neuron.pruning_score == 0 - async with templates.validator(bob_wallet, netuid): + async with templates.validator(bob_wallet, alice_subnet_netuid): await asyncio.sleep(5) # wait for 5 seconds for the Validator to process - await wait_epoch(subtensor, netuid=netuid) + await wait_epoch(subtensor, netuid=alice_subnet_netuid) # Refresh metagraph - metagraph = subtensor.metagraph(netuid) + metagraph = subtensor.metagraph(alice_subnet_netuid) # Refresh validator neuron updated_neuron = metagraph.neurons[1] diff --git a/tests/e2e_tests/test_hotkeys.py b/tests/e2e_tests/test_hotkeys.py index 86ff768688..f28f4c07f9 100644 --- a/tests/e2e_tests/test_hotkeys.py +++ b/tests/e2e_tests/test_hotkeys.py @@ -5,19 +5,27 @@ sudo_set_admin_utils, wait_epoch, ) - +from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call SET_CHILDREN_COOLDOWN_PERIOD = 15 SET_CHILDREN_RATE_LIMIT = 150 -def test_hotkeys(subtensor, alice_wallet): +def test_hotkeys(subtensor, alice_wallet, dave_wallet): """ Tests: - Check if Hotkey exists - Check if Hotkey is registered """ + dave_subnet_netuid = 2 + assert subtensor.register_subnet(dave_wallet, True, True) + assert subtensor.subnet_exists(dave_subnet_netuid), ( + f"Subnet #{dave_subnet_netuid} does not exist." + ) + + assert wait_to_start_call(subtensor, dave_wallet, dave_subnet_netuid) + coldkey = alice_wallet.coldkeypub.ss58_address hotkey = alice_wallet.hotkey.ss58_address @@ -32,14 +40,14 @@ def test_hotkeys(subtensor, alice_wallet): assert ( subtensor.is_hotkey_registered_on_subnet( hotkey, - netuid=1, + netuid=dave_subnet_netuid, ) is False ) subtensor.burned_register( alice_wallet, - netuid=1, + netuid=dave_subnet_netuid, ) assert subtensor.does_hotkey_exist(hotkey) is True @@ -50,14 +58,14 @@ def test_hotkeys(subtensor, alice_wallet): assert ( subtensor.is_hotkey_registered_on_subnet( hotkey, - netuid=1, + netuid=dave_subnet_netuid, ) is True ) @pytest.mark.asyncio -async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): +async def test_children(local_chain, subtensor, alice_wallet, bob_wallet, dave_wallet): """ Tests: - Get default children (empty list) @@ -68,6 +76,14 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): - Clear children list """ + dave_subnet_netuid = 2 + assert subtensor.register_subnet(dave_wallet, True, True) + assert subtensor.subnet_exists(dave_subnet_netuid), ( + f"Subnet #{dave_subnet_netuid} does not exist." + ) + + assert wait_to_start_call(subtensor, dave_wallet, dave_subnet_netuid) + with pytest.raises(bittensor.RegistrationNotPermittedOnRootSubnet): subtensor.set_children( alice_wallet, @@ -90,23 +106,23 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, - netuid=2, + netuid=3, children=[], raise_error=True, ) subtensor.burned_register( alice_wallet, - netuid=1, + netuid=dave_subnet_netuid, ) subtensor.burned_register( bob_wallet, - netuid=1, + netuid=dave_subnet_netuid, ) success, children, error = subtensor.get_children( alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, ) assert error == "" @@ -117,7 +133,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, children=[ ( 1.0, @@ -131,7 +147,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, children=[ ( 0.1, @@ -146,7 +162,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, children=[ ( 1.0, @@ -164,7 +180,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, children=[ ( 0.5, @@ -178,10 +194,10 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): raise_error=True, ) - subtensor.set_children( + success, error = subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, children=[ ( 1.0, @@ -200,7 +216,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): success, children, error = subtensor.get_children( alice_wallet.hotkey.ss58_address, block=set_children_block, - netuid=1, + netuid=dave_subnet_netuid, ) assert success is True @@ -210,7 +226,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): # children are in pending state pending, cooldown = subtensor.get_children_pending( alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, ) assert pending == [ @@ -226,7 +242,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): success, children, error = subtensor.get_children( alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, ) assert error == "" @@ -241,7 +257,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): # pending queue is empty pending, cooldown = subtensor.get_children_pending( alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, ) assert pending == [] @@ -250,7 +266,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, children=[], raise_error=True, ) @@ -260,7 +276,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, children=[], raise_error=True, ) @@ -268,23 +284,21 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): pending, cooldown = subtensor.get_children_pending( alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, ) assert pending == [] subtensor.wait_for_block(cooldown) - await wait_epoch(subtensor, netuid=1) - success, children, error = subtensor.get_children( alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, ) assert error == "" assert success is True - assert children == [] + assert children == [(1.0, bob_wallet.hotkey.ss58_address)] subtensor.wait_for_block(set_children_block + SET_CHILDREN_RATE_LIMIT) @@ -301,7 +315,7 @@ async def test_children(local_chain, subtensor, alice_wallet, bob_wallet): subtensor.set_children( alice_wallet, alice_wallet.hotkey.ss58_address, - netuid=1, + netuid=dave_subnet_netuid, children=[ ( 1.0, diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index 56f23ddd52..a9d7d69ce1 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -2,12 +2,12 @@ import pytest +from bittensor.utils.btlogging import logging from tests.e2e_tests.utils.chain_interactions import ( - root_set_subtensor_hyperparameter_values, sudo_set_admin_utils, wait_epoch, - wait_interval, ) +from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call DURATION_OF_START_CALL = 10 @@ -27,70 +27,56 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa """ print("Testing test_incentive") - netuid = 2 + alice_subnet_netuid = subtensor.get_total_subnets() # 2 # Register root as Alice - the subnet owner and validator - assert subtensor.register_subnet(alice_wallet) + assert subtensor.register_subnet(alice_wallet, True, True), "Subnet wasn't created" # Verify subnet created successfully - assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" + assert subtensor.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) # Register Bob as a neuron on the subnet - assert subtensor.burned_register( - bob_wallet, netuid - ), "Unable to register Bob as a neuron" + assert subtensor.burned_register(bob_wallet, alice_subnet_netuid), ( + "Unable to register Bob as a neuron" + ) # Assert two neurons are in network - assert ( - len(subtensor.neurons(netuid=netuid)) == 2 - ), "Alice & Bob not registered in the subnet" + assert len(subtensor.neurons(netuid=alice_subnet_netuid)) == 2, ( + "Alice & Bob not registered in the subnet" + ) # Wait for the first epoch to pass - await wait_epoch(subtensor, netuid) - - # Get latest metagraph - metagraph = subtensor.metagraph(netuid) + await wait_epoch(subtensor, alice_subnet_netuid) # Get current miner/validator stats - alice_neuron = metagraph.neurons[0] + alice_neuron = subtensor.neurons(netuid=alice_subnet_netuid)[0] assert alice_neuron.validator_permit is True assert alice_neuron.dividends == 0 - assert alice_neuron.stake.tao == 0 assert alice_neuron.validator_trust == 0 assert alice_neuron.incentive == 0 assert alice_neuron.consensus == 0 assert alice_neuron.rank == 0 - bob_neuron = metagraph.neurons[1] + bob_neuron = subtensor.neurons(netuid=alice_subnet_netuid)[1] assert bob_neuron.incentive == 0 assert bob_neuron.consensus == 0 assert bob_neuron.rank == 0 assert bob_neuron.trust == 0 - subtensor.wait_for_block(DURATION_OF_START_CALL) - - # Subnet "Start Call" https://github.com/opentensor/bits/pull/13 - status, error = await root_set_subtensor_hyperparameter_values( - local_chain, - alice_wallet, - call_function="start_call", - call_params={ - "netuid": netuid, - }, - ) - - assert status is True, error - # update weights_set_rate_limit for fast-blocks - tempo = subtensor.tempo(netuid) + tempo = subtensor.tempo(alice_subnet_netuid) status, error = sudo_set_admin_utils( local_chain, alice_wallet, call_function="sudo_set_weights_set_rate_limit", call_params={ - "netuid": netuid, + "netuid": alice_subnet_netuid, "weights_set_rate_limit": tempo, }, ) @@ -98,49 +84,69 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa assert error is None assert status is True - async with templates.miner(bob_wallet, netuid): - async with templates.validator(alice_wallet, netuid) as validator: - # wait for the Validator to process and set_weights - await asyncio.wait_for(validator.set_weights.wait(), 60) - - # Wait till new epoch - await wait_interval(tempo, subtensor, netuid) - - # Refresh metagraph - metagraph = subtensor.metagraph(netuid) - - # Get current emissions and validate that Alice has gotten tao - alice_neuron = metagraph.neurons[0] - - assert alice_neuron.validator_permit is True - assert alice_neuron.dividends == 1.0 - assert alice_neuron.stake.tao > 0 - assert alice_neuron.validator_trust > 0.99 - assert alice_neuron.incentive < 0.5 - assert alice_neuron.consensus < 0.5 - assert alice_neuron.rank < 0.5 - - bob_neuron = metagraph.neurons[1] - - assert bob_neuron.incentive > 0.5 - assert bob_neuron.consensus > 0.5 - assert bob_neuron.rank > 0.5 - assert bob_neuron.trust == 1 - - bonds = subtensor.bonds(netuid) - - assert bonds == [ - ( - 0, - [ - (0, 65535), - (1, 65535), - ], - ), - ( - 1, - [], - ), - ] - - print("✅ Passed test_incentive") + # max attempts to run miner and validator + max_attempt = 3 + while True: + try: + async with templates.miner(bob_wallet, alice_subnet_netuid) as miner: + await asyncio.wait_for(miner.started.wait(), 60) + + async with templates.validator( + alice_wallet, alice_subnet_netuid + ) as validator: + # wait for the Validator to process and set_weights + await asyncio.wait_for(validator.set_weights.wait(), 60) + break + except asyncio.TimeoutError: + if max_attempt > 0: + max_attempt -= 1 + continue + raise + + # wait one tempo (fast block + subtensor.wait_for_block(subtensor.block + subtensor.tempo(alice_subnet_netuid)) + + while True: + try: + neurons = subtensor.neurons(netuid=alice_subnet_netuid) + logging.info(f"neurons: {neurons}") + + # Get current emissions and validate that Alice has gotten tao + alice_neuron = neurons[0] + + assert alice_neuron.validator_permit is True + assert alice_neuron.dividends == 1.0 + assert alice_neuron.stake.tao > 0 + assert alice_neuron.validator_trust > 0.99 + assert alice_neuron.incentive < 0.5 + assert alice_neuron.consensus < 0.5 + assert alice_neuron.rank < 0.5 + + bob_neuron = neurons[1] + + assert bob_neuron.incentive > 0.5 + assert bob_neuron.consensus > 0.5 + assert bob_neuron.rank > 0.5 + assert bob_neuron.trust == 1 + + bonds = subtensor.bonds(alice_subnet_netuid) + + assert bonds == [ + ( + 0, + [ + (0, 65535), + (1, 65535), + ], + ), + ( + 1, + [], + ), + ] + + print("✅ Passed test_incentive") + break + except Exception: + subtensor.wait_for_block(subtensor.block) + continue diff --git a/tests/e2e_tests/test_liquid_alpha.py b/tests/e2e_tests/test_liquid_alpha.py index cfd3b59b87..c98ace3018 100644 --- a/tests/e2e_tests/test_liquid_alpha.py +++ b/tests/e2e_tests/test_liquid_alpha.py @@ -39,9 +39,9 @@ def test_liquid_alpha(local_chain, subtensor, alice_wallet): assert subtensor.subnet_exists(netuid) # Register a neuron (Alice) to the subnet - assert subtensor.burned_register( - alice_wallet, netuid - ), "Unable to register Alice as a neuron" + assert subtensor.burned_register(alice_wallet, netuid), ( + "Unable to register Alice as a neuron" + ) # Stake to become to top neuron after the first epoch subtensor.add_stake( @@ -87,12 +87,12 @@ def test_liquid_alpha(local_chain, subtensor, alice_wallet): call_function="sudo_set_alpha_values", call_params=call_params, ), "Unable to set alpha_values" - assert ( - subtensor.get_subnet_hyperparameters(netuid).alpha_high == 54099 - ), "Failed to set alpha high" - assert ( - subtensor.get_subnet_hyperparameters(netuid).alpha_low == 87 - ), "Failed to set alpha low" + assert subtensor.get_subnet_hyperparameters(netuid).alpha_high == 54099, ( + "Failed to set alpha high" + ) + assert subtensor.get_subnet_hyperparameters(netuid).alpha_low == 87, ( + "Failed to set alpha low" + ) # Testing alpha high upper and lower bounds @@ -166,19 +166,19 @@ def test_liquid_alpha(local_chain, subtensor, alice_wallet): call_params=call_params, ), "Unable to set liquid alpha values" - assert ( - subtensor.get_subnet_hyperparameters(netuid).alpha_high == 53083 - ), "Failed to set alpha high" - assert ( - subtensor.get_subnet_hyperparameters(netuid).alpha_low == 6553 - ), "Failed to set alpha low" + assert subtensor.get_subnet_hyperparameters(netuid).alpha_high == 53083, ( + "Failed to set alpha high" + ) + assert subtensor.get_subnet_hyperparameters(netuid).alpha_low == 6553, ( + "Failed to set alpha low" + ) # Disable Liquid Alpha assert sudo_set_hyperparameter_bool( local_chain, alice_wallet, "sudo_set_liquid_alpha_enabled", False, netuid ), "Unable to disable liquid alpha" - assert ( - subtensor.get_subnet_hyperparameters(netuid).liquid_alpha_enabled is False - ), "Failed to disable liquid alpha" + assert subtensor.get_subnet_hyperparameters(netuid).liquid_alpha_enabled is False, ( + "Failed to disable liquid alpha" + ) logging.console.info("✅ Passed test_liquid_alpha") diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index 29bb9ceaa9..3338a0959e 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -6,6 +6,7 @@ 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.e2e_test_utils import wait_to_start_call NULL_KEY = tuple(bytearray(32)) @@ -46,98 +47,106 @@ def test_metagraph(subtensor, alice_wallet, bob_wallet, dave_wallet): AssertionError: If any of the checks or verifications fail """ logging.console.info("Testing test_metagraph_command") - netuid = 2 + alice_subnet_netuid = subtensor.get_total_subnets() # 2 - # Register the subnet through Alice - assert subtensor.register_subnet(alice_wallet), "Unable to register the subnet" + logging.console.info("Register the subnet through Alice") + assert subtensor.register_subnet(alice_wallet, True, True), ( + "Unable to register the subnet" + ) + + logging.console.info("Verify subnet was created successfully") + assert subtensor.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) - # Verify subnet was created successfully - assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" + logging.console.info("Make sure we passed start_call limit (10 blocks)") + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) - # Initialize metagraph - metagraph = subtensor.metagraph(netuid=netuid) + logging.console.info("Initialize metagraph") + metagraph = subtensor.metagraph(netuid=alice_subnet_netuid) - # Assert metagraph has only Alice (owner) + logging.console.info("Assert metagraph has only Alice (owner)") assert len(metagraph.uids) == 1, "Metagraph doesn't have exactly 1 neuron" - # Register Bob to the subnet - assert subtensor.burned_register( - bob_wallet, netuid - ), "Unable to register Bob as a neuron" + logging.console.info("Register Bob to the subnet") + assert subtensor.burned_register(bob_wallet, alice_subnet_netuid), ( + "Unable to register Bob as a neuron" + ) - # Refresh the metagraph + logging.console.info("Refresh the metagraph") metagraph.sync(subtensor=subtensor) - # Assert metagraph has Alice and Bob neurons + logging.console.info("Assert metagraph has Alice and Bob neurons") assert len(metagraph.uids) == 2, "Metagraph doesn't have exactly 2 neurons" - assert ( - metagraph.hotkeys[0] == alice_wallet.hotkey.ss58_address - ), "Alice's hotkey doesn't match in metagraph" - assert ( - metagraph.hotkeys[1] == bob_wallet.hotkey.ss58_address - ), "Bob's hotkey doesn't match in metagraph" + assert metagraph.hotkeys[0] == alice_wallet.hotkey.ss58_address, ( + "Alice's hotkey doesn't match in metagraph" + ) + assert metagraph.hotkeys[1] == bob_wallet.hotkey.ss58_address, ( + "Bob's hotkey doesn't match in metagraph" + ) assert len(metagraph.coldkeys) == 2, "Metagraph doesn't have exactly 2 coldkey" assert metagraph.n.max() == 2, "Metagraph's max n is not 2" assert metagraph.n.min() == 2, "Metagraph's min n is not 2" assert len(metagraph.addresses) == 2, "Metagraph doesn't have exactly 2 address" - # Fetch UID of Bob + logging.console.info("Fetch UID of Bob") uid = subtensor.get_uid_for_hotkey_on_subnet( - bob_wallet.hotkey.ss58_address, netuid=netuid + bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid ) - # Fetch neuron info of Bob through subtensor and metagraph - neuron_info_bob = subtensor.neuron_for_uid(uid, netuid=netuid) + logging.console.info("Fetch neuron info of Bob through subtensor and metagraph") + neuron_info_bob = subtensor.neuron_for_uid(uid, netuid=alice_subnet_netuid) + metagraph_dict = neuron_to_dict(metagraph.neurons[uid]) subtensor_dict = neuron_to_dict(neuron_info_bob) - # Verify neuron info is the same in both objects - assert ( - metagraph_dict == subtensor_dict - ), "Neuron info of Bob doesn't match b/w metagraph & subtensor" + logging.console.info("Verify neuron info is the same in both objects") + assert metagraph_dict == subtensor_dict, ( + "Neuron info of Bob doesn't match b/w metagraph & subtensor" + ) - # Create pre_dave metagraph for future verifications - metagraph_pre_dave = subtensor.metagraph(netuid=netuid) + logging.console.info("Create pre_dave metagraph for future verifications") + metagraph_pre_dave = subtensor.metagraph(netuid=alice_subnet_netuid) - # Register Dave as a neuron - assert subtensor.burned_register( - dave_wallet, netuid - ), "Unable to register Dave as a neuron" + logging.console.info("Register Dave as a neuron") + assert subtensor.burned_register(dave_wallet, alice_subnet_netuid), ( + "Unable to register Dave as a neuron" + ) metagraph.sync(subtensor=subtensor) - # Assert metagraph now includes Dave's neuron - assert ( - len(metagraph.uids) == 3 - ), "Metagraph doesn't have exactly 3 neurons post Dave" - assert ( - metagraph.hotkeys[2] == dave_wallet.hotkey.ss58_address - ), "Neuron's hotkey in metagraph doesn't match" - assert ( - len(metagraph.coldkeys) == 3 - ), "Metagraph doesn't have exactly 3 coldkeys post Dave" + logging.console.info("Assert metagraph now includes Dave's neuron") + assert len(metagraph.uids) == 3, ( + "Metagraph doesn't have exactly 3 neurons post Dave" + ) + assert metagraph.hotkeys[2] == dave_wallet.hotkey.ss58_address, ( + "Neuron's hotkey in metagraph doesn't match" + ) + assert len(metagraph.coldkeys) == 3, ( + "Metagraph doesn't have exactly 3 coldkeys post Dave" + ) assert metagraph.n.max() == 3, "Metagraph's max n is not 3 post Dave" assert metagraph.n.min() == 3, "Metagraph's min n is not 3 post Dave" assert len(metagraph.addresses) == 3, "Metagraph doesn't have 3 addresses post Dave" - # Add stake by Bob + logging.console.info("Add stake by Bob") tao = Balance.from_tao(10_000) - alpha, _ = subtensor.subnet(netuid).tao_to_alpha_with_slippage(tao) + alpha, _ = subtensor.subnet(alice_subnet_netuid).tao_to_alpha_with_slippage(tao) assert subtensor.add_stake( bob_wallet, - netuid=netuid, + netuid=alice_subnet_netuid, amount=tao, wait_for_inclusion=True, wait_for_finalization=True, ), "Failed to add stake for Bob" - # Assert stake is added after updating metagraph + logging.console.info("Assert stake is added after updating metagraph") metagraph.sync(subtensor=subtensor) - assert ( - 0.95 < metagraph.neurons[1].stake.rao / alpha.rao < 1.05 - ), "Bob's stake not updated in metagraph" + assert 0.95 < metagraph.neurons[1].stake.rao / alpha.rao < 1.05, ( + "Bob's stake not updated in metagraph" + ) - # Test the save() and load() mechanism + logging.console.info("Test the save() and load() mechanism") # We save the metagraph and pre_dave loads it # We do this in the /tmp dir to avoid interfering or interacting with user data metagraph_save_root_dir = ["/", "tmp", "bittensor-e2e", "metagraphs"] @@ -149,35 +158,35 @@ def test_metagraph(subtensor, alice_wallet, bob_wallet, dave_wallet): finally: shutil.rmtree(os.path.join(*metagraph_save_root_dir)) - # Ensure data is synced between two metagraphs - assert len(metagraph.uids) == len( - metagraph_pre_dave.uids - ), "UID count mismatch after save and load" - assert ( - metagraph.uids == metagraph_pre_dave.uids - ).all(), "UIDs don't match after save and load" - - assert len(metagraph.axons) == len( - metagraph_pre_dave.axons - ), "Axon count mismatch after save and load" - assert ( - metagraph.axons[1].hotkey == metagraph_pre_dave.axons[1].hotkey - ), "Axon hotkey mismatch after save and load" - assert ( - metagraph.axons == metagraph_pre_dave.axons - ), "Axons don't match after save and load" - - assert len(metagraph.neurons) == len( - metagraph_pre_dave.neurons - ), "Neuron count mismatch after save and load" - assert ( - metagraph.neurons == metagraph_pre_dave.neurons - ), "Neurons don't match after save and load" + logging.console.info("Ensure data is synced between two metagraphs") + assert len(metagraph.uids) == len(metagraph_pre_dave.uids), ( + "UID count mismatch after save and load" + ) + assert (metagraph.uids == metagraph_pre_dave.uids).all(), ( + "UIDs don't match after save and load" + ) + + assert len(metagraph.axons) == len(metagraph_pre_dave.axons), ( + "Axon count mismatch after save and load" + ) + assert metagraph.axons[1].hotkey == metagraph_pre_dave.axons[1].hotkey, ( + "Axon hotkey mismatch after save and load" + ) + assert metagraph.axons == metagraph_pre_dave.axons, ( + "Axons don't match after save and load" + ) + + assert len(metagraph.neurons) == len(metagraph_pre_dave.neurons), ( + "Neuron count mismatch after save and load" + ) + assert metagraph.neurons == metagraph_pre_dave.neurons, ( + "Neurons don't match after save and load" + ) logging.console.info("✅ Passed test_metagraph") -def test_metagraph_info(subtensor, alice_wallet): +def test_metagraph_info(subtensor, alice_wallet, bob_wallet): """ Tests: - Check MetagraphInfo @@ -186,6 +195,9 @@ def test_metagraph_info(subtensor, alice_wallet): - Check MetagraphInfo is updated """ + alice_subnet_netuid = subtensor.get_total_subnets() # 2 + subtensor.register_subnet(alice_wallet, True, True) + metagraph_info = subtensor.get_metagraph_info(netuid=1, block=1) assert metagraph_info == MetagraphInfo( @@ -243,7 +255,7 @@ def test_metagraph_info(subtensor, alice_wallet): bonds_moving_avg=4.87890977618477e-14, hotkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], coldkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], - identities={}, + identities=[None], axons=( { "block": 0, @@ -358,51 +370,66 @@ def test_metagraph_info(subtensor, alice_wallet): metagraph_info, ] - subtensor.burned_register( - alice_wallet, - netuid=1, + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + assert subtensor.burned_register( + bob_wallet, + netuid=alice_subnet_netuid, wait_for_inclusion=True, wait_for_finalization=True, ) - metagraph_info = subtensor.get_metagraph_info(netuid=1) + metagraph_info = subtensor.get_metagraph_info(netuid=alice_subnet_netuid) assert metagraph_info.num_uids == 2 assert metagraph_info.hotkeys == [ - "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", alice_wallet.hotkey.ss58_address, + bob_wallet.hotkey.ss58_address, ] assert metagraph_info.coldkeys == [ - "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", alice_wallet.coldkey.ss58_address, + bob_wallet.coldkey.ss58_address, ] assert metagraph_info.tao_dividends_per_hotkey == [ - ("5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", Balance(0)), - (alice_wallet.hotkey.ss58_address, Balance(0)), + ( + alice_wallet.hotkey.ss58_address, + metagraph_info.tao_dividends_per_hotkey[0][1], + ), + (bob_wallet.hotkey.ss58_address, metagraph_info.tao_dividends_per_hotkey[1][1]), ] assert metagraph_info.alpha_dividends_per_hotkey == [ - ("5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", Balance(0)), - (alice_wallet.hotkey.ss58_address, Balance(0)), + ( + alice_wallet.hotkey.ss58_address, + metagraph_info.alpha_dividends_per_hotkey[0][1], + ), + ( + bob_wallet.hotkey.ss58_address, + metagraph_info.alpha_dividends_per_hotkey[1][1], + ), ] - subtensor.register_subnet( + alice_subnet_netuid = subtensor.get_total_subnets() # 3 + assert 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) + metagraph_info = subtensor.get_metagraph_info( + netuid=alice_subnet_netuid, 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 len(metagraph_infos) == 4 assert metagraph_infos[-1] == metagraph_info - metagraph_info = subtensor.get_metagraph_info(netuid=3) + # non-existed subnet + metagraph_info = subtensor.get_metagraph_info(netuid=alice_subnet_netuid + 1) assert metagraph_info is None diff --git a/tests/e2e_tests/test_neuron_certificate.py b/tests/e2e_tests/test_neuron_certificate.py index 8ada77dd35..bd319e27a4 100644 --- a/tests/e2e_tests/test_neuron_certificate.py +++ b/tests/e2e_tests/test_neuron_certificate.py @@ -25,9 +25,9 @@ async def test_neuron_certificate(subtensor, alice_wallet): assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" # Register Alice as a neuron on the subnet - assert subtensor.burned_register( - alice_wallet, netuid - ), "Unable to register Alice as a neuron" + assert subtensor.burned_register(alice_wallet, netuid), ( + "Unable to register Alice as a neuron" + ) # Serve Alice's axon with a certificate axon = Axon(wallet=alice_wallet) diff --git a/tests/e2e_tests/test_reveal_commitements.py b/tests/e2e_tests/test_reveal_commitments.py similarity index 76% rename from tests/e2e_tests/test_reveal_commitements.py rename to tests/e2e_tests/test_reveal_commitments.py index 37af2eebd2..40cc8794a1 100644 --- a/tests/e2e_tests/test_reveal_commitements.py +++ b/tests/e2e_tests/test_reveal_commitments.py @@ -3,6 +3,7 @@ import pytest from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call @pytest.mark.parametrize("local_chain", [True], indirect=True) @@ -28,28 +29,36 @@ async def test_set_reveal_commitment(local_chain, subtensor, alice_wallet, bob_w BLOCK_TIME = 0.25 # 12 for non-fast-block, 0.25 for fast block BLOCKS_UNTIL_REVEAL = 10 - NETUID = 2 + alice_subnet_netuid = subtensor.get_total_subnets() # 2 logging.console.info("Testing Drand encrypted commitments.") # Register subnet as Alice - assert subtensor.register_subnet( - alice_wallet, True, True - ), "Unable to register the subnet" + assert subtensor.register_subnet(alice_wallet, True, True), ( + "Unable to register the subnet" + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) # Register Bob's neuron - assert subtensor.burned_register( - bob_wallet, NETUID, True, True - ), "Bob's neuron was not register." + assert subtensor.burned_register(bob_wallet, alice_subnet_netuid, True, True), ( + "Bob's neuron was not register." + ) # Verify subnet 2 created successfully - assert subtensor.subnet_exists(NETUID), "Subnet wasn't created successfully" + assert subtensor.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) # Set commitment from Alice hotkey message_alice = f"This is test message with time {time.time()} from Alice." response = subtensor.set_reveal_commitment( - alice_wallet, NETUID, message_alice, BLOCKS_UNTIL_REVEAL, BLOCK_TIME + alice_wallet, + alice_subnet_netuid, + message_alice, + BLOCKS_UNTIL_REVEAL, + BLOCK_TIME, ) assert response[0] is True @@ -57,7 +66,11 @@ async def test_set_reveal_commitment(local_chain, subtensor, alice_wallet, bob_w message_bob = f"This is test message with time {time.time()} from Bob." response = subtensor.set_reveal_commitment( - bob_wallet, NETUID, message_bob, BLOCKS_UNTIL_REVEAL, block_time=BLOCK_TIME + bob_wallet, + alice_subnet_netuid, + message_bob, + BLOCKS_UNTIL_REVEAL, + block_time=BLOCK_TIME, ) assert response[0] is True @@ -71,7 +84,7 @@ async def test_set_reveal_commitment(local_chain, subtensor, alice_wallet, bob_w print(f"Current last reveled drand round {subtensor.last_drand_round()}") time.sleep(3) - actual_all = subtensor.get_all_revealed_commitments(NETUID) + actual_all = subtensor.get_all_revealed_commitments(alice_subnet_netuid) alice_result = actual_all.get(alice_wallet.hotkey.ss58_address) assert alice_result is not None, "Alice's commitment was not received." @@ -89,11 +102,11 @@ async def test_set_reveal_commitment(local_chain, subtensor, alice_wallet, bob_w # Assertions for get_revealed_commitment (based of hotkey) actual_alice_block, actual_alice_message = subtensor.get_revealed_commitment( - NETUID, 0 + alice_subnet_netuid, 0 + )[0] + actual_bob_block, actual_bob_message = subtensor.get_revealed_commitment( + alice_subnet_netuid, 1 )[0] - actual_bob_block, actual_bob_message = subtensor.get_revealed_commitment(NETUID, 1)[ - 0 - ] assert message_alice == actual_alice_message assert message_bob == actual_bob_message diff --git a/tests/e2e_tests/test_set_subnet_identity_extrinsic.py b/tests/e2e_tests/test_set_subnet_identity_extrinsic.py index e60cc69db3..60d91fa3d1 100644 --- a/tests/e2e_tests/test_set_subnet_identity_extrinsic.py +++ b/tests/e2e_tests/test_set_subnet_identity_extrinsic.py @@ -19,9 +19,9 @@ async def test_set_subnet_identity_extrinsic_happy_pass(subtensor, alice_wallet) assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" # make sure subnet_identity is empty - assert ( - subtensor.subnet(netuid).subnet_identity is None - ), "Subnet identity should be None before set" + assert subtensor.subnet(netuid).subnet_identity is None, ( + "Subnet identity should be None before set" + ) # prepare SubnetIdentity for subnet subnet_identity = SubnetIdentity( @@ -78,9 +78,9 @@ async def test_set_subnet_identity_extrinsic_failed( assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" # make sure subnet_identity is empty - assert ( - subtensor.subnet(netuid).subnet_identity is None - ), "Subnet identity should be None before set" + assert subtensor.subnet(netuid).subnet_identity is None, ( + "Subnet identity should be None before set" + ) # prepare SubnetIdentity for subnet subnet_identity = SubnetIdentity( diff --git a/tests/e2e_tests/test_set_weights.py b/tests/e2e_tests/test_set_weights.py index 8211e62aa9..ce8e628bb6 100644 --- a/tests/e2e_tests/test_set_weights.py +++ b/tests/e2e_tests/test_set_weights.py @@ -1,13 +1,14 @@ import numpy as np import pytest +import retry from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit from tests.e2e_tests.utils.chain_interactions import ( sudo_set_hyperparameter_bool, sudo_set_admin_utils, - use_and_wait_for_next_nonce, - wait_epoch, + execute_and_wait_for_next_nonce, ) @@ -26,7 +27,11 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) Raises: AssertionError: If any of the checks or verifications fail """ + netuids = [2, 3] + subnet_tempo = 50 + BLOCK_TIME = 0.25 # 12 for non-fast-block, 0.25 for fast block + print("Testing test_set_weights_uses_next_nonce") # Lower the network registration rate limit and cost @@ -56,6 +61,20 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) for netuid in netuids: assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" + # weights sensitive to epoch changes + assert sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tempo", + call_params={ + "netuid": netuid, + "tempo": subnet_tempo, + }, + ) + + # make sure 2 epochs are passed + subtensor.wait_for_block(subnet_tempo * 2 + 1) + # Stake to become to top neuron after the first epoch for netuid in netuids: subtensor.add_stake( @@ -79,11 +98,11 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) netuid, ), "Failed to enable commit/reveal" - assert ( - subtensor.weights_rate_limit(netuid=netuid) > 0 - ), "Weights rate limit is below 0" + assert subtensor.weights_rate_limit(netuid=netuid) > 0, ( + "Weights rate limit is below 0" + ) - # Lower the rate limit + # Lower set weights rate limit status, error = sudo_set_admin_utils( local_chain, alice_wallet, @@ -102,37 +121,52 @@ async def test_set_weights_uses_next_nonce(local_chain, subtensor, alice_wallet) # Weights values uids = np.array([0], dtype=np.int64) - weights = np.array([0.1], dtype=np.float32) + weights = np.array([0.5], dtype=np.float32) weight_uids, weight_vals = convert_weights_and_uids_for_emit( uids=uids, weights=weights ) - # Set weights for each subnet - for netuid in netuids: - async with use_and_wait_for_next_nonce(subtensor, alice_wallet): - success, message = subtensor.set_weights( - alice_wallet, - netuid, - uids=weight_uids, - weights=weight_vals, - wait_for_inclusion=False, # Don't wait for inclusion, we are testing the nonce when there is a tx in the pool - wait_for_finalization=False, - ) + logging.console.info( + f"[orange]Nonce before first set_weights: " + f"{subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" + ) - assert success is True, message + # 3 time doing call if nonce wasn't updated, then raise error + @retry.retry(exceptions=Exception, tries=3, delay=1) + @execute_and_wait_for_next_nonce(subtensor=subtensor, wallet=alice_wallet) + def set_weights(netuid_): + success, message = subtensor.set_weights( + wallet=alice_wallet, + netuid=netuid_, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=False, + period=subnet_tempo, + ) + assert success is True, message + + logging.console.info( + f"[orange]Nonce after second set_weights: " + f"{subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" + ) - # Wait for the txs to be included in the chain - await wait_epoch(subtensor, netuid=netuids[-1], times=4) + # Set weights for each subnet + for netuid in netuids: + set_weights(netuid) for netuid in netuids: # Query the Weights storage map for all three subnets - weights = subtensor.query_module( + query = subtensor.query_module( module="SubtensorModule", name="Weights", params=[netuid, 0], # Alice should be the only UID - ).value + ) + + weights = query.value + logging.console.info(f"Weights for subnet {netuid}: {weights}") assert weights is not None, f"Weights not found for subnet {netuid}" - assert weights == list( - zip(weight_uids, weight_vals) - ), f"Weights do not match for subnet {netuid}" + assert weights == list(zip(weight_uids, weight_vals)), ( + f"Weights do not match for subnet {netuid}" + ) diff --git a/tests/e2e_tests/test_stake_fee.py b/tests/e2e_tests/test_stake_fee.py index 32062b6c6c..729dce1914 100644 --- a/tests/e2e_tests/test_stake_fee.py +++ b/tests/e2e_tests/test_stake_fee.py @@ -33,9 +33,9 @@ async def test_stake_fee_api(local_chain, subtensor, alice_wallet, bob_wallet): hotkey_ss58=alice_wallet.hotkey.ss58_address, ) assert isinstance(stake_fee_0, Balance), "Stake fee should be a Balance object" - assert ( - stake_fee_0 >= min_stake_fee - ), "Stake fee should be greater than the minimum stake fee" + assert stake_fee_0 >= min_stake_fee, ( + "Stake fee should be greater than the minimum stake fee" + ) # Test unstake fee stake_fee_1 = subtensor.get_unstake_fee( @@ -45,9 +45,9 @@ async def test_stake_fee_api(local_chain, subtensor, alice_wallet, bob_wallet): hotkey_ss58=bob_wallet.hotkey.ss58_address, ) assert isinstance(stake_fee_1, Balance), "Stake fee should be a Balance object" - assert ( - stake_fee_1 >= min_stake_fee - ), "Stake fee should be greater than the minimum stake fee" + assert stake_fee_1 >= min_stake_fee, ( + "Stake fee should be greater than the minimum stake fee" + ) # Test various stake movement scenarios movement_scenarios = [ @@ -91,15 +91,15 @@ async def test_stake_fee_api(local_chain, subtensor, alice_wallet, bob_wallet): destination_coldkey_ss58=scenario["dest_coldkey"], ) assert isinstance(stake_fee, Balance), "Stake fee should be a Balance object" - assert ( - stake_fee >= min_stake_fee - ), "Stake fee should be greater than the minimum stake fee" + assert stake_fee >= min_stake_fee, ( + "Stake fee should be greater than the minimum stake fee" + ) # Test cross-subnet movement netuid2 = 3 - assert subtensor.register_subnet( - alice_wallet - ), "Unable to register the second subnet" + assert subtensor.register_subnet(alice_wallet), ( + "Unable to register the second subnet" + ) assert subtensor.subnet_exists(netuid2), "Second subnet wasn't created successfully" stake_fee = subtensor.get_stake_movement_fee( @@ -112,6 +112,6 @@ async def test_stake_fee_api(local_chain, subtensor, alice_wallet, bob_wallet): destination_coldkey_ss58=alice_wallet.coldkeypub.ss58_address, ) assert isinstance(stake_fee, Balance), "Stake fee should be a Balance object" - assert ( - stake_fee >= min_stake_fee - ), "Stake fee should be greater than the minimum stake fee" + assert stake_fee >= min_stake_fee, ( + "Stake fee should be greater than the minimum stake fee" + ) diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index 7742b97414..55afa58beb 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -3,6 +3,7 @@ from bittensor.utils.balance import Balance from tests.e2e_tests.utils.chain_interactions import get_dynamic_balance from tests.helpers.helpers import ApproxBalance +from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call logging.enable_info() @@ -14,16 +15,27 @@ def test_single_operation(subtensor, alice_wallet, bob_wallet): - Unstaking using `unstake` - Checks StakeInfo """ + alice_subnet_netuid = subtensor.get_total_subnets() # 2 + + # Register root as Alice - the subnet owner and validator + assert subtensor.register_subnet(alice_wallet, True, True) + + # Verify subnet created successfully + assert subtensor.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) subtensor.burned_register( alice_wallet, - netuid=1, + netuid=alice_subnet_netuid, wait_for_inclusion=True, wait_for_finalization=True, ) subtensor.burned_register( bob_wallet, - netuid=1, + netuid=alice_subnet_netuid, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -31,15 +43,15 @@ def test_single_operation(subtensor, alice_wallet, bob_wallet): stake = subtensor.get_stake( alice_wallet.coldkey.ss58_address, bob_wallet.hotkey.ss58_address, - netuid=1, + netuid=alice_subnet_netuid, ) - assert stake == Balance(0) + assert stake == Balance(0).set_unit(alice_subnet_netuid) success = subtensor.add_stake( alice_wallet, bob_wallet.hotkey.ss58_address, - netuid=1, + netuid=alice_subnet_netuid, amount=Balance.from_tao(1), wait_for_inclusion=True, wait_for_finalization=True, @@ -47,24 +59,40 @@ def test_single_operation(subtensor, alice_wallet, bob_wallet): assert success is True - stake = subtensor.get_stake( + stake_alice = subtensor.get_stake( + alice_wallet.coldkey.ss58_address, + alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + + stake_bob = subtensor.get_stake( alice_wallet.coldkey.ss58_address, bob_wallet.hotkey.ss58_address, - netuid=1, + netuid=alice_subnet_netuid, ) - assert stake > Balance(0) + assert stake_bob > Balance(0).set_unit(alice_subnet_netuid) stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) assert stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[0].stake.rao, alice_subnet_netuid), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance(stakes[0].emission.rao, alice_subnet_netuid), + drain=0, + is_registered=True, + ), 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), + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[1].stake.rao, alice_subnet_netuid), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance(stakes[1].emission.rao, alice_subnet_netuid), drain=0, is_registered=True, ), @@ -73,13 +101,23 @@ def test_single_operation(subtensor, alice_wallet, bob_wallet): stakes = subtensor.get_stake_info_for_coldkey(alice_wallet.coldkey.ss58_address) assert stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[0].stake.rao, alice_subnet_netuid), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance(stakes[0].emission.rao, alice_subnet_netuid), + drain=0, + is_registered=True, + ), 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), + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[1].stake.rao, alice_subnet_netuid), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance(stakes[1].emission.rao, alice_subnet_netuid), drain=0, is_registered=True, ), @@ -105,10 +143,20 @@ def test_single_operation(subtensor, alice_wallet, bob_wallet): hotkey_ss58=bob_wallet.hotkey.ss58_address, coldkey_ss58=alice_wallet.coldkey.ss58_address, netuid=1, - stake=stake, + stake=stake.set_unit(1), locked=Balance.from_tao(0, netuid=1), emission=Balance.from_tao(0, netuid=1), drain=0, + is_registered=False, + ), + 2: StakeInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[2].stake.rao, alice_subnet_netuid), + locked=Balance.from_tao(0, netuid=alice_subnet_netuid), + emission=get_dynamic_balance(stakes[2].emission.rao, alice_subnet_netuid), + drain=0, is_registered=True, ), } @@ -116,8 +164,8 @@ def test_single_operation(subtensor, alice_wallet, bob_wallet): success = subtensor.unstake( alice_wallet, bob_wallet.hotkey.ss58_address, - netuid=1, - amount=stake, + netuid=alice_subnet_netuid, + amount=stake_bob, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -127,10 +175,10 @@ def test_single_operation(subtensor, alice_wallet, bob_wallet): stake = subtensor.get_stake( alice_wallet.coldkey.ss58_address, bob_wallet.hotkey.ss58_address, - netuid=1, + netuid=alice_subnet_netuid, ) - assert stake == Balance(0) + assert stake == Balance(0).set_unit(alice_subnet_netuid) def test_batch_operations(subtensor, alice_wallet, bob_wallet): @@ -154,6 +202,10 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): wait_for_finalization=True, ) + # make sure we passed start_call limit for both subnets + for netuid in netuids: + assert wait_to_start_call(subtensor, alice_wallet, netuid) + for netuid in netuids: subtensor.burned_register( bob_wallet, @@ -259,22 +311,26 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): 2. Succeeds with strict threshold (0.5%) and partial staking allowed 3. Succeeds with lenient threshold (10% and 30%) and no partial staking """ - netuid = 2 + alice_subnet_netuid = subtensor.get_total_subnets() # 2 # Register root as Alice - the subnet owner and validator - assert subtensor.register_subnet(alice_wallet) + assert subtensor.register_subnet(alice_wallet, True, True) # Verify subnet created successfully - assert subtensor.subnet_exists(netuid), "Subnet wasn't created successfully" + assert subtensor.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) subtensor.burned_register( alice_wallet, - netuid=netuid, + netuid=alice_subnet_netuid, wait_for_inclusion=True, wait_for_finalization=True, ) subtensor.burned_register( bob_wallet, - netuid=netuid, + netuid=alice_subnet_netuid, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -282,7 +338,7 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): initial_stake = subtensor.get_stake( alice_wallet.coldkey.ss58_address, bob_wallet.hotkey.ss58_address, - netuid=netuid, + netuid=alice_subnet_netuid, ) assert initial_stake == Balance(0) @@ -293,7 +349,7 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): success = subtensor.add_stake( alice_wallet, bob_wallet.hotkey.ss58_address, - netuid=netuid, + netuid=alice_subnet_netuid, amount=stake_amount, wait_for_inclusion=True, wait_for_finalization=True, @@ -306,7 +362,7 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): current_stake = subtensor.get_stake( alice_wallet.coldkey.ss58_address, bob_wallet.hotkey.ss58_address, - netuid=netuid, + netuid=alice_subnet_netuid, ) assert current_stake == Balance(0), "Stake should not change after failed attempt" @@ -314,7 +370,7 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): success = subtensor.add_stake( alice_wallet, bob_wallet.hotkey.ss58_address, - netuid=netuid, + netuid=alice_subnet_netuid, amount=stake_amount, wait_for_inclusion=True, wait_for_finalization=True, @@ -327,19 +383,19 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): partial_stake = subtensor.get_stake( alice_wallet.coldkey.ss58_address, bob_wallet.hotkey.ss58_address, - netuid=netuid, + netuid=alice_subnet_netuid, ) assert partial_stake > Balance(0), "Partial stake should be added" - assert ( - partial_stake < stake_amount - ), "Partial stake should be less than requested amount" + assert partial_stake < stake_amount, ( + "Partial stake should be less than requested amount" + ) # 3. Higher threshold - should succeed fully amount = Balance.from_tao(100) success = subtensor.add_stake( alice_wallet, bob_wallet.hotkey.ss58_address, - netuid=netuid, + netuid=alice_subnet_netuid, amount=amount, wait_for_inclusion=True, wait_for_finalization=True, @@ -352,7 +408,7 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): full_stake = subtensor.get_stake( alice_wallet.coldkey.ss58_address, bob_wallet.hotkey.ss58_address, - netuid=netuid, + netuid=alice_subnet_netuid, ) # Test Unstaking Scenarios @@ -360,7 +416,7 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): success = subtensor.unstake( alice_wallet, bob_wallet.hotkey.ss58_address, - netuid=netuid, + netuid=alice_subnet_netuid, amount=stake_amount, wait_for_inclusion=True, wait_for_finalization=True, @@ -373,17 +429,17 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): current_stake = subtensor.get_stake( alice_wallet.coldkey.ss58_address, bob_wallet.hotkey.ss58_address, - netuid=netuid, + netuid=alice_subnet_netuid, + ) + assert current_stake == full_stake, ( + "Stake should not change after failed unstake attempt" ) - assert ( - current_stake == full_stake - ), "Stake should not change after failed unstake attempt" # 2. Partial allowed - should succeed partially success = subtensor.unstake( alice_wallet, bob_wallet.hotkey.ss58_address, - netuid=netuid, + netuid=alice_subnet_netuid, amount=current_stake, wait_for_inclusion=True, wait_for_finalization=True, @@ -396,7 +452,7 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): partial_unstake = subtensor.get_stake( alice_wallet.coldkey.ss58_address, bob_wallet.hotkey.ss58_address, - netuid=netuid, + netuid=alice_subnet_netuid, ) assert partial_unstake > Balance(0), "Some stake should remain" @@ -404,7 +460,7 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): success = subtensor.unstake( alice_wallet, bob_wallet.hotkey.ss58_address, - netuid=netuid, + netuid=alice_subnet_netuid, amount=partial_unstake, wait_for_inclusion=True, wait_for_finalization=True, @@ -425,12 +481,16 @@ def test_safe_swap_stake_scenarios(subtensor, alice_wallet, bob_wallet): """ # Create new subnet (netuid 2) and register Alice origin_netuid = 2 - assert subtensor.register_subnet(bob_wallet) + assert subtensor.register_subnet(bob_wallet, True, True) assert subtensor.subnet_exists(origin_netuid), "Subnet wasn't created successfully" dest_netuid = 3 - assert subtensor.register_subnet(bob_wallet) + assert subtensor.register_subnet(bob_wallet, True, True) assert subtensor.subnet_exists(dest_netuid), "Subnet wasn't created successfully" + # make sure we passed start_call limit for both subnets + assert wait_to_start_call(subtensor, bob_wallet, origin_netuid) + assert wait_to_start_call(subtensor, bob_wallet, dest_netuid) + # Register Alice on both subnets subtensor.burned_register( alice_wallet, @@ -486,9 +546,9 @@ def test_safe_swap_stake_scenarios(subtensor, alice_wallet, bob_wallet): alice_wallet.hotkey.ss58_address, netuid=dest_netuid, ) - assert dest_stake == Balance( - 0 - ), "Destination stake should remain 0 after failed swap" + assert dest_stake == Balance(0), ( + "Destination stake should remain 0 after failed swap" + ) # 2. Try swap with higher threshold and less amount - should succeed stake_swap_amount = Balance.from_tao(100) @@ -517,9 +577,9 @@ def test_safe_swap_stake_scenarios(subtensor, alice_wallet, bob_wallet): alice_wallet.hotkey.ss58_address, netuid=dest_netuid, ) - assert dest_stake > Balance( - 0 - ), "Destination stake should be non-zero after successful swap" + assert dest_stake > Balance(0), ( + "Destination stake should be non-zero after successful swap" + ) def test_move_stake(subtensor, alice_wallet, bob_wallet): @@ -529,10 +589,17 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): - Moving stake from one hotkey-subnet pair to another """ - netuid = 1 + alice_subnet_netuid = subtensor.get_total_subnets() # 2 + assert subtensor.register_subnet(alice_wallet, True, True) + assert subtensor.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + subtensor.burned_register( alice_wallet, - netuid=1, + netuid=alice_subnet_netuid, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -540,7 +607,7 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): assert subtensor.add_stake( alice_wallet, alice_wallet.hotkey.ss58_address, - netuid=netuid, + netuid=alice_subnet_netuid, amount=Balance.from_tao(1_000), wait_for_inclusion=True, wait_for_finalization=True, @@ -552,23 +619,29 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): StakeInfo( hotkey_ss58=alice_wallet.hotkey.ss58_address, coldkey_ss58=alice_wallet.coldkey.ss58_address, - netuid=netuid, - stake=get_dynamic_balance(stakes[0].stake.rao, netuid), + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[0].stake.rao, alice_subnet_netuid), locked=Balance(0), - emission=get_dynamic_balance(stakes[0].emission.rao, netuid), + emission=get_dynamic_balance(stakes[0].emission.rao, alice_subnet_netuid), drain=0, is_registered=True, ), ] - subtensor.register_subnet(bob_wallet) + bob_subnet_netuid = subtensor.get_total_subnets() # 3 + subtensor.register_subnet(bob_wallet, True, True) + assert subtensor.subnet_exists(bob_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, bob_wallet, bob_subnet_netuid) assert subtensor.move_stake( alice_wallet, origin_hotkey=alice_wallet.hotkey.ss58_address, - origin_netuid=1, + origin_netuid=alice_subnet_netuid, destination_hotkey=bob_wallet.hotkey.ss58_address, - destination_netuid=2, + destination_netuid=bob_subnet_netuid, amount=stakes[0].stake, wait_for_finalization=True, wait_for_inclusion=True, @@ -576,15 +649,24 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) - netuid = 2 assert stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[0].stake.rao, bob_subnet_netuid), + locked=Balance(0).set_unit(bob_subnet_netuid), + emission=get_dynamic_balance(stakes[0].emission.rao, bob_subnet_netuid), + drain=0, + is_registered=True, + ), StakeInfo( hotkey_ss58=bob_wallet.hotkey.ss58_address, coldkey_ss58=alice_wallet.coldkey.ss58_address, - netuid=netuid, - stake=get_dynamic_balance(stakes[0].stake.rao, netuid), - locked=Balance(0), - emission=get_dynamic_balance(stakes[0].emission.rao, netuid), + netuid=bob_subnet_netuid, + stake=get_dynamic_balance(stakes[1].stake.rao, bob_subnet_netuid), + locked=Balance(0).set_unit(bob_subnet_netuid), + emission=get_dynamic_balance(stakes[1].emission.rao, bob_subnet_netuid), drain=0, is_registered=True, ), @@ -597,11 +679,18 @@ def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): - Adding stake - Transferring stake from one coldkey-subnet pair to another """ - netuid = 1 + alice_subnet_netuid = subtensor.get_total_subnets() # 2 + + assert subtensor.register_subnet(alice_wallet, True, True) + assert subtensor.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) subtensor.burned_register( alice_wallet, - netuid=netuid, + netuid=alice_subnet_netuid, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -609,7 +698,7 @@ def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): assert subtensor.add_stake( alice_wallet, alice_wallet.hotkey.ss58_address, - netuid=netuid, + netuid=alice_subnet_netuid, amount=Balance.from_tao(1_000), wait_for_inclusion=True, wait_for_finalization=True, @@ -621,10 +710,12 @@ def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): StakeInfo( hotkey_ss58=alice_wallet.hotkey.ss58_address, coldkey_ss58=alice_wallet.coldkey.ss58_address, - netuid=netuid, - stake=get_dynamic_balance(alice_stakes[0].stake.rao, netuid), + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(alice_stakes[0].stake.rao, alice_subnet_netuid), locked=Balance(0), - emission=get_dynamic_balance(alice_stakes[0].emission.rao, netuid), + emission=get_dynamic_balance( + alice_stakes[0].emission.rao, alice_subnet_netuid + ), drain=0, is_registered=True, ), @@ -634,10 +725,14 @@ def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): assert bob_stakes == [] - subtensor.register_subnet(dave_wallet) + dave_subnet_netuid = subtensor.get_total_subnets() # 3 + subtensor.register_subnet(dave_wallet, True, True) + + assert wait_to_start_call(subtensor, dave_wallet, dave_subnet_netuid) + subtensor.burned_register( bob_wallet, - netuid=2, + netuid=dave_subnet_netuid, wait_for_inclusion=True, wait_for_finalization=True, ) @@ -646,8 +741,8 @@ def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): alice_wallet, destination_coldkey_ss58=bob_wallet.coldkey.ss58_address, hotkey_ss58=alice_wallet.hotkey.ss58_address, - origin_netuid=1, - destination_netuid=2, + origin_netuid=alice_subnet_netuid, + destination_netuid=dave_subnet_netuid, amount=alice_stakes[0].stake, wait_for_inclusion=True, wait_for_finalization=True, @@ -655,19 +750,33 @@ def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): alice_stakes = subtensor.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) - assert alice_stakes == [] + assert alice_stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(alice_stakes[0].stake.rao, alice_subnet_netuid), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance( + alice_stakes[0].emission.rao, alice_subnet_netuid + ), + drain=0, + is_registered=True, + ), + ] bob_stakes = subtensor.get_stake_for_coldkey(bob_wallet.coldkey.ss58_address) - netuid = 2 assert bob_stakes == [ StakeInfo( hotkey_ss58=alice_wallet.hotkey.ss58_address, coldkey_ss58=bob_wallet.coldkey.ss58_address, - netuid=2, - stake=get_dynamic_balance(bob_stakes[0].stake.rao, netuid), + netuid=dave_subnet_netuid, + stake=get_dynamic_balance(bob_stakes[0].stake.rao, dave_subnet_netuid), locked=Balance(0), - emission=get_dynamic_balance(bob_stakes[0].emission.rao, netuid), + emission=get_dynamic_balance( + bob_stakes[0].emission.rao, dave_subnet_netuid + ), drain=0, is_registered=False, ), diff --git a/tests/e2e_tests/test_subtensor_functions.py b/tests/e2e_tests/test_subtensor_functions.py index e1040c5f98..7d34ce3ce3 100644 --- a/tests/e2e_tests/test_subtensor_functions.py +++ b/tests/e2e_tests/test_subtensor_functions.py @@ -4,9 +4,9 @@ from bittensor.utils.balance import Balance from tests.e2e_tests.utils.chain_interactions import ( - sudo_set_admin_utils, wait_epoch, ) +from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call """ Verifies: @@ -43,7 +43,7 @@ async def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wall Raises: AssertionError: If any of the checks or verifications fail """ - netuid = 2 + netuid = subtensor.get_total_subnets() # 22 # Initial balance for Alice, defined in the genesis file of localnet initial_alice_balance = Balance.from_tao(1_000_000) # Current Existential deposit for all accounts in bittensor @@ -55,15 +55,17 @@ async def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wall # Assert correct balance is fetched for Alice alice_balance = subtensor.get_balance(alice_wallet.coldkeypub.ss58_address) - assert ( - alice_balance == initial_alice_balance - ), "Balance for Alice wallet doesn't match with pre-def value" + assert alice_balance == initial_alice_balance, ( + "Balance for Alice wallet doesn't match with pre-def value" + ) # Subnet burn cost is initially lower before we register a subnet pre_subnet_creation_cost = subtensor.get_subnet_burn_cost() # Register subnet - assert subtensor.register_subnet(alice_wallet), "Unable to register the subnet" + assert subtensor.register_subnet(alice_wallet, True, True), ( + "Unable to register the subnet" + ) # Subnet burn cost is increased immediately after a subnet is registered post_subnet_creation_cost = subtensor.get_subnet_burn_cost() @@ -75,9 +77,9 @@ async def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wall # Assert amount is deducted once a subnetwork is registered by Alice alice_balance_post_sn = subtensor.get_balance(alice_wallet.coldkeypub.ss58_address) - assert ( - alice_balance_post_sn + pre_subnet_creation_cost == initial_alice_balance - ), "Balance is the same even after registering a subnet" + assert alice_balance_post_sn + pre_subnet_creation_cost == initial_alice_balance, ( + "Balance is the same even after registering a subnet" + ) # Subnet 2 is added after registration assert subtensor.get_subnets() == [0, 1, 2] @@ -87,9 +89,9 @@ async def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wall assert subtensor.subnet_exists(netuid) # Default subnetwork difficulty - assert ( - subtensor.difficulty(netuid) == 10_000_000 - ), "Couldn't fetch correct subnet difficulty" + assert subtensor.difficulty(netuid) == 10_000_000, ( + "Couldn't fetch correct subnet difficulty" + ) # Verify Alice is registered to netuid 2 and Bob isn't registered to any assert subtensor.get_netuids_for_hotkey( @@ -128,10 +130,12 @@ async def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wall bob_balance = subtensor.get_balance(bob_wallet.coldkeypub.ss58_address) + assert wait_to_start_call(subtensor, alice_wallet, netuid) + # Register Bob to the subnet - assert subtensor.burned_register( - bob_wallet, netuid - ), "Unable to register Bob as a neuron" + assert subtensor.burned_register(bob_wallet, netuid), ( + "Unable to register Bob as a neuron" + ) # Verify Bob's UID on netuid 2 is 1 assert ( @@ -146,9 +150,9 @@ async def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wall bob_balance_post_reg = subtensor.get_balance(bob_wallet.coldkeypub.ss58_address) # Ensure recycled amount is only deducted from the balance after registration - assert ( - bob_balance - recycle_amount == bob_balance_post_reg - ), "Balance for Bob is not correct after burned register" + assert bob_balance - recycle_amount == bob_balance_post_reg, ( + "Balance for Bob is not correct after burned register" + ) neuron_info_old = subtensor.get_neuron_for_pubkey_and_subnet( alice_wallet.hotkey.ss58_address, netuid=netuid @@ -170,9 +174,9 @@ async def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wall # ), "Neuron info not updated after running validator" # Fetch and assert existential deposit for an account in the network - assert ( - subtensor.get_existential_deposit() == existential_deposit - ), "Existential deposit value doesn't match with pre-defined value" + assert subtensor.get_existential_deposit() == existential_deposit, ( + "Existential deposit value doesn't match with pre-defined value" + ) # Fetching all subnets in the network all_subnets = subtensor.get_all_subnets_info() @@ -180,16 +184,16 @@ async def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wall # Assert all netuids are present in all_subnets expected_netuids = [0, 1, 2] actual_netuids = [subnet.netuid for subnet in all_subnets] - assert ( - actual_netuids == expected_netuids - ), f"Expected netuids {expected_netuids}, but found {actual_netuids}" + assert actual_netuids == expected_netuids, ( + f"Expected netuids {expected_netuids}, but found {actual_netuids}" + ) # Assert that the owner_ss58 of subnet 2 matches Alice's coldkey address expected_owner = alice_wallet.coldkeypub.ss58_address subnet_2 = next((subnet for subnet in all_subnets if subnet.netuid == netuid), None) actual_owner = subnet_2.owner_ss58 - assert ( - actual_owner == expected_owner - ), f"Expected owner {expected_owner}, but found {actual_owner}" + assert actual_owner == expected_owner, ( + f"Expected owner {expected_owner}, but found {actual_owner}" + ) print("✅ Passed test_subtensor_extrinsics") diff --git a/tests/e2e_tests/test_transfer.py b/tests/e2e_tests/test_transfer.py index 9ca6ac59e7..7a0728de72 100644 --- a/tests/e2e_tests/test_transfer.py +++ b/tests/e2e_tests/test_transfer.py @@ -42,8 +42,8 @@ def test_transfer(subtensor, alice_wallet): balance_after = subtensor.get_balance(alice_wallet.coldkeypub.ss58_address) # Assert correct transfer calculations - assert ( - balance_before - transfer_fee - transfer_value == balance_after - ), f"Expected {balance_before - transfer_value - transfer_fee}, got {balance_after}" + assert balance_before - transfer_fee - transfer_value == balance_after, ( + f"Expected {balance_before - transfer_value - transfer_fee}, got {balance_after}" + ) print("✅ Passed test_transfer") diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index d571f7ff80..c5871d3155 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -4,7 +4,8 @@ """ import asyncio -import contextlib +import functools +import time from typing import Union, Optional, TYPE_CHECKING from bittensor.utils.balance import Balance @@ -93,19 +94,18 @@ async def wait_epoch(subtensor: "Subtensor", netuid: int = 1, **kwargs): await wait_interval(tempo, subtensor, netuid, **kwargs) -def next_tempo(current_block: int, tempo: int, netuid: int) -> int: +def next_tempo(current_block: int, tempo: int) -> int: """ Calculates the next tempo block for a specific subnet. Args: current_block (int): The current block number. tempo (int): The tempo value for the subnet. - netuid (int): The unique identifier of the subnet. Returns: int: The next tempo block number. """ - return (((current_block + netuid) // tempo) + 1) * tempo + 1 + return ((current_block // tempo) + 1) * tempo + 1 async def wait_interval( @@ -127,7 +127,7 @@ async def wait_interval( next_tempo_block_start = current_block for _ in range(times): - next_tempo_block_start = next_tempo(next_tempo_block_start, tempo, netuid) + next_tempo_block_start = next_tempo(next_tempo_block_start, tempo) last_reported = None @@ -146,28 +146,49 @@ async def wait_interval( ) -@contextlib.asynccontextmanager -async def use_and_wait_for_next_nonce( - subtensor: "Subtensor", - wallet: "Wallet", - sleep: float = 0.25, - timeout: float = 15.0, +def execute_and_wait_for_next_nonce( + subtensor, wallet, sleep=0.25, timeout=60.0, max_retries=3 ): """ - ContextManager that makes sure the Nonce has been consumed after sending Extrinsic. + Decorator that ensures the nonce has been consumed after a blockchain extrinsic call. """ - nonce = subtensor.substrate.get_account_next_index(wallet.hotkey.ss58_address) + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + for attempt in range(max_retries): + start_nonce = subtensor.substrate.get_account_next_index( + wallet.hotkey.ss58_address + ) + + result = func(*args, **kwargs) + + start_time = time.time() + + while time.time() - start_time < timeout: + current_nonce = subtensor.substrate.get_account_next_index( + wallet.hotkey.ss58_address + ) + + if current_nonce != start_nonce: + logging.console.info( + f"✅ Nonce changed from {start_nonce} to {current_nonce}" + ) + return result + + logging.console.info( + f"⏳ Waiting for nonce increment. Current: {current_nonce}" + ) + time.sleep(sleep) - yield + logging.warning( + f"⚠️ Attempt {attempt + 1}/{max_retries}: Nonce did not increment." + ) + raise TimeoutError(f"❌ Nonce did not change after {max_retries} attempts.") - async def wait_for_new_nonce(): - while nonce == subtensor.substrate.get_account_next_index( - wallet.hotkey.ss58_address - ): - await asyncio.sleep(sleep) + return wrapper - await asyncio.wait_for(wait_for_new_nonce(), timeout) + return decorator # Helper to execute sudo wrapped calls on the chain diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py index e10cbfa6d8..a688156c7b 100644 --- a/tests/e2e_tests/utils/e2e_test_utils.py +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -93,6 +93,8 @@ def __init__(self, dir, wallet, netuid): self.started = asyncio.Event() async def __aenter__(self): + env = os.environ.copy() + env["BT_LOGGING_INFO"] = "1" self.process = await asyncio.create_subprocess_exec( sys.executable, f"{self.dir}/miner.py", @@ -108,15 +110,19 @@ async def __aenter__(self): self.wallet.name, "--wallet.hotkey", "default", - env={ - "BT_LOGGING_INFO": "1", - }, + env=env, stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, ) self.__reader_task = asyncio.create_task(self._reader()) - await asyncio.wait_for(self.started.wait(), 30) + try: + await asyncio.wait_for(self.started.wait(), 60) + except asyncio.TimeoutError: + self.process.kill() + await self.process.wait() + raise RuntimeError("Miner failed to start within timeout") return self @@ -128,6 +134,14 @@ async def __aexit__(self, exc_type, exc_value, traceback): async def _reader(self): async for line in self.process.stdout: + try: + bittensor.logging.console.info( + f"[green]MINER LOG: {line.split(b'|')[-1].strip().decode()}[/blue]" + ) + except BaseException: + # skipp empty lines + pass + if b"Starting main loop" in line: self.started.set() @@ -142,6 +156,8 @@ def __init__(self, dir, wallet, netuid): self.set_weights = asyncio.Event() async def __aenter__(self): + env = os.environ.copy() + env["BT_LOGGING_INFO"] = "1" self.process = await asyncio.create_subprocess_exec( sys.executable, f"{self.dir}/validator.py", @@ -157,15 +173,19 @@ async def __aenter__(self): self.wallet.name, "--wallet.hotkey", "default", - env={ - "BT_LOGGING_INFO": "1", - }, + env=env, stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, ) self.__reader_task = asyncio.create_task(self._reader()) - await asyncio.wait_for(self.started.wait(), 30) + try: + await asyncio.wait_for(self.started.wait(), 60) + except asyncio.TimeoutError: + self.process.kill() + await self.process.wait() + raise RuntimeError("Validator failed to start within timeout") return self @@ -177,9 +197,19 @@ async def __aexit__(self, exc_type, exc_value, traceback): async def _reader(self): async for line in self.process.stdout: + try: + bittensor.logging.console.info( + f"[orange]VALIDATOR LOG: {line.split(b'|')[-1].strip().decode()}[/orange]" + ) + except BaseException: + # skipp empty lines + pass + if b"Starting validator loop." in line: + bittensor.logging.console.info("Validator started.") self.started.set() elif b"Successfully set weights and Finalized." in line: + bittensor.logging.console.info("Validator is setting weights.") self.set_weights.set() def __init__(self): @@ -196,3 +226,42 @@ def miner(self, wallet, netuid): def validator(self, wallet, netuid): return self.Validator(self.dir, wallet, netuid) + + +def wait_to_start_call( + subtensor: "bittensor.Subtensor", + subnet_owner_wallet: "bittensor.Wallet", + netuid: int, + in_blocks: int = 10, +): + """Waits for a certain number of blocks before making a start call.""" + # make sure we passed start_call limit + subtensor.wait_for_block(subtensor.block + in_blocks + 1) + status, message = subtensor.start_call( + wallet=subnet_owner_wallet, + netuid=netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert status, message + return True + + +async def async_wait_to_start_call( + subtensor: "bittensor.AsyncSubtensor", + subnet_owner_wallet: "bittensor.Wallet", + netuid: int, + in_blocks: int = 10, +): + """Waits for a certain number of blocks before making a start call.""" + # make sure we passed start_call limit + current_block = await subtensor.block + await subtensor.wait_for_block(current_block + in_blocks + 1) + status, message = await subtensor.start_call( + wallet=subnet_owner_wallet, + netuid=netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert status, message + return True diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py index f5fda8db6a..511d13742c 100644 --- a/tests/helpers/helpers.py +++ b/tests/helpers/helpers.py @@ -105,15 +105,15 @@ def get_mock_neuron(**kwargs) -> NeuronInfo: """ mock_neuron_d = dict( - # TODO fix the AxonInfo here — it doesn't work { "netuid": -1, # mock netuid "axon_info": AxonInfo( - block=0, version=1, - ip=0, + ip="0.0.0.0", port=0, ip_type=0, + hotkey=get_mock_hotkey(), + coldkey=get_mock_coldkey(), protocol=0, placeholder1=0, placeholder2=0, diff --git a/tests/integration_tests/test_metagraph_integration.py b/tests/integration_tests/test_metagraph_integration.py index 5406219c5e..efcb49d267 100644 --- a/tests/integration_tests/test_metagraph_integration.py +++ b/tests/integration_tests/test_metagraph_integration.py @@ -42,10 +42,11 @@ def test_sync_block_0(self): self.metagraph.sync(lite=True, block=0, subtensor=self.sub) def test_load_sync_save(self): - with mock.patch.object( - self.sub, "neurons_lite", return_value=[] - ), mock.patch.object( - self.sub, "get_metagraph_info", return_value=mock.MagicMock() + with ( + mock.patch.object(self.sub, "neurons_lite", return_value=[]), + mock.patch.object( + self.sub, "get_metagraph_info", return_value=mock.MagicMock() + ), ): self.metagraph.sync(lite=True, subtensor=self.sub) self.metagraph.save() @@ -53,10 +54,11 @@ def test_load_sync_save(self): self.metagraph.save() def test_load_sync_save_from_torch(self): - with mock.patch.object( - self.sub, "neurons_lite", return_value=[] - ), mock.patch.object( - self.sub, "get_metagraph_info", return_value=mock.MagicMock() + with ( + mock.patch.object(self.sub, "neurons_lite", return_value=[]), + mock.patch.object( + self.sub, "get_metagraph_info", return_value=mock.MagicMock() + ), ): self.metagraph.sync(lite=True, subtensor=self.sub) diff --git a/tests/integration_tests/test_timelock.py b/tests/integration_tests/test_timelock.py new file mode 100644 index 0000000000..33e31db782 --- /dev/null +++ b/tests/integration_tests/test_timelock.py @@ -0,0 +1,83 @@ +import struct +import time + +import pytest + +from bittensor.core import timelock + + +def test_encrypt_returns_valid_tuple(): + """Test that encrypt() returns a (bytes, int) tuple.""" + encrypted, reveal_round = timelock.encrypt("Bittensor", n_blocks=1) + assert isinstance(encrypted, bytes) + assert isinstance(reveal_round, int) + assert reveal_round > 0 + + +def test_encrypt_with_fast_block_time(): + """Test encrypt() with fast-blocks mode (block_time = 0.25s).""" + encrypted, reveal_round = timelock.encrypt("Fast mode", 5, block_time=0.25) + assert isinstance(encrypted, bytes) + assert isinstance(reveal_round, int) + + +def test_decrypt_returns_bytes_or_none(): + """Test that decrypt() returns bytes after reveal round, or None before.""" + data = b"Decode me" + encrypted, reveal_round = timelock.encrypt(data, 1) + + current_round = timelock.get_latest_round() + if current_round < reveal_round: + decrypted = timelock.decrypt(encrypted) + assert decrypted is None + else: + decrypted = timelock.decrypt(encrypted) + assert decrypted == data + + +def test_decrypt_raises_if_no_errors_false_and_invalid_data(): + """Test that decrypt() raises an error on invalid data when no_errors=False.""" + with pytest.raises(Exception): + timelock.decrypt(b"corrupt data", no_errors=False) + + +def test_decrypt_with_return_str(): + """Test decrypt() with return_str=True returns a string.""" + plaintext = "Stringified!" + encrypted, _ = timelock.encrypt(plaintext, 1, block_time=0.25) + result = timelock.decrypt(encrypted, no_errors=True, return_str=True) + if result is not None: + assert isinstance(result, str) + + +def test_get_latest_round_is_monotonic(): + """Test that get_latest_round() is monotonic over time.""" + r1 = timelock.get_latest_round() + time.sleep(3) + r2 = timelock.get_latest_round() + assert r2 >= r1 + + +def test_wait_reveal_and_decrypt_auto_round(): + """Test wait_reveal_and_decrypt() without explicit reveal_round.""" + msg = "Reveal and decrypt test" + encrypted, _ = timelock.encrypt(msg, 1) + result = timelock.wait_reveal_and_decrypt(encrypted, return_str=True) + assert result == msg + + +def test_wait_reveal_and_decrypt_manual_round(): + """Test wait_reveal_and_decrypt() with explicit reveal_round.""" + msg = "Manual round decryption" + encrypted, reveal_round = timelock.encrypt(msg, 1) + result = timelock.wait_reveal_and_decrypt(encrypted, reveal_round, return_str=True) + assert result == msg + + +def test_unpack_reveal_round_struct(): + """Test that reveal_round can be extracted from encrypted data.""" + encrypted, reveal_round = timelock.encrypt("parse test", 1) + parsed = struct.unpack( + "