Skip to content

Commit 84b64f4

Browse files
basfromanthewhaleking
authored andcommitted
added to Subtensor: burned_register, get_subnet_burn_cost, recycle and related extrinsics (#2359)
* added to Subtensor: `burned_register`, `get_subnet_burn_cost`, `recycle` and related extrinsics * formatter * Update bittensor/core/extrinsics/registration.py Co-authored-by: Benjamin Himes <[email protected]> --------- Co-authored-by: Benjamin Himes <[email protected]>
1 parent 68ce71d commit 84b64f4

File tree

4 files changed

+425
-6
lines changed

4 files changed

+425
-6
lines changed

bittensor/core/extrinsics/registration.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import time
1919
from typing import Union, Optional, TYPE_CHECKING
2020

21+
from bittensor_wallet.errors import KeyFileError
2122
from retry import retry
2223
from rich.prompt import Confirm
2324

@@ -285,3 +286,166 @@ def register_extrinsic(
285286
# Failed to register after max attempts.
286287
bt_console.print("[red]No more attempts.[/red]")
287288
return False
289+
290+
291+
@ensure_connected
292+
def _do_burned_register(
293+
self,
294+
netuid: int,
295+
wallet: "Wallet",
296+
wait_for_inclusion: bool = False,
297+
wait_for_finalization: bool = True,
298+
) -> tuple[bool, Optional[str]]:
299+
"""
300+
Performs a burned register extrinsic call to the Subtensor chain.
301+
302+
This method sends a registration transaction to the Subtensor blockchain using the burned register mechanism. It
303+
retries the call up to three times with exponential backoff in case of failures.
304+
305+
Args:
306+
self (bittensor.core.subtensor.Subtensor): Subtensor instance.
307+
netuid (int): The network unique identifier to register on.
308+
wallet (bittensor_wallet.Wallet): The wallet to be registered.
309+
wait_for_inclusion (bool): Whether to wait for the transaction to be included in a block. Default is False.
310+
wait_for_finalization (bool): Whether to wait for the transaction to be finalized. Default is True.
311+
312+
Returns:
313+
Tuple[bool, Optional[str]]: A tuple containing a boolean indicating success or failure, and an optional error message.
314+
"""
315+
316+
@retry(delay=1, tries=3, backoff=2, max_delay=4)
317+
def make_substrate_call_with_retry():
318+
# create extrinsic call
319+
call = self.substrate.compose_call(
320+
call_module="SubtensorModule",
321+
call_function="burned_register",
322+
call_params={
323+
"netuid": netuid,
324+
"hotkey": wallet.hotkey.ss58_address,
325+
},
326+
)
327+
extrinsic = self.substrate.create_signed_extrinsic(
328+
call=call, keypair=wallet.coldkey
329+
)
330+
response = self.substrate.submit_extrinsic(
331+
extrinsic,
332+
wait_for_inclusion=wait_for_inclusion,
333+
wait_for_finalization=wait_for_finalization,
334+
)
335+
336+
# We only wait here if we expect finalization.
337+
if not wait_for_finalization and not wait_for_inclusion:
338+
return True, None
339+
340+
# process if registration successful, try again if pow is still valid
341+
response.process_events()
342+
if not response.is_success:
343+
return False, format_error_message(response.error_message)
344+
# Successful registration
345+
else:
346+
return True, None
347+
348+
return make_substrate_call_with_retry()
349+
350+
351+
def burned_register_extrinsic(
352+
subtensor: "Subtensor",
353+
wallet: "Wallet",
354+
netuid: int,
355+
wait_for_inclusion: bool = False,
356+
wait_for_finalization: bool = True,
357+
prompt: bool = False,
358+
) -> bool:
359+
"""Registers the wallet to chain by recycling TAO.
360+
361+
Args:
362+
subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance.
363+
wallet (bittensor.wallet): Bittensor wallet object.
364+
netuid (int): The ``netuid`` of the subnet to register on.
365+
wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout.
366+
wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout.
367+
prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding.
368+
369+
Returns:
370+
success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``.
371+
"""
372+
if not subtensor.subnet_exists(netuid):
373+
bt_console.print(
374+
":cross_mark: [red]Failed[/red]: error: [bold white]subnet:{}[/bold white] does not exist.".format(
375+
netuid
376+
)
377+
)
378+
return False
379+
380+
try:
381+
wallet.unlock_coldkey()
382+
except KeyFileError:
383+
bt_console.print(
384+
":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]"
385+
)
386+
return False
387+
with bt_console.status(
388+
f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]..."
389+
):
390+
neuron = subtensor.get_neuron_for_pubkey_and_subnet(
391+
wallet.hotkey.ss58_address, netuid=netuid
392+
)
393+
394+
old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address)
395+
396+
recycle_amount = subtensor.recycle(netuid=netuid)
397+
if not neuron.is_null:
398+
bt_console.print(
399+
":white_heavy_check_mark: [green]Already Registered[/green]:\n"
400+
"uid: [bold white]{}[/bold white]\n"
401+
"netuid: [bold white]{}[/bold white]\n"
402+
"hotkey: [bold white]{}[/bold white]\n"
403+
"coldkey: [bold white]{}[/bold white]".format(
404+
neuron.uid, neuron.netuid, neuron.hotkey, neuron.coldkey
405+
)
406+
)
407+
return True
408+
409+
if prompt:
410+
# Prompt user for confirmation.
411+
if not Confirm.ask(f"Recycle {recycle_amount} to register on subnet:{netuid}?"):
412+
return False
413+
414+
with bt_console.status(":satellite: Recycling TAO for Registration..."):
415+
success, err_msg = _do_burned_register(
416+
self=subtensor,
417+
netuid=netuid,
418+
wallet=wallet,
419+
wait_for_inclusion=wait_for_inclusion,
420+
wait_for_finalization=wait_for_finalization,
421+
)
422+
423+
if not success:
424+
bt_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}")
425+
time.sleep(0.5)
426+
return False
427+
# Successful registration, final check for neuron and pubkey
428+
else:
429+
bt_console.print(":satellite: Checking Balance...")
430+
block = subtensor.get_current_block()
431+
new_balance = subtensor.get_balance(
432+
wallet.coldkeypub.ss58_address, block=block
433+
)
434+
435+
bt_console.print(
436+
"Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format(
437+
old_balance, new_balance
438+
)
439+
)
440+
is_registered = subtensor.is_hotkey_registered(
441+
netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address
442+
)
443+
if is_registered:
444+
bt_console.print(":white_heavy_check_mark: [green]Registered[/green]")
445+
return True
446+
else:
447+
# neuron not found, try again
448+
bt_console.print(
449+
":cross_mark: [red]Unknown error. Neuron not found.[/red]"
450+
)
451+
return False

