Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions bittensor_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions bittensor_cli/src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
112 changes: 111 additions & 1 deletion bittensor_cli/src/commands/subnets/subnets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -34,6 +34,7 @@
prompt_for_identity,
get_subnet_name,
unlock_key,
blocks_to_duration,
json_console,
)

Expand Down Expand Up @@ -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
74 changes: 74 additions & 0 deletions tests/e2e_tests/test_unstaking.py
Original file line number Diff line number Diff line change
@@ -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):
"""
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
42 changes: 42 additions & 0 deletions tests/e2e_tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading