diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 75606c306..9df39d24e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -875,6 +875,12 @@ def __init__(self): self.subnets_app.command( "get-identity", rich_help_panel=HELP_PANELS["SUBNETS"]["IDENTITY"] )(self.subnets_get_identity) + self.subnets_app.command( + "start", rich_help_panel=HELP_PANELS["SUBNETS"]["CREATION"] + )(self.subnets_start) + self.subnets_app.command( + "check-start", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] + )(self.subnets_check_start) # weights commands self.weights_app.command( @@ -4979,6 +4985,70 @@ def subnets_create( ) ) + def subnets_check_start( + self, + network: Optional[list[str]] = Options.network, + netuid: int = Options.netuid, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Checks if a subnet's emission schedule can be started. + + This command verifies if a subnet's emission schedule can be started based on the subnet's registration block. + + Example: + [green]$[/green] btcli subnets check_start --netuid 1 + """ + self.verbosity_handler(quiet, verbose) + return self._run_command( + subnets.get_start_schedule(self.initialize_chain(network), netuid) + ) + + def subnets_start( + self, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + network: Optional[list[str]] = Options.network, + netuid: int = Options.netuid, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Starts a subnet's emission schedule. + + The owner of the subnet must call this command to start the emission schedule. + + Example: + [green]$[/green] btcli subnets start --netuid 1 + [green]$[/green] btcli subnets start --netuid 1 --wallet-name alice + """ + self.verbosity_handler(quiet, verbose) + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue] [dim](which you used to create the subnet)[/dim]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[ + WO.NAME, + ], + validate=WV.WALLET, + ) + return self._run_command( + subnets.start_subnet( + wallet, + self.initialize_chain(network), + netuid, + prompt, + ) + ) + def subnets_get_identity( self, network: Optional[list[str]] = Options.network, diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index f83a4ff18..5c508812c 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -38,6 +38,7 @@ class Constants: "test": "0x8f9cf856bf558a14440e75569c9e58594757048d7b3a84b5d25f6bd978263105", } delegates_detail_url = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" + emission_start_schedule = 7 * 24 * 60 * 60 / 12 # 7 days @dataclass diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index fac969791..f5eb939c3 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -10,7 +10,7 @@ from rich.table import Column, Table from rich import box -from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src import COLOR_PALETTE, Constants from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.registration import ( register_extrinsic, @@ -34,6 +34,7 @@ prompt_for_identity, get_subnet_name, unlock_key, + blocks_to_duration, json_console, ) @@ -2308,3 +2309,112 @@ async def get_identity( else: console.print(table) return identity + + +async def get_start_schedule( + subtensor: "SubtensorInterface", + netuid: int, +) -> None: + """Fetch and display existing emission schedule information.""" + + if not await subtensor.subnet_exists(netuid): + print_error(f"Subnet {netuid} does not exist.") + return None + + registration_block = await subtensor.query( + module="SubtensorModule", + storage_function="NetworkRegisteredAt", + params=[netuid], + ) + min_blocks_to_start = Constants.emission_start_schedule + current_block = await subtensor.substrate.get_block_number() + + potential_start_block = registration_block + min_blocks_to_start + if current_block < potential_start_block: + blocks_to_wait = potential_start_block - current_block + time_to_wait = blocks_to_duration(blocks_to_wait) + + console.print( + f"[blue]Subnet {netuid}[/blue]:\n" + f"[blue]Registered at:[/blue] {registration_block}\n" + f"[blue]Minimum start block:[/blue] {potential_start_block}\n" + f"[blue]Current block:[/blue] {current_block}\n" + f"[blue]Blocks remaining:[/blue] {blocks_to_wait}\n" + f"[blue]Time to wait:[/blue] {time_to_wait}" + ) + else: + console.print( + f"[blue]Subnet {netuid}[/blue]:\n" + f"[blue]Registered at:[/blue] {registration_block}\n" + f"[blue]Current block:[/blue] {current_block}\n" + f"[blue]Minimum start block:[/blue] {potential_start_block}\n" + f"[dark_sea_green3]Emission schedule can be started[/dark_sea_green3]" + ) + + return + + +async def start_subnet( + wallet: "Wallet", + subtensor: "SubtensorInterface", + netuid: int, + prompt: bool = False, +) -> bool: + """Start a subnet's emission schedule""" + + if not await subtensor.subnet_exists(netuid): + print_error(f"Subnet {netuid} does not exist.") + return False + + subnet_owner = await subtensor.query( + module="SubtensorModule", + storage_function="SubnetOwner", + params=[netuid], + ) + if subnet_owner != wallet.coldkeypub.ss58_address: + print_error(":cross_mark: This wallet doesn't own the specified subnet.") + return False + + if prompt: + if not Confirm.ask( + f"Are you sure you want to start subnet {netuid}'s emission schedule?" + ): + return False + + if not unlock_key(wallet).success: + return False + + with console.status( + f":satellite: Starting subnet {netuid}'s emission schedule...", spinner="earth" + ): + start_call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="start_call", + call_params={"netuid": netuid}, + ) + + signed_ext = await subtensor.substrate.create_signed_extrinsic( + call=start_call, + keypair=wallet.coldkey, + ) + + response = await subtensor.substrate.submit_extrinsic( + extrinsic=signed_ext, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + if await response.is_success: + console.print( + f":white_heavy_check_mark: [green]Successfully started subnet {netuid}'s emission schedule.[/green]" + ) + return True + else: + error_msg = format_error_message(await response.error_message) + if "FirstEmissionBlockNumberAlreadySet" in error_msg: + console.print(f"[dark_sea_green3]Subnet {netuid} already has an emission schedule.[/dark_sea_green3]") + return True + + await get_start_schedule(subtensor, netuid) + print_error(f":cross_mark: Failed to start subnet: {error_msg}") + return False diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index c9f99796c..02b25489c 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -1,8 +1,10 @@ +import asyncio import json import re from bittensor_cli.src.bittensor.balances import Balance +from btcli.tests.e2e_tests.utils import set_storage_extrinsic def test_unstaking(local_chain, wallet_setup): """ @@ -34,6 +36,19 @@ def test_unstaking(local_chain, wallet_setup): wallet_path_bob ) + # Call to make Alice root owner + items = [ + ( + bytes.fromhex("658faa385070e074c85bf6b568cf055536e3e82152c8758267395fe524fbbd160000"), + bytes.fromhex("d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d") + ) + ] + asyncio.run(set_storage_extrinsic( + local_chain, + wallet=wallet_alice, + items=items, + )) + # Create first subnet (netuid = 2) result = exec_command_alice( command="subnets", @@ -98,6 +113,65 @@ def test_unstaking(local_chain, wallet_setup): ) assert "✅ Registered subnetwork with netuid: 3" in result.stdout + # Start emission schedule for subnets + start_call_netuid_0 = exec_command_alice( + command="subnets", + sub_command="start", + extra_args=[ + "--netuid", + "0", + "--wallet-name", + wallet_alice.name, + "--no-prompt", + "--chain", + "ws://127.0.0.1:9945", + "--wallet-path", + wallet_path_alice, + ], + ) + assert ( + "Successfully started subnet 0's emission schedule." + in start_call_netuid_0.stdout + ) + start_call_netuid_2 = exec_command_alice( + command="subnets", + sub_command="start", + extra_args=[ + "--netuid", + "2", + "--wallet-name", + wallet_alice.name, + "--no-prompt", + "--chain", + "ws://127.0.0.1:9945", + "--wallet-path", + wallet_path_alice, + ], + ) + assert ( + "Successfully started subnet 2's emission schedule." + in start_call_netuid_2.stdout + ) + + start_call_netuid_3 = exec_command_alice( + command="subnets", + sub_command="start", + extra_args=[ + "--netuid", + "3", + "--wallet-name", + wallet_alice.name, + "--no-prompt", + "--chain", + "ws://127.0.0.1:9945", + "--wallet-path", + wallet_path_alice, + ], + ) + assert ( + "Successfully started subnet 3's emission schedule." + in start_call_netuid_3.stdout + ) # Register Bob in one subnet register_result = exec_command_bob( command="subnets", diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 1f2e3a2b0..07f2fbd93 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -300,3 +300,45 @@ async def call_add_proposal( ) return await response.is_success + + +async def set_storage_extrinsic( + substrate: "AsyncSubstrateInterface", + wallet: "Wallet", + items: list[tuple[bytes, bytes]], +) -> bool: + """Sets storage items using sudo permissions. + + Args: + subtensor: initialized SubtensorInterface object + wallet: bittensor wallet object with sudo permissions + items: List of (key, value) tuples where both key and value are bytes + + Returns: + bool: True if successful, False otherwise + """ + + storage_call = await substrate.compose_call( + call_module="System", call_function="set_storage", call_params={"items": items} + ) + + sudo_call = await substrate.compose_call( + call_module="Sudo", call_function="sudo", call_params={"call": storage_call} + ) + + extrinsic = await substrate.create_signed_extrinsic( + call=sudo_call, + keypair=wallet.coldkey, + ) + response = await substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + if not response: + print(response) + else: + print(":white_heavy_check_mark: [dark_sea_green_3]Success[/dark_sea_green_3]") + + return response