bittensor/core/subtensor.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@
5454
do_serve_prometheus,
5555
prometheus_extrinsic,
5656
)
57-
from bittensor.core.extrinsics.registration import register_extrinsic
57+
from bittensor.core.extrinsics.registration import (
58+
burned_register_extrinsic,
59+
register_extrinsic,
60+
)
5861
from bittensor.core.extrinsics.serving import (
5962
do_serve_axon,
6063
serve_axon_extrinsic,
@@ -958,6 +961,36 @@ def register(
958961
log_verbose=log_verbose,
959962
)
960963

964+
def burned_register(
965+
self,
966+
wallet: "Wallet",
967+
netuid: int,
968+
wait_for_inclusion: bool = False,
969+
wait_for_finalization: bool = True,
970+
prompt: bool = False,
971+
) -> bool:
972+
"""
973+
Registers a neuron on the Bittensor network by recycling TAO. This method of registration involves recycling TAO tokens, allowing them to be re-mined by performing work on the network.
974+
975+
Args:
976+
wallet (bittensor_wallet.Wallet): The wallet associated with the neuron to be registered.
977+
netuid (int): The unique identifier of the subnet.
978+
wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Defaults to `False`.
979+
wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. Defaults to `True`.
980+
prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. Defaults to `False`.
981+
982+
Returns:
983+
bool: ``True`` if the registration is successful, False otherwise.
984+
"""
985+
return burned_register_extrinsic(
986+
subtensor=self,
987+
wallet=wallet,
988+
netuid=netuid,
989+
wait_for_inclusion=wait_for_inclusion,
990+
wait_for_finalization=wait_for_finalization,
991+
prompt=prompt,
992+
)
993+
961994
def serve_axon(
962995
self,
963996
netuid: int,
@@ -1412,6 +1445,30 @@ def bonds(
14121445

14131446
return b_map
14141447

1448+
def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[str]:
1449+
"""
1450+
Retrieves the burn cost for registering a new subnet within the Bittensor network. This cost represents the amount of Tao that needs to be locked or burned to establish a new subnet.
1451+
1452+
Args:
1453+
block (Optional[int]): The blockchain block number for the query.
1454+
1455+
Returns:
1456+
int: The burn cost for subnet registration.
1457+
1458+
The subnet burn cost is an important economic parameter, reflecting the network's mechanisms for controlling the proliferation of subnets and ensuring their commitment to the network's long-term viability.
1459+
"""
1460+
lock_cost = self.query_runtime_api(
1461+
runtime_api="SubnetRegistrationRuntimeApi",
1462+
method="get_network_registration_cost",
1463+
params=[],
1464+
block=block,
1465+
)
1466+
1467+
if lock_cost is None:
1468+
return None
1469+
1470+
return lock_cost
1471+
14151472
# Metagraph uses this method
14161473
def neurons(self, netuid: int, block: Optional[int] = None) -> list["NeuronInfo"]:
14171474
"""
@@ -1812,6 +1869,22 @@ def difficulty(self, netuid: int, block: Optional[int] = None) -> Optional[int]:
18121869
return None
18131870
return int(call)
18141871

1872+
def recycle(self, netuid: int, block: Optional[int] = None) -> Optional["Balance"]:
1873+
"""
1874+
Retrieves the 'Burn' hyperparameter for a specified subnet. The 'Burn' parameter represents the amount of Tao that is effectively recycled within the Bittensor network.
1875+
1876+
Args:
1877+
netuid (int): The unique identifier of the subnet.
1878+
block (Optional[int]): The blockchain block number for the query.
1879+
1880+
Returns:
1881+
Optional[Balance]: The value of the 'Burn' hyperparameter if the subnet exists, None otherwise.
1882+
1883+
Understanding the 'Burn' rate is essential for analyzing the network registration usage, particularly how it is correlated with user activity and the overall cost of participation in a given subnet.
1884+
"""
1885+
call = self._get_hyperparameter(param_name="Burn", netuid=netuid, block=block)
1886+
return None if call is None else Balance.from_rao(int(call))
1887+
18151888
# Subnet 27 uses this method
18161889
_do_serve_prometheus = do_serve_prometheus
18171890
# Subnet 27 uses this method name

tests/unit_tests/extrinsics/test_registration.py

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@
1818
import pytest
1919
from bittensor_wallet import Wallet
2020

21-
from bittensor.core.extrinsics.registration import (
22-
register_extrinsic,
23-
)
21+
from bittensor.core.extrinsics import registration
2422
from bittensor.core.subtensor import Subtensor
2523
from bittensor.utils.registration import POWSolution
2624

@@ -95,7 +93,7 @@ def test_register_extrinsic_without_pow(
9593
"rich.prompt.Confirm.ask", return_value=prompt_response
9694
), mocker.patch("torch.cuda.is_available", return_value=cuda_available):
9795
# Act
98-
result = register_extrinsic(
96+
result = registration.register_extrinsic(
9997
subtensor=mock_subtensor,
10098
wallet=mock_wallet,
10199
netuid=123,
@@ -160,7 +158,7 @@ def test_register_extrinsic_with_pow(
160158
return_value=hotkey_registered
161159
)
162160

163-
result = register_extrinsic(
161+
result = registration.register_extrinsic(
164162
subtensor=mock_subtensor,
165163
wallet=mock_wallet,
166164
netuid=123,
@@ -179,3 +177,57 @@ def test_register_extrinsic_with_pow(
179177

180178
# Assert
181179
assert result == expected_result, f"Test failed for test_id: {test_id}."
180+
181+
182+
@pytest.mark.parametrize(
183+
"subnet_exists, neuron_is_null, recycle_success, prompt, prompt_response, is_registered, expected_result, test_id",
184+
[
185+
# Happy paths
186+
(True, False, None, False, None, None, True, "neuron-not-null"),
187+
(True, True, True, True, True, True, True, "happy-path-wallet-registered"),
188+
# Error paths
189+
(False, True, False, False, None, None, False, "subnet-non-existence"),
190+
(True, True, True, True, False, None, False, "prompt-declined"),
191+
(True, True, False, True, True, False, False, "error-path-recycling-failed"),
192+
(True, True, True, True, True, False, False, "error-path-not-registered"),
193+
],
194+
)
195+
def test_burned_register_extrinsic(
196+
mock_subtensor,
197+
mock_wallet,
198+
subnet_exists,
199+
neuron_is_null,
200+
recycle_success,
201+
prompt,
202+
prompt_response,
203+
is_registered,
204+
expected_result,
205+
test_id,
206+
mocker,
207+
):
208+
# Arrange
209+
with mocker.patch.object(
210+
mock_subtensor, "subnet_exists", return_value=subnet_exists
211+
), mocker.patch.object(
212+
mock_subtensor,
213+
"get_neuron_for_pubkey_and_subnet",
214+
return_value=mocker.MagicMock(is_null=neuron_is_null),
215+
), mocker.patch(
216+
"bittensor.core.extrinsics.registration._do_burned_register",
217+
return_value=(recycle_success, "Mock error message"),
218+
), mocker.patch.object(
219+
mock_subtensor, "is_hotkey_registered", return_value=is_registered
220+
):
221+
mock_confirm = mocker.MagicMock(return_value=prompt_response)
222+
registration.Confirm.ask = mock_confirm
223+
# Act
224+
result = registration.burned_register_extrinsic(
225+
subtensor=mock_subtensor, wallet=mock_wallet, netuid=123, prompt=prompt
226+
)
227+
# Assert
228+
assert result == expected_result, f"Test failed for test_id: {test_id}"
229+
230+
if prompt:
231+
mock_confirm.assert_called_once()
232+
else:
233+
mock_confirm.assert_not_called()

0 commit comments

Comments
 (0)