Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.
Open
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
59 changes: 14 additions & 45 deletions bittensor_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
COLORS,
HYPERPARAMS,
HYPERPARAMS_METADATA,
RootSudoOnly,
WalletOptions,
)
from bittensor_cli.src.bittensor import utils
Expand Down Expand Up @@ -7304,12 +7303,17 @@ def sudo_set(
self.verbosity_handler(quiet, verbose, json_output, prompt, decline)
proxy = self.is_valid_proxy_name_or_ss58(proxy, False)

param_names: list[str] = []
if not param_name or not param_value:
hyperparams = self._run_command(
sudo.get_hyperparameters(self.initialize_chain(network), netuid),
success, param_names = self._run_command(
sudo.get_hyperparameters(
self.initialize_chain(network),
netuid,
numbered=not param_name,
),
exit_early=False,
)
if not hyperparams:
) or (False, [])
if not success:
# Ensure we don't leave the websocket connection hanging.
if self.subtensor:
try:
Expand All @@ -7328,49 +7332,13 @@ def sudo_set(
"Param name not supplied with `--no-prompt` flag. Cannot continue"
)
return False
hyperparam_list = sorted(HYPERPARAMS.keys())
console.print("Available hyperparameters:\n")

# Create a table to show hyperparameters with descriptions
param_table = Table(
Column("[white]#", style="dim", width=4),
Column("[white]HYPERPARAMETER", style=COLORS.SU.HYPERPARAMETER),
Column("[white]OWNER SETTABLE", style="bright_cyan", width=18),
Column("[white]DESCRIPTION", style="dim", overflow="fold"),
box=box.SIMPLE,
show_edge=False,
pad_edge=False,
)

for idx, param in enumerate(hyperparam_list, start=1):
metadata = HYPERPARAMS_METADATA.get(param, {})
description = metadata.get("description", "No description available.")

# Check ownership from HYPERPARAMS
_, root_sudo = HYPERPARAMS.get(param, ("", RootSudoOnly.FALSE))
if root_sudo == RootSudoOnly.TRUE:
owner_settable_str = "[red]No (Root Only)[/red]"
elif root_sudo == RootSudoOnly.COMPLICATED:
owner_settable_str = "[yellow]COMPLICATED[/yellow]"
else:
owner_settable_str = "[green]Yes[/green]"

param_table.add_row(
str(idx),
f"[bold]{param}[/bold]",
owner_settable_str,
description,
)

console.print(param_table)
console.print()

choice = IntPrompt.ask(
"Enter the [bold]number[/bold] of the hyperparameter",
choices=[str(i) for i in range(1, len(hyperparam_list) + 1)],
choices=[str(i) for i in range(1, len(param_names) + 1)],
show_choices=False,
)
param_name = hyperparam_list[choice - 1]
param_name = param_names[choice - 1]

# Show additional info for selected parameter
metadata = HYPERPARAMS_METADATA.get(param_name, {})
Expand Down Expand Up @@ -7561,11 +7529,12 @@ def sudo_get(
[green]$[/green] btcli sudo get --netuid 1
"""
self.verbosity_handler(quiet, verbose, json_output, False)
return self._run_command(
success, _ = self._run_command(
sudo.get_hyperparameters(
self.initialize_chain(network), netuid, json_output
)
)
) or (False, [])
return success

def sudo_senate(
self,
Expand Down
17 changes: 17 additions & 0 deletions bittensor_cli/src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,23 @@ class RootSudoOnly(Enum):
"activity_cutoff_factor": "SubtensorModule",
}

# Settable hyperparameters not reported by the get_subnet_hyperparams_v3 runtime
# API but readable from SubtensorModule storage (netuid-keyed maps).
HYPERPARAMS_STORAGE: dict[str, str] = {
"max_allowed_uids": "MaxAllowedUids",
"min_allowed_uids": "MinAllowedUids",
"network_pow_registration_allowed": "NetworkPowRegistrationAllowed",
"recycle_or_burn": "RecycleOrBurn",
"sn_owner_hotkey": "SubnetOwnerHotkey",
}

# Setter-only registry entries reachable from other displayed rows: selecting
# alpha_high/alpha_low routes to alpha_values, yuma_version routes to
# yuma3_enabled, and subnet_owner_hotkey is an alias of sn_owner_hotkey.
HYPERPARAMS_SETTER_ALIASES = frozenset(
{"alpha_values", "subnet_owner_hotkey", "yuma3_enabled"}
)

# Hyperparameters whose root-sudo path uses a different extrinsic than the owner path.
# Maps btcli name -> (pallet, extrinsic) for the call wrapped in Sudo. Owner-side
# set_tempo is bounded to [360, 50400] and rate-limited; root's sudo_set_tempo accepts
Expand Down
68 changes: 59 additions & 9 deletions bittensor_cli/src/commands/sudo.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
HYPERPARAMS_MODULE,
HYPERPARAMS_METADATA,
HYPERPARAMS_ROOT_EXTRINSIC,
HYPERPARAMS_SETTER_ALIASES,
HYPERPARAMS_STORAGE,
RootSudoOnly,
COLOR_PALETTE,
)
Expand Down Expand Up @@ -44,6 +46,7 @@
)

if TYPE_CHECKING:
from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters
from bittensor_cli.src.bittensor.subtensor_interface import (
SubtensorInterface,
ProposalVoteData,
Expand Down Expand Up @@ -1138,13 +1141,39 @@ def _sanitize_json_string(
return value


async def _supplement_hyperparameters_from_storage(
subtensor: "SubtensorInterface", subnet: "SubnetHyperparameters", netuid: int
) -> None:
"""Fill in values for settable hyperparameters absent from the runtime API
response but readable from SubtensorModule storage."""
missing = [name for name in HYPERPARAMS_STORAGE if name not in subnet]
values = await asyncio.gather(
*(
subtensor.query("SubtensorModule", HYPERPARAMS_STORAGE[name], [netuid])
for name in missing
)
)
for name, value in zip(missing, values):
if value is not None:
subnet.hyperparameters[name] = value


async def get_hyperparameters(
subtensor: "SubtensorInterface",
netuid: int,
json_output: bool = False,
show_descriptions: bool = True,
) -> bool:
"""View hyperparameters of a subnetwork."""
numbered: bool = False,
) -> tuple[bool, list[str]]:
"""View hyperparameters of a subnetwork.

When `numbered` is True, each row is prefixed with a selection number and
settable hyperparameters not reported by the runtime API are added (with
their values read from storage where available), so callers can prompt the
user to pick a row from this table.

Returns (success, hyperparameter names in display order).
"""
print_verbose("Fetching hyperparameters")
try:
if not await subtensor.subnet_exists(netuid):
Expand All @@ -1153,7 +1182,7 @@ async def get_hyperparameters(
json_console.print_json(data={"error": error_msg})
else:
print_error(error_msg)
return False
return False, []
subnet, subnet_info = await asyncio.gather(
subtensor.get_subnet_hyperparameters(netuid), subtensor.subnet(netuid)
)
Expand All @@ -1163,19 +1192,24 @@ async def get_hyperparameters(
json_console.print_json(data={"error": error_msg})
else:
print_error(error_msg)
return False
return False, []
except Exception as e:
if json_output:
json_console.print_json(data={"error": str(e)})
else:
raise
return False
return False, []

# Determine if we should show extended info (descriptions, ownership)
show_extended = show_descriptions and not json_output
numbered = numbered and show_extended
if numbered:
await _supplement_hyperparameters_from_storage(subtensor, subnet, netuid)

if show_extended:
columns = [Column("[white]#", style="dim")] if numbered else []
table = Table(
*columns,
Column("[white]HYPERPARAMETER", style=COLOR_PALETTE.SU.HYPERPARAMETER),
Column("[white]VALUE", style=COLOR_PALETTE.SU.VALUE),
Column("[white]NORMALIZED", style=COLOR_PALETTE.SU.NORMAL),
Expand Down Expand Up @@ -1209,10 +1243,23 @@ async def get_hyperparameters(
show_edge=True,
)
dict_out = []
param_names = []

normalized_values = normalize_hyperparameters(subnet, json_output=json_output)
sorted_values = sorted(normalized_values, key=lambda x: x[0])
if numbered:
# Keep settable hyperparameters without a readable value selectable from
# this single table, except setter aliases already reachable from other
# rows (see HYPERPARAMS_SETTER_ALIASES).
displayed = {param for param, _, _ in sorted_values}
sorted_values += [
(param, "-", "-")
for param in sorted(
set(HYPERPARAMS) - displayed - HYPERPARAMS_SETTER_ALIASES
)
]
for param, value, norm_value in sorted_values:
param_names.append(param)
if not json_output:
if show_extended:
# Get metadata for this hyperparameter
Expand All @@ -1236,13 +1283,16 @@ async def get_hyperparameters(
else:
description_with_link = description

table.add_row(
row = [
" " + param,
value,
norm_value,
owner_settable_str,
description_with_link,
)
]
if numbered:
row.insert(0, str(len(param_names)))
table.add_row(*row)
else:
table.add_row(" " + param, value, norm_value)
else:
Expand Down Expand Up @@ -1272,7 +1322,7 @@ async def get_hyperparameters(
json_str = json.dumps(dict_out, ensure_ascii=True)
sys.stdout.write(json_str + "\n")
sys.stdout.flush()
return True
return True, param_names
else:
console.print(table)
if show_extended:
Expand All @@ -1295,7 +1345,7 @@ async def get_hyperparameters(
console.print(
"[dim]📚 For detailed documentation, visit: [link]https://docs.bittensor.com[/link]"
)
return True
return True, param_names


async def get_senate(
Expand Down
111 changes: 111 additions & 0 deletions tests/unit_tests/test_sudo_get_hyperparameters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""
Unit tests for sudo.get_hyperparameters.

Covers the (success, hyperparameter names) return contract and the `numbered`
selection mode used by the interactive `btcli sudo set` flow: one table where
params missing from the runtime API are filled in from SubtensorModule storage
and setter aliases are excluded.
"""

import pytest
from unittest.mock import AsyncMock

from bittensor_cli.src import (
HYPERPARAMS,
HYPERPARAMS_SETTER_ALIASES,
HYPERPARAMS_STORAGE,
)
from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters
from bittensor_cli.src.commands.sudo import get_hyperparameters

CHAIN_PARAMS = {"immunity_period": 65535, "kappa": 32767, "tempo": 360}
STORAGE_VALUES = {
"MaxAllowedUids": 257,
"MinAllowedUids": 64,
"NetworkPowRegistrationAllowed": True,
"RecycleOrBurn": "Burn",
"SubnetOwnerHotkey": "5HdrwVQQbMa8Wh271PDzvMHmM44wYM5wfnXCW3o97gDisuaY",
}


def _prepare_subtensor(mock_subtensor) -> SubnetHyperparameters:
subnet = SubnetHyperparameters(hyperparameters=dict(CHAIN_PARAMS))
mock_subtensor.get_subnet_hyperparameters = AsyncMock(return_value=subnet)
mock_subtensor.subnet = AsyncMock(return_value=AsyncMock(subnet_name="test"))
mock_subtensor.query = AsyncMock(
side_effect=lambda module, func, params: STORAGE_VALUES.get(func)
)
return subnet


@pytest.mark.asyncio
async def test_returns_chain_params_in_display_order(mock_subtensor):
_prepare_subtensor(mock_subtensor)

success, param_names = await get_hyperparameters(mock_subtensor, netuid=18)

assert success is True
assert param_names == sorted(CHAIN_PARAMS)
mock_subtensor.query.assert_not_awaited()


@pytest.mark.asyncio
async def test_numbered_fills_missing_values_from_storage(mock_subtensor):
subnet = _prepare_subtensor(mock_subtensor)

success, param_names = await get_hyperparameters(
mock_subtensor, netuid=18, numbered=True
)

assert success is True
for name, storage_function in HYPERPARAMS_STORAGE.items():
assert subnet.hyperparameters[name] == STORAGE_VALUES[storage_function]
assert name in param_names
mock_subtensor.query.assert_awaited_with(
"SubtensorModule", "SubnetOwnerHotkey", [18]
)


@pytest.mark.asyncio
async def test_numbered_excludes_setter_aliases_and_has_no_duplicates(mock_subtensor):
_prepare_subtensor(mock_subtensor)

_, param_names = await get_hyperparameters(mock_subtensor, netuid=18, numbered=True)

assert HYPERPARAMS_SETTER_ALIASES.isdisjoint(param_names)
assert set(HYPERPARAMS) - HYPERPARAMS_SETTER_ALIASES <= set(param_names)
assert len(param_names) == len(set(param_names))


@pytest.mark.asyncio
async def test_numbered_falls_back_to_placeholder_when_storage_empty(mock_subtensor):
_prepare_subtensor(mock_subtensor)
mock_subtensor.query = AsyncMock(return_value=None)

_, param_names = await get_hyperparameters(mock_subtensor, netuid=18, numbered=True)

assert "sn_owner_hotkey" in param_names # appended with "-" placeholders


@pytest.mark.asyncio
async def test_numbered_has_no_effect_on_json_output(mock_subtensor, capsys):
_prepare_subtensor(mock_subtensor)

success, param_names = await get_hyperparameters(
mock_subtensor, netuid=18, json_output=True, numbered=True
)

assert success is True
assert param_names == sorted(CHAIN_PARAMS)
assert "sn_owner_hotkey" not in capsys.readouterr().out
mock_subtensor.query.assert_not_awaited()


@pytest.mark.asyncio
async def test_nonexistent_subnet_returns_failure(mock_subtensor):
mock_subtensor.subnet_exists = AsyncMock(return_value=False)

success, param_names = await get_hyperparameters(mock_subtensor, netuid=999)

assert success is False
assert param_names == []