Skip to content
37 changes: 8 additions & 29 deletions bittensor/core/async_subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2901,6 +2901,14 @@ async def burned_register(
bool: ``True`` if the registration is successful, False otherwise.
"""
async with self:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is unrelated to this PR, but why do we do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably to make sure Subtensor/Substrate is properly initialized?

if netuid == 0:
return await root_register_extrinsic(
subtensor=self,
wallet=wallet,
wait_for_inclusion=wait_for_inclusion,
wait_for_finalization=wait_for_finalization,
)

return await burned_register_extrinsic(
subtensor=self,
wallet=wallet,
Expand Down Expand Up @@ -3191,35 +3199,6 @@ async def root_register(
Returns:
`True` if registration was successful, otherwise `False`.
"""
netuid = 0
logging.info(
f"Registering on netuid [blue]0[/blue] on network: [blue]{self.network}[/blue]"
)

# Check current recycle amount
logging.info("Fetching recycle amount & balance.")
block_hash = block_hash if block_hash else await self.get_block_hash()

try:
recycle_call, balance = await asyncio.gather(
self.get_hyperparameter(
param_name="Burn", netuid=netuid, block_hash=block_hash
),
self.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash),
)
except TypeError as e:
logging.error(f"Unable to retrieve current recycle. {e}")
return False

current_recycle = Balance.from_rao(int(recycle_call))

# Check balance is sufficient
if balance < current_recycle:
logging.error(
f"[red]Insufficient balance {balance} to register neuron. "
f"Current recycle is {current_recycle} TAO[/red]."
)
return False

