diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index ca4b9c53b9..669d2fc326 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -97,7 +97,6 @@ ) from bittensor.core.extrinsics.asyncex.registration import ( burned_register_extrinsic, - register_extrinsic, register_limit_extrinsic, register_subnet_extrinsic, set_subnet_identity_extrinsic, @@ -7818,14 +7817,7 @@ async def register( self: "AsyncSubtensor", wallet: "Wallet", netuid: int, - max_allowed_attempts: int = 3, - output_in_place: bool = False, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, + limit_price: Optional[Balance] = None, *, mev_protection: bool = DEFAULT_MEV_PROTECTION, period: Optional[int] = DEFAULT_PERIOD, @@ -7834,63 +7826,71 @@ async def register( wait_for_finalization: bool = True, wait_for_revealed_execution: bool = True, ) -> ExtrinsicResponse: - """ - Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. + """Registers a neuron on the Bittensor network by recycling TAO, with automatic price protection. + + Uses ``register_limit`` under the hood. If ``limit_price`` is not provided, it is automatically + calculated as the current recycle (burn) cost plus a 0.5% tolerance to protect against price fluctuations. - Registration is a critical step for a neuron to become an active participant in the network, enabling it to - stake, set weights, and receive incentives. + For root subnet (``netuid == 0``), delegates to ``root_register_extrinsic``. Parameters: wallet: The wallet associated with the neuron to be registered. netuid: The unique identifier of the subnet. - max_allowed_attempts: Maximum number of attempts to register the wallet. - output_in_place: If `True`, prints the progress of the proof of work to the console in-place. Meaning the - progress is printed on the same lines. - cuda: If `true`, the wallet should be registered using CUDA device(s). - dev_id: The CUDA device id to use, or a list of device ids. - tpb: The number of threads per block (CUDA). - num_processes: The number of processes to use to register. - update_interval: The number of nonces to solve between updates. - log_verbose: If `true`, the registration process will log more information. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + limit_price: Maximum acceptable burn price as a Balance instance. If ``None``, automatically calculated + as ``recycle * 1.005`` (0.5% tolerance). If the on-chain burn price exceeds this value, the + transaction will fail with RegistrationPriceLimitExceeded. + mev_protection: If ``True``, encrypts and submits the transaction through the MEV Shield pallet to protect against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. + decrypt and execute it. If ``False``, submits the transaction directly without encryption. 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. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. + raise_error: Raises a relevant exception rather than returning ``False`` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. Returns: ExtrinsicResponse: The result object of the extrinsic execution. - This function facilitates the entry of new neurons into the network, supporting the decentralized growth and - scalability of the Bittensor ecosystem. - Notes: - Rate Limits: """ - return await register_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - max_allowed_attempts=max_allowed_attempts, - tpb=tpb, - update_interval=update_interval, - num_processes=num_processes, - cuda=cuda, - dev_id=dev_id, - output_in_place=output_in_place, - log_verbose=log_verbose, - mev_protection=mev_protection, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) + async with self: + if netuid == 0: + return await root_register_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + if limit_price is not None: + check_balance_amount(limit_price) + else: + recycle = await self.recycle(netuid=netuid) + if recycle is None: + return ExtrinsicResponse( + False, f"Subnet {netuid} does not exist." + ).with_log() + limit_price = Balance.from_rao(recycle.rao * 1005 // 1000) + + return await register_limit_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + limit_price=limit_price, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) async def register_limit( self, @@ -7950,7 +7950,7 @@ async def register_limit( ) async def register_subnet( - self: "AsyncSubtensor", + self, wallet: "Wallet", *, mev_protection: bool = DEFAULT_MEV_PROTECTION, diff --git a/bittensor/core/extrinsics/asyncex/registration.py b/bittensor/core/extrinsics/asyncex/registration.py index 5b4897fbf9..3457fd915a 100644 --- a/bittensor/core/extrinsics/asyncex/registration.py +++ b/bittensor/core/extrinsics/asyncex/registration.py @@ -1,9 +1,9 @@ """ -This module provides async functionalities for registering a wallet with the subtensor network using Proof-of-Work (PoW). +This module provides async functionalities for registering a wallet with the subtensor network. """ import asyncio -from typing import Optional, Union, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from bittensor.core.errors import BalanceTypeError, RegistrationError from bittensor.core.extrinsics.asyncex.mev_shield import submit_encrypted_extrinsic @@ -12,7 +12,6 @@ from bittensor.core.types import ExtrinsicResponse from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging -from bittensor.utils.registration import create_pow_async, log_no_torch_error, torch if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -392,212 +391,6 @@ async def register_subnet_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) -async def register_extrinsic( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - netuid: int, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = None, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, -) -> ExtrinsicResponse: - """Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. - - Registration is a critical step for a neuron to become an active participant in the network, enabling it to stake, - set weights, and receive incentives. - - Parameters: - subtensor: Subtensor object to use for chain interactions - wallet: Bittensor wallet object. - netuid: The ``netuid`` of the subnet to register on. - max_allowed_attempts: Maximum number of attempts to register the wallet. - output_in_place: Whether the POW solving should be outputted to the console as it goes along. - cuda: If `True`, the wallet should be registered using CUDA device(s). - dev_id: The CUDA device id to use, or a list of device ids. - tpb: The number of threads per block (CUDA). - num_processes: The number of processes to use to register. - update_interval: The number of nonces to solve between updates. - log_verbose: If `True`, the registration process will log more information. - mev_protection: If True, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If False, submits the transaction directly without encryption. - 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. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - """ - try: - if not ( - unlocked := ExtrinsicResponse.unlock_wallet( - wallet, raise_error, unlock_type="both" - ) - ).success: - return unlocked - - block_hash = await subtensor.substrate.get_chain_head() - if not await subtensor.subnet_exists(netuid, block_hash=block_hash): - return ExtrinsicResponse( - False, f"Subnet {netuid} does not exist." - ).with_log() - - neuron = await subtensor.get_neuron_for_pubkey_and_subnet( - hotkey_ss58=wallet.hotkey.ss58_address, netuid=netuid, block_hash=block_hash - ) - - if not neuron.is_null: - message = "Already registered." - logging.debug(f"[green]{message}[/green]") - logging.debug(f"\t\tuid: [blue]{neuron.uid}[/blue]") - logging.debug(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") - logging.debug(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") - logging.debug(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") - return ExtrinsicResponse(message=message, data={"neuron": neuron}) - - logging.debug( - f"Registration hotkey: [blue]{wallet.hotkey.ss58_address}[/blue], Public coldkey: " - f"[blue]{wallet.coldkey.ss58_address}[/blue] in the network: [blue]{subtensor.network}[/blue]." - ) - - if not torch: - log_no_torch_error() - return ExtrinsicResponse(False, "Torch is not installed.").with_log() - - # Attempt rolling registration. - attempts = 1 - - while True: - # Solve latest POW. - if cuda: - if not torch.cuda.is_available(): - return ExtrinsicResponse(False, "CUDA not available.").with_log() - - logging.debug("Creating a POW with CUDA.") - pow_result = await create_pow_async( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - logging.debug("Creating a POW.") - pow_result = await create_pow_async( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - cuda=cuda, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - # pow failed - if not pow_result: - # might be registered already on this subnet - is_registered = await subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - message = f"Already registered in subnet {netuid}." - logging.debug(f"[green]{message}[/green]") - return ExtrinsicResponse(message=message) - - # pow successful, proceed to submit pow to chain for registration - else: - # check if a pow result is still valid - while not await pow_result.is_stale_async(subtensor=subtensor): - call = await SubtensorModule(subtensor).register( - netuid=netuid, - coldkey=wallet.coldkeypub.ss58_address, - hotkey=wallet.hotkey.ss58_address, - block_number=pow_result.block_number, - nonce=pow_result.nonce, - work=[int(byte_) for byte_ in pow_result.seal], - ) - if mev_protection: - response = await submit_encrypted_extrinsic( - subtensor=subtensor, - wallet=wallet, - call=call, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - else: - response = await subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not response.success: - # Look error here - # https://github.com/opentensor/subtensor/blob/development/pallets/subtensor/src/errors.rs - if "HotKeyAlreadyRegisteredInSubNet" in response.message: - logging.debug( - f"[green]Already registered on subnet:[/green] [blue]{netuid}[/blue]." - ) - return response - await asyncio.sleep(0.5) - - if response.success: - is_registered = await subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - logging.debug("[green]Registered.[/green]") - return response - - # neuron not found, try again - logging.warning("[red]Unknown error. Neuron not found.[/red]") - continue - else: - # Exited loop because pow is no longer valid. - logging.warning("[red]POW is stale.[/red]") - # Try again. - - if attempts < max_allowed_attempts: - # Failed registration, retry pow - attempts += 1 - logging.warning( - f"Failed registration, retrying pow ... [blue]({attempts}/{max_allowed_attempts})[/blue]" - ) - else: - # Failed to register after max attempts. - return ExtrinsicResponse(False, "No more attempts.").with_log() - - except Exception as error: - return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) - - async def set_subnet_identity_extrinsic( subtensor: "AsyncSubtensor", wallet: "Wallet", diff --git a/bittensor/core/extrinsics/pallets/subtensor_module.py b/bittensor/core/extrinsics/pallets/subtensor_module.py index 019ecc58f0..0dddc4c3e8 100644 --- a/bittensor/core/extrinsics/pallets/subtensor_module.py +++ b/bittensor/core/extrinsics/pallets/subtensor_module.py @@ -243,37 +243,6 @@ def move_stake( alpha_amount=alpha_amount, ) - def register( - self, - netuid: int, - coldkey: str, - hotkey: str, - block_number: int, - nonce: int, - work: list[int], - ) -> Call: - """Returns GenericCall instance for Subtensor function SubtensorModule.register. - - Parameters: - netuid: The netuid of the subnet to register on. - coldkey: The coldkey SS58 address associated with the neuron. - hotkey: The hotkey SS58 address associated with the neuron. - block_number: POW block number. - nonce: POW nonce. - work: List representation of POW seal. - - Returns: - GenericCall instance. - """ - return self.create_composed_call( - netuid=netuid, - coldkey=coldkey, - hotkey=hotkey, - block_number=block_number, - nonce=nonce, - work=work, - ) - def register_limit( self, netuid: int, diff --git a/bittensor/core/extrinsics/registration.py b/bittensor/core/extrinsics/registration.py index cb24a6345c..3823b951e8 100644 --- a/bittensor/core/extrinsics/registration.py +++ b/bittensor/core/extrinsics/registration.py @@ -1,9 +1,9 @@ """ -This module provides sync functionalities for registering a wallet with the subtensor network using Proof-of-Work (PoW). +This module provides sync functionalities for registering a wallet with the subtensor network. """ import time -from typing import Optional, Union, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from bittensor.core.errors import BalanceTypeError, RegistrationError from bittensor.core.extrinsics.mev_shield import submit_encrypted_extrinsic @@ -12,7 +12,6 @@ from bittensor.core.types import ExtrinsicResponse from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging -from bittensor.utils.registration import create_pow, log_no_torch_error, torch if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -382,210 +381,6 @@ def register_subnet_extrinsic( return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) -def register_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, - *, - mev_protection: bool = DEFAULT_MEV_PROTECTION, - period: Optional[int] = None, - raise_error: bool = False, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - wait_for_revealed_execution: bool = True, -) -> ExtrinsicResponse: - """Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. - - Parameters: - subtensor: Subtensor object to use for chain interactions - wallet: Bittensor wallet object. - netuid: The ``netuid`` of the subnet to register on. - max_allowed_attempts: Maximum number of attempts to register the wallet. - output_in_place: Whether the POW solving should be outputted to the console as it goes along. - cuda: If `True`, the wallet should be registered using CUDA device(s). - dev_id: The CUDA device id to use, or a list of device ids. - tpb: The number of threads per block (CUDA). - num_processes: The number of processes to use to register. - update_interval: The number of nonces to solve between updates. - log_verbose: If `True`, the registration process will log more information. - mev_protection: If True, encrypts and submits the transaction through the MEV Shield pallet to protect - against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If False, submits the transaction directly without encryption. - 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. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. - wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. - - Returns: - ExtrinsicResponse: The result object of the extrinsic execution. - """ - try: - if not ( - unlocked := ExtrinsicResponse.unlock_wallet( - wallet, raise_error, unlock_type="both" - ) - ).success: - return unlocked - - block = subtensor.get_current_block() - if not subtensor.subnet_exists(netuid, block=block): - return ExtrinsicResponse( - False, f"Subnet {netuid} does not exist." - ).with_log() - - neuron = subtensor.get_neuron_for_pubkey_and_subnet( - hotkey_ss58=wallet.hotkey.ss58_address, netuid=netuid, block=block - ) - - if not neuron.is_null: - message = "Already registered." - logging.debug(f"[green]{message}[/green]") - logging.debug(f"\t\tuid: [blue]{neuron.uid}[/blue]") - logging.debug(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") - logging.debug(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") - logging.debug(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") - return ExtrinsicResponse(message=message, data={"neuron": neuron}) - - logging.debug( - f"Registration hotkey: [blue]{wallet.hotkey.ss58_address}[/blue], Public coldkey: " - f"[blue]{wallet.coldkey.ss58_address}[/blue] in the network: [blue]{subtensor.network}[/blue]." - ) - - if not torch: - log_no_torch_error() - return ExtrinsicResponse(False, "Torch is not installed.").with_log() - - # Attempt rolling registration. - attempts = 1 - - while True: - # Solve latest POW. - if cuda: - if not torch.cuda.is_available(): - return ExtrinsicResponse(False, "CUDA not available.").with_log() - - logging.debug("Creating a POW with CUDA.") - pow_result = create_pow( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - logging.debug("Creating a POW.") - pow_result = create_pow( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - cuda=cuda, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - # pow failed - if not pow_result: - # might be registered already on this subnet - is_registered = subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - message = f"Already registered in subnet {netuid}." - logging.debug(f"[green]{message}[/green]") - return ExtrinsicResponse(message=message) - - # pow successful, proceed to submit pow to chain for registration - else: - # check if a pow result is still valid - while not pow_result.is_stale(subtensor=subtensor): - # create extrinsic call - call = SubtensorModule(subtensor).register( - netuid=netuid, - coldkey=wallet.coldkeypub.ss58_address, - hotkey=wallet.hotkey.ss58_address, - block_number=pow_result.block_number, - nonce=pow_result.nonce, - work=[int(byte_) for byte_ in pow_result.seal], - ) - if mev_protection: - response = submit_encrypted_extrinsic( - subtensor=subtensor, - wallet=wallet, - call=call, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - wait_for_revealed_execution=wait_for_revealed_execution, - ) - else: - response = subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - period=period, - raise_error=raise_error, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not response.success: - # Look error here - # https://github.com/opentensor/subtensor/blob/development/pallets/subtensor/src/errors.rs - if "HotKeyAlreadyRegisteredInSubNet" in response.message: - logging.debug( - f"[green]Already registered on subnet:[/green] [blue]{netuid}[/blue]." - ) - return response - time.sleep(0.5) - - if response.success: - is_registered = subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - logging.debug("[green]Registered.[/green]") - return response - - # neuron not found, try again - logging.warning("[red]Unknown error. Neuron not found.[/red]") - continue - else: - # Exited loop because pow is no longer valid. - logging.warning("[red]POW is stale.[/red]") - # Try again. - - if attempts < max_allowed_attempts: - # Failed registration, retry pow - attempts += 1 - logging.warning( - f"Failed registration, retrying pow ... [blue]({attempts}/{max_allowed_attempts})[/blue]" - ) - else: - # Failed to register after max attempts. - return ExtrinsicResponse(False, "No more attempts.").with_log() - - except Exception as error: - return ExtrinsicResponse.from_exception(raise_error=raise_error, error=error) - - def set_subnet_identity_extrinsic( subtensor: "Subtensor", wallet: "Wallet", diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index f15319cb0d..6c634aa2e0 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -97,7 +97,6 @@ ) from bittensor.core.extrinsics.registration import ( burned_register_extrinsic, - register_extrinsic, register_limit_extrinsic, register_subnet_extrinsic, set_subnet_identity_extrinsic, @@ -6660,14 +6659,7 @@ def register( self, wallet: "Wallet", netuid: int, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, + limit_price: Optional[Balance] = None, *, mev_protection: bool = DEFAULT_MEV_PROTECTION, period: Optional[int] = DEFAULT_PERIOD, @@ -6676,56 +6668,63 @@ def register( wait_for_finalization: bool = True, wait_for_revealed_execution: bool = True, ) -> ExtrinsicResponse: - """ - Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. + """Registers a neuron on the Bittensor network by recycling TAO, with automatic price protection. - Registration is a critical step for a neuron to become an active participant in the network, enabling it to - stake, set weights, and receive incentives. + Uses ``register_limit`` under the hood. If ``limit_price`` is not provided, it is automatically + calculated as the current recycle (burn) cost plus a 0.5% tolerance to protect against price fluctuations. + + For root subnet (``netuid == 0``), delegates to ``root_register_extrinsic``. Parameters: wallet: The wallet associated with the neuron to be registered. netuid: The unique identifier of the subnet. - max_allowed_attempts: Maximum number of attempts to register the wallet. - output_in_place: If `True`, prints the progress of the proof of work to the console in-place. Meaning the - progress is printed on the same lines. - cuda: If `true`, the wallet should be registered using CUDA device(s). - dev_id: The CUDA device id to use, or a list of device ids. - tpb: The number of threads per block (CUDA). - num_processes: The number of processes to use to register. - update_interval: The number of nonces to solve between updates. - log_verbose: If `true`, the registration process will log more information. - mev_protection: If `True`, encrypts and submits the transaction through the MEV Shield pallet to protect + limit_price: Maximum acceptable burn price as a Balance instance. If ``None``, automatically calculated + as ``recycle * 1.005`` (0.5% tolerance). If the on-chain burn price exceeds this value, the + transaction will fail with RegistrationPriceLimitExceeded. + mev_protection: If ``True``, encrypts and submits the transaction through the MEV Shield pallet to protect against front-running and MEV attacks. The transaction remains encrypted in the mempool until validators - decrypt and execute it. If `False`, submits the transaction directly without encryption. + decrypt and execute it. If ``False``, submits the transaction directly without encryption. 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. - raise_error: Raises a relevant exception rather than returning `False` if unsuccessful. - wait_for_inclusion: Whether to wait for the inclusion of the transaction. - wait_for_finalization: Whether to wait for the finalization of the transaction. + raise_error: Raises a relevant exception rather than returning ``False`` if unsuccessful. + wait_for_inclusion: Waits for the transaction to be included in a block. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. wait_for_revealed_execution: Whether to wait for the revealed execution of transaction if mev_protection used. Returns: ExtrinsicResponse: The result object of the extrinsic execution. - This function facilitates the entry of new neurons into the network, supporting the decentralized growth and - scalability of the Bittensor ecosystem. - Notes: - Rate Limits: """ - return register_extrinsic( + if netuid == 0: + return root_register_extrinsic( + subtensor=self, + wallet=wallet, + mev_protection=mev_protection, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + wait_for_revealed_execution=wait_for_revealed_execution, + ) + + if limit_price is not None: + check_balance_amount(limit_price) + else: + recycle = self.recycle(netuid=netuid) + if recycle is None: + return ExtrinsicResponse( + False, f"Subnet {netuid} does not exist." + ).with_log() + limit_price = Balance.from_rao(recycle.rao * 1005 // 1000) + + return register_limit_extrinsic( subtensor=self, wallet=wallet, netuid=netuid, - max_allowed_attempts=max_allowed_attempts, - tpb=tpb, - update_interval=update_interval, - num_processes=num_processes, - cuda=cuda, - dev_id=dev_id, - output_in_place=output_in_place, - log_verbose=log_verbose, + limit_price=limit_price, mev_protection=mev_protection, period=period, raise_error=raise_error, diff --git a/bittensor/utils/registration/__init__.py b/bittensor/utils/registration/__init__.py index ddcc7a1248..3ac18def71 100644 --- a/bittensor/utils/registration/__init__.py +++ b/bittensor/utils/registration/__init__.py @@ -1,21 +1,15 @@ -from bittensor.utils.registration.pow import ( - create_pow, +from bittensor.utils.registration.torch_utils import ( legacy_torch_api_compat, log_no_torch_error, torch, use_torch, LazyLoadedTorch, - POWSolution, ) -from bittensor.utils.registration.async_pow import create_pow_async __all__ = [ - "create_pow", - "create_pow_async", "legacy_torch_api_compat", "log_no_torch_error", "torch", "use_torch", "LazyLoadedTorch", - "POWSolution", ] diff --git a/bittensor/utils/registration/async_pow.py b/bittensor/utils/registration/async_pow.py deleted file mode 100644 index aa45cf502d..0000000000 --- a/bittensor/utils/registration/async_pow.py +++ /dev/null @@ -1,538 +0,0 @@ -"""This module provides async utilities for solving Proof-of-Work (PoW) challenges in Bittensor network.""" - -import math -import time -from multiprocessing import Event, Lock, Array, Value, Queue -from queue import Empty -from typing import Callable, Union, Optional, TYPE_CHECKING - -from bittensor.core.errors import SubstrateRequestException -from bittensor.utils.btlogging import logging -from bittensor.utils.registration.pow import ( - get_cpu_count, - update_curr_block, - terminate_workers_and_wait_for_exit, - CUDASolver, - torch, - RegistrationStatistics, - RegistrationStatisticsLogger, - Solver, - UsingSpawnStartMethod, -) - -if TYPE_CHECKING: - from bittensor.core.async_subtensor import AsyncSubtensor - from bittensor_wallet import Wallet - from bittensor.utils.registration import POWSolution - - -async def _get_block_with_retry( - subtensor: "AsyncSubtensor", netuid: int -) -> tuple[int, int, str]: - """ - Gets the current block number, difficulty, and block hash from the substrate node. - - Parameters: - subtensor: The subtensor object to use to get the block number, difficulty, and block hash. - netuid: The netuid of the network to get the block number, difficulty, and block hash from. - - Returns: - The current block number, difficulty of the subnet, block hash - - Raises: - Exception: If the block hash is None. - ValueError: If the difficulty is None. - """ - block = await subtensor.substrate.get_block() - block_hash = block["header"]["hash"] - block_number = block["header"]["number"] - try: - difficulty = ( - 1_000_000 - if netuid == -1 - else int( - await subtensor.get_hyperparameter( - param_name="Difficulty", netuid=netuid, block_hash=block_hash - ) - ) - ) - except TypeError: - raise ValueError("Chain error. Difficulty is None") - except SubstrateRequestException: - raise Exception( - "Network error. Could not connect to substrate to get block hash" - ) - return block_number, difficulty, block_hash - - -async def _check_for_newest_block_and_update( - subtensor: "AsyncSubtensor", - netuid: int, - old_block_number: int, - hotkey_bytes: bytes, - curr_diff: Array, - curr_block: Array, - curr_block_num: Value, - update_curr_block_: "Callable", - check_block: Lock, - solvers: list[Solver], - curr_stats: "RegistrationStatistics", -) -> int: - """ - Check for the newest block and update block-related information and states across solvers if a new block is detected. - - Parameters: - subtensor: The subtensor instance interface. - netuid: The network UID for the blockchain. - old_block_number: The previously known block number. - hotkey_bytes: The bytes representation of the hotkey. - curr_diff: The current difficulty level. - curr_block: The current block information. - curr_block_num: The current block number. - update_curr_block_: Function to update current block information. - check_block: Lock object for synchronizing block checking. - solvers: List of solvers to notify of new blocks. - curr_stats: Current registration statistics to update. - - Returns: - int: The updated block number which is the same as the new block - number if it was detected, otherwise the old block number. - """ - block_number = await subtensor.substrate.get_block_number(None) - if block_number != old_block_number: - old_block_number = block_number - # update block information - block_number, difficulty, block_hash = await _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - block_bytes = bytes.fromhex(block_hash[2:]) - - update_curr_block_( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - # Set new block events for each solver - - for worker in solvers: - worker.newBlockEvent.set() - - # update stats - curr_stats.block_number = block_number - curr_stats.block_hash = block_hash - curr_stats.difficulty = difficulty - - return old_block_number - - -async def _block_solver( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - num_processes: int, - netuid: int, - dev_id: list[int], - tpb: int, - update_interval: int, - curr_block, - curr_block_num, - curr_diff, - n_samples, - alpha_, - output_in_place, - log_verbose, - cuda: bool, -): - """Shared code used by the Solvers to solve the POW solution.""" - limit = int(math.pow(2, 256)) - 1 - - if cuda: - num_processes = len(dev_id) - - # Establish communication queues - # See the _Solver class for more information on the queues. - stop_event = Event() - stop_event.clear() - - solution_queue = Queue() - finished_queues = [Queue() for _ in range(num_processes)] - check_block = Lock() - - hotkey_bytes = ( - wallet.coldkeypub.public_key if netuid == -1 else wallet.hotkey.public_key - ) - - if cuda: - # Create a worker per CUDA device - solvers = [ - CUDASolver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stop_event, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - dev_id[i], - tpb, - ) - for i in range(num_processes) - ] - else: - # Start consumers - solvers = [ - Solver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stop_event, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ) - for i in range(num_processes) - ] - - # Get first block - block_number, difficulty, block_hash = await _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - - block_bytes = bytes.fromhex(block_hash[2:]) - old_block_number = block_number - # Set to current block - update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - - # Set new block events for each solver to start at the initial block - for worker in solvers: - worker.newBlockEvent.set() - - for worker in solvers: - worker.start() # start the solver processes - - start_time = time.time() # time that the registration started - time_last = start_time # time that the last work blocks completed - - curr_stats = RegistrationStatistics( - time_spent_total=0.0, - time_average=0.0, - rounds_total=0, - time_spent=0.0, - hash_rate_perpetual=0.0, - hash_rate=0.0, - difficulty=difficulty, - block_number=block_number, - block_hash=block_hash, - ) - - start_time_perpetual = time.time() - - logger = RegistrationStatisticsLogger(output_in_place=output_in_place) - logger.start() - - solution = None - - hash_rates = [0] * n_samples # The last n true hash_rates - weights = [alpha_**i for i in range(n_samples)] # weights decay by alpha - - timeout = 0.15 if cuda else 0.15 - while netuid == -1 or not await subtensor.is_hotkey_registered( - wallet.hotkey.ss58_address, netuid - ): - # Wait until a solver finds a solution - try: - solution = solution_queue.get(block=True, timeout=timeout) - if solution is not None: - break - except Empty: - # No solution found, try again - pass - - # check for new block - old_block_number = await _check_for_newest_block_and_update( - subtensor=subtensor, - netuid=netuid, - hotkey_bytes=hotkey_bytes, - old_block_number=old_block_number, - curr_diff=curr_diff, - curr_block=curr_block, - curr_block_num=curr_block_num, - curr_stats=curr_stats, - update_curr_block_=update_curr_block, - check_block=check_block, - solvers=solvers, - ) - - num_time = 0 - for finished_queue in finished_queues: - try: - finished_queue.get(timeout=0.1) - num_time += 1 - - except Empty: - continue - - time_now = time.time() # get current time - time_since_last = time_now - time_last # get time since last work block(s) - if num_time > 0 and time_since_last > 0.0: - # create EWMA of the hash_rate to make measure more robust - - if cuda: - hash_rate_ = (num_time * tpb * update_interval) / time_since_last - else: - hash_rate_ = (num_time * update_interval) / time_since_last - hash_rates.append(hash_rate_) - hash_rates.pop(0) # remove the 0th data point - curr_stats.hash_rate = sum( - [hash_rates[i] * weights[i] for i in range(n_samples)] - ) / (sum(weights)) - - # update time last to now - time_last = time_now - - curr_stats.time_average = ( - curr_stats.time_average * curr_stats.rounds_total - + curr_stats.time_spent - ) / (curr_stats.rounds_total + num_time) - curr_stats.rounds_total += num_time - - # Update stats - curr_stats.time_spent = time_since_last - new_time_spent_total = time_now - start_time_perpetual - if cuda: - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * (tpb * update_interval) - ) / new_time_spent_total - else: - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * update_interval - ) / new_time_spent_total - curr_stats.time_spent_total = new_time_spent_total - - # Update the logger - logger.update(curr_stats, verbose=log_verbose) - - # exited while, solution contains the nonce or wallet is registered - stop_event.set() # stop all other processes - logger.stop() - - # terminate and wait for all solvers to exit - terminate_workers_and_wait_for_exit(solvers) - - return solution - - -async def _solve_for_difficulty_fast_cuda( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - update_interval: int = 50_000, - tpb: int = 512, - dev_id: Union[list[int], int] = 0, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional["POWSolution"]: - """ - Solves the registration fast using CUDA - - Parameters: - subtensor: The subtensor object to use to get the block number, difficulty, and block hash. - wallet: The wallet to register - netuid: The netuid of the subnet to register to. - output_in_place: If true, prints the output in place, otherwise prints to new lines - update_interval: The number of nonces to try before checking for more blocks - tpb: The number of threads per block. CUDA param that should match the GPU capability - dev_id: The CUDA device IDs to execute the registration on, either a single device or a list of devices - n_samples: The number of samples of the hash_rate to keep for the EWMA - alpha_: The alpha for the EWMA for the hash_rate calculation - log_verbose: If true, prints more verbose logging of the registration metrics. - - Note: - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - """ - if isinstance(dev_id, int): - dev_id = [dev_id] - elif dev_id is None: - dev_id = [0] - - num_processes = min(1, get_cpu_count()) - - if update_interval is None: - update_interval = 50_000 - - if not torch.cuda.is_available(): - raise Exception("CUDA not available") - - # Set mp start to use spawn so CUDA doesn't complain - with UsingSpawnStartMethod(force=True): - curr_block, curr_block_num, curr_diff = CUDASolver.create_shared_memory() - - solution = await _block_solver( - subtensor=subtensor, - wallet=wallet, - num_processes=num_processes, - netuid=netuid, - dev_id=dev_id, - tpb=tpb, - update_interval=update_interval, - curr_block=curr_block, - curr_block_num=curr_block_num, - curr_diff=curr_diff, - n_samples=n_samples, - alpha_=alpha_, - output_in_place=output_in_place, - log_verbose=log_verbose, - cuda=True, - ) - - return solution - - -async def _solve_for_difficulty_fast( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional["POWSolution"]: - """ - Solves the POW for registration using multiprocessing. - - Parameters: - subtensor: The subtensor object to use to get the block number, difficulty, and block hash. - wallet: wallet to use for registration. - netuid: The netuid of the subnet to register to. - output_in_place: If true, prints the status in place. Otherwise, prints the status on a new line. - num_processes: Number of processes to use. - update_interval: Number of nonces to solve before updating block information. - n_samples: The number of samples of the hash_rate to keep for the EWMA - alpha_: The alpha for the EWMA for the hash_rate calculation - log_verbose: If true, prints more verbose logging of the registration metrics. - - Notes: - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - We can also modify the update interval to do smaller blocks of work, while still updating the block information - after a different number of nonces, to increase the transparency of the process while still keeping the speed. - """ - if not num_processes: - # get the number of allowed processes for this process - num_processes = min(1, get_cpu_count()) - - if update_interval is None: - update_interval = 50_000 - - curr_block, curr_block_num, curr_diff = Solver.create_shared_memory() - - solution = await _block_solver( - subtensor=subtensor, - wallet=wallet, - num_processes=num_processes, - netuid=netuid, - dev_id=None, - tpb=None, - update_interval=update_interval, - curr_block=curr_block, - curr_block_num=curr_block_num, - curr_diff=curr_diff, - n_samples=n_samples, - alpha_=alpha_, - output_in_place=output_in_place, - log_verbose=log_verbose, - cuda=False, - ) - - return solution - - -async def create_pow_async( - subtensor: "AsyncSubtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: int = None, - update_interval: int = None, - log_verbose: bool = False, -) -> "POWSolution": - """ - Creates a proof of work for the given subtensor and wallet. - - Parameters: - subtensor: The subtensor instance. - wallet: The wallet to create a proof of work for. - netuid: The netuid for the subnet to create a proof of work for. - output_in_place: If true, prints the progress of the proof of work to the console in-place. Meaning the progress - is printed on the same lines. - cuda: If true, uses CUDA to solve the proof of work. - dev_id: The CUDA device id(s) to use. If cuda is true and dev_id is a list, then multiple CUDA devices will be - used to solve the proof of work. - tpb: The number of threads per block to use when solving the proof of work. Should be a multiple of 32. - num_processes: The number of processes to use when solving the proof of work. If None, then the number of - processes is equal to the number of CPU cores. - update_interval: The number of nonces to run before checking for a new block. - log_verbose: If true, prints the progress of the proof of work more verbosely. - - Returns: - The proof of work solution or None if the wallet is already registered or there is a different error. - - Raises: - ValueError: If the subnet does not exist. - """ - if netuid != -1: - if not await subtensor.subnet_exists(netuid=netuid): - raise ValueError(f"Subnet {netuid} does not exist") - solution: Optional[POWSolution] - if cuda: - logging.debug("Solve difficulty with CUDA.") - solution = await _solve_for_difficulty_fast_cuda( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - dev_id=dev_id, - tpb=tpb, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - logging.debug("Solve difficulty.") - solution = await _solve_for_difficulty_fast( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - return solution diff --git a/bittensor/utils/registration/pow.py b/bittensor/utils/registration/pow.py deleted file mode 100644 index 1fa4c23ee8..0000000000 --- a/bittensor/utils/registration/pow.py +++ /dev/null @@ -1,1181 +0,0 @@ -"""This module provides utilities for solving Proof-of-Work (PoW) challenges in Bittensor network.""" - -import binascii -import functools -import hashlib -import math -import multiprocessing as mp -import os -import random -import subprocess -import time -from dataclasses import dataclass -from datetime import timedelta -from multiprocessing.queues import Queue as QueueType -from queue import Empty, Full -from typing import Callable, Optional, Union, TYPE_CHECKING - -import numpy -from Crypto.Hash import keccak - -from bittensor.utils.btlogging import logging -from bittensor.utils.formatting import get_human_readable, millify -from bittensor.utils.registration.register_cuda import solve_cuda - - -def use_torch() -> bool: - """Force the use of torch over numpy for certain operations.""" - return True if os.getenv("USE_TORCH") == "1" else False - - -def legacy_torch_api_compat(func): - """ - Convert function operating on numpy Input&Output to legacy torch Input&Output API if `use_torch()` is True. - - Parameters: - func: Function with numpy Input/Output to be decorated. - - Returns: - decorated: Decorated function. - """ - - @functools.wraps(func) - def decorated(*args, **kwargs): - if use_torch(): - # if argument is a Torch tensor, convert it to numpy - args = [ - arg.cpu().numpy() if isinstance(arg, torch.Tensor) else arg - for arg in args - ] - kwargs = { - key: value.cpu().numpy() if isinstance(value, torch.Tensor) else value - for key, value in kwargs.items() - } - ret = func(*args, **kwargs) - if use_torch(): - # if return value is a numpy array, convert it to Torch tensor - if isinstance(ret, numpy.ndarray): - ret = torch.from_numpy(ret) - return ret - - return decorated - - -@functools.cache -def _get_real_torch(): - try: - import torch as _real_torch - except ImportError: - _real_torch = None - return _real_torch - - -def log_no_torch_error(): - logging.error( - "This command requires torch. You can install torch for bittensor" - ' with `pip install bittensor[torch]` or `pip install ".[torch]"`' - " if installing from source, and then run the command with USE_TORCH=1 {command}" - ) - - -class LazyLoadedTorch: - """A lazy-loading proxy for the torch module.""" - - def __bool__(self): - return bool(_get_real_torch()) - - def __getattr__(self, name): - if real_torch := _get_real_torch(): - return getattr(real_torch, name) - else: - log_no_torch_error() - raise ImportError("torch not installed") - - -if TYPE_CHECKING: - import torch - from bittensor.core.subtensor import Subtensor - from bittensor.core.async_subtensor import AsyncSubtensor - from bittensor_wallet import Wallet -else: - torch = LazyLoadedTorch() - - -def _hex_bytes_to_u8_list(hex_bytes: bytes) -> list[int]: - """ """ - return [int(hex_bytes[i : i + 2], 16) for i in range(0, len(hex_bytes), 2)] - - -def _create_seal_hash(block_and_hotkey_hash_bytes: bytes, nonce: int) -> bytes: - """ - Create a cryptographic seal hash from the given block and hotkey hash bytes and nonce. - - This function generates a seal hash by combining the given block and hotkey hash bytes with a nonce. - It first converts the nonce to a byte representation, then concatenates it with the first 64 hex characters of the - block and hotkey hash bytes. The result is then hashed using SHA-256 followed by the Keccak-256 algorithm to produce - the final seal hash. - - Parameters: - block_and_hotkey_hash_bytes: The combined hash bytes of the block and hotkey. - nonce: The nonce value used for hashing. - - Returns: - The resulting seal hash. - """ - nonce_bytes = binascii.hexlify(nonce.to_bytes(8, "little")) - pre_seal = nonce_bytes + binascii.hexlify(block_and_hotkey_hash_bytes)[:64] - seal_sh256 = hashlib.sha256(bytearray(_hex_bytes_to_u8_list(pre_seal))).digest() - kec = keccak.new(digest_bits=256) - seal = kec.update(seal_sh256).digest() - return seal - - -def _seal_meets_difficulty(seal: bytes, difficulty: int, limit: int) -> bool: - """Determines if a seal meets the specified difficulty.""" - seal_number = int.from_bytes(seal, "big") - product = seal_number * difficulty - return product < limit - - -@dataclass -class POWSolution: - """A solution to the registration PoW problem.""" - - nonce: int - block_number: int - difficulty: int - seal: bytes - - def is_stale(self, subtensor: "Subtensor") -> bool: - """ - Synchronous implementation. Returns True if the POW is stale. - - This means the block the POW is solved for is within 3 blocks of the current block. - """ - return self.block_number < subtensor.get_current_block() - 3 - - async def is_stale_async(self, subtensor: "AsyncSubtensor") -> bool: - """ - Asynchronous implementation. Returns True if the POW is stale. - - This means the block the POW is solved for is within 3 blocks of the current block. - """ - current_block = await subtensor.substrate.get_block_number(None) - return self.block_number < current_block - 3 - - -class UsingSpawnStartMethod: - def __init__(self, force: bool = False): - self._old_start_method = None - self._force = force - - def __enter__(self): - self._old_start_method = mp.get_start_method(allow_none=True) - if self._old_start_method is None: - self._old_start_method = "spawn" # default to spawn - - mp.set_start_method("spawn", force=self._force) - - def __exit__(self, *args): - # restore the old start method - mp.set_start_method(self._old_start_method, force=True) - - -class _SolverBase(mp.Process): - """ - A process that solves the registration PoW problem. - - Parameters: - proc_num: The number of the process being created. - num_proc: The total number of processes running. - update_interval: The number of nonces to try to solve before checking for a new block. - finished_queue: The queue to put the process number when a process finishes each update_interval. Used for - calculating the average time per update_interval across all processes. - solution_queue: The queue to put the solution the process has found during the pow solve. - stopEvent: The event to set by the main process when all the solver processes should stop. The solver process - will check for the event after each update_interval. The solver process will stop when the event is set. - Used to stop the solver processes when a solution is found. - curr_block: The array containing this process's current block hash. The main process will set the array to the - new block hash when a new block is finalized in the network. The solver process will get the new block hash - from this array when newBlockEvent is set. - curr_block_num: The value containing this process's current block number. The main process will set the value to - the new block number when a new block is finalized in the network. The solver process will get the new block - number from this value when newBlockEvent is set. - curr_diff: The array containing this process's current difficulty. The main process will set the array to the - new difficulty when a new block is finalized in the network. The solver process will get the new difficulty - from this array when newBlockEvent is set. - check_block: The lock to prevent this process from getting the new block data while the main process is updating - the data. - limit: The limit of the pow solve for a valid solution. - """ - - proc_num: int - num_proc: int - update_interval: int - finished_queue: "mp.Queue" - solution_queue: "mp.Queue" - # newBlockEvent: "mp.Event" - newBlockEvent: "mp.Event" - stopEvent: "mp.Event" - hotkey_bytes: bytes - curr_block: "mp.Array" - curr_block_num: "mp.Value" - curr_diff: "mp.Array" - check_block: "mp.Lock" - limit: int - - def __init__( - self, - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ): - mp.Process.__init__(self, daemon=True) - self.proc_num = proc_num - self.num_proc = num_proc - self.update_interval = update_interval - self.finished_queue = finished_queue - self.solution_queue = solution_queue - self.newBlockEvent = mp.Event() - self.newBlockEvent.clear() - self.curr_block = curr_block - self.curr_block_num = curr_block_num - self.curr_diff = curr_diff - self.check_block = check_block - self.stopEvent = stopEvent - self.limit = limit - - def run(self): - raise NotImplementedError("_SolverBase is an abstract class") - - @staticmethod - def create_shared_memory() -> tuple["mp.Array", "mp.Value", "mp.Array"]: - """Creates shared memory for the solver processes to use.""" - curr_block = mp.Array("h", 32, lock=True) # byte array - curr_block_num = mp.Value("i", 0, lock=True) # int - curr_diff = mp.Array("Q", [0, 0], lock=True) # [high, low] - - return curr_block, curr_block_num, curr_diff - - -class Solver(_SolverBase): - def run(self): - block_number: int - block_and_hotkey_hash_bytes: bytes - block_difficulty: int - nonce_limit = int(math.pow(2, 64)) - 1 - - # Start at random nonce - nonce_start = random.randint(0, nonce_limit) - nonce_end = nonce_start + self.update_interval - while not self.stopEvent.is_set(): - if self.newBlockEvent.is_set(): - with self.check_block: - block_number = self.curr_block_num.value - block_and_hotkey_hash_bytes = bytes(self.curr_block) - block_difficulty = _registration_diff_unpack(self.curr_diff) - - self.newBlockEvent.clear() - - # Do a block of nonces - solution = _solve_for_nonce_block( - nonce_start, - nonce_end, - block_and_hotkey_hash_bytes, - block_difficulty, - self.limit, - block_number, - ) - if solution is not None: - self.solution_queue.put(solution) - - try: - # Send time - self.finished_queue.put_nowait(self.proc_num) - except Full: - pass - - nonce_start = random.randint(0, nonce_limit) - nonce_start = nonce_start % nonce_limit - nonce_end = nonce_start + self.update_interval - - -class CUDASolver(_SolverBase): - dev_id: int - tpb: int - - def __init__( - self, - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - dev_id: int, - tpb: int, - ): - super().__init__( - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ) - self.dev_id = dev_id - self.tpb = tpb - - def run(self): - block_number: int = 0 # dummy value - block_and_hotkey_hash_bytes: bytes = b"0" * 32 # dummy value - block_difficulty: int = int(math.pow(2, 64)) - 1 # dummy value - nonce_limit = int(math.pow(2, 64)) - 1 # U64MAX - - # Start at random nonce - nonce_start = random.randint(0, nonce_limit) - while not self.stopEvent.is_set(): - if self.newBlockEvent.is_set(): - with self.check_block: - block_number = self.curr_block_num.value - block_and_hotkey_hash_bytes = bytes(self.curr_block) - block_difficulty = _registration_diff_unpack(self.curr_diff) - - self.newBlockEvent.clear() - - # Do a block of nonces - solution = _solve_for_nonce_block_cuda( - nonce_start, - self.update_interval, - block_and_hotkey_hash_bytes, - block_difficulty, - self.limit, - block_number, - self.dev_id, - self.tpb, - ) - if solution is not None: - self.solution_queue.put(solution) - - try: - # Signal that a nonce_block was finished using queue - # send our proc_num - self.finished_queue.put(self.proc_num) - except Full: - pass - - # increase nonce by number of nonces processed - nonce_start += self.update_interval * self.tpb - nonce_start = nonce_start % nonce_limit - - -def _solve_for_nonce_block_cuda( - nonce_start: int, - update_interval: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - block_number: int, - dev_id: int, - tpb: int, -) -> Optional["POWSolution"]: - """Tries to solve the POW on a CUDA device for a block of nonces (nonce_start, nonce_start + update_interval * tpb""" - solution, seal = solve_cuda( - nonce_start, - update_interval, - tpb, - block_and_hotkey_hash_bytes, - difficulty, - limit, - dev_id, - ) - - if solution != -1: - # Check if solution is valid (i.e., not -1) - return POWSolution(solution, block_number, difficulty, seal) - - return None - - -def _solve_for_nonce_block( - nonce_start: int, - nonce_end: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - block_number: int, -) -> Optional["POWSolution"]: - """Tries to solve the POW for a block of nonces (nonce_start, nonce_end)""" - for nonce in range(nonce_start, nonce_end): - # Create seal. - seal = _create_seal_hash(block_and_hotkey_hash_bytes, nonce) - - # Check if seal meets difficulty - if _seal_meets_difficulty(seal, difficulty, limit): - # Found a solution, save it. - return POWSolution(nonce, block_number, difficulty, seal) - - return None - - -def _registration_diff_unpack(packed_diff: "mp.Array") -> int: - """Unpacks the packed two 32-bit integers into one 64-bit integer. Little endian.""" - return int(packed_diff[0] << 32 | packed_diff[1]) - - -def _registration_diff_pack(diff: int, packed_diff: "mp.Array"): - """Packs the difficulty into two 32-bit integers. Little endian.""" - packed_diff[0] = diff >> 32 - packed_diff[1] = diff & 0xFFFFFFFF # low 32 bits - - -def _hash_block_with_hotkey(block_bytes: bytes, hotkey_bytes: bytes) -> bytes: - """Hashes the block with the hotkey using Keccak-256 to get 32 bytes""" - kec = keccak.new(digest_bits=256) - kec = kec.update(bytearray(block_bytes + hotkey_bytes)) - block_and_hotkey_hash_bytes = kec.digest() - return block_and_hotkey_hash_bytes - - -def update_curr_block( - curr_diff: "mp.Array", - curr_block: "mp.Array", - curr_block_num: "mp.Value", - block_number: int, - block_bytes: bytes, - diff: int, - hotkey_bytes: bytes, - lock: "mp.Lock", -): - """ - Update the current block data with the provided block information and difficulty. - - This function updates the current block and its difficulty in a thread-safe manner. It sets the current block - number, hashes the block with the hotkey, updates the current block bytes, and packs the difficulty. - - Parameters: - curr_diff: Shared array to store the current difficulty. - curr_block: Shared array to store the current block data. - curr_block_num: Shared value to store the current block number. - block_number: The block number to set as the current block number. - block_bytes: The block data bytes to be hashed with the hotkey. - diff: The difficulty value to be packed into the current difficulty array. - hotkey_bytes: The hotkey bytes used for hashing the block. - lock: A lock to ensure thread-safe updates. - """ - with lock: - curr_block_num.value = block_number - # Hash the block with the hotkey - block_and_hotkey_hash_bytes = _hash_block_with_hotkey(block_bytes, hotkey_bytes) - for i in range(32): - curr_block[i] = block_and_hotkey_hash_bytes[i] - _registration_diff_pack(diff, curr_diff) - - -def get_cpu_count() -> int: - """Returns the number of CPUs in the system.""" - try: - return len(os.sched_getaffinity(0)) - except AttributeError: - # macOS does not have sched_getaffinity - return os.cpu_count() - - -@dataclass -class RegistrationStatistics: - """Statistics for a registration.""" - - time_spent_total: float - rounds_total: int - time_average: float - time_spent: float - hash_rate_perpetual: float - hash_rate: float - difficulty: int - block_number: int - block_hash: str - - -class Status: - def __init__(self, status: str): - self._status = status - - def start(self): - pass - - def stop(self): - pass - - def update(self, status: str): - self._status = status - - -class Console: - @staticmethod - def status(status: str): - return Status(status) - - @staticmethod - def log(text: str): - print(text) - - -class RegistrationStatisticsLogger: - """Logs statistics for a registration.""" - - status: Optional["Status"] - - def __init__( - self, - console: Optional["Console"] = None, - output_in_place: bool = True, - ) -> None: - if console is None: - console = Console() - - self.console = console - - if output_in_place: - self.status = self.console.status("Solving") - else: - self.status = None - - def start(self) -> None: - if self.status is not None: - self.status.start() - - def stop(self) -> None: - if self.status is not None: - self.status.stop() - - @classmethod - def get_status_message( - cls, stats: "RegistrationStatistics", verbose: bool = False - ) -> str: - """Generates the status message based on registration statistics.""" - message = ( - "Solving\n" - + f"Time Spent (total): [bold white]{timedelta(seconds=stats.time_spent_total)}[/bold white]\n" - + ( - f"Time Spent This Round: {timedelta(seconds=stats.time_spent)}\n" - + f"Time Spent Average: {timedelta(seconds=stats.time_average)}\n" - if verbose - else "" - ) - + f"Registration Difficulty: [bold white]{millify(stats.difficulty)}[/bold white]\n" - + f"Iters (Inst/Perp): [bold white]{get_human_readable(stats.hash_rate, 'H')}/s / " - + f"{get_human_readable(stats.hash_rate_perpetual, 'H')}/s[/bold white]\n" - + f"Block Number: [bold white]{stats.block_number}[/bold white]\n" - + f"Block Hash: [bold white]{stats.block_hash.encode('utf-8')}[/bold white]\n" - ) - return message - - def update(self, stats: "RegistrationStatistics", verbose: bool = False) -> None: - if self.status is not None: - self.status.update(self.get_status_message(stats, verbose=verbose)) - else: - self.console.log(self.get_status_message(stats, verbose=verbose)) - - -def _solve_for_difficulty_fast( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional[POWSolution]: - """ - Solves the POW for registration using multiprocessing. - - Parameters: - subtensor: Subtensor instance. - wallet: wallet to use for registration. - netuid: The netuid of the subnet to register to. - output_in_place: If true, prints the status in place. Otherwise, prints the status on a new line. - num_processes: Number of processes to use. - update_interval: Number of nonces to solve before updating block information. - n_samples: The number of samples of the hash_rate to keep for the EWMA. - alpha_: The alpha for the EWMA for the hash_rate calculation. - log_verbose: If true, prints more verbose logging of the registration metrics. - - Note: - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - We can also modify the update interval to do smaller blocks of work, while still updating the block information - after a different number of nonces, to increase the transparency of the process while still keeping the speed. - """ - if num_processes is None: - # get the number of allowed processes for this process - num_processes = min(1, get_cpu_count()) - - if update_interval is None: - update_interval = 50_000 - - limit = int(math.pow(2, 256)) - 1 - - curr_block, curr_block_num, curr_diff = Solver.create_shared_memory() - - # Establish communication queues - # See the Solver class for more information on the queues. - stopEvent = mp.Event() - stopEvent.clear() - - solution_queue = mp.Queue() - finished_queues = [mp.Queue() for _ in range(num_processes)] - check_block = mp.Lock() - - hotkey_bytes = ( - wallet.coldkeypub.public_key if netuid == -1 else wallet.hotkey.public_key - ) - # Start consumers - solvers = [ - Solver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ) - for i in range(num_processes) - ] - - # Get first block - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - - block_bytes = bytes.fromhex(block_hash[2:]) - old_block_number = block_number - # Set to current block - update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - - # Set new block events for each solver to start at the initial block - for worker in solvers: - worker.newBlockEvent.set() - - for worker in solvers: - worker.start() # start the solver processes - - start_time = time.time() # time that the registration started - time_last = start_time # time that the last work blocks completed - - curr_stats = RegistrationStatistics( - time_spent_total=0.0, - time_average=0.0, - rounds_total=0, - time_spent=0.0, - hash_rate_perpetual=0.0, - hash_rate=0.0, - difficulty=difficulty, - block_number=block_number, - block_hash=block_hash, - ) - - start_time_perpetual = time.time() - - logger = RegistrationStatisticsLogger(output_in_place=output_in_place) - logger.start() - - solution = None - - hash_rates = [0] * n_samples # The last n true hash_rates - weights = [alpha_**i for i in range(n_samples)] # weights decay by alpha - - while netuid == -1 or not subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ): - # Wait until a solver finds a solution - try: - solution = solution_queue.get(block=True, timeout=0.25) - if solution is not None: - break - except Empty: - # No solution found, try again - pass - - # check for new block - old_block_number = _check_for_newest_block_and_update( - subtensor=subtensor, - netuid=netuid, - hotkey_bytes=hotkey_bytes, - old_block_number=old_block_number, - curr_diff=curr_diff, - curr_block=curr_block, - curr_block_num=curr_block_num, - curr_stats=curr_stats, - update_curr_block_=update_curr_block, - check_block=check_block, - solvers=solvers, - ) - - num_time = 0 - for finished_queue in finished_queues: - try: - finished_queue.get(timeout=0.1) - num_time += 1 - - except Empty: - continue - - time_now = time.time() # get current time - time_since_last = time_now - time_last # get time since last work block(s) - if num_time > 0 and time_since_last > 0.0: - # create EWMA of the hash_rate to make measure more robust - - hash_rate_ = (num_time * update_interval) / time_since_last - hash_rates.append(hash_rate_) - hash_rates.pop(0) # remove the 0th data point - curr_stats.hash_rate = sum( - [hash_rates[i] * weights[i] for i in range(n_samples)] - ) / (sum(weights)) - - # update time last to now - time_last = time_now - - curr_stats.time_average = ( - curr_stats.time_average * curr_stats.rounds_total - + curr_stats.time_spent - ) / (curr_stats.rounds_total + num_time) - curr_stats.rounds_total += num_time - - # Update stats - curr_stats.time_spent = time_since_last - new_time_spent_total = time_now - start_time_perpetual - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * update_interval - ) / new_time_spent_total - curr_stats.time_spent_total = new_time_spent_total - - # Update the logger - logger.update(curr_stats, verbose=log_verbose) - - # exited while, solution contains the nonce or wallet is registered - stopEvent.set() # stop all other processes - logger.stop() - - # terminate and wait for all solvers to exit - terminate_workers_and_wait_for_exit(solvers) - - return solution - - -def _get_block_with_retry(subtensor: "Subtensor", netuid: int) -> tuple[int, int, str]: - """ - Gets the current block number, difficulty, and block hash from the substrate node. - - Parameters: - subtensor: The subtensor instance. - netuid: The netuid of the network to get the block number, difficulty, and block hash from. - - Returns: - tuple[int, int, bytes] - - block_number: The current block number. - - difficulty: The current difficulty of the subnet. - - block_hash: The current block hash. - - Raises: - Exception: If the block hash is None. - ValueError: If the difficulty is None. - """ - block_number = subtensor.get_current_block() - difficulty = 1_000_000 if netuid == -1 else subtensor.difficulty(netuid=netuid) - block_hash = subtensor.get_block_hash(block_number) - if block_hash is None: - raise Exception( - "Network error. Could not connect to substrate to get block hash" - ) - if difficulty is None: - raise ValueError("Chain error. Difficulty is None") - return block_number, difficulty, block_hash - - -def _check_for_newest_block_and_update( - subtensor: "Subtensor", - netuid: int, - old_block_number: int, - hotkey_bytes: bytes, - curr_diff: "mp.Array", - curr_block: "mp.Array", - curr_block_num: "mp.Value", - update_curr_block_: "Callable", - check_block: "mp.Lock", - solvers: Union[list["Solver"], list["CUDASolver"]], - curr_stats: "RegistrationStatistics", -) -> int: - """ - Checks for a new block and updates the current block information if a new block is found. - - Parameters: - subtensor: Subtensor instance. - netuid: The netuid to use for retrieving the difficulty. - old_block_number: The old block number to check against. - hotkey_bytes: The bytes of the hotkey's pubkey. - curr_diff: The current difficulty as a multiprocessing array. - curr_block: Where the current block is stored as a multiprocessing array. - curr_block_num: Where the current block number is stored as a multiprocessing value. - update_curr_block_: A function that updates the current block. - check_block: A mp lock that is used to check for a new block. - solvers: A list of solvers to update the current block for. - curr_stats: The current registration statistics to update. - - Returns: - The current block number. - """ - block_number = subtensor.get_current_block() - if block_number != old_block_number: - old_block_number = block_number - # update block information - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - block_bytes = bytes.fromhex(block_hash[2:]) - - update_curr_block_( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - # Set new block events for each solver - - for worker in solvers: - worker.newBlockEvent.set() - - # update stats - curr_stats.block_number = block_number - curr_stats.block_hash = block_hash - curr_stats.difficulty = difficulty - - return old_block_number - - -def _solve_for_difficulty_fast_cuda( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - update_interval: int = 50_000, - tpb: int = 512, - dev_id: Union[list[int], int] = 0, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional["POWSolution"]: - """ - Solves the registration fast using CUDA. - - Parameters: - subtensor: Subtensor instance. - wallet: Bittensor Wallet instance. - netuid: The netuid of the subnet to register to. - output_in_place: If true, prints the output in place, otherwise prints to new lines. - update_interval: The number of nonces to try before checking for more blocks. - tpb: The number of threads per block. CUDA param that should match the GPU capability - dev_id: The CUDA device IDs to execute the registration on, either a single device or a list of devices. - n_samples: The number of samples of the hash_rate to keep for the EWMA. - alpha_: The alpha for the EWMA for the hash_rate calculation. - log_verbose: If true, prints more verbose logging of the registration metrics. - - Note: - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - """ - if isinstance(dev_id, int): - dev_id = [dev_id] - elif dev_id is None: - dev_id = [0] - - if update_interval is None: - update_interval = 50_000 - - if not torch.cuda.is_available(): - raise Exception("CUDA not available") - - limit = int(math.pow(2, 256)) - 1 - - # Set mp start to use spawn so CUDA doesn't complain - with UsingSpawnStartMethod(force=True): - curr_block, curr_block_num, curr_diff = CUDASolver.create_shared_memory() - - # Create a worker per CUDA device - num_processes = len(dev_id) - - # Establish communication queues - stopEvent = mp.Event() - stopEvent.clear() - solution_queue = mp.Queue() - finished_queues = [mp.Queue() for _ in range(num_processes)] - check_block = mp.Lock() - - hotkey_bytes = wallet.hotkey.public_key - # Start workers - solvers = [ - CUDASolver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - dev_id[i], - tpb, - ) - for i in range(num_processes) - ] - - # Get first block - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - - block_bytes = bytes.fromhex(block_hash[2:]) - old_block_number = block_number - - # Set to current block - update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - - # Set new block events for each solver to start at the initial block - for worker in solvers: - worker.newBlockEvent.set() - - for worker in solvers: - worker.start() # start the solver processes - - start_time = time.time() # time that the registration started - time_last = start_time # time that the last work blocks completed - - curr_stats = RegistrationStatistics( - time_spent_total=0.0, - time_average=0.0, - rounds_total=0, - time_spent=0.0, - hash_rate_perpetual=0.0, - hash_rate=0.0, # EWMA hash_rate (H/s) - difficulty=difficulty, - block_number=block_number, - block_hash=block_hash, - ) - - start_time_perpetual = time.time() - - logger = RegistrationStatisticsLogger(output_in_place=output_in_place) - logger.start() - - hash_rates = [0] * n_samples # The last n true hash_rates - weights = [alpha_**i for i in range(n_samples)] # weights decay by alpha - - solution = None - while netuid == -1 or not subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ): - # Wait until a solver finds a solution - try: - solution = solution_queue.get(block=True, timeout=0.15) - if solution is not None: - break - except Empty: - # No solution found, try again - pass - - # check for new block - old_block_number = _check_for_newest_block_and_update( - subtensor=subtensor, - netuid=netuid, - hotkey_bytes=hotkey_bytes, - curr_diff=curr_diff, - curr_block=curr_block, - curr_block_num=curr_block_num, - old_block_number=old_block_number, - curr_stats=curr_stats, - update_curr_block_=update_curr_block, - check_block=check_block, - solvers=solvers, - ) - - num_time = 0 - # Get times for each solver - for finished_queue in finished_queues: - try: - finished_queue.get(timeout=0.1) - num_time += 1 - - except Empty: - continue - - time_now = time.time() # get current time - time_since_last = time_now - time_last # get time since last work block(s) - if num_time > 0 and time_since_last > 0.0: - # create EWMA of the hash_rate to make measure more robust - - hash_rate_ = (num_time * tpb * update_interval) / time_since_last - hash_rates.append(hash_rate_) - hash_rates.pop(0) # remove the 0th data point - curr_stats.hash_rate = sum( - [hash_rates[i] * weights[i] for i in range(n_samples)] - ) / (sum(weights)) - - # update time last to now - time_last = time_now - - curr_stats.time_average = ( - curr_stats.time_average * curr_stats.rounds_total - + curr_stats.time_spent - ) / (curr_stats.rounds_total + num_time) - curr_stats.rounds_total += num_time - - # Update stats - curr_stats.time_spent = time_since_last - new_time_spent_total = time_now - start_time_perpetual - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * (tpb * update_interval) - ) / new_time_spent_total - curr_stats.time_spent_total = new_time_spent_total - - # Update the logger - logger.update(curr_stats, verbose=log_verbose) - - # exited while, found_solution contains the nonce or wallet is registered - - stopEvent.set() # stop all other processes - logger.stop() - - # terminate and wait for all solvers to exit - terminate_workers_and_wait_for_exit(solvers) - - return solution - - -def terminate_workers_and_wait_for_exit( - workers: list[Union[mp.Process, QueueType]], -) -> None: - for worker in workers: - if isinstance(worker, QueueType): - worker.join_thread() - else: - try: - worker.join(3.0) - except subprocess.TimeoutExpired: - worker.terminate() - try: - worker.close() - except ValueError: - worker.terminate() - - -def create_pow( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, -) -> Optional["POWSolution"]: - """ - Creates a proof of work for the given subtensor and wallet. - - Parameters: - subtensor: The Subtensor instance. - wallet: The Bittensor Wallet instance. - netuid: The netuid for the subnet to create a proof of work for. - output_in_place: If true, prints the progress of the proof of work to the console in-place. Meaning the progress - is printed on the same lines. - cuda: If true, uses CUDA to solve the proof of work. - dev_id: The CUDA device id(s) to use. If cuda is true and dev_id is a list, then multiple CUDA devices will be - used to solve the proof of work. - tpb: The number of threads per block to use when solving the proof of work. Should be a multiple of 32. - num_processes: The number of processes to use when solving the proof of work. If None, then the number of - processes is equal to the number of CPU cores. - update_interval: The number of nonces to run before checking for a new block. - log_verbose: If true, prints the progress of the proof of work more verbosely. - - Returns: - The proof of work solution or None if the wallet is already registered or there is a different error. - - Raises: - ValueError: If the subnet does not exist. - """ - if netuid != -1: - if not subtensor.subnet_exists(netuid=netuid): - raise ValueError(f"Subnet {netuid} does not exist.") - - if cuda: - logging.debug("Solve difficulty with CUDA.") - solution: Optional[POWSolution] = _solve_for_difficulty_fast_cuda( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - dev_id=dev_id, - tpb=tpb, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - logging.debug("Solve difficulty.") - solution: Optional[POWSolution] = _solve_for_difficulty_fast( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - output_in_place=output_in_place, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - return solution diff --git a/bittensor/utils/registration/register_cuda.py b/bittensor/utils/registration/register_cuda.py deleted file mode 100644 index 34c8e00115..0000000000 --- a/bittensor/utils/registration/register_cuda.py +++ /dev/null @@ -1,123 +0,0 @@ -"""This module provides functions for solving Proof of Work (PoW) problems using CUDA.""" - -import binascii -import hashlib -import io -from contextlib import redirect_stdout -from typing import Any, Union - -import numpy as np -from Crypto.Hash import keccak - - -def _hex_bytes_to_u8_list(hex_bytes: bytes) -> list[int]: - """ - Convert a sequence of bytes in hexadecimal format to a list of - unsigned 8-bit integers. - - Parameters: - hex_bytes: A sequence of bytes in hexadecimal format. - - Returns: - A list of unsigned 8-bit integers. - - """ - return [int(hex_bytes[i : i + 2], 16) for i in range(0, len(hex_bytes), 2)] - - -def _create_seal_hash(block_and_hotkey_hash_hex_: bytes, nonce: int) -> bytes: - """Creates a seal hash from the block and hotkey hash and nonce.""" - nonce_bytes = binascii.hexlify(nonce.to_bytes(8, "little")) - pre_seal = nonce_bytes + block_and_hotkey_hash_hex_ - seal_sh256 = hashlib.sha256(bytearray(_hex_bytes_to_u8_list(pre_seal))).digest() - kec = keccak.new(digest_bits=256) - return kec.update(seal_sh256).digest() - - -def _seal_meets_difficulty(seal_: bytes, difficulty: int, limit: int) -> bool: - """Checks if the seal meets the given difficulty.""" - seal_number = int.from_bytes(seal_, "big") - product = seal_number * difficulty - # limit = int(math.pow(2, 256)) - 1 - return product < limit - - -def solve_cuda( - nonce_start: "np.int64", - update_interval: "np.int64", - tpb: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - dev_id: int = 0, -) -> Union[tuple[Any, bytes], tuple[int, bytes], tuple[Any, None]]: - """ - Solves the PoW problem using CUDA. - - Parameters: - nonce_start: Starting nonce. - update_interval: Number of nonces to solve before updating block information. - tpb: Threads per block. - block_and_hotkey_hash_bytes: Keccak(Bytes of the block hash + bytes of the hotkey) 64 bytes. - difficulty: Difficulty of the PoW problem. - limit: Upper limit of the nonce. - dev_id: The CUDA device ID. - - Returns: - Tuple of the nonce and the seal corresponding to the solution. Returns -1 for nonce if no solution is found. - """ - - try: - import cubit - except ImportError: - raise ImportError( - "Please install cubit. See the instruction https://github.com/opentensor/cubit?tab=readme-ov-file#install." - ) - - upper = int(limit // difficulty) - - upper_bytes = upper.to_bytes(32, byteorder="little", signed=False) - - # Call cython function - # int blockSize, uint64 nonce_start, uint64 update_interval, const unsigned char[:] limit, - # const unsigned char[:] block_bytes, int dev_id - block_and_hotkey_hash_hex = binascii.hexlify(block_and_hotkey_hash_bytes)[:64] - - solution = cubit.solve_cuda( - tpb, - nonce_start, - update_interval, - upper_bytes, - block_and_hotkey_hash_hex, - dev_id, - ) # 0 is first GPU - seal = None - if solution != -1: - seal = _create_seal_hash(block_and_hotkey_hash_hex, solution) - if _seal_meets_difficulty(seal, difficulty, limit): - return solution, seal - else: - return -1, b"\x00" * 32 - return solution, seal - - -def reset_cuda(): - """Resets the CUDA environment.""" - try: - import cubit - except ImportError: - raise ImportError("Please install cubit") - cubit.reset_cuda() - - -def log_cuda_errors() -> str: - """Logs any CUDA errors.""" - try: - import cubit - except ImportError: - raise ImportError("Please install cubit") - - file = io.StringIO() - with redirect_stdout(file): - cubit.log_cuda_errors() - return file.getvalue() diff --git a/bittensor/utils/registration/torch_utils.py b/bittensor/utils/registration/torch_utils.py new file mode 100644 index 0000000000..38fd9131a1 --- /dev/null +++ b/bittensor/utils/registration/torch_utils.py @@ -0,0 +1,82 @@ +"""Torch compatibility utilities for Bittensor.""" + +import functools +import os +from typing import TYPE_CHECKING + +import numpy + +from bittensor.utils.btlogging import logging + + +def use_torch() -> bool: + """Force the use of torch over numpy for certain operations.""" + return True if os.getenv("USE_TORCH") == "1" else False + + +def legacy_torch_api_compat(func): + """ + Convert function operating on numpy Input&Output to legacy torch Input&Output API if `use_torch()` is True. + + Parameters: + func: Function with numpy Input/Output to be decorated. + + Returns: + decorated: Decorated function. + """ + + @functools.wraps(func) + def decorated(*args, **kwargs): + if use_torch(): + args = [ + arg.cpu().numpy() if isinstance(arg, torch.Tensor) else arg + for arg in args + ] + kwargs = { + key: value.cpu().numpy() if isinstance(value, torch.Tensor) else value + for key, value in kwargs.items() + } + ret = func(*args, **kwargs) + if use_torch(): + if isinstance(ret, numpy.ndarray): + ret = torch.from_numpy(ret) + return ret + + return decorated + + +@functools.cache +def _get_real_torch(): + try: + import torch as _real_torch + except ImportError: + _real_torch = None + return _real_torch + + +def log_no_torch_error(): + logging.error( + "This command requires torch. You can install torch for bittensor" + ' with `pip install bittensor[torch]` or `pip install ".[torch]"`' + " if installing from source, and then run the command with USE_TORCH=1 {command}" + ) + + +class LazyLoadedTorch: + """A lazy-loading proxy for the torch module.""" + + def __bool__(self): + return bool(_get_real_torch()) + + def __getattr__(self, name): + if real_torch := _get_real_torch(): + return getattr(real_torch, name) + else: + log_no_torch_error() + raise ImportError("torch not installed") + + +if TYPE_CHECKING: + import torch +else: + torch = LazyLoadedTorch() diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index 0247f11d7d..b5d58bb37c 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -13,7 +13,7 @@ ) from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging -from bittensor.utils.registration.pow import LazyLoadedTorch +from bittensor.utils.registration.torch_utils import LazyLoadedTorch from tests.e2e_tests.utils import ( AdminUtils, NETUID, diff --git a/tests/e2e_tests/test_register_limit.py b/tests/e2e_tests/test_register_limit.py index adc8ad8234..cfcb8b4090 100644 --- a/tests/e2e_tests/test_register_limit.py +++ b/tests/e2e_tests/test_register_limit.py @@ -103,3 +103,40 @@ async def test_register_limit_price_exceeded_async( assert not result.success, ( "register_limit should fail with limit_price=1 (below burn)" ) + + +def test_register_auto_limit_price(subtensor, alice_wallet, bob_wallet): + """Tests successful registration via register() with auto-calculated limit_price.""" + alice_sn = TestSubnet(subtensor) + alice_sn.execute_steps( + [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + logging.console.info( + f"Registering Bob with register (auto limit_price) on SN #{alice_sn.netuid}" + ) + result = subtensor.subnets.register(bob_wallet, alice_sn.netuid) + assert result.success, "register should succeed with auto-calculated limit_price" + + +@pytest.mark.asyncio +async def test_register_auto_limit_price_async( + async_subtensor, alice_wallet, bob_wallet +): + """Tests successful async registration via register() with auto-calculated limit_price.""" + alice_sn = TestSubnet(async_subtensor) + await alice_sn.async_execute_steps( + [ + REGISTER_SUBNET(alice_wallet), + ACTIVATE_SUBNET(alice_wallet), + ] + ) + + logging.console.info( + f"Registering Bob with register (auto limit_price) on SN #{alice_sn.netuid}" + ) + result = await async_subtensor.subnets.register(bob_wallet, alice_sn.netuid) + assert result.success, "register should succeed with auto-calculated limit_price" diff --git a/tests/unit_tests/extrinsics/asyncex/test_registration.py b/tests/unit_tests/extrinsics/asyncex/test_registration.py index 30a109024b..7e842b7173 100644 --- a/tests/unit_tests/extrinsics/asyncex/test_registration.py +++ b/tests/unit_tests/extrinsics/asyncex/test_registration.py @@ -5,306 +5,6 @@ from bittensor.core.extrinsics.asyncex import registration as async_registration -@pytest.mark.asyncio -async def test_register_extrinsic_success(subtensor, fake_wallet, mocker): - """Tests successful registration.""" - # Preps - fake_wallet.hotkey.ss58_address = "hotkey_ss58" - fake_wallet.coldkey.ss58_address = "coldkey_ss58" - - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=True - ) - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=True), - ) - mocked_create_pow = mocker.patch.object( - async_registration, - "create_pow_async", - return_value=mocker.Mock( - is_stale_async=mocker.AsyncMock(return_value=False), seal=[] - ), - ) - mocked_compose_call = mocker.patch.object(subtensor, "compose_call") - mocked_sign_and_send_extrinsic = mocker.patch.object( - subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") - ) - mocked_is_hotkey_registered = mocker.patch.object( - subtensor, "is_hotkey_registered", return_value=True - ) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=1, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - 1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_get_neuron.assert_called_once_with( - hotkey_ss58="hotkey_ss58", - netuid=1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_create_pow.assert_called_once() - mocked_sign_and_send_extrinsic.assert_called_once_with( - call=mocked_compose_call.return_value, - wallet=fake_wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - period=None, - raise_error=False, - ) - mocked_is_hotkey_registered.assert_called_once_with( - netuid=1, hotkey_ss58="hotkey_ss58" - ) - - assert result[0] - - -@pytest.mark.asyncio -async def test_register_extrinsic_success_with_cuda(subtensor, fake_wallet, mocker): - """Tests successful registration with CUDA enabled.""" - # Preps - fake_wallet.hotkey.ss58_address = "hotkey_ss58" - fake_wallet.coldkey.ss58_address = "coldkey_ss58" - - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=True - ) - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=True), - ) - mocker.patch("torch.cuda.is_available", return_value=True) - mocked_create_pow = mocker.patch.object( - async_registration, - "create_pow_async", - return_value=mocker.Mock( - is_stale_async=mocker.AsyncMock(return_value=False), seal=[] - ), - ) - mocked_compose_call = mocker.patch.object(subtensor, "compose_call") - mocked_sign_and_send_extrinsic = mocker.patch.object( - subtensor, "sign_and_send_extrinsic", return_value=ExtrinsicResponse(True, "") - ) - mocked_is_hotkey_registered = mocker.patch.object( - subtensor, "is_hotkey_registered", return_value=True - ) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=1, - cuda=True, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - 1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_get_neuron.assert_called_once_with( - hotkey_ss58="hotkey_ss58", - netuid=1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_create_pow.assert_called_once() - mocked_sign_and_send_extrinsic.assert_called_once_with( - call=mocked_compose_call.return_value, - wallet=fake_wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - period=None, - raise_error=False, - ) - mocked_is_hotkey_registered.assert_called_once_with( - netuid=1, hotkey_ss58="hotkey_ss58" - ) - assert result[0] - - -@pytest.mark.asyncio -async def test_register_extrinsic_failed_with_cuda(subtensor, fake_wallet, mocker): - """Tests failed registration with CUDA enabled.""" - # Preps - fake_wallet.hotkey.ss58_address = "hotkey_ss58" - fake_wallet.coldkey.ss58_address = "coldkey_ss58" - - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=True - ) - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=True), - ) - mocker.patch("torch.cuda.is_available", return_value=False) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=1, - cuda=True, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - 1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_get_neuron.assert_called_once_with( - hotkey_ss58="hotkey_ss58", - netuid=1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - assert result == ExtrinsicResponse( - False, - "CUDA not available.", - extrinsic_function="register_extrinsic", - ) - - -@pytest.mark.asyncio -async def test_register_extrinsic_subnet_not_exists(subtensor, fake_wallet, mocker): - """Tests registration when subnet does not exist.""" - # Preps - netuid = 14 - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=False - ) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=netuid, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - netuid, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - assert result == ExtrinsicResponse( - False, - f"Subnet {netuid} does not exist.", - extrinsic_function="register_extrinsic", - ) - - -@pytest.mark.asyncio -async def test_register_extrinsic_already_registered(subtensor, fake_wallet, mocker): - """Tests registration when the key is already registered.""" - # Preps - netuid = 14 - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=False), - ) - - # Call - success, message = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=netuid, - ) - - # Asserts - mocked_get_neuron.assert_called_once_with( - hotkey_ss58=fake_wallet.hotkey.ss58_address, - netuid=netuid, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - assert success is True - assert message == "Already registered." - - -@pytest.mark.asyncio -async def test_register_extrinsic_max_attempts_reached(subtensor, fake_wallet, mocker): - # Preps - fake_wallet.hotkey.ss58_address = "hotkey_ss58" - fake_wallet.coldkey.ss58_address = "coldkey_ss58" - - stale_responses = iter([False, False, False, True]) - - async def is_stale_side_effect(*_, **__): - return next(stale_responses, True) - - fake_pow_result = mocker.Mock() - fake_pow_result.is_stale_async = mocker.AsyncMock(side_effect=is_stale_side_effect) - fake_pow_result.seal = [] - - mocked_subnet_exists = mocker.patch.object( - subtensor, "subnet_exists", return_value=True - ) - mocked_get_neuron = mocker.patch.object( - subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.Mock(is_null=True), - ) - mocked_create_pow = mocker.patch.object( - async_registration, - "create_pow_async", - return_value=fake_pow_result, - ) - mocked_compose_call = mocker.patch.object(subtensor, "compose_call") - mocked_sign_and_send_extrinsic = mocker.patch.object( - subtensor, - "sign_and_send_extrinsic", - return_value=ExtrinsicResponse(False, "Test Error"), - ) - - # Call - result = await async_registration.register_extrinsic( - subtensor=subtensor, - wallet=fake_wallet, - netuid=1, - max_allowed_attempts=3, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # Asserts - mocked_subnet_exists.assert_called_once_with( - 1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - mocked_get_neuron.assert_called_once_with( - hotkey_ss58="hotkey_ss58", - netuid=1, - block_hash=subtensor.substrate.get_chain_head.return_value, - ) - assert mocked_create_pow.call_count == 3 - assert mocked_sign_and_send_extrinsic.call_count == 3 - mocked_sign_and_send_extrinsic.assert_called_with( - call=mocked_compose_call.return_value, - wallet=fake_wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - period=None, - raise_error=False, - ) - assert result[0] is False - assert result[1] == "No more attempts." - - @pytest.mark.asyncio async def test_set_subnet_identity_extrinsic_is_success(subtensor, fake_wallet, mocker): """Verify that set_subnet_identity_extrinsic calls the correct functions and returns the correct result.""" diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py index 0ffb73c5fe..631b46b6e6 100644 --- a/tests/unit_tests/extrinsics/test_registration.py +++ b/tests/unit_tests/extrinsics/test_registration.py @@ -4,7 +4,6 @@ from bittensor.core.extrinsics import registration from bittensor.core.subtensor import Subtensor from bittensor.utils.balance import Balance -from bittensor.utils.registration import POWSolution # Mocking external dependencies @@ -26,171 +25,6 @@ def mock_wallet(mocker): return mock -@pytest.fixture -def mock_pow_solution(mocker): - mock = mocker.MagicMock(spec=POWSolution) - mock.block_number = 123 - mock.nonce = 456 - mock.seal = [0, 1, 2, 3] - mock.is_stale.return_value = False - return mock - - -@pytest.fixture -def mock_new_wallet(mocker): - mock = mocker.MagicMock(spec=Wallet) - mock.coldkeypub.ss58_address = "mock_address" - mock.coldkey = mocker.MagicMock() - mock.hotkey = mocker.MagicMock() - return mock - - -@pytest.mark.parametrize( - "subnet_exists, neuron_is_null, cuda_available, expected_result, expected_message", - [ - ( - False, - True, - True, - False, - "Subnet 123 does not exist.", - ), - (True, False, True, True, "Already registered."), - (True, True, False, False, "CUDA not available."), - ], - ids=["subnet-does-not-exist", "neuron-already-registered", "cuda-unavailable"], -) -def test_register_extrinsic_without_pow( - mock_subtensor, - mock_wallet, - mocker, - subnet_exists, - neuron_is_null, - cuda_available, - expected_result, - expected_message, -): - # Arrange - mocker.patch.object(mock_subtensor, "subnet_exists", return_value=subnet_exists) - fake_neuron = mocker.patch.object( - mock_subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=mocker.MagicMock(is_null=neuron_is_null), - ) - mocker.patch("torch.cuda.is_available", return_value=cuda_available) - mocker.patch( - "bittensor.utils.registration.pow._get_block_with_retry", - return_value=(0, 0, "00ff11ee"), - ) - - # Act - result = registration.register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - wait_for_inclusion=True, - wait_for_finalization=True, - max_allowed_attempts=3, - output_in_place=True, - cuda=True, - dev_id=0, - tpb=256, - num_processes=None, - update_interval=None, - log_verbose=False, - ) - - # Assert - data = ( - {"neuron": fake_neuron.return_value} - if fake_neuron.call_count > 0 and cuda_available - else None - ) - expected_result = ExtrinsicResponse( - expected_result, - expected_message, - extrinsic_function="register_extrinsic", - data=data, - ) - assert result == expected_result - - -@pytest.mark.parametrize( - "pow_success, pow_stale, registration_success, cuda, hotkey_registered, expected_result", - [ - (True, False, True, False, False, True), - (True, False, True, True, False, True), - # Pow failed but key was registered already - (False, False, False, False, True, True), - # Pow was a success but registration failed with error 'key already registered' - (True, False, False, False, False, False), - ], - ids=[ - "successful-with-valid-pow", - "successful-with-valid-cuda-pow", - "hotkey-registered", - "registration-fail-key-registered", - ], -) -def test_register_extrinsic_with_pow( - mock_subtensor, - mock_wallet, - mock_pow_solution, - pow_success, - pow_stale, - registration_success, - cuda, - hotkey_registered, - expected_result, - mocker, -): - # Arrange - mocker.patch( - "bittensor.utils.registration.pow._solve_for_difficulty_fast", - return_value=mock_pow_solution if pow_success else None, - ) - mocker.patch( - "bittensor.utils.registration.pow._solve_for_difficulty_fast_cuda", - return_value=mock_pow_solution if pow_success else None, - ) - mocker.patch.object( - mock_subtensor, - "sign_and_send_extrinsic", - return_value=ExtrinsicResponse( - registration_success, "HotKeyAlreadyRegisteredInSubNet" - ), - ) - mocker.patch("torch.cuda.is_available", return_value=cuda) - - # Act - if pow_success: - mock_pow_solution.is_stale.return_value = pow_stale - - if not pow_success and hotkey_registered: - mock_subtensor.is_hotkey_registered = mocker.MagicMock( - return_value=hotkey_registered - ) - - result = registration.register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - wait_for_inclusion=True, - wait_for_finalization=True, - max_allowed_attempts=3, - output_in_place=True, - cuda=cuda, - dev_id=0, - tpb=256, - num_processes=None, - update_interval=None, - log_verbose=False, - ) - - # Assert - assert result[0] is expected_result - - @pytest.mark.parametrize( "subnet_exists, neuron_is_null, recycle_success, is_registered, expected_result, test_id", [ diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py index 6ab924869d..aba3085fc2 100644 --- a/tests/unit_tests/test_async_subtensor.py +++ b/tests/unit_tests/test_async_subtensor.py @@ -2504,39 +2504,53 @@ async def test_transfer_success(subtensor, fake_wallet, mocker): @pytest.mark.asyncio async def test_register_success(subtensor, fake_wallet, mocker): - """Tests register when there is enough balance and registration succeeds.""" + """Tests register with auto-calculated limit_price from recycle.""" # Preps fake_netuid = 1 - mocked_register_extrinsic = mocker.AsyncMock() + mocked_register_limit_extrinsic = mocker.AsyncMock() mocker.patch.object( - async_subtensor, "register_extrinsic", mocked_register_extrinsic + async_subtensor, "register_limit_extrinsic", mocked_register_limit_extrinsic + ) + mocker.patch.object( + subtensor, "recycle", return_value=Balance.from_rao(1_000_000_000) ) # Call result = await subtensor.register(wallet=fake_wallet, netuid=fake_netuid) # Asserts - mocked_register_extrinsic.assert_awaited_once_with( + mocked_register_limit_extrinsic.assert_awaited_once_with( + subtensor=subtensor, wallet=fake_wallet, - cuda=False, - dev_id=0, - log_verbose=False, - max_allowed_attempts=3, netuid=1, - num_processes=None, - output_in_place=False, - subtensor=subtensor, - tpb=256, - update_interval=None, + limit_price=Balance.from_rao(1_005_000_000), mev_protection=DEFAULT_MEV_PROTECTION, period=DEFAULT_PERIOD, raise_error=False, - wait_for_finalization=True, wait_for_inclusion=True, + wait_for_finalization=True, wait_for_revealed_execution=True, ) - assert result == mocked_register_extrinsic.return_value + assert result == mocked_register_limit_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_register_on_root(mock_substrate, subtensor, fake_wallet, mocker): + mock_substrate.submit_extrinsic.return_value = mocker.AsyncMock( + is_success=mocker.AsyncMock(return_value=True)(), + ) + mocked_root_register_extrinsic = mocker.patch.object( + async_subtensor, + "root_register_extrinsic", + ) + + response = await subtensor.register( + wallet=fake_wallet, + netuid=0, + ) + + assert response == mocked_root_register_extrinsic.return_value @pytest.mark.asyncio diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index d08f4c41fa..0b19bbfec5 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -6293,3 +6293,51 @@ def test_dispute_coldkey_swap(mocker, subtensor): wait_for_revealed_execution=True, ) assert response == mocked_dispute_coldkey_swap_extrinsic.return_value + + +def test_register_success(subtensor, fake_wallet, mocker): + """Tests register with auto-calculated limit_price from recycle.""" + # Preps + fake_netuid = 1 + + mocked_register_limit_extrinsic = mocker.patch.object( + subtensor_module, "register_limit_extrinsic" + ) + mocker.patch.object( + subtensor, "recycle", return_value=Balance.from_rao(1_000_000_000) + ) + + # Call + result = subtensor.register(wallet=fake_wallet, netuid=fake_netuid) + + # Asserts + mocked_register_limit_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=1, + limit_price=Balance.from_rao(1_005_000_000), + mev_protection=DEFAULT_MEV_PROTECTION, + period=DEFAULT_PERIOD, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + wait_for_revealed_execution=True, + ) + assert result == mocked_register_limit_extrinsic.return_value + + +def test_register_on_root(mock_substrate, subtensor, fake_wallet, mocker): + mock_substrate.submit_extrinsic.return_value = MagicMock( + is_success=True, + ) + mocked_root_register_extrinsic = mocker.patch.object( + subtensor_module, + "root_register_extrinsic", + ) + + response = subtensor.register( + wallet=fake_wallet, + netuid=0, + ) + + assert response == mocked_root_register_extrinsic.return_value diff --git a/tests/unit_tests/utils/test_registration.py b/tests/unit_tests/utils/test_registration.py index a4ec066279..4b9f6ab3eb 100644 --- a/tests/unit_tests/utils/test_registration.py +++ b/tests/unit_tests/utils/test_registration.py @@ -14,7 +14,7 @@ def error(self, message): @pytest.fixture def mock_bittensor_logging(monkeypatch): mock_logger = MockBittensorLogging() - monkeypatch.setattr("bittensor.utils.registration.pow.logging", mock_logger) + monkeypatch.setattr("bittensor.utils.registration.torch_utils.logging", mock_logger) return mock_logger @@ -32,7 +32,7 @@ def test_lazy_loaded_torch__torch_installed(monkeypatch, mock_bittensor_logging) def test_lazy_loaded_torch__no_torch(monkeypatch, mock_bittensor_logging): monkeypatch.setattr( - "bittensor.utils.registration.pow._get_real_torch", lambda: None + "bittensor.utils.registration.torch_utils._get_real_torch", lambda: None ) torch = LazyLoadedTorch()