Skip to content

Commit 93bf713

Browse files
authored
Merge pull request #434 from opentensor/feat/start-call
Feat/start call
2 parents 1e83740 + cff2e34 commit 93bf713

File tree

5 files changed

+298
-1
lines changed

5 files changed

+298
-1
lines changed

bittensor_cli/cli.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,12 @@ def __init__(self):
875875
self.subnets_app.command(
876876
"get-identity", rich_help_panel=HELP_PANELS["SUBNETS"]["IDENTITY"]
877877
)(self.subnets_get_identity)
878+
self.subnets_app.command(
879+
"start", rich_help_panel=HELP_PANELS["SUBNETS"]["CREATION"]
880+
)(self.subnets_start)
881+
self.subnets_app.command(
882+
"check-start", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"]
883+
)(self.subnets_check_start)
878884

879885
# weights commands
880886
self.weights_app.command(
@@ -4979,6 +4985,70 @@ def subnets_create(
49794985
)
49804986
)
49814987

4988+
def subnets_check_start(
4989+
self,
4990+
network: Optional[list[str]] = Options.network,
4991+
netuid: int = Options.netuid,
4992+
quiet: bool = Options.quiet,
4993+
verbose: bool = Options.verbose,
4994+
):
4995+
"""
4996+
Checks if a subnet's emission schedule can be started.
4997+
4998+
This command verifies if a subnet's emission schedule can be started based on the subnet's registration block.
4999+
5000+
Example:
5001+
[green]$[/green] btcli subnets check_start --netuid 1
5002+
"""
5003+
self.verbosity_handler(quiet, verbose)
5004+
return self._run_command(
5005+
subnets.get_start_schedule(self.initialize_chain(network), netuid)
5006+
)
5007+
5008+
def subnets_start(
5009+
self,
5010+
wallet_name: str = Options.wallet_name,
5011+
wallet_path: str = Options.wallet_path,
5012+
wallet_hotkey: str = Options.wallet_hotkey,
5013+
network: Optional[list[str]] = Options.network,
5014+
netuid: int = Options.netuid,
5015+
prompt: bool = Options.prompt,
5016+
quiet: bool = Options.quiet,
5017+
verbose: bool = Options.verbose,
5018+
):
5019+
"""
5020+
Starts a subnet's emission schedule.
5021+
5022+
The owner of the subnet must call this command to start the emission schedule.
5023+
5024+
Example:
5025+
[green]$[/green] btcli subnets start --netuid 1
5026+
[green]$[/green] btcli subnets start --netuid 1 --wallet-name alice
5027+
"""
5028+
self.verbosity_handler(quiet, verbose)
5029+
if not wallet_name:
5030+
wallet_name = Prompt.ask(
5031+
"Enter the [blue]wallet name[/blue] [dim](which you used to create the subnet)[/dim]",
5032+
default=self.config.get("wallet_name") or defaults.wallet.name,
5033+
)
5034+
wallet = self.wallet_ask(
5035+
wallet_name,
5036+
wallet_path,
5037+
wallet_hotkey,
5038+
ask_for=[
5039+
WO.NAME,
5040+
],
5041+
validate=WV.WALLET,
5042+
)
5043+
return self._run_command(
5044+
subnets.start_subnet(
5045+
wallet,
5046+
self.initialize_chain(network),
5047+
netuid,
5048+
prompt,
5049+
)
5050+
)
5051+
49825052
def subnets_get_identity(
49835053
self,
49845054
network: Optional[list[str]] = Options.network,

bittensor_cli/src/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class Constants:
3838
"test": "0x8f9cf856bf558a14440e75569c9e58594757048d7b3a84b5d25f6bd978263105",
3939
}
4040
delegates_detail_url = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json"
41+
emission_start_schedule = 7 * 24 * 60 * 60 / 12 # 7 days
4142

4243

4344
@dataclass

bittensor_cli/src/commands/subnets/subnets.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from rich.table import Column, Table
1111
from rich import box
1212

13-
from bittensor_cli.src import COLOR_PALETTE
13+
from bittensor_cli.src import COLOR_PALETTE, Constants
1414
from bittensor_cli.src.bittensor.balances import Balance
1515
from bittensor_cli.src.bittensor.extrinsics.registration import (
1616
register_extrinsic,
@@ -34,6 +34,7 @@
3434
prompt_for_identity,
3535
get_subnet_name,
3636
unlock_key,
37+
blocks_to_duration,
3738
json_console,
3839
)
3940