return await root_register_extrinsic(
subtensor=self,
Expand Down
33 changes: 33 additions & 0 deletions bittensor/core/extrinsics/asyncex/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from bittensor.core.errors import SubstrateRequestException
from bittensor.utils import u16_normalized_float, format_error_message, unlock_key
from bittensor.utils.balance import Balance
from bittensor.utils.btlogging import logging
from bittensor.utils.weight_utils import (
normalize_max_weight,
Expand Down Expand Up @@ -62,6 +63,38 @@ async def root_register_extrinsic(
the response is `True`.
"""
netuid = 0
logging.info(
f"Registering on netuid [blue]{netuid}[/blue] on network: [blue]{subtensor.network}[/blue]"
)

logging.info("Fetching recycle amount & balance.")
block_hash = await subtensor.get_block_hash()

try:
recycle_call, balance = await asyncio.gather(
subtensor.get_hyperparameter(
param_name="Burn",
netuid=netuid,
block_hash=block_hash,
),
subtensor.get_balance(
wallet.coldkeypub.ss58_address,
block_hash=block_hash,
),
)
except TypeError as e:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually a fairly difficult one. asyncio.gather returns errors in an odd non-standard way, where doing it like this can usually catch fine, but sometimes will not. You'll instead want to use asyncio.gather(*cos, return_exceptions=True) and then check to see if the returns are the exception type. It's not great, but it's the spec.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really understand. Can you provide an example of "non-catchable" exception thrown from asyncio.gather?

Copy link
Contributor Author

@zyzniewski-reef zyzniewski-reef Mar 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think TypeError very likely can be safely removed anyway (I've copied it from existing root_register method code) as it seems to be a leftover from this: 835dfdb#diff-87f2770377552e515f60ce0d2e31a3a9bb129bffa240ab2133e3233d3e22f6c3R1141-R1145

logging.error(f"Unable to retrieve current recycle. {e}")
return False

current_recycle = Balance.from_rao(int(recycle_call))

if balance < current_recycle:
logging.error(
f"[red]Insufficient balance {balance} to register neuron. "
f"Current recycle is {current_recycle} TAO[/red]."
)
return False

if not (unlock := unlock_key(wallet)).success:
logging.error(unlock.message)
return False
Expand Down
30 changes: 30 additions & 0 deletions bittensor/core/extrinsics/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
unlock_key,
torch,
)
from bittensor.utils.balance import Balance
from bittensor.utils.btlogging import logging
from bittensor.utils.weight_utils import (
normalize_max_weight,
Expand Down Expand Up @@ -65,6 +66,35 @@ def root_register_extrinsic(
response is `True`.
"""
netuid = 0
logging.info(
f"Registering on netuid [blue]{netuid}[/blue] on network: [blue]{subtensor.network}[/blue]"
)

logging.info("Fetching recycle amount & balance.")
block = subtensor.get_current_block()

try:
recycle_call = subtensor.get_hyperparameter(
param_name="Burn",
netuid=netuid,
block=block,
)
balance = subtensor.get_balance(
wallet.coldkeypub.ss58_address,
block=block,
)
except TypeError as e:
logging.error(f"Unable to retrieve current recycle. {e}")
return False

current_recycle = Balance.from_rao(int(recycle_call))

if balance < current_recycle:
logging.error(
f"[red]Insufficient balance {balance} to register neuron. "
f"Current recycle is {current_recycle} TAO[/red]."
)
return False

if not (unlock := unlock_key(wallet)).success:
logging.error(unlock.message)
Expand Down
35 changes: 9 additions & 26 deletions bittensor/core/subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2219,6 +2219,15 @@ def burned_register(
Returns:
bool: ``True`` if the registration is successful, False otherwise.
"""

if netuid == 0:
return root_register_extrinsic(
subtensor=self,
wallet=wallet,
wait_for_inclusion=wait_for_inclusion,
wait_for_finalization=wait_for_finalization,
)

return burned_register_extrinsic(
subtensor=self,
wallet=wallet,
Expand Down Expand Up @@ -2506,32 +2515,6 @@ def root_register(
Returns:
`True` if registration was successful, otherwise `False`.
"""
logging.info(
f"Registering on netuid [blue]0[/blue] on network: [blue]{self.network}[/blue]"
)

# Check current recycle amount
logging.info("Fetching recycle amount & balance.")
block = self.get_current_block()

try:
recycle_call = cast(
int, self.get_hyperparameter(param_name="Burn", netuid=0, block=block)
)
balance = self.get_balance(wallet.coldkeypub.ss58_address, block=block)
except TypeError as e:
logging.error(f"Unable to retrieve current recycle. {e}")
return False

current_recycle = Balance.from_rao(int(recycle_call))

# Check balance is sufficient
if balance < current_recycle:
logging.error(
f"[red]Insufficient balance {balance} to register neuron. "
f"Current recycle is {current_recycle} TAO[/red]."
)
return False

return root_register_extrinsic(
subtensor=self,
Expand Down
85 changes: 85 additions & 0 deletions tests/unit_tests/extrinsics/asyncex/test_root.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from bittensor.core.errors import SubstrateRequestException
from bittensor.core.extrinsics.asyncex import root as async_root

from bittensor.utils.balance import Balance


@pytest.mark.asyncio
async def test_get_limits_success(subtensor, mocker):
Expand Down Expand Up @@ -62,6 +64,16 @@ async def test_root_register_extrinsic_success(subtensor, fake_wallet, mocker):
"query",
return_value=fake_uid,
)
mocker.patch.object(
subtensor,
"get_hyperparameter",
return_value=Balance(0),
)
mocker.patch.object(
subtensor,
"get_balance",
return_value=Balance(1),
)

# Call
result = await async_root.root_register_extrinsic(
Expand All @@ -86,10 +98,53 @@ async def test_root_register_extrinsic_success(subtensor, fake_wallet, mocker):
assert result is True


@pytest.mark.asyncio
async def test_root_register_extrinsic_insufficient_balance(
subtensor,
fake_wallet,
mocker,
):
mocker.patch.object(
subtensor,
"get_hyperparameter",
return_value=Balance(1),
)
mocker.patch.object(
subtensor,
"get_balance",
return_value=Balance(0),
)

result = await async_root.root_register_extrinsic(
subtensor=subtensor,
wallet=fake_wallet,
wait_for_inclusion=True,
wait_for_finalization=True,
)

assert result is False

subtensor.get_balance.assert_called_once_with(
fake_wallet.coldkeypub.ss58_address,
block_hash=subtensor.substrate.get_chain_head.return_value,
)
subtensor.substrate.submit_extrinsic.assert_not_called()


@pytest.mark.asyncio
async def test_root_register_extrinsic_unlock_failed(subtensor, fake_wallet, mocker):
"""Tests registration fails due to unlock failure."""
# Preps
mocker.patch.object(
subtensor,
"get_hyperparameter",
return_value=Balance(0),
)
mocker.patch.object(
subtensor,
"get_balance",
return_value=Balance(1),
)
mocked_unlock_key = mocker.patch.object(
async_root,
"unlock_key",
Expand Down Expand Up @@ -117,6 +172,16 @@ async def test_root_register_extrinsic_already_registered(
# Preps
fake_wallet.hotkey.ss58_address = "fake_hotkey_address"

mocker.patch.object(
subtensor,
"get_hyperparameter",
return_value=Balance(0),
)
mocker.patch.object(
subtensor,
"get_balance",
return_value=Balance(1),
)
mocked_unlock_key = mocker.patch.object(
async_root,
"unlock_key",
Expand Down Expand Up @@ -152,6 +217,16 @@ async def test_root_register_extrinsic_transaction_failed(
# Preps
fake_wallet.hotkey.ss58_address = "fake_hotkey_address"

mocker.patch.object(
subtensor,
"get_hyperparameter",
return_value=Balance(0),
)
mocker.patch.object(
subtensor,
"get_balance",
return_value=Balance(1),
)
mocked_unlock_key = mocker.patch.object(
async_root,
"unlock_key",
Expand Down Expand Up @@ -193,6 +268,16 @@ async def test_root_register_extrinsic_uid_not_found(subtensor, fake_wallet, moc
# Preps
fake_wallet.hotkey.ss58_address = "fake_hotkey_address"

mocker.patch.object(
subtensor,
"get_hyperparameter",
return_value=Balance(0),
)
mocker.patch.object(
subtensor,
"get_balance",
return_value=Balance(1),
)
mocked_unlock_key = mocker.patch.object(
async_root,
"unlock_key",
Expand Down
31 changes: 31 additions & 0 deletions tests/unit_tests/extrinsics/test_root.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
from bittensor.core.subtensor import Subtensor
from bittensor.core.extrinsics import root
from bittensor.utils.balance import Balance


@pytest.fixture
Expand Down Expand Up @@ -75,6 +76,11 @@ def test_root_register_extrinsic(
"query",
return_value=hotkey_registered[1],
)
mocker.patch.object(
mock_subtensor,
"get_balance",
return_value=Balance(1),
)

# Act
result = root.root_register_extrinsic(
Expand All @@ -100,6 +106,31 @@ def test_root_register_extrinsic(
)


def test_root_register_extrinsic_insufficient_balance(
mock_subtensor,
mock_wallet,
mocker,
):
mocker.patch.object(
mock_subtensor,
"get_balance",
return_value=Balance(0),
)

success = root.root_register_extrinsic(
subtensor=mock_subtensor,
wallet=mock_wallet,
)

assert success is False

mock_subtensor.get_balance.assert_called_once_with(
mock_wallet.coldkeypub.ss58_address,
block=mock_subtensor.get_current_block.return_value,
)
mock_subtensor.substrate.submit_extrinsic.assert_not_called()


@pytest.mark.parametrize(
"wait_for_inclusion, wait_for_finalization, netuids, weights, expected_success",
[
Expand Down
Loading
Loading