diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 265b358a84..e88eb588f2 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -32,19 +32,41 @@ jobs: steps: - name: Check-out repository under $GITHUB_WORKSPACE uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install deps for collection + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" - name: Find test files id: get-tests - run: | - test_files=$(find tests/e2e_tests -name "test*.py" | jq -R -s -c 'split("\n") | map(select(. != ""))') - # keep it here for future debug - # test_files=$(find tests/e2e_tests -type f -name "test*.py" | grep -E 'test_(hotkeys|staking)\.py$' | jq -R -s -c 'split("\n") | map(select(. != ""))') - echo "Found test files: $test_files" - echo "test-files=$test_files" >> "$GITHUB_OUTPUT" shell: bash + run: | + set -euo pipefail + test_matrix=$( + pytest -q --collect-only tests/e2e_tests \ + | sed -n '/^e2e_tests\//p' \ + | sed 's|^|tests/|' \ + | jq -R -s -c ' + split("\n") + | map(select(. != "")) + | map({nodeid: ., label: (sub("^tests/e2e_tests/"; ""))}) + ' + ) + echo "Found tests: $test_matrix" + echo "test-files=$test_matrix" >> "$GITHUB_OUTPUT" # Pull docker image pull-docker-image: + needs: find-tests runs-on: ubuntu-latest outputs: image-name: ${{ steps.set-image.outputs.image }} @@ -111,7 +133,7 @@ jobs: # Job to run tests in parallel run-fast-blocks-e2e-test: - name: "FB: ${{ matrix.test-file }} / Python ${{ matrix.python-version }}" + name: "${{ matrix.test.label }} / Py ${{ matrix.python-version }}" needs: - find-tests - pull-docker-image @@ -119,15 +141,18 @@ 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 ubuntu-latest runner) + max-parallel: 64 # Set the maximum number of parallel jobs (same as we have cores in ubuntu-latest runner) matrix: os: - ubuntu-latest - test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} + test: ${{ fromJson(needs.find-tests.outputs.test-files) }} python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - name: Check-out repository uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 @@ -154,7 +179,7 @@ jobs: run: | for i in 1 2 3; do echo "::group::🔁 Test attempt $i" - if uv run pytest ${{ matrix.test-file }} -s; then + if uv run pytest "${{ matrix.test.nodeid }}" -s; then echo "✅ Tests passed on attempt $i" echo "::endgroup::" exit 0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1201b090a0..0229e03163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 9.10.0 /2025-08-28 + +## What's Changed +* Fixes broken e2e tests by @thewhaleking in https://github.com/opentensor/bittensor/pull/3020 +* Add async crv4 e2e test by @basfroman in https://github.com/opentensor/bittensor/pull/3022 +* Use `TimelockedWeightCommits` instead of `CRV3WeightCommitsV2` by @basfroman in https://github.com/opentensor/bittensor/pull/3023 +* fix: reflect correct return types for get_delegated by @Arthurdw in https://github.com/opentensor/bittensor/pull/3016 +* Fix `flaky` e2e test (tests.e2e_tests.test_staking.test_safe_staking_scenarios) by @basfroman in https://github.com/opentensor/bittensor/pull/3025 +* Separation of test modules into separate text elements as independent matrix elements by @basfroman in https://github.com/opentensor/bittensor/pull/3027 +* Improve `move_stake` extrinsic (add `move_all_stake` parameter) by @basfroman in https://github.com/opentensor/bittensor/pull/3028 +* Fix tests related with disabled `sudo_set_commit_reveal_weights_enabled` by @basfroman in https://github.com/opentensor/bittensor/pull/3026 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.9.0...v9.10.0 + ## 9.9.0 /2025-08-11 ## What's Changed diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 98542c89a6..e40b1f4465 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -1880,7 +1880,7 @@ async def get_delegated( block: Optional[int] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> list[tuple[DelegateInfo, Balance]]: + ) -> list[DelegatedInfo]: """ Retrieves a list of delegates and their associated stakes for a given coldkey. This function identifies the delegates that a specific account has staked tokens on. @@ -1892,7 +1892,7 @@ async def get_delegated( reuse_block: Whether to reuse the last-used blockchain block hash. Returns: - A list of tuples, each containing a delegate's information and staked amount. + A list containing the delegated information for the specified coldkey. This function is important for account holders to understand their stake allocations and their involvement in the network's delegation and consensus mechanisms. @@ -2519,6 +2519,7 @@ async def get_next_epoch_start_block( netuid=netuid, block=block, block_hash=block_hash, reuse_block=reuse_block ) + block = block or await self.substrate.get_block_number(block_hash=block_hash) if block and blocks_since_last_step is not None and tempo: return block - blocks_since_last_step + tempo + 1 return None @@ -2747,6 +2748,45 @@ async def get_subnet_prices( prices.update({0: Balance.from_tao(1)}) return prices + async def get_timelocked_weight_commits( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[tuple[str, int, str, int]]: + """ + Retrieves CRv4 weight commit information for a specific subnet. + + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. Default is ``None``. + block_hash: The hash of the block to retrieve the stake from. Do not specify if using block + or reuse_block + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + A list of commit details, where each item contains: + - ss58_address: The address of the committer. + - commit_block: The block number when the commitment was made. + - commit_message: The commit message. + - reveal_round: The round when the commitment was revealed. + + The list may be empty if there are no commits found. + """ + block_hash = await self.determine_block_hash( + block=block, block_hash=block_hash, reuse_block=reuse_block + ) + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="TimelockedWeightCommits", + params=[netuid], + block_hash=block_hash, + ) + + commits = result.records[0][1] if result.records else [] + return [WeightCommitInfo.from_vec_u8_v2(commit) for commit in commits] + # TODO: remove unused parameters in SDK.v10 async def get_unstake_fee( self, @@ -4617,10 +4657,11 @@ async def move_stake( origin_netuid: int, destination_hotkey: str, destination_netuid: int, - amount: Balance, + amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + move_all_stake: bool = False, ) -> bool: """ Moves stake to a different hotkey and/or subnet. @@ -4637,6 +4678,7 @@ async def move_stake( period: The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. + move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. Returns: success: True if the stake movement was successful. @@ -4653,6 +4695,7 @@ async def move_stake( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, period=period, + move_all_stake=move_all_stake, ) async def register( @@ -5548,7 +5591,7 @@ async def unstake( self, wallet: "Wallet", hotkey_ss58: Optional[str] = None, - netuid: Optional[int] = None, + netuid: Optional[int] = None, # TODO why is this optional? amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, diff --git a/bittensor/core/extrinsics/asyncex/move_stake.py b/bittensor/core/extrinsics/asyncex/move_stake.py index 8ed6a29141..d52f86f868 100644 --- a/bittensor/core/extrinsics/asyncex/move_stake.py +++ b/bittensor/core/extrinsics/asyncex/move_stake.py @@ -305,28 +305,34 @@ async def move_stake_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + move_all_stake: bool = False, ) -> bool: """ Moves stake from one hotkey to another within subnets in the Bittensor network. Args: - subtensor (Subtensor): The subtensor instance to interact with the blockchain. - wallet (Wallet): The wallet containing the coldkey to authorize the move. - origin_hotkey (str): SS58 address of the origin hotkey associated with the stake. - origin_netuid (int): Network UID of the origin subnet. - destination_hotkey (str): SS58 address of the destination hotkey. - destination_netuid (int): Network UID of the destination subnet. - amount (Balance): The amount of stake to move as a `Balance` object. - wait_for_inclusion (bool): If True, waits for transaction inclusion in a block. Defaults to True. - wait_for_finalization (bool): If True, waits for transaction finalization. Defaults to False. - period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. + subtensor: The subtensor instance to interact with the blockchain. + wallet: The wallet containing the coldkey to authorize the move. + origin_hotkey: SS58 address of the origin hotkey associated with the stake. + origin_netuid: Network UID of the origin subnet. + destination_hotkey: SS58 address of the destination hotkey. + destination_netuid: Network UID of the destination subnet. + amount: The amount of stake to move as a `Balance` object. + wait_for_inclusion: If True, waits for transaction inclusion in a block. Defaults to True. + wait_for_finalization: If True, waits for transaction finalization. Defaults to False. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. Returns: bool: True if the move was successful, False otherwise. """ - amount.set_unit(netuid=origin_netuid) + if not amount and not move_all_stake: + logging.error( + ":cross_mark: [red]Failed[/red]: Please specify an `amount` or `move_all_stake` argument to move stake." + ) + return False # Check sufficient stake stake_in_origin, stake_in_destination = await _get_stake_in_origin_and_dest( @@ -338,13 +344,18 @@ async def move_stake_extrinsic( origin_netuid=origin_netuid, destination_netuid=destination_netuid, ) - if stake_in_origin < amount: + if move_all_stake: + amount = stake_in_origin + + elif stake_in_origin < amount: logging.error( f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {origin_hotkey}. " f"Stake: {stake_in_origin}, amount: {amount}" ) return False + amount.set_unit(netuid=origin_netuid) + try: logging.info( f"Moving stake from hotkey [blue]{origin_hotkey}[/blue] to hotkey [blue]{destination_hotkey}[/blue]\n" diff --git a/bittensor/core/extrinsics/asyncex/registration.py b/bittensor/core/extrinsics/asyncex/registration.py index eba6e9390b..d8ac53c499 100644 --- a/bittensor/core/extrinsics/asyncex/registration.py +++ b/bittensor/core/extrinsics/asyncex/registration.py @@ -10,6 +10,7 @@ import asyncio from typing import Optional, Union, TYPE_CHECKING +from bittensor.core.extrinsics.asyncex.utils import get_extrinsic_fee from bittensor.utils import unlock_key from bittensor.utils.btlogging import logging from bittensor.utils.registration import log_no_torch_error, create_pow_async, torch @@ -57,6 +58,12 @@ async def _do_burned_register( "hotkey": wallet.hotkey.ss58_address, }, ) + fee = await get_extrinsic_fee( + subtensor=subtensor, call=call, keypair=wallet.coldkeypub + ) + logging.info( + f"The registration fee for SN #[blue]{netuid}[/blue] is [blue]{fee}[/blue]." + ) return await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -127,7 +134,6 @@ async def burned_register_extrinsic( return True logging.debug(":satellite: [magenta]Recycling TAO for Registration...[/magenta]") - logging.info(f"Recycling {recycle_amount} to register on subnet:{netuid}") success, err_msg = await _do_burned_register( subtensor=subtensor, diff --git a/bittensor/core/extrinsics/asyncex/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index f5721cd59f..fcc0416dd5 100644 --- a/bittensor/core/extrinsics/asyncex/unstaking.py +++ b/bittensor/core/extrinsics/asyncex/unstaking.py @@ -2,7 +2,7 @@ from typing import Optional, TYPE_CHECKING from async_substrate_interface.errors import SubstrateRequestException - +from bittensor.core.extrinsics.asyncex.utils import get_extrinsic_fee from bittensor.core.extrinsics.utils import get_old_stakes from bittensor.utils import unlock_key, format_error_message from bittensor.utils.balance import Balance @@ -114,14 +114,14 @@ async def unstake_extrinsic( else: price_with_tolerance = base_price * (1 - rate_tolerance) - logging.info( + 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"price limit: [green]{price_with_tolerance}[/green], " f"original price: [green]{base_price}[/green], " f"with partial unstake: [green]{allow_partial_stake}[/green] " - f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]" + f"on [blue]{subtensor.network}[/blue]" ) limit_price = Balance.from_tao(price_with_tolerance).rao @@ -133,10 +133,10 @@ async def unstake_extrinsic( ) call_function = "remove_stake_limit" else: - logging.info( + logging_info = ( f":satellite: [magenta]Unstaking from:[/magenta] " f"netuid: [green]{netuid}[/green], amount: [green]{unstaking_balance}[/green] " - f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]" + f"on [blue]{subtensor.network}[/blue]" ) call_function = "remove_stake" @@ -145,6 +145,10 @@ async def unstake_extrinsic( call_function=call_function, call_params=call_params, ) + fee = await get_extrinsic_fee( + subtensor=subtensor, call=call, keypair=wallet.coldkeypub, netuid=netuid + ) + logging.info(f"{logging_info} for fee [blue]{fee}[/blue][magenta]...[/magenta]") success, message = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -381,10 +385,6 @@ async def unstake_multiple_extrinsic( continue try: - logging.info( - f"Unstaking [blue]{unstaking_balance}[/blue] from hotkey: [magenta]{hotkey_ss58}[/magenta] on netuid: " - f"[blue]{netuid}[/blue]" - ) call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="remove_stake", @@ -394,6 +394,13 @@ async def unstake_multiple_extrinsic( "netuid": netuid, }, ) + fee = await get_extrinsic_fee( + subtensor=subtensor, call=call, keypair=wallet.coldkeypub, netuid=netuid + ) + logging.info( + f"Unstaking [blue]{unstaking_balance}[/blue] from hotkey: [magenta]{hotkey_ss58}[/magenta] on netuid: " + f"[blue]{netuid}[/blue] for fee [blue]{fee}[/blue]" + ) staking_response, err_msg = await subtensor.sign_and_send_extrinsic( call=call, diff --git a/bittensor/core/extrinsics/asyncex/utils.py b/bittensor/core/extrinsics/asyncex/utils.py new file mode 100644 index 0000000000..7c756e2499 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/utils.py @@ -0,0 +1,34 @@ +from typing import TYPE_CHECKING, Optional + +from bittensor.utils.balance import Balance + +if TYPE_CHECKING: + from scalecodec import GenericCall + from bittensor_wallet import Keypair + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def get_extrinsic_fee( + subtensor: "AsyncSubtensor", + call: "GenericCall", + keypair: "Keypair", + netuid: Optional[int] = None, +): + """ + Get extrinsic fee for a given extrinsic call and keypair for a given SN's netuid. + + Arguments: + subtensor: The Subtensor instance. + netuid: The SN's netuid. + call: The extrinsic call. + keypair: The keypair associated with the extrinsic. + + Returns: + Balance object representing the extrinsic fee in RAO. + """ + payment_info = await subtensor.substrate.get_payment_info( + call=call, keypair=keypair + ) + return Balance.from_rao(amount=payment_info["partial_fee"]).set_unit( + netuid=netuid or 0 + ) diff --git a/bittensor/core/extrinsics/move_stake.py b/bittensor/core/extrinsics/move_stake.py index d3874f1e68..f3aae363ce 100644 --- a/bittensor/core/extrinsics/move_stake.py +++ b/bittensor/core/extrinsics/move_stake.py @@ -301,29 +301,34 @@ def move_stake_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + move_all_stake: bool = False, ) -> bool: """ Moves stake to a different hotkey and/or subnet while keeping the same coldkey owner. Args: - subtensor (Subtensor): Subtensor instance. - wallet (bittensor.wallet): The wallet to move stake from. - origin_hotkey (str): The SS58 address of the source hotkey. - origin_netuid (int): The netuid of the source subnet. - destination_hotkey (str): The SS58 address of the destination hotkey. - destination_netuid (int): The netuid of the destination subnet. - amount (Union[Balance, float]): Amount to move. - wait_for_inclusion (bool): If true, waits for inclusion before returning. - wait_for_finalization (bool): If true, waits for finalization before returning. - period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If - the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. + subtensor: Subtensor instance. + wallet: The wallet to move stake from. + origin_hotkey: The SS58 address of the source hotkey. + origin_netuid: The netuid of the source subnet. + destination_hotkey: The SS58 address of the destination hotkey. + destination_netuid: The netuid of the destination subnet. + amount: Amount to move. + wait_for_inclusion: If true, waits for inclusion before returning. + wait_for_finalization: If true, waits for finalization before returning. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You can + think of it as an expiration date for the transaction. + move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. Returns: - success (bool): True if the move was successful. + success: True if the move was successful. Otherwise, False. """ - - amount.set_unit(netuid=origin_netuid) + if not amount and not move_all_stake: + logging.error( + ":cross_mark: [red]Failed[/red]: Please specify an `amount` or `move_all_stake` argument to move stake." + ) + return False # Check sufficient stake stake_in_origin, stake_in_destination = _get_stake_in_origin_and_dest( @@ -335,12 +340,18 @@ def move_stake_extrinsic( origin_coldkey_ss58=wallet.coldkeypub.ss58_address, destination_coldkey_ss58=wallet.coldkeypub.ss58_address, ) - if stake_in_origin < amount: + if move_all_stake: + amount = stake_in_origin + + elif stake_in_origin < amount: logging.error( - f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {origin_hotkey}. Stake: {stake_in_origin}, amount: {amount}" + f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {origin_hotkey}. " + f"Stake: {stake_in_origin}, amount: {amount}" ) return False + amount.set_unit(netuid=origin_netuid) + try: logging.info( f"Moving stake from hotkey [blue]{origin_hotkey}[/blue] to hotkey [blue]{destination_hotkey}[/blue]\n" diff --git a/bittensor/core/extrinsics/registration.py b/bittensor/core/extrinsics/registration.py index ddf5604544..8c090c3065 100644 --- a/bittensor/core/extrinsics/registration.py +++ b/bittensor/core/extrinsics/registration.py @@ -9,6 +9,7 @@ import time from typing import Optional, Union, TYPE_CHECKING +from bittensor.core.extrinsics.utils import get_extrinsic_fee from bittensor.utils import unlock_key from bittensor.utils.btlogging import logging from bittensor.utils.registration import create_pow, log_no_torch_error, torch @@ -56,6 +57,10 @@ def _do_burned_register( "hotkey": wallet.hotkey.ss58_address, }, ) + fee = get_extrinsic_fee(subtensor=subtensor, call=call, keypair=wallet.coldkeypub) + logging.info( + f"The registration fee for SN #[blue]{netuid}[/blue] is [blue]{fee}[/blue]." + ) return subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -197,6 +202,7 @@ def _do_pow_register( "coldkey": wallet.coldkeypub.ss58_address, }, ) + logging.debug(":satellite: [magenta]Sending POW Register Extrinsic...[/magenta]") return subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index beff7f1993..8f0ce3329d 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -1,6 +1,7 @@ from typing import Optional, TYPE_CHECKING from async_substrate_interface.errors import SubstrateRequestException +from bittensor.core.extrinsics.utils import get_extrinsic_fee from bittensor.core.extrinsics.utils import get_old_stakes from bittensor.utils import unlock_key, format_error_message @@ -112,14 +113,14 @@ def unstake_extrinsic( else: price_with_tolerance = base_price * (1 - rate_tolerance) - logging.info( + 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"price limit: [green]{price_with_tolerance}[/green], " f"original price: [green]{base_price}[/green], " f"with partial unstake: [green]{allow_partial_stake}[/green] " - f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]" + f"on [blue]{subtensor.network}[/blue]" ) limit_price = Balance.from_tao(price_with_tolerance).rao @@ -131,10 +132,10 @@ def unstake_extrinsic( ) call_function = "remove_stake_limit" else: - logging.info( + logging_info = ( f":satellite: [magenta]Unstaking from:[/magenta] " f"netuid: [green]{netuid}[/green], amount: [green]{unstaking_balance}[/green] " - f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]" + f"on [blue]{subtensor.network}[/blue]" ) call_function = "remove_stake" @@ -143,6 +144,10 @@ def unstake_extrinsic( call_function=call_function, call_params=call_params, ) + fee = get_extrinsic_fee( + subtensor=subtensor, netuid=netuid, call=call, keypair=wallet.coldkeypub + ) + logging.info(f"{logging_info} for fee [blue]{fee}[/blue][magenta]...[/magenta]") success, message = subtensor.sign_and_send_extrinsic( call=call, @@ -372,9 +377,6 @@ def unstake_multiple_extrinsic( continue try: - logging.info( - f"Unstaking [blue]{unstaking_balance}[/blue] from [magenta]{hotkey_ss58}[/magenta] on [blue]{netuid}[/blue]" - ) call = subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="remove_stake", @@ -384,6 +386,14 @@ def unstake_multiple_extrinsic( "netuid": netuid, }, ) + fee = get_extrinsic_fee( + subtensor=subtensor, netuid=netuid, call=call, keypair=wallet.coldkeypub + ) + logging.info( + f"Unstaking [blue]{unstaking_balance}[/blue] from hotkey: [magenta]{hotkey_ss58}[/magenta] on netuid: " + f"[blue]{netuid}[/blue] for fee [blue]{fee}[/blue]" + ) + staking_response, err_msg = subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, diff --git a/bittensor/core/extrinsics/utils.py b/bittensor/core/extrinsics/utils.py index 7566aa9bc2..5092791f66 100644 --- a/bittensor/core/extrinsics/utils.py +++ b/bittensor/core/extrinsics/utils.py @@ -1,12 +1,14 @@ """Module with helper functions for extrinsics.""" -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from bittensor.utils.balance import Balance if TYPE_CHECKING: - from bittensor_wallet import Wallet + from scalecodec import GenericCall + from bittensor_wallet import Wallet, Keypair from bittensor.core.chain_data import StakeInfo + from bittensor.core.subtensor import Subtensor def get_old_stakes( @@ -42,3 +44,27 @@ def get_old_stakes( ) for hotkey_ss58, netuid in zip(hotkey_ss58s, netuids) ] + + +def get_extrinsic_fee( + call: "GenericCall", + keypair: "Keypair", + subtensor: "Subtensor", + netuid: Optional[int] = None, +): + """ + Get extrinsic fee for a given extrinsic call and keypair for a given SN's netuid. + + Arguments: + subtensor: The Subtensor instance. + call: The extrinsic call. + keypair: The keypair associated with the extrinsic. + netuid: The SN's netuid. + + Returns: + Balance object representing the extrinsic fee in RAO. + """ + payment_info = subtensor.substrate.get_payment_info(call=call, keypair=keypair) + return Balance.from_rao(amount=payment_info["partial_fee"]).set_unit( + netuid=netuid or 0 + ) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index d1b95521d8..28371fb7d6 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1223,7 +1223,7 @@ def get_delegate_take(self, hotkey_ss58: str, block: Optional[int] = None) -> fl def get_delegated( self, coldkey_ss58: str, block: Optional[int] = None - ) -> list[tuple["DelegateInfo", Balance]]: + ) -> list[DelegatedInfo]: """ Retrieves a list of delegates and their associated stakes for a given coldkey. This function identifies the delegates that a specific account has staked tokens on. @@ -1233,7 +1233,7 @@ def get_delegated( block (Optional[int]): The blockchain block number for the query. Returns: - A list of tuples, each containing a delegate's information and staked amount. + A list containing the delegated information for the specified coldkey. This function is important for account holders to understand their stake allocations and their involvement in the network's delegation and consensus mechanisms. @@ -1945,6 +1945,35 @@ def get_subnet_prices( prices.update({0: Balance.from_tao(1)}) return prices + def get_timelocked_weight_commits( + self, netuid: int, block: Optional[int] = None + ) -> list[tuple[str, int, str, int]]: + """ + Retrieves CRv4 weight commit information for a specific subnet. + + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. Default is ``None``. + + Returns: + A list of commit details, where each item contains: + - ss58_address: The address of the committer. + - commit_block: The block number when the commitment was made. + - commit_message: The commit message. + - reveal_round: The round when the commitment was revealed. + + The list may be empty if there are no commits found. + """ + result = self.substrate.query_map( + module="SubtensorModule", + storage_function="TimelockedWeightCommits", + params=[netuid], + block_hash=self.determine_block_hash(block=block), + ) + + commits = result.records[0][1] if result.records else [] + return [WeightCommitInfo.from_vec_u8_v2(commit) for commit in commits] + # TODO: remove unused parameters in SDK.v10 def get_unstake_fee( self, @@ -3464,10 +3493,11 @@ def move_stake( origin_netuid: int, destination_hotkey: str, destination_netuid: int, - amount: Balance, + amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + move_all_stake: bool = False, ) -> bool: """ Moves stake to a different hotkey and/or subnet. @@ -3484,6 +3514,7 @@ def move_stake( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. + move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. Returns: success (bool): True if the stake movement was successful. @@ -3500,6 +3531,7 @@ def move_stake( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, period=period, + move_all_stake=move_all_stake, ) def register( @@ -4368,7 +4400,7 @@ def unstake( self, wallet: "Wallet", hotkey_ss58: Optional[str] = None, - netuid: Optional[int] = None, + netuid: Optional[int] = None, # TODO why is this optional? amount: Optional[Balance] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, diff --git a/bittensor/core/subtensor_api/commitments.py b/bittensor/core/subtensor_api/commitments.py index 8a9c49bd30..ff130a3e54 100644 --- a/bittensor/core/subtensor_api/commitments.py +++ b/bittensor/core/subtensor_api/commitments.py @@ -22,5 +22,6 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_revealed_commitment_by_hotkey = ( subtensor.get_revealed_commitment_by_hotkey ) + self.get_timelocked_weight_commits = subtensor.get_timelocked_weight_commits self.set_commitment = subtensor.set_commitment self.set_reveal_commitment = subtensor.set_reveal_commitment diff --git a/bittensor/core/subtensor_api/staking.py b/bittensor/core/subtensor_api/staking.py index 979d8a2632..40464e7ccd 100644 --- a/bittensor/core/subtensor_api/staking.py +++ b/bittensor/core/subtensor_api/staking.py @@ -22,6 +22,7 @@ def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): self.get_stake_operations_fee = subtensor.get_stake_operations_fee self.get_stake_weight = subtensor.get_stake_weight self.get_unstake_fee = subtensor.get_unstake_fee + self.move_stake = subtensor.move_stake self.unstake = subtensor.unstake self.unstake_all = subtensor.unstake_all self.unstake_multiple = subtensor.unstake_multiple diff --git a/bittensor/core/subtensor_api/utils.py b/bittensor/core/subtensor_api/utils.py index adef4f31b1..67399a37ed 100644 --- a/bittensor/core/subtensor_api/utils.py +++ b/bittensor/core/subtensor_api/utils.py @@ -106,6 +106,9 @@ def add_legacy_methods(subtensor: "SubtensorApi"): subtensor._subtensor.get_subnet_validator_permits ) subtensor.get_subnets = subtensor._subtensor.get_subnets + subtensor.get_timelocked_weight_commits = ( + subtensor._subtensor.get_timelocked_weight_commits + ) subtensor.get_timestamp = subtensor._subtensor.get_timestamp subtensor.get_total_subnets = subtensor._subtensor.get_total_subnets subtensor.get_transfer_fee = subtensor._subtensor.get_transfer_fee diff --git a/pyproject.toml b/pyproject.toml index 71f245a935..40a7e7f85f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor" -version = "9.9.0" +version = "9.10.0" description = "Bittensor" readme = "README.md" authors = [ @@ -36,7 +36,7 @@ dependencies = [ "uvicorn", "bittensor-drand>=1.0.0,<2.0.0", "bittensor-wallet>=4.0.0,<5.0", - "async-substrate-interface>=1.4.2" + "async-substrate-interface>=1.5.1" ] [project.optional-dependencies] diff --git a/tests/e2e_tests/test_commit_reveal.py b/tests/e2e_tests/test_commit_reveal.py new file mode 100644 index 0000000000..2531326583 --- /dev/null +++ b/tests/e2e_tests/test_commit_reveal.py @@ -0,0 +1,462 @@ +import re + +import numpy as np +import pytest + +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 ( + async_wait_interval, + sudo_set_admin_utils, + sudo_set_hyperparameter_bool, + wait_interval, + next_tempo, +) + + +# @pytest.mark.parametrize("local_chain", [True], indirect=True) +@pytest.mark.asyncio +async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_wallet): + """ + Tests the commit/reveal weights mechanism (CR3) + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Enable a commit-reveal mechanism on subnet + 4. Lower weights rate limit + 5. Change the tempo for subnet 1 + 5. Commit weights and ensure they are committed. + 6. Wait interval & reveal weights and verify + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing `test_commit_and_reveal_weights_cr4`") + + # 12 for non-fast-block, 0.25 for fast block + BLOCK_TIME, TEMPO_TO_SET = ( + (0.25, 100) if subtensor.chain.is_fast_blocks() else (12.0, 20) + ) + + logging.console.info(f"Using block time: {BLOCK_TIME}") + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + + # Register root as Alice + assert subtensor.extrinsics.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Verify subnet 2 created successfully + assert subtensor.subnet_exists(alice_subnet_netuid), ( + f"SN #{alice_subnet_netuid} wasn't created successfully" + ) + + logging.console.success(f"SN #{alice_subnet_netuid} is registered.") + + # Enable commit_reveal on the subnet + assert sudo_set_hyperparameter_bool( + substrate=local_chain, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + value=True, + netuid=alice_subnet_netuid, + ), f"Unable to enable commit reveal on the SN #{alice_subnet_netuid}" + + # Verify commit_reveal was enabled + assert subtensor.subnets.commit_reveal_enabled(alice_subnet_netuid), ( + "Failed to enable commit/reveal" + ) + logging.console.success("Commit reveal enabled") + + cr_version = subtensor.substrate.query( + module="SubtensorModule", storage_function="CommitRevealWeightsVersion" + ) + assert cr_version == 4, f"Commit reveal version is not 3, got {cr_version}" + + # Change the weights rate limit on the subnet + status, error = sudo_set_admin_utils( + substrate=local_chain, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": alice_subnet_netuid, "weights_set_rate_limit": "0"}, + ) + + assert status is True + assert error is None + + # Verify weights rate limit was changed + assert ( + subtensor.subnets.get_subnet_hyperparameters( + netuid=alice_subnet_netuid + ).weights_rate_limit + == 0 + ), "Failed to set weights_rate_limit" + assert subtensor.weights_rate_limit(netuid=alice_subnet_netuid) == 0 + logging.console.success("sudo_set_weights_set_rate_limit executed: set to 0") + + # Change the tempo of the subnet + assert ( + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": alice_subnet_netuid, "tempo": TEMPO_TO_SET}, + )[0] + is True + ) + + tempo = subtensor.subnets.get_subnet_hyperparameters( + netuid=alice_subnet_netuid + ).tempo + assert tempo == TEMPO_TO_SET, "SN tempos has not been changed." + logging.console.success(f"SN #{alice_subnet_netuid} tempo set to {TEMPO_TO_SET}") + + # Commit-reveal values - setting weights to self + uids = np.array([0], dtype=np.int64) + weights = np.array([0.1], dtype=np.float32) + + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) + logging.console.info( + f"Committing weights: uids {weight_uids}, weights {weight_vals}" + ) + + # Fetch current block and calculate next tempo for the subnet + current_block = subtensor.chain.get_current_block() + 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}" + ) + + # Lower than this might mean weights will get revealed before we can check them + if upcoming_tempo - current_block < 6: + await wait_interval( + tempo, + subtensor, + netuid=alice_subnet_netuid, + reporting_interval=1, + ) + current_block = subtensor.chain.get_current_block() + latest_drand_round = subtensor.chain.last_drand_round() + upcoming_tempo = next_tempo(current_block, tempo) + logging.console.info( + f"Post first wait_interval (to ensure window isn't too low): {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" + ) + + # commit_block is the block when weights were committed on the chain (transaction block) + expected_commit_block = subtensor.block + 1 + # Commit weights + success, message = subtensor.extrinsics.set_weights( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=True, + block_time=BLOCK_TIME, + period=16, + ) + + # Assert committing was a success + assert success is True, message + assert bool(re.match(r"reveal_round:\d+", message)) + + # Parse expected reveal_round + expected_reveal_round = int(message.split(":")[1]) + logging.console.success( + f"Successfully set weights: uids {weight_uids}, weights {weight_vals}, reveal_round: {expected_reveal_round}" + ) + + # Fetch current commits pending on the chain + commits_on_chain = subtensor.commitments.get_timelocked_weight_commits( + netuid=alice_subnet_netuid + ) + address, commit_block, commit, reveal_round = commits_on_chain[0] + + # Assert correct values are committed on the chain + assert expected_reveal_round == reveal_round + assert address == alice_wallet.hotkey.ss58_address + + # bc of the drand delay, the commit block can be either the previous block or the current block + assert expected_commit_block in [commit_block - 1, commit_block, commit_block + 1] + + # Ensure no weights are available as of now + assert subtensor.weights(netuid=alice_subnet_netuid) == [] + logging.console.success("No weights are available before next epoch.") + + # 5 is safety drand offset + expected_reveal_block = ( + subtensor.subnets.get_next_epoch_start_block(alice_subnet_netuid) + 5 + ) + + logging.console.info( + f"Waiting for the next epoch to ensure weights are revealed: block {expected_reveal_block}" + ) + subtensor.wait_for_block(expected_reveal_block) + + # Fetch the latest drand pulse + latest_drand_round = subtensor.chain.last_drand_round() + logging.console.info( + f"Latest drand round after waiting for tempo: {latest_drand_round}" + ) + + # Fetch weights on the chain as they should be revealed now + subnet_weights = subtensor.subnets.weights(netuid=alice_subnet_netuid) + assert subnet_weights != [], "Weights are not available yet." + + logging.console.info(f"Revealed weights: {subnet_weights}") + + revealed_weights = subnet_weights[0][1] + # Assert correct weights were revealed + assert weight_uids[0] == revealed_weights[0][0] + assert weight_vals[0] == revealed_weights[0][1] + + logging.console.success( + f"Successfully revealed weights: uids {weight_uids}, weights {weight_vals}" + ) + + # Now that the commit has been revealed, there shouldn't be any pending commits + assert ( + subtensor.commitments.get_timelocked_weight_commits(netuid=alice_subnet_netuid) + == [] + ) + + # Ensure the drand_round is always in the positive w.r.t expected when revealed + assert latest_drand_round - expected_reveal_round >= -3, ( + f"latest_drand_round ({latest_drand_round}) is less than expected_reveal_round ({expected_reveal_round})" + ) + + logging.console.success("✅ Passed `test_commit_and_reveal_weights_cr4`") + + +@pytest.mark.asyncio +async def test_async_commit_and_reveal_weights_cr4( + local_chain, async_subtensor, alice_wallet +): + """ + Tests the commit/reveal weights mechanism (CR3) + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Enable a commit-reveal mechanism on subnet + 4. Lower weights rate limit + 5. Change the tempo for subnet 1 + 5. Commit weights and ensure they are committed. + 6. Wait interval & reveal weights and verify + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing `test_commit_and_reveal_weights_cr4`") + + async with async_subtensor: + # 12 for non-fast-block, 0.25 for fast block + BLOCK_TIME, TEMPO_TO_SET = ( + (0.25, 100) if await async_subtensor.chain.is_fast_blocks() else (12.0, 20) + ) + + logging.console.info(f"Using block time: {BLOCK_TIME}") + + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + + # Register root as Alice + assert await async_subtensor.extrinsics.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Verify subnet 2 created successfully + assert await async_subtensor.subnet_exists(alice_subnet_netuid), ( + f"SN #{alice_subnet_netuid} wasn't created successfully" + ) + + logging.console.success(f"SN #{alice_subnet_netuid} is registered.") + + # Enable commit_reveal on the subnet + assert sudo_set_hyperparameter_bool( + substrate=local_chain, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + value=True, + netuid=alice_subnet_netuid, + ), f"Unable to enable commit reveal on the SN #{alice_subnet_netuid}" + + # Verify commit_reveal was enabled + assert await async_subtensor.subnets.commit_reveal_enabled( + alice_subnet_netuid + ), "Failed to enable commit/reveal" + logging.console.success("Commit reveal enabled") + + cr_version = await async_subtensor.substrate.query( + module="SubtensorModule", storage_function="CommitRevealWeightsVersion" + ) + assert cr_version == 4, f"Commit reveal version is not 3, got {cr_version}" + + # Change the weights rate limit on the subnet + status, error = sudo_set_admin_utils( + substrate=local_chain, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": alice_subnet_netuid, "weights_set_rate_limit": "0"}, + ) + + assert status is True + assert error is None + + # Verify weights rate limit was changed + assert ( + await async_subtensor.subnets.get_subnet_hyperparameters( + netuid=alice_subnet_netuid + ) + ).weights_rate_limit == 0, "Failed to set weights_rate_limit" + assert await async_subtensor.weights_rate_limit(netuid=alice_subnet_netuid) == 0 + logging.console.success("sudo_set_weights_set_rate_limit executed: set to 0") + + # Change the tempo of the subnet + assert ( + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": alice_subnet_netuid, "tempo": TEMPO_TO_SET}, + )[0] + is True + ) + + tempo = ( + await async_subtensor.subnets.get_subnet_hyperparameters( + netuid=alice_subnet_netuid + ) + ).tempo + assert tempo == TEMPO_TO_SET, "SN tempos has not been changed." + logging.console.success( + f"SN #{alice_subnet_netuid} tempo set to {TEMPO_TO_SET}" + ) + + # Commit-reveal values - setting weights to self + uids = np.array([0], dtype=np.int64) + weights = np.array([0.1], dtype=np.float32) + + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) + logging.console.info( + f"Committing weights: uids {weight_uids}, weights {weight_vals}" + ) + + # Fetch current block and calculate next tempo for the subnet + current_block = await async_subtensor.chain.get_current_block() + 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}" + ) + + # Lower than this might mean weights will get revealed before we can check them + if upcoming_tempo - current_block < 6: + await async_wait_interval( + tempo, + async_subtensor, + netuid=alice_subnet_netuid, + reporting_interval=1, + ) + current_block = await async_subtensor.chain.get_current_block() + latest_drand_round = await async_subtensor.chain.last_drand_round() + upcoming_tempo = next_tempo(current_block, tempo) + logging.console.info( + f"Post first wait_interval (to ensure window isn't too low): {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" + ) + + # commit_block is the block when weights were committed on the chain (transaction block) + expected_commit_block = await async_subtensor.block + 1 + # Commit weights + success, message = await async_subtensor.extrinsics.set_weights( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=True, + block_time=BLOCK_TIME, + period=16, + ) + + # Assert committing was a success + assert success is True, message + assert bool(re.match(r"reveal_round:\d+", message)) + + # Parse expected reveal_round + expected_reveal_round = int(message.split(":")[1]) + logging.console.success( + f"Successfully set weights: uids {weight_uids}, weights {weight_vals}, reveal_round: {expected_reveal_round}" + ) + + # Fetch current commits pending on the chain + commits_on_chain = ( + await async_subtensor.commitments.get_timelocked_weight_commits( + netuid=alice_subnet_netuid + ) + ) + address, commit_block, commit, reveal_round = commits_on_chain[0] + + # Assert correct values are committed on the chain + assert expected_reveal_round == reveal_round + assert address == alice_wallet.hotkey.ss58_address + + # bc of the drand delay, the commit block can be either the previous block or the current block + # assert expected_commit_block in [commit_block - 1, commit_block, commit_block + 1] + + # Ensure no weights are available as of now + assert await async_subtensor.weights(netuid=alice_subnet_netuid) == [] + logging.console.success("No weights are available before next epoch.") + + # 5 is safety drand offset + expected_reveal_block = ( + await async_subtensor.subnets.get_next_epoch_start_block( + alice_subnet_netuid + ) + + 5 + ) + + logging.console.info( + f"Waiting for the next epoch to ensure weights are revealed: block {expected_reveal_block}" + ) + await async_subtensor.wait_for_block(expected_reveal_block) + + # Fetch the latest drand pulse + latest_drand_round = await async_subtensor.chain.last_drand_round() + logging.console.info( + f"Latest drand round after waiting for tempo: {latest_drand_round}" + ) + + # Fetch weights on the chain as they should be revealed now + subnet_weights = await async_subtensor.subnets.weights( + netuid=alice_subnet_netuid + ) + assert subnet_weights != [], "Weights are not available yet." + + logging.console.info(f"Revealed weights: {subnet_weights}") + + revealed_weights = subnet_weights[0][1] + # Assert correct weights were revealed + assert weight_uids[0] == revealed_weights[0][0] + assert weight_vals[0] == revealed_weights[0][1] + + logging.console.success( + f"Successfully revealed weights: uids {weight_uids}, weights {weight_vals}" + ) + + # Now that the commit has been revealed, there shouldn't be any pending commits + assert ( + await async_subtensor.commitments.get_timelocked_weight_commits( + netuid=alice_subnet_netuid + ) + == [] + ) + + # Ensure the drand_round is always in the positive w.r.t expected when revealed + assert latest_drand_round - expected_reveal_round >= -3, ( + f"latest_drand_round ({latest_drand_round}) is less than expected_reveal_round ({expected_reveal_round})" + ) + + logging.console.success("✅ Passed `test_commit_and_reveal_weights_cr4`") diff --git a/tests/e2e_tests/test_commit_reveal_v3.py b/tests/e2e_tests/test_commit_reveal_v3.py deleted file mode 100644 index b729d0a874..0000000000 --- a/tests/e2e_tests/test_commit_reveal_v3.py +++ /dev/null @@ -1,204 +0,0 @@ -import re -import time - -import numpy as np -import pytest -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, - wait_interval, - next_tempo, -) - - -# @pytest.mark.parametrize("local_chain", [True], indirect=True) -@pytest.mark.asyncio -async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_wallet): - """ - Tests the commit/reveal weights mechanism (CR3) - - Steps: - 1. Register a subnet through Alice - 2. Register Alice's neuron and add stake - 3. Enable a commit-reveal mechanism on subnet - 4. Lower weights rate limit - 5. Change the tempo for subnet 1 - 5. Commit weights and ensure they are committed. - 6. Wait interval & reveal weights and verify - Raises: - AssertionError: If any of the checks or verifications fail - """ - logging.console.info("Testing `test_commit_and_reveal_weights_cr4`") - - BLOCK_TIME = ( - 0.25 if subtensor.is_fast_blocks() else 12.0 - ) # 12 for non-fast-block, 0.25 for fast block - - logging.console.info(f"Using block time: {BLOCK_TIME}") - - netuid = subtensor.get_total_subnets() # 2 - - # Register root as Alice - assert subtensor.register_subnet(alice_wallet), "Unable to register the subnet" - - # Verify subnet 2 created successfully - assert subtensor.subnet_exists(netuid), f"SN #{netuid} wasn't created successfully" - - logging.console.success(f"SN #{netuid} is registered.") - - # Enable commit_reveal on the subnet - assert sudo_set_hyperparameter_bool( - substrate=local_chain, - wallet=alice_wallet, - call_function="sudo_set_commit_reveal_weights_enabled", - value=True, - netuid=netuid, - ), f"Unable to enable commit reveal on the SN #{netuid}" - - # Verify commit_reveal was enabled - assert subtensor.subnets.commit_reveal_enabled(netuid), ( - "Failed to enable commit/reveal" - ) - logging.console.success("Commit reveal enabled") - - cr_version = subtensor.substrate.query( - module="SubtensorModule", storage_function="CommitRevealWeightsVersion" - ) - assert cr_version == 4, f"Commit reveal version is not 3, got {cr_version}" - - # Change the weights rate limit on the subnet - status, error = sudo_set_admin_utils( - substrate=local_chain, - wallet=alice_wallet, - call_function="sudo_set_weights_set_rate_limit", - call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, - ) - - assert status is True - assert error is None - - # Verify weights rate limit was changed - 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 - logging.console.success("sudo_set_weights_set_rate_limit executed: set to 0") - - # Change the tempo of the subnet - tempo_set = 100 if subtensor.is_fast_blocks() else 10 - assert ( - sudo_set_admin_utils( - local_chain, - alice_wallet, - call_function="sudo_set_tempo", - call_params={"netuid": netuid, "tempo": tempo_set}, - )[0] - is True - ) - - tempo = subtensor.get_subnet_hyperparameters(netuid=netuid).tempo - assert tempo_set == tempo, "SN tempos has not been changed." - logging.console.success(f"SN #{netuid} tempo set to {tempo_set}") - - # Commit-reveal values - setting weights to self - uids = np.array([0], dtype=np.int64) - weights = np.array([0.1], dtype=np.float32) - weight_uids, weight_vals = convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) - - # Fetch current block and calculate next tempo for the subnet - current_block = subtensor.get_current_block() - 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}" - ) - - # Lower than this might mean weights will get revealed before we can check them - if upcoming_tempo - current_block < 6: - await wait_interval( - tempo, - subtensor, - netuid=netuid, - reporting_interval=1, - ) - current_block = subtensor.get_current_block() - expected_commit_block = current_block + 1 - latest_drand_round = subtensor.last_drand_round() - upcoming_tempo = next_tempo(current_block, tempo) - logging.console.info( - f"Post first wait_interval (to ensure window isn't too low): {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" - ) - - # Commit weights - success, message = subtensor.set_weights( - wallet=alice_wallet, - netuid=netuid, - uids=weight_uids, - weights=weight_vals, - wait_for_inclusion=True, - wait_for_finalization=True, - block_time=BLOCK_TIME, - period=16, - ) - - # Assert committing was a success - assert success is True, message - assert bool(re.match(r"reveal_round:\d+", message)) - - # Parse expected reveal_round - expected_reveal_round = int(message.split(":")[1]) - logging.console.success( - f"Successfully set weights: uids {weight_uids}, weights {weight_vals}, reveal_round: {expected_reveal_round}" - ) - - # Fetch current commits pending on the chain - commits_on_chain = subtensor.get_current_weight_commit_info_v2(netuid=netuid) - address, commit_block, commit, reveal_round = commits_on_chain[0] - - # Assert correct values are committed on the chain - assert expected_reveal_round == reveal_round - assert address == alice_wallet.hotkey.ss58_address - assert commit_block == expected_commit_block + 1 - - # Ensure no weights are available as of now - assert subtensor.weights(netuid=netuid) == [] - logging.console.success("No weights are available before next epoch.") - - expected_reveal_block = ( - subtensor.subnets.get_next_epoch_start_block(netuid) + 5 - ) # 5 is safety drand offset - logging.console.info( - f"Waiting for the next epoch to ensure weights are revealed: block {expected_reveal_block}" - ) - subtensor.wait_for_block(expected_reveal_block) - - # Fetch the latest drand pulse - latest_drand_round = subtensor.last_drand_round() - logging.console.info( - f"Latest drand round after waiting for tempo: {latest_drand_round}" - ) - - # Fetch weights on the chain as they should be revealed now - subnet_weights = subtensor.weights(netuid=netuid) - - revealed_weights = subnet_weights[0][1] - # Assert correct weights were revealed - assert weight_uids[0] == revealed_weights[0][0] - assert weight_vals[0] == revealed_weights[0][1] - - logging.console.success( - f"Successfully revealed weights: uids {weight_uids}, weights {weight_vals}" - ) - - # Now that the commit has been revealed, there shouldn't be any pending commits - assert subtensor.commitments.get_current_weight_commit_info_v2(netuid=netuid) == [] - - # Ensure the drand_round is always in the positive w.r.t expected when revealed - assert latest_drand_round - expected_reveal_round >= -3, ( - f"latest_drand_round ({latest_drand_round}) is less than expected_reveal_round ({expected_reveal_round})" - ) - - logging.console.success("✅ Passed `test_commit_and_reveal_weights_cr4`") diff --git a/tests/e2e_tests/test_delegate.py b/tests/e2e_tests/test_delegate.py index 9fb4b9dd1a..949b1324be 100644 --- a/tests/e2e_tests/test_delegate.py +++ b/tests/e2e_tests/test_delegate.py @@ -13,7 +13,7 @@ vote, ) from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call -from tests.helpers.helpers import CLOSE_IN_VALUE +from tests.helpers.helpers import CloseInValue DEFAULT_DELEGATE_TAKE = 0.179995422293431 @@ -449,7 +449,7 @@ def test_get_vote_data(subtensor, alice_wallet): assert proposal == ProposalVoteData( ayes=[], - end=CLOSE_IN_VALUE(1_000_000, subtensor.block), + end=CloseInValue(1_000_000, subtensor.block), index=0, nays=[], threshold=3, @@ -475,7 +475,7 @@ def test_get_vote_data(subtensor, alice_wallet): ayes=[ alice_wallet.hotkey.ss58_address, ], - end=CLOSE_IN_VALUE(1_000_000, subtensor.block), + end=CloseInValue(1_000_000, subtensor.block), index=0, nays=[], threshold=3, diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index 01e4e0ea27..8e834eb7f7 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -37,6 +37,18 @@ async def test_incentive(local_chain, subtensor, templates, alice_wallet, bob_wa "Subnet wasn't created successfully" ) + # Disable commit_reveal on the subnet to check proper behavior + status, error = sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + call_params={ + "netuid": alice_subnet_netuid, + "enabled": False, + }, + ) + assert status is True, error + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) # Register Bob as a neuron on the subnet diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 780aa272c8..e037d81fba 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -1,6 +1,6 @@ import pytest -from bittensor import Balance +from bittensor import Balance, logging from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call from bittensor.utils.liquidity import LiquidityPosition @@ -67,16 +67,26 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): assert success, message assert message == "", "❌ Cannot enable user liquidity." - # In non fast-blocks node Alice doesn't have stake - if not subtensor.chain.is_fast_blocks(): - assert subtensor.extrinsics.add_stake( - wallet=alice_wallet, - hotkey_ss58=alice_wallet.hotkey.ss58_address, - netuid=alice_subnet_netuid, - amount=Balance.from_tao(1), - wait_for_inclusion=True, - wait_for_finalization=True, - ), "❌ Cannot cannot add stake to Alice from Alice." + # Add steak to call add_liquidity + assert subtensor.extrinsics.add_stake( + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + amount=Balance.from_tao(1), + wait_for_inclusion=True, + wait_for_finalization=True, + ), "❌ Cannot cannot add stake to Alice from Alice." + + # wait for the next block to give the chain time to update the stake + subtensor.wait_for_block() + + current_balance = subtensor.get_balance(alice_wallet.hotkey.ss58_address) + current_sn_stake = subtensor.staking.get_stake_info_for_coldkey( + coldkey_ss58=alice_wallet.coldkey.ss58_address + ) + logging.console.info( + f"Alice balance: {current_balance} and stake: {current_sn_stake}" + ) # Add liquidity success, message = subtensor.extrinsics.add_liquidity( @@ -184,6 +194,17 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): wait_for_finalization=True, ), "❌ Cannot add stake from Bob to Alice." + # wait for the next block to give the chain time to update the stake + subtensor.wait_for_block() + + current_balance = subtensor.get_balance(alice_wallet.hotkey.ss58_address) + current_sn_stake = subtensor.staking.get_stake_info_for_coldkey( + coldkey_ss58=alice_wallet.coldkey.ss58_address + ) + logging.console.info( + f"Alice balance: {current_balance} and stake: {current_sn_stake}" + ) + # Add second liquidity position success, message = subtensor.extrinsics.add_liquidity( wallet=alice_wallet, diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index 4f2ebae9b0..0e2fc4723c 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -248,7 +248,7 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): target_regs_per_interval=2, max_regs_per_block=1, serving_rate_limit=50, - commit_reveal_weights_enabled=False, + commit_reveal_weights_enabled=True, commit_reveal_period=1, liquid_alpha_enabled=False, alpha_high=0.9000076295109484, @@ -344,7 +344,7 @@ def test_metagraph_info(subtensor, alice_wallet, bob_wallet): target_regs_per_interval=1, max_regs_per_block=1, serving_rate_limit=50, - commit_reveal_weights_enabled=False, + commit_reveal_weights_enabled=True, commit_reveal_period=1, liquid_alpha_enabled=False, alpha_high=0.9000076295109484, diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index 086c570490..b0c21b6c22 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -1,11 +1,15 @@ import pytest -from bittensor.core.errors import ChainError from bittensor import logging from bittensor.core.chain_data.stake_info import StakeInfo +from bittensor.core.errors import ChainError from bittensor.utils.balance import Balance -from tests.e2e_tests.utils.chain_interactions import get_dynamic_balance +from tests.e2e_tests.utils.chain_interactions import ( + get_dynamic_balance, + sudo_set_admin_utils, +) from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call +from tests.helpers.helpers import CloseInValue def test_single_operation(subtensor, alice_wallet, bob_wallet): @@ -277,6 +281,25 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): assert balances == expected_balances + expected_fee_paid = Balance(0) + for netuid in netuids: + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": bob_wallet.hotkey.ss58_address, + "amount_unstaked": Balance.from_tao(100).rao, + "netuid": netuid, + }, + ) + payment_info = subtensor.substrate.get_payment_info( + call, alice_wallet.coldkeypub + ) + fee_alpha = Balance.from_rao(payment_info["partial_fee"]).set_unit(netuid) + dynamic_info = subtensor.subnet(netuid) + fee_tao = dynamic_info.alpha_to_tao(fee_alpha) + expected_fee_paid += fee_tao + success = subtensor.unstake_multiple( alice_wallet, hotkey_ss58s=[bob_wallet.hotkey.ss58_address for _ in netuids], @@ -300,20 +323,17 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): bob_wallet.coldkey.ss58_address, ) - expected_balances = { - alice_wallet.coldkey.ss58_address: get_dynamic_balance( - balances[alice_wallet.coldkey.ss58_address].rao, - ), - bob_wallet.coldkey.ss58_address: Balance.from_tao(999_999.8), - } - - assert balances == expected_balances + assert CloseInValue( # Make sure we are within 0.0001 TAO due to tx fees + balances[bob_wallet.coldkey.ss58_address], Balance.from_rao(100_000) + ) == Balance.from_tao(999_999.7994) assert balances[alice_wallet.coldkey.ss58_address] > alice_balance logging.console.success("✅ Test [green]test_batch_operations[/green] passed") -def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): +def test_safe_staking_scenarios( + local_chain, subtensor, alice_wallet, bob_wallet, eve_wallet +): """ Tests safe staking scenarios with different parameters. @@ -322,44 +342,56 @@ 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 """ - alice_subnet_netuid = subtensor.get_total_subnets() # 2 + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 # Register root as Alice - the subnet owner and validator - assert subtensor.register_subnet(alice_wallet, True, True) + assert subtensor.extrinsics.register_subnet(alice_wallet, True, True) # Verify subnet created successfully - assert subtensor.subnet_exists(alice_subnet_netuid), ( + assert subtensor.subnets.subnet_exists(alice_subnet_netuid), ( "Subnet wasn't created successfully" ) + # Change the tempo of the subnet + TEMPO_TO_SET = 100 if subtensor.chain.is_fast_blocks() else 20 + assert ( + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": alice_subnet_netuid, "tempo": TEMPO_TO_SET}, + )[0] + is True + ) + tempo = subtensor.subnets.get_subnet_hyperparameters( + netuid=alice_subnet_netuid + ).tempo + assert tempo == TEMPO_TO_SET, "SN tempos has not been changed." + logging.console.success(f"SN #{alice_subnet_netuid} tempo set to {TEMPO_TO_SET}") + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) - subtensor.burned_register( - alice_wallet, - netuid=alice_subnet_netuid, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - subtensor.burned_register( - bob_wallet, + subtensor.extrinsics.burned_register( + wallet=bob_wallet, netuid=alice_subnet_netuid, wait_for_inclusion=True, wait_for_finalization=True, ) - initial_stake = subtensor.get_stake( - alice_wallet.coldkey.ss58_address, - bob_wallet.hotkey.ss58_address, + initial_stake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, ) assert initial_stake == Balance(0).set_unit(alice_subnet_netuid) + logging.console.info(f"[orange]Initial stake: {initial_stake}[orange]") # Test Staking Scenarios stake_amount = Balance.from_tao(100) # 1. Strict params - should fail - success = subtensor.add_stake( - alice_wallet, - bob_wallet.hotkey.ss58_address, + success = subtensor.staking.add_stake( + wallet=alice_wallet, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, amount=stake_amount, wait_for_inclusion=True, @@ -370,19 +402,20 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): ) assert success is False - current_stake = subtensor.get_stake( - alice_wallet.coldkey.ss58_address, - bob_wallet.hotkey.ss58_address, + current_stake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, ) assert current_stake == Balance(0).set_unit(alice_subnet_netuid), ( "Stake should not change after failed attempt" ) + logging.console.info(f"[orange]Current stake: {current_stake}[orange]") # 2. Partial allowed - should succeed partially - success = subtensor.add_stake( - alice_wallet, - bob_wallet.hotkey.ss58_address, + success = subtensor.staking.add_stake( + wallet=alice_wallet, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, amount=stake_amount, wait_for_inclusion=True, @@ -393,9 +426,9 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): ) assert success is True - partial_stake = subtensor.get_stake( - alice_wallet.coldkey.ss58_address, - bob_wallet.hotkey.ss58_address, + partial_stake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, ) assert partial_stake > Balance(0).set_unit(alice_subnet_netuid), ( @@ -407,9 +440,9 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): # 3. Higher threshold - should succeed fully amount = Balance.from_tao(100) - success = subtensor.add_stake( - alice_wallet, - bob_wallet.hotkey.ss58_address, + success = subtensor.staking.add_stake( + wallet=alice_wallet, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, amount=amount, wait_for_inclusion=True, @@ -420,17 +453,17 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): ) assert success is True - full_stake = subtensor.get_stake( - alice_wallet.coldkey.ss58_address, - bob_wallet.hotkey.ss58_address, + full_stake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, ) # Test Unstaking Scenarios # 1. Strict params - should fail - success = subtensor.unstake( - alice_wallet, - bob_wallet.hotkey.ss58_address, + success = subtensor.staking.unstake( + wallet=alice_wallet, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, amount=full_stake, wait_for_inclusion=True, @@ -441,9 +474,9 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): ) assert success is False, "Unstake should fail." - current_stake = subtensor.get_stake( - alice_wallet.coldkey.ss58_address, - bob_wallet.hotkey.ss58_address, + current_stake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, ) @@ -455,9 +488,9 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): ) # 2. Partial allowed - should succeed partially - success = subtensor.unstake( - alice_wallet, - bob_wallet.hotkey.ss58_address, + success = subtensor.staking.unstake( + wallet=alice_wallet, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, amount=current_stake, wait_for_inclusion=True, @@ -469,8 +502,8 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): assert success is True partial_unstake = subtensor.get_stake( - alice_wallet.coldkey.ss58_address, - bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, ) logging.console.info(f"[orange]Partial unstake: {partial_unstake}[orange]") @@ -480,8 +513,8 @@ def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet): # 3. Higher threshold - should succeed fully success = subtensor.unstake( - alice_wallet, - bob_wallet.hotkey.ss58_address, + wallet=alice_wallet, + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, amount=partial_unstake, wait_for_inclusion=True, @@ -610,11 +643,12 @@ def test_safe_swap_stake_scenarios(subtensor, alice_wallet, bob_wallet): ) -def test_move_stake(subtensor, alice_wallet, bob_wallet): +def test_move_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): """ Tests: - Adding stake - Moving stake from one hotkey-subnet pair to another + - Testing `move_stake` method with `move_all_stake=True` flag. """ alice_subnet_netuid = subtensor.get_total_subnets() # 2 @@ -625,16 +659,9 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) - subtensor.burned_register( - alice_wallet, - netuid=alice_subnet_netuid, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - assert subtensor.add_stake( - alice_wallet, - alice_wallet.hotkey.ss58_address, + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid, amount=Balance.from_tao(1_000), wait_for_inclusion=True, @@ -664,8 +691,22 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): assert wait_to_start_call(subtensor, bob_wallet, bob_subnet_netuid) + subtensor.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + subtensor.burned_register( + wallet=dave_wallet, + netuid=alice_subnet_netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert subtensor.move_stake( - alice_wallet, + wallet=alice_wallet, origin_hotkey=alice_wallet.hotkey.ss58_address, origin_netuid=alice_subnet_netuid, destination_hotkey=bob_wallet.hotkey.ss58_address, @@ -710,9 +751,59 @@ def test_move_stake(subtensor, alice_wallet, bob_wallet): ) expected_stakes += fast_block_stake - assert stakes == expected_stakes - logging.console.success("✅ Test [green]test_move_stake[/green] passed") + + # test move_stake with move_all_stake=True + dave_stake = subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + ) + logging.console.info(f"[orange]Dave stake before adding: {dave_stake}[orange]") + + assert subtensor.staking.add_stake( + wallet=dave_wallet, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + amount=Balance.from_tao(1000), + wait_for_inclusion=True, + wait_for_finalization=True, + allow_partial_stake=True, + ) + + dave_stake = subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + ) + logging.console.info(f"[orange]Dave stake after adding: {dave_stake}[orange]") + + # let chain to process the transaction + subtensor.wait_for_block( + subtensor.block + subtensor.subnets.tempo(netuid=bob_subnet_netuid) + ) + + assert subtensor.staking.move_stake( + wallet=dave_wallet, + origin_hotkey=dave_wallet.hotkey.ss58_address, + origin_netuid=bob_subnet_netuid, + destination_hotkey=bob_wallet.hotkey.ss58_address, + destination_netuid=bob_subnet_netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + move_all_stake=True, + ) + + dave_stake = subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + ) + logging.console.info(f"[orange]Dave stake after moving all: {dave_stake}[orange]") + + assert dave_stake.rao == CloseInValue(0, 0.00001) + + logging.console.success("✅ Test [green]test_move_stake[/green] passed.") def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): diff --git a/tests/e2e_tests/test_subtensor_functions.py b/tests/e2e_tests/test_subtensor_functions.py index 7d34ce3ce3..6dbd9805ef 100644 --- a/tests/e2e_tests/test_subtensor_functions.py +++ b/tests/e2e_tests/test_subtensor_functions.py @@ -147,16 +147,26 @@ async def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wall # Fetch recycle_amount to register to the subnet recycle_amount = subtensor.recycle(netuid) + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": netuid, + "hotkey": bob_wallet.hotkey.ss58_address, + }, + ) + payment_info = subtensor.substrate.get_payment_info(call, bob_wallet.coldkeypub) + fee = Balance.from_rao(payment_info["partial_fee"]) 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, ( + assert bob_balance - recycle_amount - fee == 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 - ) + # neuron_info_old = subtensor.get_neuron_for_pubkey_and_subnet( + # alice_wallet.hotkey.ss58_address, netuid=netuid + # ) async with templates.validator(alice_wallet, netuid): await asyncio.sleep( diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 1914f69ac4..71692d316b 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -14,13 +14,14 @@ # for typing purposes if TYPE_CHECKING: from bittensor import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor from bittensor.core.subtensor import Subtensor from async_substrate_interface import SubstrateInterface, ExtrinsicReceipt def get_dynamic_balance(rao: int, netuid: int = 0): """Returns a Balance object with the given rao and netuid for testing purposes with dynamic values.""" - return Balance(rao).set_unit(netuid) + return Balance.from_rao(rao).set_unit(netuid) def sudo_set_hyperparameter_bool( @@ -146,6 +147,44 @@ async def wait_interval( ) +async def async_wait_interval( + tempo: int, + subtensor: "AsyncSubtensor", + netuid: int = 1, + reporting_interval: int = 1, + sleep: float = 0.25, + times: int = 1, +): + """ + Waits until the next tempo interval starts for a specific subnet. + + Calculates the next tempo block start based on the current block number + and the provided tempo, then enters a loop where it periodically checks + the current block number until the next tempo interval starts. + """ + current_block = await subtensor.get_current_block() + next_tempo_block_start = current_block + + for _ in range(times): + next_tempo_block_start = next_tempo(next_tempo_block_start, tempo) + + last_reported = None + + while current_block < next_tempo_block_start: + await asyncio.sleep( + sleep, + ) # Wait before checking the block number again + current_block = await subtensor.get_current_block() + if last_reported is None or current_block - last_reported >= reporting_interval: + last_reported = current_block + print( + f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" + ) + logging.info( + f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" + ) + + def execute_and_wait_for_next_nonce( subtensor, wallet, sleep=0.25, timeout=60.0, max_retries=3 ): diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index 624c3a08d7..9097d72d70 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -1,6 +1,6 @@ import os from .helpers import ( # noqa: F401 - CLOSE_IN_VALUE, + CloseInValue, __mock_wallet_factory__, ) from bittensor_wallet.mock.wallet_mock import ( # noqa: F401 diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py index fed6638c52..eccf716811 100644 --- a/tests/helpers/helpers.py +++ b/tests/helpers/helpers.py @@ -34,7 +34,7 @@ def __mock_wallet_factory__(*_, **__) -> _MockWallet: return mock_wallet -class CLOSE_IN_VALUE: +class CloseInValue: value: Union[float, int, Balance] tolerance: Union[float, int, Balance] @@ -53,8 +53,14 @@ def __eq__(self, __o: Union[float, int, Balance]) -> bool: (self.value - self.tolerance) <= __o <= (self.value + self.tolerance) ) or ((__o - self.tolerance) <= self.value <= (__o + self.tolerance)) + def __str__(self) -> str: + return f"CloseInValue" -class ApproxBalance(CLOSE_IN_VALUE, Balance): + def __repr__(self) -> str: + return self.__str__() + + +class ApproxBalance(CloseInValue, Balance): def __init__( self, balance: Union[float, int], diff --git a/tests/unit_tests/extrinsics/asyncex/test_unstaking.py b/tests/unit_tests/extrinsics/asyncex/test_unstaking.py index ed74c76fe4..24857e0261 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_unstaking.py +++ b/tests/unit_tests/extrinsics/asyncex/test_unstaking.py @@ -6,6 +6,9 @@ @pytest.mark.asyncio async def test_unstake_extrinsic(fake_wallet, mocker): + fake_substrate = mocker.AsyncMock( + **{"get_payment_info.return_value": {"partial_fee": 10}} + ) # Preps fake_subtensor = mocker.AsyncMock( **{ @@ -13,6 +16,7 @@ async def test_unstake_extrinsic(fake_wallet, mocker): "get_stake_for_coldkey_and_hotkey.return_value": Balance(10.0), "sign_and_send_extrinsic.return_value": (True, ""), "get_stake.return_value": Balance(10.0), + "substrate": fake_substrate, } ) @@ -108,12 +112,16 @@ async def test_unstake_all_extrinsic(fake_wallet, mocker): async def test_unstake_multiple_extrinsic(fake_wallet, mocker): """Verify that sync `unstake_multiple_extrinsic` method calls proper async method.""" # Preps + fake_substrate = mocker.AsyncMock( + **{"get_payment_info.return_value": {"partial_fee": 10}} + ) fake_subtensor = mocker.AsyncMock( **{ "get_hotkey_owner.return_value": "hotkey_owner", "get_stake_for_coldkey_and_hotkey.return_value": [Balance(10.0)], "sign_and_send_extrinsic.return_value": (True, ""), "tx_rate_limit.return_value": 0, + "substrate": fake_substrate, } ) mocker.patch.object( diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py index 1dfda4e5c1..45b07661d2 100644 --- a/tests/unit_tests/extrinsics/test_registration.py +++ b/tests/unit_tests/extrinsics/test_registration.py @@ -205,6 +205,10 @@ def test_burned_register_extrinsic( mocker, ): # Arrange + mock_substrate_ = mocker.MagicMock( + **{"get_payment_info.return_value": {"partial_fee": 10}} + ) + mocker.patch.object(mock_subtensor, "substrate", mock_substrate_) mocker.patch.object(mock_subtensor, "subnet_exists", return_value=subnet_exists) mocker.patch.object( mock_subtensor, diff --git a/tests/unit_tests/extrinsics/test_unstaking.py b/tests/unit_tests/extrinsics/test_unstaking.py index 04b93111d2..69e020854f 100644 --- a/tests/unit_tests/extrinsics/test_unstaking.py +++ b/tests/unit_tests/extrinsics/test_unstaking.py @@ -3,6 +3,9 @@ def test_unstake_extrinsic(fake_wallet, mocker): + fake_substrate = mocker.Mock( + **{"get_payment_info.return_value": {"partial_fee": 10}} + ) # Preps fake_subtensor = mocker.Mock( **{ @@ -10,6 +13,7 @@ def test_unstake_extrinsic(fake_wallet, mocker): "get_stake_for_coldkey_and_hotkey.return_value": Balance(10.0), "sign_and_send_extrinsic.return_value": (True, ""), "get_stake.return_value": Balance(10.0), + "substrate": fake_substrate, } ) fake_wallet.coldkeypub.ss58_address = "hotkey_owner" @@ -102,12 +106,16 @@ def test_unstake_all_extrinsic(fake_wallet, mocker): def test_unstake_multiple_extrinsic(fake_wallet, mocker): """Verify that sync `unstake_multiple_extrinsic` method calls proper async method.""" # Preps + fake_substrate = mocker.Mock( + **{"get_payment_info.return_value": {"partial_fee": 10}} + ) fake_subtensor = mocker.Mock( **{ "get_hotkey_owner.return_value": "hotkey_owner", "get_stake_for_coldkey_and_hotkey.return_value": [Balance(10.0)], "sign_and_send_extrinsic.return_value": (True, ""), "tx_rate_limit.return_value": 0, + "substrate": fake_substrate, } ) mocker.patch.object( diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 05dbc6f760..e1bf1420dd 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -168,6 +168,7 @@ async def test_burned_register(mock_substrate, subtensor, fake_wallet, mocker): mock_substrate.submit_extrinsic.return_value = mocker.AsyncMock( is_success=mocker.AsyncMock(return_value=True)(), ) + mock_substrate.get_payment_info.return_value = {"partial_fee": 10} mocker.patch.object( subtensor, "get_neuron_for_pubkey_and_subnet", @@ -4119,3 +4120,34 @@ async def test_get_stake_weight(subtensor, mocker): block_hash=mock_determine_block_hash.return_value, ) assert result == expected_result + + +@pytest.mark.asyncio +async def test_get_timelocked_weight_commits(subtensor, mocker): + """Verify that `get_timelocked_weight_commits` method calls proper methods and returns the correct value.""" + # Preps + netuid = mocker.Mock() + + mock_determine_block_hash = mocker.patch.object( + subtensor, + "determine_block_hash", + ) + mocked_query_map = mocker.AsyncMock( + autospec=async_subtensor.AsyncSubstrateInterface.query_map, + ) + subtensor.substrate.query_map = mocked_query_map + + # Call + result = await subtensor.get_timelocked_weight_commits(netuid=netuid) + + # Asserts + mock_determine_block_hash.assert_awaited_once_with( + block=None, block_hash=None, reuse_block=False + ) + mocked_query_map.assert_awaited_once_with( + module="SubtensorModule", + storage_function="TimelockedWeightCommits", + params=[netuid], + block_hash=mock_determine_block_hash.return_value, + ) + assert result == [] diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index aa5121408e..78d9ffeaa9 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -4305,3 +4305,31 @@ def test_get_stake_weight(subtensor, mocker): block_hash=mock_determine_block_hash.return_value, ) assert result == expected_result + + +def test_get_timelocked_weight_commits(subtensor, mocker): + """Verify that `get_timelocked_weight_commits` method calls proper methods and returns the correct value.""" + # Preps + netuid = mocker.Mock() + + mock_determine_block_hash = mocker.patch.object( + subtensor, + "determine_block_hash", + ) + mocked_query_map = mocker.patch.object( + subtensor.substrate, + "query_map", + ) + + # Call + result = subtensor.get_timelocked_weight_commits(netuid=netuid) + + # Asserts + mock_determine_block_hash.assert_called_once_with(block=None) + mocked_query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="TimelockedWeightCommits", + params=[netuid], + block_hash=mock_determine_block_hash.return_value, + ) + assert result == [] diff --git a/tests/unit_tests/test_subtensor_extended.py b/tests/unit_tests/test_subtensor_extended.py index ec6015174b..8869084668 100644 --- a/tests/unit_tests/test_subtensor_extended.py +++ b/tests/unit_tests/test_subtensor_extended.py @@ -168,6 +168,7 @@ def test_bonds(mock_substrate, subtensor, mocker): def test_burned_register(mock_substrate, subtensor, fake_wallet, mocker): + mock_substrate.get_payment_info.return_value = {"partial_fee": 10} mocker.patch.object( subtensor, "get_neuron_for_pubkey_and_subnet", diff --git a/tests/unit_tests/utils/test_balance.py b/tests/unit_tests/utils/test_balance.py index 43531d3721..4ff97bdb81 100644 --- a/tests/unit_tests/utils/test_balance.py +++ b/tests/unit_tests/utils/test_balance.py @@ -7,7 +7,7 @@ from hypothesis import strategies as st from bittensor.utils.balance import Balance -from tests.helpers import CLOSE_IN_VALUE +from tests.helpers import CloseInValue valid_tao_numbers_strategy = st.one_of( @@ -36,7 +36,7 @@ def test_balance_init(balance: Union[int, float]): if isinstance(balance, int): assert balance_.rao == balance elif isinstance(balance, float): - assert balance_.tao == CLOSE_IN_VALUE(balance, 0.00001) + assert balance_.tao == CloseInValue(balance, 0.00001) @given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) @@ -59,7 +59,7 @@ def test_balance_add(balance: Union[int, float], balance2: Union[int, float]): sum_ = balance_ + balance2_ assert isinstance(sum_, Balance) - assert CLOSE_IN_VALUE(sum_.rao, 5) == rao_ + rao2_ + assert CloseInValue(sum_.rao, 5) == rao_ + rao2_ @given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) @@ -82,7 +82,7 @@ def test_balance_add_other_not_balance( sum_ = balance_ + balance2_ assert isinstance(sum_, Balance) - assert CLOSE_IN_VALUE(sum_.rao, 5) == rao_ + rao2_ + assert CloseInValue(sum_.rao, 5) == rao_ + rao2_ @given(balance=valid_tao_numbers_strategy) @@ -95,7 +95,7 @@ def test_balance_eq_other_not_balance(balance: Union[int, float]): # convert balance2 to rao. This assumes balance2 is a rao value rao2_ = int(balance_.rao) - assert CLOSE_IN_VALUE(rao2_, 5) == balance_ + assert CloseInValue(rao2_, 5) == balance_ @given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) @@ -118,7 +118,7 @@ def test_balance_radd_other_not_balance( sum_ = balance2_ + balance_ # This is an radd assert isinstance(sum_, Balance) - assert CLOSE_IN_VALUE(sum_.rao, 5) == rao2_ + rao_ + assert CloseInValue(sum_.rao, 5) == rao2_ + rao_ @given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) @@ -141,7 +141,7 @@ def test_balance_sub(balance: Union[int, float], balance2: Union[int, float]): diff_ = balance_ - balance2_ assert isinstance(diff_, Balance) - assert CLOSE_IN_VALUE(diff_.rao, 5) == rao_ - rao2_ + assert CloseInValue(diff_.rao, 5) == rao_ - rao2_ @given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) @@ -164,7 +164,7 @@ def test_balance_sub_other_not_balance( diff_ = balance_ - balance2_ assert isinstance(diff_, Balance) - assert CLOSE_IN_VALUE(diff_.rao, 5) == rao_ - rao2_ + assert CloseInValue(diff_.rao, 5) == rao_ - rao2_ @given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) @@ -187,7 +187,7 @@ def test_balance_rsub_other_not_balance( diff_ = balance2_ - balance_ # This is an rsub assert isinstance(diff_, Balance) - assert CLOSE_IN_VALUE(diff_.rao, 5) == rao2_ - rao_ + assert CloseInValue(diff_.rao, 5) == rao2_ - rao_ @given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) @@ -373,7 +373,7 @@ def test_balance_floordiv(balance: Union[int, float], balance2: Union[int, float quot_ = balance_ // balance2_ assert isinstance(quot_, Balance) - assert CLOSE_IN_VALUE(quot_.rao, 5) == rao_ // rao2_ + assert CloseInValue(quot_.rao, 5) == rao_ // rao2_ @given(