@@ -2308,3 +2309,112 @@ async def get_identity(
23082309
else:
23092310
console.print(table)
23102311
return identity
2312+
2313+
2314+
async def get_start_schedule(
2315+
subtensor: "SubtensorInterface",
2316+
netuid: int,
2317+
) -> None:
2318+
"""Fetch and display existing emission schedule information."""
2319+
2320+
if not await subtensor.subnet_exists(netuid):
2321+
print_error(f"Subnet {netuid} does not exist.")
2322+
return None
2323+
2324+
registration_block = await subtensor.query(
2325+
module="SubtensorModule",
2326+
storage_function="NetworkRegisteredAt",
2327+
params=[netuid],
2328+
)
2329+
min_blocks_to_start = Constants.emission_start_schedule
2330+
current_block = await subtensor.substrate.get_block_number()
2331+
2332+
potential_start_block = registration_block + min_blocks_to_start
2333+
if current_block < potential_start_block:
2334+
blocks_to_wait = potential_start_block - current_block
2335+
time_to_wait = blocks_to_duration(blocks_to_wait)
2336+
2337+
console.print(
2338+
f"[blue]Subnet {netuid}[/blue]:\n"
2339+
f"[blue]Registered at:[/blue] {registration_block}\n"
2340+
f"[blue]Minimum start block:[/blue] {potential_start_block}\n"
2341+
f"[blue]Current block:[/blue] {current_block}\n"
2342+
f"[blue]Blocks remaining:[/blue] {blocks_to_wait}\n"
2343+
f"[blue]Time to wait:[/blue] {time_to_wait}"
2344+
)
2345+
else:
2346+
console.print(
2347+
f"[blue]Subnet {netuid}[/blue]:\n"
2348+
f"[blue]Registered at:[/blue] {registration_block}\n"
2349+
f"[blue]Current block:[/blue] {current_block}\n"
2350+
f"[blue]Minimum start block:[/blue] {potential_start_block}\n"
2351+
f"[dark_sea_green3]Emission schedule can be started[/dark_sea_green3]"
2352+
)
2353+
2354+
return
2355+
2356+
2357+
async def start_subnet(
2358+
wallet: "Wallet",
2359+
subtensor: "SubtensorInterface",
2360+
netuid: int,
2361+
prompt: bool = False,
2362+
) -> bool:
2363+
"""Start a subnet's emission schedule"""
2364+
2365+
if not await subtensor.subnet_exists(netuid):
2366+
print_error(f"Subnet {netuid} does not exist.")
2367+
return False
2368+
2369+
subnet_owner = await subtensor.query(
2370+
module="SubtensorModule",
2371+
storage_function="SubnetOwner",
2372+
params=[netuid],
2373+
)
2374+
if subnet_owner != wallet.coldkeypub.ss58_address:
2375+
print_error(":cross_mark: This wallet doesn't own the specified subnet.")
2376+
return False
2377+
2378+
if prompt:
2379+
if not Confirm.ask(
2380+
f"Are you sure you want to start subnet {netuid}'s emission schedule?"
2381+
):
2382+
return False
2383+
2384+
if not unlock_key(wallet).success:
2385+
return False
2386+
2387+
with console.status(
2388+
f":satellite: Starting subnet {netuid}'s emission schedule...", spinner="earth"
2389+
):
2390+
start_call = await subtensor.substrate.compose_call(
2391+
call_module="SubtensorModule",
2392+
call_function="start_call",
2393+
call_params={"netuid": netuid},
2394+
)
2395+
2396+
signed_ext = await subtensor.substrate.create_signed_extrinsic(
2397+
call=start_call,
2398+
keypair=wallet.coldkey,
2399+
)
2400+
2401+
response = await subtensor.substrate.submit_extrinsic(
2402+
extrinsic=signed_ext,
2403+
wait_for_inclusion=True,
2404+
wait_for_finalization=True,
2405+
)
2406+
2407+
if await response.is_success:
2408+
console.print(
2409+
f":white_heavy_check_mark: [green]Successfully started subnet {netuid}'s emission schedule.[/green]"
2410+
)
2411+
return True
2412+
else:
2413+
error_msg = format_error_message(await response.error_message)
2414+
if "FirstEmissionBlockNumberAlreadySet" in error_msg:
2415+
console.print(f"[dark_sea_green3]Subnet {netuid} already has an emission schedule.[/dark_sea_green3]")
2416+
return True
2417+
2418+
await get_start_schedule(subtensor, netuid)
2419+
print_error(f":cross_mark: Failed to start subnet: {error_msg}")
2420+
return False

tests/e2e_tests/test_unstaking.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import asyncio
12
import json
23
import re
34

45
from bittensor_cli.src.bittensor.balances import Balance
56

7+
from btcli.tests.e2e_tests.utils import set_storage_extrinsic
68

79
def test_unstaking(local_chain, wallet_setup):
810
"""
@@ -34,6 +36,19 @@ def test_unstaking(local_chain, wallet_setup):
3436
wallet_path_bob
3537
)
3638

39+
# Call to make Alice root owner
40+
items = [
41+
(
42+
bytes.fromhex("658faa385070e074c85bf6b568cf055536e3e82152c8758267395fe524fbbd160000"),
43+
bytes.fromhex("d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d")
44+
)
45+
]
46+
asyncio.run(set_storage_extrinsic(
47+
local_chain,
48+
wallet=wallet_alice,
49+
items=items,
50+
))
51+
3752
# Create first subnet (netuid = 2)
3853
result = exec_command_alice(
3954
command="subnets",
@@ -98,6 +113,65 @@ def test_unstaking(local_chain, wallet_setup):
98113
)
99114
assert "✅ Registered subnetwork with netuid: 3" in result.stdout
100115

116+
# Start emission schedule for subnets
117+
start_call_netuid_0 = exec_command_alice(
118+
command="subnets",
119+
sub_command="start",
120+
extra_args=[
121+
"--netuid",
122+
"0",
123+
"--wallet-name",
124+
wallet_alice.name,
125+
"--no-prompt",
126+
"--chain",
127+
"ws://127.0.0.1:9945",
128+
"--wallet-path",
129+
wallet_path_alice,
130+
],
131+
)
132+
assert (
133+
"Successfully started subnet 0's emission schedule."
134+
in start_call_netuid_0.stdout
135+
)
136+
start_call_netuid_2 = exec_command_alice(
137+
command="subnets",
138+
sub_command="start",
139+
extra_args=[
140+
"--netuid",
141+
"2",
142+
"--wallet-name",
143+
wallet_alice.name,
144+
"--no-prompt",
145+
"--chain",
146+
"ws://127.0.0.1:9945",
147+
"--wallet-path",
148+
wallet_path_alice,
149+
],
150+
)
151+
assert (
152+
"Successfully started subnet 2's emission schedule."
153+
in start_call_netuid_2.stdout
154+
)
155+
156+
start_call_netuid_3 = exec_command_alice(
157+
command="subnets",
158+
sub_command="start",
159+
extra_args=[
160+
"--netuid",
161+
"3",
162+
"--wallet-name",
163+
wallet_alice.name,
164+
"--no-prompt",
165+
"--chain",
166+
"ws://127.0.0.1:9945",
167+
"--wallet-path",
168+
wallet_path_alice,
169+
],
170+
)
171+
assert (
172+
"Successfully started subnet 3's emission schedule."
173+
in start_call_netuid_3.stdout
174+
)
101175
# Register Bob in one subnet
102176
register_result = exec_command_bob(
103177
command="subnets",

tests/e2e_tests/utils.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,3 +300,45 @@ async def call_add_proposal(
300300
)
301301

302302
return await response.is_success
303+
304+
305+
async def set_storage_extrinsic(
306+
substrate: "AsyncSubstrateInterface",
307+
wallet: "Wallet",
308+
items: list[tuple[bytes, bytes]],
309+
) -> bool:
310+
"""Sets storage items using sudo permissions.
311+
312+
Args:
313+
subtensor: initialized SubtensorInterface object
314+
wallet: bittensor wallet object with sudo permissions
315+
items: List of (key, value) tuples where both key and value are bytes
316+
317+
Returns:
318+
bool: True if successful, False otherwise
319+
"""
320+
321+
storage_call = await substrate.compose_call(
322+
call_module="System", call_function="set_storage", call_params={"items": items}
323+
)
324+
325+
sudo_call = await substrate.compose_call(
326+
call_module="Sudo", call_function="sudo", call_params={"call": storage_call}
327+
)
328+
329+
extrinsic = await substrate.create_signed_extrinsic(
330+
call=sudo_call,
331+
keypair=wallet.coldkey,
332+
)
333+
response = await substrate.submit_extrinsic(
334+
extrinsic,
335+
wait_for_inclusion=True,
336+
wait_for_finalization=True,
337+
)
338+
339+
if not response:
340+
print(response)
341+
else:
342+
print(":white_heavy_check_mark: [dark_sea_green_3]Success[/dark_sea_green_3]")
343+
344+
return response

0 commit comments

Comments
 (0)