From ac8142118ae9c82d5bcd8a3d309c7dea9033cf15 Mon Sep 17 00:00:00 2001 From: Arthurdw Date: Thu, 14 Aug 2025 06:40:53 +0200 Subject: [PATCH 01/42] fix: reflect correct return types for get_delegated --- bittensor/core/async_subtensor.py | 4 ++-- bittensor/core/subtensor.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 98542c89a6..f6744121b1 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[DelegateInfo]: """ 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. diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index d1b95521d8..5da59d7780 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["DelegateInfo"]: """ 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. From ad9aa79627e352a414168c202edbd0280696a90e Mon Sep 17 00:00:00 2001 From: Arthurdw Date: Thu, 14 Aug 2025 06:50:00 +0200 Subject: [PATCH 02/42] fix: it is actually delegated info not delegate info --- bittensor/core/async_subtensor.py | 2 +- bittensor/core/subtensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index f6744121b1..d01e1bffa1 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[DelegateInfo]: + ) -> 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. diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 5da59d7780..06cd6af09a 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["DelegateInfo"]: + ) -> 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. From 3edcbb4a72065e1277cd13bae7891d6104d0a3cb Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 11:54:23 +0200 Subject: [PATCH 03/42] Added fee calculation and fixed tests for burned register. --- .../core/extrinsics/asyncex/registration.py | 24 +++++++++++++++++-- bittensor/core/extrinsics/registration.py | 15 +++++++++++- tests/e2e_tests/test_subtensor_functions.py | 18 ++++++++++---- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/registration.py b/bittensor/core/extrinsics/asyncex/registration.py index eba6e9390b..bff9c2d7d4 100644 --- a/bittensor/core/extrinsics/asyncex/registration.py +++ b/bittensor/core/extrinsics/asyncex/registration.py @@ -11,6 +11,7 @@ from typing import Optional, Union, TYPE_CHECKING from bittensor.utils import unlock_key +from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.registration import log_no_torch_error, create_pow_async, torch @@ -92,6 +93,22 @@ async def burned_register_extrinsic( success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for finalization / inclusion, the response is ``True``. """ + + async def get_registration_fee(): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + }, + block_hash=block_hash, + ) + payment_info = await subtensor.substrate.get_payment_info( + call, wallet.coldkeypub + ) + return Balance.from_rao(payment_info["partial_fee"]) + block_hash = await subtensor.substrate.get_chain_head() if not await subtensor.subnet_exists(netuid, block_hash=block_hash): logging.error( @@ -110,12 +127,13 @@ async def burned_register_extrinsic( # We could do this as_completed because we don't need old_balance and recycle # if neuron is null, but the complexity isn't worth it considering the small performance # gains we'd hypothetically receive in this situation - neuron, old_balance, recycle_amount = await asyncio.gather( + neuron, old_balance, recycle_amount, registration_fee = await asyncio.gather( subtensor.get_neuron_for_pubkey_and_subnet( wallet.hotkey.ss58_address, netuid=netuid, block_hash=block_hash ), subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), subtensor.recycle(netuid=netuid, block_hash=block_hash), + get_registration_fee(), ) if not neuron.is_null: @@ -127,7 +145,9 @@ 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}") + logging.info( + f"Recycling {recycle_amount} to register on subnet:{netuid} for fee {registration_fee}" + ) success, err_msg = await _do_burned_register( subtensor=subtensor, diff --git a/bittensor/core/extrinsics/registration.py b/bittensor/core/extrinsics/registration.py index ddf5604544..bda3b34768 100644 --- a/bittensor/core/extrinsics/registration.py +++ b/bittensor/core/extrinsics/registration.py @@ -10,6 +10,7 @@ from typing import Optional, Union, TYPE_CHECKING from bittensor.utils import unlock_key +from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.registration import create_pow, log_no_torch_error, torch @@ -120,8 +121,20 @@ def burned_register_extrinsic( return True recycle_amount = subtensor.recycle(netuid=netuid, block=block) + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + }, + ) + payment_info = subtensor.substrate.get_payment_info(call, wallet.coldkeypub) + fee = Balance.from_rao(payment_info["partial_fee"]) logging.debug(":satellite: [magenta]Recycling TAO for Registration...[/magenta]") - logging.info(f"Recycling {recycle_amount} to register on subnet:{netuid}") + logging.info( + f"Recycling {recycle_amount} to register on subnet:{netuid}, with fee: {fee}" + ) success, err_msg = _do_burned_register( subtensor=subtensor, 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( From 46d7847a7851415100e86db9b3146b26b7a41938 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 12:40:08 +0200 Subject: [PATCH 04/42] Added fee calculation for unstaking. Added TODOs with questions. --- bittensor/core/async_subtensor.py | 2 +- .../core/extrinsics/asyncex/unstaking.py | 29 ++++++++++---- bittensor/core/extrinsics/unstaking.py | 9 +++-- bittensor/core/subtensor.py | 2 +- tests/e2e_tests/test_staking.py | 38 +++++++++++++++---- tests/e2e_tests/utils/chain_interactions.py | 2 +- 6 files changed, 60 insertions(+), 22 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 98542c89a6..6f085a0f7f 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -5548,7 +5548,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/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index f5721cd59f..25c412e79a 100644 --- a/bittensor/core/extrinsics/asyncex/unstaking.py +++ b/bittensor/core/extrinsics/asyncex/unstaking.py @@ -2,6 +2,7 @@ from typing import Optional, TYPE_CHECKING from async_substrate_interface.errors import SubstrateRequestException +from scalecodec import GenericCall from bittensor.core.extrinsics.utils import get_old_stakes from bittensor.utils import unlock_key, format_error_message @@ -53,6 +54,10 @@ async def unstake_extrinsic( - `True` and a success message if the unstake operation succeeded; - `False` and an error message otherwise. """ + async def get_unstaking_fee(call_: GenericCall, netuid_: int): + payment_info = await subtensor.substrate.get_payment_info(call_, wallet.coldkeypub) + return Balance.from_rao(payment_info["partial_fee"]).set_unit(netuid_) + if amount and unstake_all: raise ValueError("Cannot specify both `amount` and `unstake_all`.") @@ -114,14 +119,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 +138,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 +150,8 @@ async def unstake_extrinsic( call_function=call_function, call_params=call_params, ) + fee = await get_unstaking_fee(call, 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, @@ -301,6 +308,11 @@ async def unstake_multiple_extrinsic( - `True` and a success message if the unstake operation succeeded; - `False` and an error message otherwise. """ + + async def get_unstaking_fee(call_: GenericCall, netuid_: int): + payment_info = await subtensor.substrate.get_payment_info(call_, wallet.coldkeypub) + return Balance.from_rao(payment_info["partial_fee"]).set_unit(netuid_) + if amounts and unstake_all: raise ValueError("Cannot specify both `amounts` and `unstake_all`.") @@ -381,10 +393,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 +402,11 @@ async def unstake_multiple_extrinsic( "netuid": netuid, }, ) + fee = await get_unstaking_fee(call, 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/unstaking.py b/bittensor/core/extrinsics/unstaking.py index beff7f1993..bc4b8be8c9 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -372,9 +372,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 +381,12 @@ def unstake_multiple_extrinsic( "netuid": netuid, }, ) + payment_info = subtensor.substrate.get_payment_info(call, wallet.coldkeypub) + fee = Balance.from_rao(payment_info["partial_fee"]).set_unit(netuid) + logging.info( + f"Unstaking [blue]{unstaking_balance}[/blue] from [magenta]{hotkey_ss58}[/magenta]" + f" on [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/subtensor.py b/bittensor/core/subtensor.py index d1b95521d8..74cec3fbd7 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -4368,7 +4368,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/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index 086c570490..d191f3a37b 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -276,6 +276,25 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): } assert balances == expected_balances + # This does not work, because of the changing price between each unstaking operation we perform within + # the `unstake_multiple` extrinsic + # + # 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, @@ -300,14 +319,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 + # We can't actually calculate this correctly because of the slightly changing between each staking operation + # + # performed within `unstake_multiple` extrinsic + # expected_balances = { + # alice_wallet.coldkey.ss58_address: get_dynamic_balance( + # balances[alice_wallet.coldkey.ss58_address].rao, # what does this even do? + # ), + # bob_wallet.coldkey.ss58_address: Balance.from_tao(999_999.8), + # } + # + # assert balances == expected_balances assert balances[alice_wallet.coldkey.ss58_address] > alice_balance logging.console.success("✅ Test [green]test_batch_operations[/green] passed") diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 1914f69ac4..d32a37dfb4 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -20,7 +20,7 @@ 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( From a7b76b57ad5461f1aa8a051adb373f046b760f85 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 12:42:59 +0200 Subject: [PATCH 05/42] Added fee calculation for sync unstaking aswell --- bittensor/core/extrinsics/unstaking.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index bc4b8be8c9..d2a20145af 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 scalecodec import GenericCall from bittensor.core.extrinsics.utils import get_old_stakes from bittensor.utils import unlock_key, format_error_message @@ -52,6 +53,10 @@ def unstake_extrinsic( - `True` and a success message if the unstake operation succeeded; - `False` and an error message otherwise. """ + def get_unstaking_fee(call_: GenericCall): + payment_info = subtensor.substrate.get_payment_info(call_, wallet.coldkeypub) + return Balance.from_rao(payment_info["partial_fee"]).set_unit(netuid) + if amount and unstake_all: raise ValueError("Cannot specify both `amount` and `unstake_all`.") @@ -112,14 +117,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 +136,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 +148,8 @@ def unstake_extrinsic( call_function=call_function, call_params=call_params, ) + fee = get_unstaking_fee(call) + logging.info(f"{logging_info} for fee [blue]{fee}[/blue][magenta]...[/magenta]") success, message = subtensor.sign_and_send_extrinsic( call=call, From 39911156e84dd1332367dac4c5f0d146e0ffbf81 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 12:43:08 +0200 Subject: [PATCH 06/42] Ruff --- bittensor/core/extrinsics/asyncex/unstaking.py | 9 +++++++-- bittensor/core/extrinsics/unstaking.py | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index 25c412e79a..b3d81aaaeb 100644 --- a/bittensor/core/extrinsics/asyncex/unstaking.py +++ b/bittensor/core/extrinsics/asyncex/unstaking.py @@ -54,8 +54,11 @@ async def unstake_extrinsic( - `True` and a success message if the unstake operation succeeded; - `False` and an error message otherwise. """ + async def get_unstaking_fee(call_: GenericCall, netuid_: int): - payment_info = await subtensor.substrate.get_payment_info(call_, wallet.coldkeypub) + payment_info = await subtensor.substrate.get_payment_info( + call_, wallet.coldkeypub + ) return Balance.from_rao(payment_info["partial_fee"]).set_unit(netuid_) if amount and unstake_all: @@ -310,7 +313,9 @@ async def unstake_multiple_extrinsic( """ async def get_unstaking_fee(call_: GenericCall, netuid_: int): - payment_info = await subtensor.substrate.get_payment_info(call_, wallet.coldkeypub) + payment_info = await subtensor.substrate.get_payment_info( + call_, wallet.coldkeypub + ) return Balance.from_rao(payment_info["partial_fee"]).set_unit(netuid_) if amounts and unstake_all: diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index d2a20145af..0d1be2ce88 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -53,6 +53,7 @@ def unstake_extrinsic( - `True` and a success message if the unstake operation succeeded; - `False` and an error message otherwise. """ + def get_unstaking_fee(call_: GenericCall): payment_info = subtensor.substrate.get_payment_info(call_, wallet.coldkeypub) return Balance.from_rao(payment_info["partial_fee"]).set_unit(netuid) From d610bf5cdaf20190d4cdf1ce9e108b96baabbf94 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 13:00:52 +0200 Subject: [PATCH 07/42] Fix unit tests --- tests/unit_tests/extrinsics/asyncex/test_unstaking.py | 8 ++++++++ tests/unit_tests/extrinsics/test_registration.py | 4 ++++ tests/unit_tests/extrinsics/test_unstaking.py | 8 ++++++++ tests/unit_tests/test_async_subtensor.py | 1 + tests/unit_tests/test_subtensor_extended.py | 1 + 5 files changed, 22 insertions(+) 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..8c6a5e9eb9 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", 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", From feecd2bac7fb91fa3ae24b72a0f14ac10b7b9ff3 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 19 Aug 2025 15:54:16 -0700 Subject: [PATCH 08/42] fix `test_commit_and_reveal_weights_cr4` (included non-fast-runtime) --- ...mit_reveal_v3.py => test_commit_reveal.py} | 96 ++++++++++++------- 1 file changed, 62 insertions(+), 34 deletions(-) rename tests/e2e_tests/{test_commit_reveal_v3.py => test_commit_reveal.py} (66%) diff --git a/tests/e2e_tests/test_commit_reveal_v3.py b/tests/e2e_tests/test_commit_reveal.py similarity index 66% rename from tests/e2e_tests/test_commit_reveal_v3.py rename to tests/e2e_tests/test_commit_reveal.py index b729d0a874..d5e5bba88c 100644 --- a/tests/e2e_tests/test_commit_reveal_v3.py +++ b/tests/e2e_tests/test_commit_reveal.py @@ -1,8 +1,8 @@ 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 ( @@ -32,21 +32,26 @@ async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_walle """ 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 + # 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}") - netuid = subtensor.get_total_subnets() # 2 + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 # Register root as Alice - assert subtensor.register_subnet(alice_wallet), "Unable to register the subnet" + assert subtensor.extrinsics.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" + assert subtensor.subnet_exists(alice_subnet_netuid), ( + f"SN #{alice_subnet_netuid} wasn't created successfully" + ) - logging.console.success(f"SN #{netuid} is registered.") + logging.console.success(f"SN #{alice_subnet_netuid} is registered.") # Enable commit_reveal on the subnet assert sudo_set_hyperparameter_bool( @@ -54,11 +59,11 @@ async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_walle 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}" + 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(netuid), ( + assert subtensor.subnets.commit_reveal_enabled(alice_subnet_netuid), ( "Failed to enable commit/reveal" ) logging.console.success("Commit reveal enabled") @@ -73,7 +78,7 @@ async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_walle substrate=local_chain, wallet=alice_wallet, call_function="sudo_set_weights_set_rate_limit", - call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, + call_params={"netuid": alice_subnet_netuid, "weights_set_rate_limit": "0"}, ) assert status is True @@ -81,36 +86,44 @@ async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_walle # Verify weights rate limit was changed assert ( - subtensor.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit == 0 + 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=netuid) == 0 + 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 - 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}, + call_params={"netuid": alice_subnet_netuid, "tempo": TEMPO_TO_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}") + 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.get_current_block() + 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}" @@ -121,21 +134,22 @@ async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_walle await wait_interval( tempo, subtensor, - netuid=netuid, + netuid=alice_subnet_netuid, reporting_interval=1, ) - current_block = subtensor.get_current_block() - expected_commit_block = current_block + 1 - latest_drand_round = subtensor.last_drand_round() + 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.set_weights( + success, message = subtensor.extrinsics.set_weights( wallet=alice_wallet, - netuid=netuid, + netuid=alice_subnet_netuid, uids=weight_uids, weights=weight_vals, wait_for_inclusion=True, @@ -155,34 +169,43 @@ async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_walle ) # Fetch current commits pending on the chain - commits_on_chain = subtensor.get_current_weight_commit_info_v2(netuid=netuid) + commits_on_chain = subtensor.commitments.get_current_weight_commit_info_v2( + 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 - assert commit_block == expected_commit_block + 1 + + # 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=netuid) == [] + 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(netuid) + 5 - ) # 5 is safety drand offset + 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.last_drand_round() + 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.weights(netuid=netuid) + 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 @@ -194,7 +217,12 @@ async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_walle ) # 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) == [] + assert ( + subtensor.commitments.get_current_weight_commit_info_v2( + 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, ( From 5df01b6b19a1ee71e8e60f314041d6b717a5fe06 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 19 Aug 2025 16:40:08 -0700 Subject: [PATCH 09/42] move `get_unstaking_fee` to `utils.py` modules. update extrinsics --- .../core/extrinsics/asyncex/unstaking.py | 25 +++++----------- bittensor/core/extrinsics/asyncex/utils.py | 29 +++++++++++++++++++ bittensor/core/extrinsics/unstaking.py | 21 +++++++------- bittensor/core/extrinsics/utils.py | 23 ++++++++++++++- 4 files changed, 68 insertions(+), 30 deletions(-) create mode 100644 bittensor/core/extrinsics/asyncex/utils.py diff --git a/bittensor/core/extrinsics/asyncex/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index b3d81aaaeb..63354047f3 100644 --- a/bittensor/core/extrinsics/asyncex/unstaking.py +++ b/bittensor/core/extrinsics/asyncex/unstaking.py @@ -2,8 +2,7 @@ from typing import Optional, TYPE_CHECKING from async_substrate_interface.errors import SubstrateRequestException -from scalecodec import GenericCall - +from bittensor.core.extrinsics.asyncex.utils import get_unstaking_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 @@ -54,13 +53,6 @@ async def unstake_extrinsic( - `True` and a success message if the unstake operation succeeded; - `False` and an error message otherwise. """ - - async def get_unstaking_fee(call_: GenericCall, netuid_: int): - payment_info = await subtensor.substrate.get_payment_info( - call_, wallet.coldkeypub - ) - return Balance.from_rao(payment_info["partial_fee"]).set_unit(netuid_) - if amount and unstake_all: raise ValueError("Cannot specify both `amount` and `unstake_all`.") @@ -153,7 +145,9 @@ async def get_unstaking_fee(call_: GenericCall, netuid_: int): call_function=call_function, call_params=call_params, ) - fee = await get_unstaking_fee(call, netuid_=netuid) + fee = await get_unstaking_fee( + subtensor=subtensor, netuid=netuid, call=call, keypair=wallet.coldkeypub + ) logging.info(f"{logging_info} for fee [blue]{fee}[/blue][magenta]...[/magenta]") success, message = await subtensor.sign_and_send_extrinsic( call=call, @@ -311,13 +305,6 @@ async def unstake_multiple_extrinsic( - `True` and a success message if the unstake operation succeeded; - `False` and an error message otherwise. """ - - async def get_unstaking_fee(call_: GenericCall, netuid_: int): - payment_info = await subtensor.substrate.get_payment_info( - call_, wallet.coldkeypub - ) - return Balance.from_rao(payment_info["partial_fee"]).set_unit(netuid_) - if amounts and unstake_all: raise ValueError("Cannot specify both `amounts` and `unstake_all`.") @@ -407,7 +394,9 @@ async def get_unstaking_fee(call_: GenericCall, netuid_: int): "netuid": netuid, }, ) - fee = await get_unstaking_fee(call, netuid) + fee = await get_unstaking_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]" diff --git a/bittensor/core/extrinsics/asyncex/utils.py b/bittensor/core/extrinsics/asyncex/utils.py new file mode 100644 index 0000000000..bcfd82ae46 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/utils.py @@ -0,0 +1,29 @@ +from typing import TYPE_CHECKING + +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_unstaking_fee( + subtensor: "AsyncSubtensor", netuid: int, call: "GenericCall", keypair: "Keypair" +): + """ + Get unstaking 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 unstaking 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) diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index 0d1be2ce88..bd693188ee 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -1,7 +1,7 @@ from typing import Optional, TYPE_CHECKING from async_substrate_interface.errors import SubstrateRequestException -from scalecodec import GenericCall +from bittensor.core.extrinsics.utils import get_unstaking_fee from bittensor.core.extrinsics.utils import get_old_stakes from bittensor.utils import unlock_key, format_error_message @@ -53,11 +53,6 @@ def unstake_extrinsic( - `True` and a success message if the unstake operation succeeded; - `False` and an error message otherwise. """ - - def get_unstaking_fee(call_: GenericCall): - payment_info = subtensor.substrate.get_payment_info(call_, wallet.coldkeypub) - return Balance.from_rao(payment_info["partial_fee"]).set_unit(netuid) - if amount and unstake_all: raise ValueError("Cannot specify both `amount` and `unstake_all`.") @@ -149,7 +144,9 @@ def get_unstaking_fee(call_: GenericCall): call_function=call_function, call_params=call_params, ) - fee = get_unstaking_fee(call) + fee = get_unstaking_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( @@ -389,12 +386,14 @@ def unstake_multiple_extrinsic( "netuid": netuid, }, ) - payment_info = subtensor.substrate.get_payment_info(call, wallet.coldkeypub) - fee = Balance.from_rao(payment_info["partial_fee"]).set_unit(netuid) + fee = get_unstaking_fee( + subtensor=subtensor, netuid=netuid, call=call, keypair=wallet.coldkeypub + ) logging.info( - f"Unstaking [blue]{unstaking_balance}[/blue] from [magenta]{hotkey_ss58}[/magenta]" - f" on [blue]{netuid}[/blue] for fee [blue]{fee}[/blue" + 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..f8235f2bda 100644 --- a/bittensor/core/extrinsics/utils.py +++ b/bittensor/core/extrinsics/utils.py @@ -5,8 +5,10 @@ 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,22 @@ def get_old_stakes( ) for hotkey_ss58, netuid in zip(hotkey_ss58s, netuids) ] + + +def get_unstaking_fee( + subtensor: "Subtensor", netuid: int, call: "GenericCall", keypair: "Keypair" +): + """ + Get unstaking 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 unstaking 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) From 737aa3e36c3be8757b2bef921ae432bccc27e807 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 19 Aug 2025 17:11:13 -0700 Subject: [PATCH 10/42] improve `get_unstaking_fee` --- bittensor/core/extrinsics/asyncex/utils.py | 13 ++++++++----- bittensor/core/extrinsics/utils.py | 19 ++++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/utils.py b/bittensor/core/extrinsics/asyncex/utils.py index bcfd82ae46..4aacfd017a 100644 --- a/bittensor/core/extrinsics/asyncex/utils.py +++ b/bittensor/core/extrinsics/asyncex/utils.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from bittensor.utils.balance import Balance @@ -8,11 +8,14 @@ from bittensor.core.async_subtensor import AsyncSubtensor -async def get_unstaking_fee( - subtensor: "AsyncSubtensor", netuid: int, call: "GenericCall", keypair: "Keypair" +async def get_extrinsic_fee( + subtensor: "AsyncSubtensor", + call: "GenericCall", + keypair: "Keypair", + netuid: Optional[int] = None, ): """ - Get unstaking fee for a given extrinsic call and keypair for a given SN's netuid. + Get extrinsic fee for a given extrinsic call and keypair for a given SN's netuid. Arguments: subtensor: The Subtensor instance. @@ -21,7 +24,7 @@ async def get_unstaking_fee( keypair: The keypair associated with the extrinsic. Returns: - Balance object representing the unstaking fee in RAO. + Balance object representing the extrinsic fee in RAO. """ payment_info = await subtensor.substrate.get_payment_info( call=call, keypair=keypair diff --git a/bittensor/core/extrinsics/utils.py b/bittensor/core/extrinsics/utils.py index f8235f2bda..5092791f66 100644 --- a/bittensor/core/extrinsics/utils.py +++ b/bittensor/core/extrinsics/utils.py @@ -1,6 +1,6 @@ """Module with helper functions for extrinsics.""" -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from bittensor.utils.balance import Balance @@ -46,20 +46,25 @@ def get_old_stakes( ] -def get_unstaking_fee( - subtensor: "Subtensor", netuid: int, call: "GenericCall", keypair: "Keypair" +def get_extrinsic_fee( + call: "GenericCall", + keypair: "Keypair", + subtensor: "Subtensor", + netuid: Optional[int] = None, ): """ - Get unstaking fee for a given extrinsic call and keypair for a given SN's netuid. + 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. + netuid: The SN's netuid. Returns: - Balance object representing the unstaking fee in RAO. + 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) + return Balance.from_rao(amount=payment_info["partial_fee"]).set_unit( + netuid=netuid or 0 + ) From ef3f95a9f7af522684b7fe763666b1bfd7dd7faf Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 19 Aug 2025 17:11:32 -0700 Subject: [PATCH 11/42] improve `unstaking` extrinsics logging --- bittensor/core/extrinsics/asyncex/unstaking.py | 10 +++++----- bittensor/core/extrinsics/unstaking.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index 63354047f3..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_unstaking_fee +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 @@ -145,8 +145,8 @@ async def unstake_extrinsic( call_function=call_function, call_params=call_params, ) - fee = await get_unstaking_fee( - subtensor=subtensor, netuid=netuid, call=call, keypair=wallet.coldkeypub + 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( @@ -394,8 +394,8 @@ async def unstake_multiple_extrinsic( "netuid": netuid, }, ) - fee = await get_unstaking_fee( - subtensor=subtensor, netuid=netuid, call=call, keypair=wallet.coldkeypub + 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: " diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index bd693188ee..8f0ce3329d 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -1,7 +1,7 @@ from typing import Optional, TYPE_CHECKING from async_substrate_interface.errors import SubstrateRequestException -from bittensor.core.extrinsics.utils import get_unstaking_fee +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 @@ -144,7 +144,7 @@ def unstake_extrinsic( call_function=call_function, call_params=call_params, ) - fee = get_unstaking_fee( + 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]") @@ -386,7 +386,7 @@ def unstake_multiple_extrinsic( "netuid": netuid, }, ) - fee = get_unstaking_fee( + fee = get_extrinsic_fee( subtensor=subtensor, netuid=netuid, call=call, keypair=wallet.coldkeypub ) logging.info( From ac0f9c54a57ab2892f86e006ffcbc91f9e575691 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 19 Aug 2025 17:11:50 -0700 Subject: [PATCH 12/42] improve `_do_burned_register` extrinsics calls --- .../core/extrinsics/asyncex/registration.py | 30 +++++-------------- bittensor/core/extrinsics/registration.py | 21 +++++-------- 2 files changed, 15 insertions(+), 36 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/registration.py b/bittensor/core/extrinsics/asyncex/registration.py index bff9c2d7d4..d8ac53c499 100644 --- a/bittensor/core/extrinsics/asyncex/registration.py +++ b/bittensor/core/extrinsics/asyncex/registration.py @@ -10,8 +10,8 @@ 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.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.registration import log_no_torch_error, create_pow_async, torch @@ -58,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, @@ -93,22 +99,6 @@ async def burned_register_extrinsic( success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for finalization / inclusion, the response is ``True``. """ - - async def get_registration_fee(): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="burned_register", - call_params={ - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - }, - block_hash=block_hash, - ) - payment_info = await subtensor.substrate.get_payment_info( - call, wallet.coldkeypub - ) - return Balance.from_rao(payment_info["partial_fee"]) - block_hash = await subtensor.substrate.get_chain_head() if not await subtensor.subnet_exists(netuid, block_hash=block_hash): logging.error( @@ -127,13 +117,12 @@ async def get_registration_fee(): # We could do this as_completed because we don't need old_balance and recycle # if neuron is null, but the complexity isn't worth it considering the small performance # gains we'd hypothetically receive in this situation - neuron, old_balance, recycle_amount, registration_fee = await asyncio.gather( + neuron, old_balance, recycle_amount = await asyncio.gather( subtensor.get_neuron_for_pubkey_and_subnet( wallet.hotkey.ss58_address, netuid=netuid, block_hash=block_hash ), subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), subtensor.recycle(netuid=netuid, block_hash=block_hash), - get_registration_fee(), ) if not neuron.is_null: @@ -145,9 +134,6 @@ async def get_registration_fee(): return True logging.debug(":satellite: [magenta]Recycling TAO for Registration...[/magenta]") - logging.info( - f"Recycling {recycle_amount} to register on subnet:{netuid} for fee {registration_fee}" - ) success, err_msg = await _do_burned_register( subtensor=subtensor, diff --git a/bittensor/core/extrinsics/registration.py b/bittensor/core/extrinsics/registration.py index bda3b34768..8c090c3065 100644 --- a/bittensor/core/extrinsics/registration.py +++ b/bittensor/core/extrinsics/registration.py @@ -9,8 +9,8 @@ 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.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.registration import create_pow, log_no_torch_error, torch @@ -57,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, @@ -121,20 +125,8 @@ def burned_register_extrinsic( return True recycle_amount = subtensor.recycle(netuid=netuid, block=block) - call = subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="burned_register", - call_params={ - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - }, - ) - payment_info = subtensor.substrate.get_payment_info(call, wallet.coldkeypub) - fee = Balance.from_rao(payment_info["partial_fee"]) logging.debug(":satellite: [magenta]Recycling TAO for Registration...[/magenta]") - logging.info( - f"Recycling {recycle_amount} to register on subnet:{netuid}, with fee: {fee}" - ) + logging.info(f"Recycling {recycle_amount} to register on subnet:{netuid}") success, err_msg = _do_burned_register( subtensor=subtensor, @@ -210,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, From 7c1c54330300efd1e71b3b2f20adf1d829456e9d Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 19 Aug 2025 17:17:58 -0700 Subject: [PATCH 13/42] opps, fix async `get_extrinsic_fee` --- bittensor/core/extrinsics/asyncex/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bittensor/core/extrinsics/asyncex/utils.py b/bittensor/core/extrinsics/asyncex/utils.py index 4aacfd017a..7c756e2499 100644 --- a/bittensor/core/extrinsics/asyncex/utils.py +++ b/bittensor/core/extrinsics/asyncex/utils.py @@ -29,4 +29,6 @@ async def get_extrinsic_fee( 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) + return Balance.from_rao(amount=payment_info["partial_fee"]).set_unit( + netuid=netuid or 0 + ) From 101d71663f36c5094141f9e3ae880a6f4ae65237 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 19 Aug 2025 18:13:29 -0700 Subject: [PATCH 14/42] try to fix `tests/e2e_tests/test_liquidity.py` --- tests/e2e_tests/test_liquidity.py | 41 +++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 780aa272c8..9c333d9bad 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 @@ -68,15 +68,25 @@ async def test_liquidity(local_chain, subtensor, alice_wallet, bob_wallet): 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." + 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, From f670b1d65894ff3ed02946a38baa7c30245b421f Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 19 Aug 2025 18:25:06 -0700 Subject: [PATCH 15/42] fix comment --- tests/e2e_tests/test_liquidity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 9c333d9bad..e037d81fba 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -67,7 +67,7 @@ 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 + # Add steak to call add_liquidity assert subtensor.extrinsics.add_stake( wallet=alice_wallet, hotkey_ss58=alice_wallet.hotkey.ss58_address, From bfe5a0e9433a5453d3ce264c8bb2085e6537a6f6 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 20 Aug 2025 09:54:16 -0700 Subject: [PATCH 16/42] use `CLOSE_IN_VALUE` in `test_batch_operations` --- tests/e2e_tests/test_staking.py | 55 ++++++++++++++------------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index d191f3a37b..b201805d55 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -1,11 +1,12 @@ 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.e2e_test_utils import wait_to_start_call +from tests.helpers.helpers import CLOSE_IN_VALUE def test_single_operation(subtensor, alice_wallet, bob_wallet): @@ -276,25 +277,25 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): } assert balances == expected_balances - # This does not work, because of the changing price between each unstaking operation we perform within - # the `unstake_multiple` extrinsic - # - # 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 + + 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, @@ -319,17 +320,9 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): bob_wallet.coldkey.ss58_address, ) - # We can't actually calculate this correctly because of the slightly changing between each staking operation - # - # performed within `unstake_multiple` extrinsic - # expected_balances = { - # alice_wallet.coldkey.ss58_address: get_dynamic_balance( - # balances[alice_wallet.coldkey.ss58_address].rao, # what does this even do? - # ), - # bob_wallet.coldkey.ss58_address: Balance.from_tao(999_999.8), - # } - # - # assert balances == expected_balances + assert CLOSE_IN_VALUE( # 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") From 9a59981529609f74e6f6161adc008990600cc3a0 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 20 Aug 2025 19:39:33 -0700 Subject: [PATCH 17/42] fix bug in `bittensor.core.async_subtensor.AsyncSubtensor.get_next_epoch_start_block` --- bittensor/core/async_subtensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 6f085a0f7f..7eff4115f2 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -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 From 960502840126cd636e012285330eb421b7612386 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 20 Aug 2025 19:40:41 -0700 Subject: [PATCH 18/42] add `tests.e2e_tests.utils.chain_interactions.async_wait_interval` for async subtensor --- tests/e2e_tests/utils/chain_interactions.py | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index d32a37dfb4..71692d316b 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -14,6 +14,7 @@ # 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 @@ -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 ): From a143cffad21ce61762bdc3aea16d8b02c5303f08 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 20 Aug 2025 19:40:59 -0700 Subject: [PATCH 19/42] add `tests.e2e_tests.test_commit_reveal.test_async_commit_and_reveal_weights_cr4` --- tests/e2e_tests/test_commit_reveal.py | 232 ++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) diff --git a/tests/e2e_tests/test_commit_reveal.py b/tests/e2e_tests/test_commit_reveal.py index d5e5bba88c..d4088ed0f5 100644 --- a/tests/e2e_tests/test_commit_reveal.py +++ b/tests/e2e_tests/test_commit_reveal.py @@ -6,6 +6,7 @@ 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, @@ -230,3 +231,234 @@ async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_walle ) 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_current_weight_commit_info_v2( + 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_current_weight_commit_info_v2( + 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`") From 95d6f6869cb194816fec833e36e9e2bc1627bf45 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 21 Aug 2025 10:24:06 -0700 Subject: [PATCH 20/42] add `get_timelocked_weight_commits` method --- bittensor/core/async_subtensor.py | 39 +++++++++++++++++++++++++++++++ bittensor/core/subtensor.py | 29 +++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 7eff4115f2..894a6310ae 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -2748,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, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 74cec3fbd7..5158a40b7e 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -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, From 30fdd80fcb5ce40a223f0de52888e46d4f02580f Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 21 Aug 2025 10:24:31 -0700 Subject: [PATCH 21/42] update SubtensorApi --- bittensor/core/subtensor_api/commitments.py | 1 + bittensor/core/subtensor_api/utils.py | 3 +++ 2 files changed, 4 insertions(+) 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/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 From b7a5e4032967c06d4a57189bb46e8d45c007c84a Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 21 Aug 2025 10:24:44 -0700 Subject: [PATCH 22/42] add unit tests --- tests/unit_tests/test_async_subtensor.py | 31 ++++++++++++++++++++++++ tests/unit_tests/test_subtensor.py | 28 +++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 8c6a5e9eb9..e1bf1420dd 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -4120,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 == [] From f4603a5ae36b5a2eb63ec768120d252859293933 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 21 Aug 2025 10:24:52 -0700 Subject: [PATCH 23/42] update e2e test --- tests/e2e_tests/test_commit_reveal.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/e2e_tests/test_commit_reveal.py b/tests/e2e_tests/test_commit_reveal.py index d4088ed0f5..2531326583 100644 --- a/tests/e2e_tests/test_commit_reveal.py +++ b/tests/e2e_tests/test_commit_reveal.py @@ -170,7 +170,7 @@ async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_walle ) # Fetch current commits pending on the chain - commits_on_chain = subtensor.commitments.get_current_weight_commit_info_v2( + commits_on_chain = subtensor.commitments.get_timelocked_weight_commits( netuid=alice_subnet_netuid ) address, commit_block, commit, reveal_round = commits_on_chain[0] @@ -219,9 +219,7 @@ async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_walle # Now that the commit has been revealed, there shouldn't be any pending commits assert ( - subtensor.commitments.get_current_weight_commit_info_v2( - netuid=alice_subnet_netuid - ) + subtensor.commitments.get_timelocked_weight_commits(netuid=alice_subnet_netuid) == [] ) @@ -395,7 +393,7 @@ async def test_async_commit_and_reveal_weights_cr4( # Fetch current commits pending on the chain commits_on_chain = ( - await async_subtensor.commitments.get_current_weight_commit_info_v2( + await async_subtensor.commitments.get_timelocked_weight_commits( netuid=alice_subnet_netuid ) ) @@ -450,7 +448,7 @@ async def test_async_commit_and_reveal_weights_cr4( # Now that the commit has been revealed, there shouldn't be any pending commits assert ( - await async_subtensor.commitments.get_current_weight_commit_info_v2( + await async_subtensor.commitments.get_timelocked_weight_commits( netuid=alice_subnet_netuid ) == [] From c50bea74a07a1c4ff1bce9ac234d88348b0f6c34 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 22 Aug 2025 17:28:03 -0700 Subject: [PATCH 24/42] increase tempo, readability --- tests/e2e_tests/test_staking.py | 112 ++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 47 deletions(-) diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index b201805d55..0e18ff7b08 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -4,7 +4,10 @@ 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 CLOSE_IN_VALUE @@ -328,7 +331,9 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): 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. @@ -337,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, @@ -385,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, @@ -408,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), ( @@ -422,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, @@ -435,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, @@ -456,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, ) @@ -470,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, @@ -484,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]") @@ -495,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, From bf979e8f3767797e78e8afc8b1ffeffbddf44eba Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 11:27:45 -0700 Subject: [PATCH 25/42] fix `test_incentive` --- tests/e2e_tests/test_incentive.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 From 7ab832f44059c3904b69ab4946f5277decac2efc Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 11:28:08 -0700 Subject: [PATCH 26/42] fix `test_metagraph` --- tests/e2e_tests/test_metagraph.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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, From 1505949218d9c22d65c674d2313b9b9818f6566a Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 12:59:40 -0700 Subject: [PATCH 27/42] split test cases to each matrix item --- .github/workflows/e2e-subtensor-tests.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 265b358a84..e79e46e167 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -33,10 +33,24 @@ jobs: - name: Check-out repository under $GITHUB_WORKSPACE uses: actions/checkout@v4 + - 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(. != ""))') + test_files=$(pytest -q --collect-only tests/e2e_tests \ + | sed -n '/^e2e_tests\//p' \ + | sed 's|^|tests/|' \ + | 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" From 74bc8a229625ccd90eeaae6060a07c4797920b81 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 13:07:57 -0700 Subject: [PATCH 28/42] quote matrix test-file in pytest command --- .github/workflows/e2e-subtensor-tests.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index e79e46e167..4ac9db11ed 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -59,6 +59,7 @@ jobs: # Pull docker image pull-docker-image: + needs: find-tests runs-on: ubuntu-latest outputs: image-name: ${{ steps.set-image.outputs.image }} @@ -168,7 +169,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-file }}" -s; then echo "✅ Tests passed on attempt $i" echo "::endgroup::" exit 0 From baf62396574a4d6b86873e8a4d42de8906901dd5 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 13:27:04 -0700 Subject: [PATCH 29/42] try apply labels and increase `max-parallel` --- .github/workflows/e2e-subtensor-tests.yaml | 28 ++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 4ac9db11ed..692e91552d 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -45,17 +45,21 @@ jobs: - name: Find test files id: get-tests + shell: bash run: | - test_files=$(find tests/e2e_tests -name "test*.py" | jq -R -s -c 'split("\n") | map(select(. != ""))') - test_files=$(pytest -q --collect-only tests/e2e_tests \ + 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(. != ""))') - # 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 + | 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: @@ -126,7 +130,7 @@ jobs: # Job to run tests in parallel run-fast-blocks-e2e-test: - name: "FB: ${{ matrix.test-file }} / Python ${{ matrix.python-version }}" + name: "FB: ${{ matrix.label }} / Python ${{ matrix.python-version }}" # ← было matrix.test-file needs: - find-tests - pull-docker-image @@ -134,11 +138,11 @@ 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) }} + include: ${{ fromJson(needs.find-tests.outputs.test-files) }} python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - name: Check-out repository @@ -169,7 +173,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.nodeid }}" -s; then echo "✅ Tests passed on attempt $i" echo "::endgroup::" exit 0 From 21efc5be1ba244f127c480f5898208e6551280d5 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 13:40:46 -0700 Subject: [PATCH 30/42] include -> test + refer --- .github/workflows/e2e-subtensor-tests.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 692e91552d..859b313484 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -130,7 +130,7 @@ jobs: # Job to run tests in parallel run-fast-blocks-e2e-test: - name: "FB: ${{ matrix.label }} / Python ${{ matrix.python-version }}" # ← было matrix.test-file + name: "FB: ${{ matrix.test.label }} / Python ${{ matrix.python-version }}" needs: - find-tests - pull-docker-image @@ -142,7 +142,7 @@ jobs: matrix: os: - ubuntu-latest - include: ${{ 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 @@ -173,7 +173,7 @@ jobs: run: | for i in 1 2 3; do echo "::group::🔁 Test attempt $i" - if uv run pytest "${{ matrix.nodeid }}" -s; then + if uv run pytest "${{ matrix.test.nodeid }}" -s; then echo "✅ Tests passed on attempt $i" echo "::endgroup::" exit 0 From fc93bbca4702a100ef92237b476f92c771b26a07 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 13:46:42 -0700 Subject: [PATCH 31/42] make test name shorter --- .github/workflows/e2e-subtensor-tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index 859b313484..d56e9a726d 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -130,7 +130,7 @@ jobs: # Job to run tests in parallel run-fast-blocks-e2e-test: - name: "FB: ${{ matrix.test.label }} / Python ${{ matrix.python-version }}" + name: "${{ matrix.test.label }} / Py ${{ matrix.python-version }}" needs: - find-tests - pull-docker-image From 9aa4b450d122997277b2c3801b4f4e488d05b350 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 18:20:02 -0700 Subject: [PATCH 32/42] Use CamelCase in the name `CLOSE_IN_VALUE` --- tests/helpers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 2b1789ef8546571413fc455ac699d82ef5ff6221 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 18:20:37 -0700 Subject: [PATCH 33/42] add `move_all_stake` parameter to `move_stake_extrinsic` extrinsic --- .../core/extrinsics/asyncex/move_stake.py | 39 ++++++++++------ bittensor/core/extrinsics/move_stake.py | 45 ++++++++++++------- 2 files changed, 53 insertions(+), 31 deletions(-) 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/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" From c4c93d59a035687ed917ff3df4c777be02dd2621 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 18:21:01 -0700 Subject: [PATCH 34/42] update `move_stake` methods in Subtensor class --- bittensor/core/async_subtensor.py | 5 ++++- bittensor/core/subtensor.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 6665f9d9e4..e40b1f4465 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -4657,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. @@ -4677,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. @@ -4693,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( diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 177ce99e09..28371fb7d6 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -3493,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. @@ -3513,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. @@ -3529,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( From f4526990ffed45593d9261e3043837624ce24dac Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 18:21:20 -0700 Subject: [PATCH 35/42] update SubtensorApi --- bittensor/core/subtensor_api/staking.py | 1 + 1 file changed, 1 insertion(+) 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 From e1826b2cd506c38cab6773cfcabdea927cc8caea Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 18:21:36 -0700 Subject: [PATCH 36/42] update helper --- tests/helpers/helpers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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], From 9a3cf94e5217d607675aaa0454ff1025d254e481 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 18:21:56 -0700 Subject: [PATCH 37/42] refactoring --- tests/e2e_tests/test_delegate.py | 6 +++--- tests/unit_tests/utils/test_balance.py | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) 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/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( From c9415a5caf2ef09da1d2c7d56c3e5e2e4f4b0426 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 25 Aug 2025 18:22:46 -0700 Subject: [PATCH 38/42] extend e2e test to check `move_stake` method behavior with `move_all_stake=True` argument --- tests/e2e_tests/test_staking.py | 88 +++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 15 deletions(-) diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py index 0e18ff7b08..b0c21b6c22 100644 --- a/tests/e2e_tests/test_staking.py +++ b/tests/e2e_tests/test_staking.py @@ -9,7 +9,7 @@ sudo_set_admin_utils, ) 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 def test_single_operation(subtensor, alice_wallet, bob_wallet): @@ -323,7 +323,7 @@ def test_batch_operations(subtensor, alice_wallet, bob_wallet): bob_wallet.coldkey.ss58_address, ) - assert CLOSE_IN_VALUE( # Make sure we are within 0.0001 TAO due to tx fees + 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) @@ -643,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 @@ -658,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, @@ -697,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, @@ -743,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): From 1d108502420a4b7bb36c7374b2bda0e7f46b9169 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 28 Aug 2025 14:04:32 -0700 Subject: [PATCH 39/42] improve deps and version --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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] From c47fa432e5a9fa4b8201d723e11b5540e0128b0a Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 28 Aug 2025 14:11:17 -0700 Subject: [PATCH 40/42] CHANGELOG.md --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 From d9c6e16d90c654034d4062e557447279892bd669 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 28 Aug 2025 15:32:55 -0700 Subject: [PATCH 41/42] `actions/checkout@v4` should take all commits history --- .github/workflows/e2e-subtensor-tests.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index d56e9a726d..f2bbc542b8 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -32,6 +32,8 @@ jobs: steps: - name: Check-out repository under $GITHUB_WORKSPACE uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 @@ -147,6 +149,8 @@ jobs: steps: - name: Check-out repository uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 From 42d593d253646aeea99f86fedc1f07e8044172cb Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 28 Aug 2025 15:54:48 -0700 Subject: [PATCH 42/42] Checkout PR head instead of merge --- .github/workflows/e2e-subtensor-tests.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml index f2bbc542b8..e88eb588f2 100644 --- a/.github/workflows/e2e-subtensor-tests.yaml +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -34,6 +34,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Python uses: actions/setup-python@v5 @@ -151,6 +152,7 @@ jobs: 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