bittensor-cli 9.23.0rc1__tar.gz → 9.23.0rc3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/PKG-INFO +1 -1
  2. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/cli.py +112 -79
  3. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/__init__.py +31 -10
  4. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/extrinsics/registration.py +1 -1
  5. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/subtensor_interface.py +32 -7
  6. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/utils.py +2 -0
  7. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/stake/children_hotkeys.py +14 -4
  8. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/subnets/subnets.py +3 -2
  9. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/sudo.py +136 -11
  10. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/wallets.py +3 -1
  11. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/weights.py +30 -1
  12. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/pyproject.toml +1 -1
  13. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/README.md +0 -0
  14. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/__init__.py +0 -0
  15. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/doc_generation_helper.py +0 -0
  16. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/__init__.py +0 -0
  17. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/balances.py +0 -0
  18. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/chain_data.py +0 -0
  19. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/extrinsics/__init__.py +0 -0
  20. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +0 -0
  21. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/extrinsics/root.py +0 -0
  22. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/extrinsics/serving.py +0 -0
  23. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/extrinsics/transfer.py +0 -0
  24. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/locks.py +0 -0
  25. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/minigraph.py +0 -0
  26. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/networking.py +0 -0
  27. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/templates/main-filters.j2 +0 -0
  28. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/templates/main-header.j2 +0 -0
  29. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/templates/neuron-details.j2 +0 -0
  30. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/templates/price-multi.j2 +0 -0
  31. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/templates/price-single.j2 +0 -0
  32. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/templates/subnet-details-header.j2 +0 -0
  33. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/templates/subnet-details.j2 +0 -0
  34. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/templates/subnet-metrics.j2 +0 -0
  35. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/templates/subnets-table.j2 +0 -0
  36. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/templates/table.j2 +0 -0
  37. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/templates/view.css +0 -0
  38. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/templates/view.j2 +0 -0
  39. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/bittensor/templates/view.js +0 -0
  40. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/__init__.py +0 -0
  41. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/axon/__init__.py +0 -0
  42. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/axon/axon.py +0 -0
  43. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/crowd/__init__.py +0 -0
  44. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/crowd/contribute.py +0 -0
  45. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/crowd/contributors.py +0 -0
  46. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/crowd/create.py +0 -0
  47. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/crowd/dissolve.py +0 -0
  48. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/crowd/refund.py +0 -0
  49. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/crowd/update.py +0 -0
  50. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/crowd/utils.py +0 -0
  51. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/crowd/view.py +0 -0
  52. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/lock/__init__.py +0 -0
  53. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/lock/add.py +0 -0
  54. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/lock/common.py +0 -0
  55. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/lock/list.py +0 -0
  56. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/lock/mode.py +0 -0
  57. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/lock/move.py +0 -0
  58. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/proxy.py +0 -0
  59. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/stake/__init__.py +0 -0
  60. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/stake/add.py +0 -0
  61. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/stake/auto_staking.py +0 -0
  62. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/stake/claim.py +0 -0
  63. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/stake/list.py +0 -0
  64. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/stake/move.py +0 -0
  65. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/stake/remove.py +0 -0
  66. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/stake/wizard.py +0 -0
  67. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/subnets/__init__.py +0 -0
  68. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/subnets/mechanisms.py +0 -0
  69. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/subnets/price.py +0 -0
  70. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/src/commands/view.py +0 -0
  71. {bittensor_cli-9.23.0rc1 → bittensor_cli-9.23.0rc3}/bittensor_cli/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bittensor-cli
3
- Version: 9.23.0rc1
3
+ Version: 9.23.0rc3
4
4
  Summary: Bittensor CLI
5
5
  Author: bittensor.com
6
6
  Requires-Python: >=3.10
@@ -1198,6 +1198,9 @@ class CLIManager:
1198
1198
  self.sudo_app.command("trim", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])(
1199
1199
  self.sudo_trim
1200
1200
  )
1201
+ self.sudo_app.command(
1202
+ "trigger-epoch", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"]
1203
+ )(self.sudo_trigger_epoch)
1201
1204
  self.sudo_app.command(
1202
1205
  "stake-burn", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"]
1203
1206
  )(self.sudo_stake_burn)
@@ -7329,7 +7332,6 @@ class CLIManager:
7329
7332
  console.print("Available hyperparameters:\n")
7330
7333
 
7331
7334
  # Create a table to show hyperparameters with descriptions
7332
-
7333
7335
  param_table = Table(
7334
7336
  Column("[white]#", style="dim", width=4),
7335
7337
  Column("[white]HYPERPARAMETER", style=COLORS.SU.HYPERPARAMETER),
@@ -7377,19 +7379,13 @@ class CLIManager:
7377
7379
  description = metadata.get("description", "No description available.")
7378
7380
  docs_link = metadata.get("docs_link", "")
7379
7381
  if docs_link:
7380
- # Show description text followed by clickable blue [link] at the end
7381
7382
  console.print(
7382
7383
  f"{description} [bright_blue underline link=https://{docs_link}]link[/]"
7383
7384
  )
7384
7385
  else:
7385
7386
  console.print(f"{description}")
7386
- side_effects = metadata.get("side_effects", "")
7387
- if side_effects:
7387
+ if side_effects := metadata.get("side_effects", ""):
7388
7388
  console.print(f"[dim]Side Effects:[/dim] {side_effects}")
7389
- if docs_link:
7390
- console.print(
7391
- f"[dim]📚 Docs:[/dim] [link]https://{docs_link}[/link]\n"
7392
- )
7393
7389
 
7394
7390
  if param_name in ["alpha_high", "alpha_low"]:
7395
7391
  if not prompt:
@@ -7398,16 +7394,13 @@ class CLIManager:
7398
7394
  "They must be set together via the alpha_values parameter."
7399
7395
  )
7400
7396
  if json_output:
7401
- json_str = json.dumps(
7402
- {
7397
+ json_console.print_json(
7398
+ data={
7403
7399
  "success": False,
7404
7400
  "err_msg": err_msg,
7405
7401
  "extrinsic_identifier": None,
7406
- },
7407
- ensure_ascii=True,
7402
+ }
7408
7403
  )
7409
- sys.stdout.write(json_str + "\n")
7410
- sys.stdout.flush()
7411
7404
  else:
7412
7405
  print_error(
7413
7406
  f"[{COLORS.SU.HYPERPARAM}]alpha_high[/{COLORS.SU.HYPERPARAM}] and "
@@ -7419,23 +7412,20 @@ class CLIManager:
7419
7412
  low_val = FloatPrompt.ask(f"Enter the new value for {arg__('alpha_low')}")
7420
7413
  high_val = FloatPrompt.ask(f"Enter the new value for {arg__('alpha_high')}")
7421
7414
  param_value = f"{low_val},{high_val}"
7422
- if param_name == "yuma_version":
7415
+ elif param_name == "yuma_version":
7423
7416
  if not prompt:
7424
7417
  err_msg = (
7425
7418
  "yuma_version is set using a different hyperparameter (yuma3_enabled), "
7426
7419
  "and thus cannot be set with `--no-prompt`"
7427
7420
  )
7428
7421
  if json_output:
7429
- json_str = json.dumps(
7430
- {
7422
+ json_console.print_json(
7423
+ data={
7431
7424
  "success": False,
7432
7425
  "err_msg": err_msg,
7433
7426
  "extrinsic_identifier": None,
7434
- },
7435
- ensure_ascii=True,
7427
+ }
7436
7428
  )
7437
- sys.stdout.write(json_str + "\n")
7438
- sys.stdout.flush()
7439
7429
  else:
7440
7430
  print_error(
7441
7431
  f"[{COLORS.SU.HYPERPARAM}]yuma_version[/{COLORS.SU.HYPERPARAM}]"
@@ -7456,22 +7446,45 @@ class CLIManager:
7456
7446
  param_value = "true" if question == "enable" else "false"
7457
7447
  else:
7458
7448
  return False
7449
+ elif param_name == "activity_cutoff":
7450
+ err_msg = (
7451
+ "activity_cutoff is now derived from activity_cutoff_factor "
7452
+ "(cutoff blocks = factor × tempo ÷ 1000) and can no longer be set "
7453
+ "directly. Set activity_cutoff_factor instead (per-mille units; "
7454
+ "1000 = one full tempo)."
7455
+ )
7456
+ if json_output:
7457
+ json_console.print_json(
7458
+ data={
7459
+ "success": False,
7460
+ "err_msg": err_msg,
7461
+ "extrinsic_identifier": None,
7462
+ }
7463
+ )
7464
+ else:
7465
+ print_error(
7466
+ f"[{COLORS.SU.HYPERPARAM}]activity_cutoff[/{COLORS.SU.HYPERPARAM}] "
7467
+ f"is now derived from "
7468
+ f"[{COLORS.SU.HYPERPARAM}]activity_cutoff_factor[/{COLORS.SU.HYPERPARAM}] "
7469
+ f"(cutoff blocks = factor × tempo ÷ 1000). Set "
7470
+ f"[{COLORS.SU.HYPERPARAM}]activity_cutoff_factor[/{COLORS.SU.HYPERPARAM}] "
7471
+ f"instead (per-mille units; 1000 = one full tempo)."
7472
+ )
7473
+ return False
7474
+
7459
7475
  if param_name == "subnet_is_active":
7460
7476
  err_msg = (
7461
7477
  "subnet_is_active is set by using the 'btcli subnets start' command, "
7462
7478
  "not via sudo set"
7463
7479
  )
7464
7480
  if json_output:
7465
- json_str = json.dumps(
7466
- {
7481
+ json_console.print_json(
7482
+ data={
7467
7483
  "success": False,
7468
7484
  "err_msg": err_msg,
7469
7485
  "extrinsic_identifier": None,
7470
- },
7471
- ensure_ascii=True,
7486
+ }
7472
7487
  )
7473
- sys.stdout.write(json_str + "\n")
7474
- sys.stdout.flush()
7475
7488
  else:
7476
7489
  print_error(
7477
7490
  f"[{COLORS.SU.HYPERPARAM}]subnet_is_active[/{COLORS.SU.HYPERPARAM}] "
@@ -7506,60 +7519,29 @@ class CLIManager:
7506
7519
  f"param_name: {param_name}\n"
7507
7520
  f"param_value: {param_value}"
7508
7521
  )
7522
+
7523
+ result, err_msg, ext_id = self._run_command(
7524
+ sudo.sudo_set_hyperparameter(
7525
+ wallet=wallet,
7526
+ subtensor=self.initialize_chain(network),
7527
+ netuid=netuid,
7528
+ proxy=proxy,
7529
+ param_name=param_name,
7530
+ param_value=param_value,
7531
+ normalize=normalize_value,
7532
+ prompt=prompt,
7533
+ json_output=json_output,
7534
+ )
7535
+ )
7509
7536
  if json_output:
7510
- try:
7511
- result, err_msg, ext_id = self._run_command(
7512
- sudo.sudo_set_hyperparameter(
7513
- wallet=wallet,
7514
- subtensor=self.initialize_chain(network),
7515
- netuid=netuid,
7516
- proxy=proxy,
7517
- param_name=param_name,
7518
- param_value=param_value,
7519
- normalize=normalize_value,
7520
- prompt=prompt,
7521
- json_output=json_output,
7522
- )
7523
- )
7524
- json_str = json.dumps(
7525
- {
7526
- "success": result,
7527
- "err_msg": err_msg,
7528
- "extrinsic_identifier": ext_id,
7529
- },
7530
- ensure_ascii=True,
7531
- )
7532
- sys.stdout.write(json_str + "\n")
7533
- sys.stdout.flush()
7534
- return result
7535
- except Exception as e:
7536
- # Ensure JSON output even on exceptions
7537
- json_str = json.dumps(
7538
- {
7539
- "success": False,
7540
- "err_msg": str(e),
7541
- "extrinsic_identifier": None,
7542
- },
7543
- ensure_ascii=True,
7544
- )
7545
- sys.stdout.write(json_str + "\n")
7546
- sys.stdout.flush()
7547
- raise
7548
- else:
7549
- result, err_msg, ext_id = self._run_command(
7550
- sudo.sudo_set_hyperparameter(
7551
- wallet=wallet,
7552
- subtensor=self.initialize_chain(network),
7553
- netuid=netuid,
7554
- proxy=proxy,
7555
- param_name=param_name,
7556
- param_value=param_value,
7557
- normalize=normalize_value,
7558
- prompt=prompt,
7559
- json_output=json_output,
7560
- )
7537
+ json_console.print_json(
7538
+ data={
7539
+ "success": result,
7540
+ "err_msg": err_msg,
7541
+ "extrinsic_identifier": ext_id,
7542
+ }
7561
7543
  )
7562
- return result
7544
+ return result
7563
7545
 
7564
7546
  def sudo_get(
7565
7547
  self,
@@ -7838,6 +7820,57 @@ class CLIManager:
7838
7820
  )
7839
7821
  )
7840
7822
 
7823
+ def sudo_trigger_epoch(
7824
+ self,
7825
+ network: Optional[list[str]] = Options.network,
7826
+ wallet_name: Optional[str] = Options.wallet_name,
7827
+ wallet_path: Optional[str] = Options.wallet_path,
7828
+ wallet_hotkey: Optional[str] = Options.wallet_hotkey,
7829
+ netuid: int = Options.netuid,
7830
+ proxy: Optional[str] = Options.proxy,
7831
+ quiet: bool = Options.quiet,
7832
+ verbose: bool = Options.verbose,
7833
+ json_output: bool = Options.json_output,
7834
+ prompt: bool = Options.prompt,
7835
+ decline: bool = Options.decline,
7836
+ period: int = Options.period,
7837
+ ):
7838
+ """
7839
+ Manually triggers an epoch for a subnet you own.
7840
+
7841
+ The epoch fires after the chain's admin freeze window has elapsed, during which
7842
+ admin operations on the subnet are locked. This is rate-limited on-chain and
7843
+ fails if a trigger is already pending, the next automatic epoch is imminent, or
7844
+ commit-reveal is enabled on the subnet (disable it first via
7845
+ 'btcli sudo set --param commit_reveal_weights_enabled --value false').
7846
+
7847
+ EXAMPLE
7848
+ [green]$[/green] btcli sudo trigger-epoch --netuid 95 --wallet-name my_wallet --wallet-hotkey my_hotkey
7849
+ """
7850
+ self.verbosity_handler(quiet, verbose, json_output, prompt)
7851
+ proxy = self.is_valid_proxy_name_or_ss58(proxy, False)
7852
+
7853
+ wallet = self.wallet_ask(
7854
+ wallet_name,
7855
+ wallet_path,
7856
+ wallet_hotkey,
7857
+ ask_for=[WO.NAME, WO.PATH],
7858
+ validate=WV.WALLET,
7859
+ )
7860
+ self._run_command(
7861
+ sudo.trigger_epoch(
7862
+ subtensor=self.initialize_chain(network),
7863
+ wallet=wallet,
7864
+ netuid=netuid,
7865
+ period=period,
7866
+ proxy=proxy,
7867
+ json_output=json_output,
7868
+ prompt=prompt,
7869
+ decline=decline,
7870
+ quiet=quiet,
7871
+ )
7872
+ )
7873
+
7841
7874
  def sudo_stake_burn(
7842
7875
  self,
7843
7876
  network: Optional[list[str]] = Options.network,
@@ -584,10 +584,10 @@ HYPERPARAMS = {
584
584
  "kappa": ("sudo_set_kappa", RootSudoOnly.TRUE),
585
585
  "immunity_period": ("sudo_set_immunity_period", RootSudoOnly.FALSE),
586
586
  "min_allowed_weights": ("sudo_set_min_allowed_weights", RootSudoOnly.FALSE),
587
- "tempo": ("sudo_set_tempo", RootSudoOnly.TRUE),
587
+ "tempo": ("set_tempo", RootSudoOnly.COMPLICATED),
588
588
  "weights_version": ("sudo_set_weights_version_key", RootSudoOnly.FALSE),
589
589
  "weights_rate_limit": ("sudo_set_weights_set_rate_limit", RootSudoOnly.TRUE),
590
- "activity_cutoff": ("sudo_set_activity_cutoff", RootSudoOnly.FALSE),
590
+ "activity_cutoff_factor": ("set_activity_cutoff_factor", RootSudoOnly.COMPLICATED),
591
591
  "target_regs_per_interval": (
592
592
  "sudo_set_target_registrations_per_interval",
593
593
  RootSudoOnly.TRUE,
@@ -635,6 +635,10 @@ HYPERPARAMS = {
635
635
  "alpha_low": ("", RootSudoOnly.FALSE), # Derived from alpha_values
636
636
  #
637
637
  "subnet_is_active": ("", RootSudoOnly.FALSE), # Set via btcli subnets start
638
+ "activity_cutoff": (
639
+ "",
640
+ RootSudoOnly.FALSE,
641
+ ), # Derived: activity_cutoff_factor × tempo / 1000
638
642
  "yuma_version": ("", RootSudoOnly.FALSE), # Related to yuma3_enabled
639
643
  "max_allowed_uids": ("sudo_set_max_allowed_uids", RootSudoOnly.FALSE),
640
644
  "burn_increase_mult": ("sudo_set_burn_increase_mult", RootSudoOnly.FALSE),
@@ -645,9 +649,20 @@ HYPERPARAMS = {
645
649
  ),
646
650
  }
647
651
 
648
- # Maps a hyperparameter to a non-default pallet for sudo set calls. Empty by default
649
- # (all current hyperparameters live in the default pallet).
650
- HYPERPARAMS_MODULE: dict[str, str] = {}
652
+ # Maps a hyperparameter to a non-default pallet for sudo set calls. Hyperparameters
653
+ # not listed here live in the default pallet (AdminUtils).
654
+ HYPERPARAMS_MODULE: dict[str, str] = {
655
+ "tempo": "SubtensorModule",
656
+ "activity_cutoff_factor": "SubtensorModule",
657
+ }
658
+
659
+ # Hyperparameters whose root-sudo path uses a different extrinsic than the owner path.
660
+ # Maps btcli name -> (pallet, extrinsic) for the call wrapped in Sudo. Owner-side
661
+ # set_tempo is bounded to [360, 50400] and rate-limited; root's sudo_set_tempo accepts
662
+ # any u16 value.
663
+ HYPERPARAMS_ROOT_EXTRINSIC: dict[str, tuple[str, str]] = {
664
+ "tempo": ("AdminUtils", "sudo_set_tempo"),
665
+ }
651
666
 
652
667
  # Hyperparameter metadata: descriptions, side-effects, ownership, and documentation links
653
668
  HYPERPARAMS_METADATA = {
@@ -670,9 +685,9 @@ HYPERPARAMS_METADATA = {
670
685
  "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#minallowedweights",
671
686
  },
672
687
  "tempo": {
673
- "description": "Number of blocks between epoch transitions",
674
- "side_effects": "Lower tempo means more frequent updates but higher chain load. Higher tempo reduces frequency but may slow responsiveness.",
675
- "owner_settable": False,
688
+ "description": "Number of blocks between automatic epoch transitions. Owner-settable between 360 and 50400 blocks (rate-limited to one change per 360 blocks); root can set any value via sudo.",
689
+ "side_effects": "Lower tempo means more frequent updates but higher chain load. Higher tempo reduces frequency but may slow responsiveness. Changing tempo resets the epoch cycle, so the next epoch fires a full tempo after the change.",
690
+ "owner_settable": True,
676
691
  "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#tempo",
677
692
  },
678
693
  "weights_version": {
@@ -688,8 +703,14 @@ HYPERPARAMS_METADATA = {
688
703
  "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#weightsratelimit--commitmentratelimit",
689
704
  },
690
705
  "activity_cutoff": {
691
- "description": "Minimum activity level required for neurons to remain active.",
692
- "side_effects": "Lower values keep more neurons active; higher values prune inactive neurons more aggressively.",
706
+ "description": "Effective validator inactivity cutoff in blocks, computed as activity_cutoff_factor × tempo ÷ 1000. Read-only; set activity_cutoff_factor instead.",
707
+ "side_effects": "Lower values prune inactive validators more aggressively; higher values keep more validators active.",
708
+ "owner_settable": False,
709
+ "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#activitycutoff",
710
+ },
711
+ "activity_cutoff_factor": {
712
+ "description": "Tolerated validator inactivity as per-mille of tempo (1000 = one full tempo). Effective cutoff in blocks = factor × tempo ÷ 1000. Allowed range: 1000 to 50000.",
713
+ "side_effects": "Lower factors prune inactive validators more aggressively; higher factors tolerate longer inactivity. The effective cutoff scales with tempo.",
693
714
  "owner_settable": True,
694
715
  "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#activitycutoff",
695
716
  },
@@ -97,7 +97,7 @@ async def burned_register_extrinsic(
97
97
  block_hash=block_hash,
98
98
  ),
99
99
  )
100
- validity_period = tempo - blocks_since_last_step
100
+ validity_period = max(tempo - blocks_since_last_step, 8)
101
101
  era_ = {
102
102
  "period": validity_period,
103
103
  "current": current_block,
@@ -113,12 +113,14 @@ class SubtensorInterface:
113
113
  if (use_disk_cache or os.getenv("DISK_CACHE", "1") == "1")
114
114
  else AsyncSubstrateInterface
115
115
  )
116
- self.substrate = substrate_class(
117
- url=self.chain_endpoint,
118
- ss58_format=SS58_FORMAT,
119
- type_registry=TYPE_REGISTRY,
120
- chain_name="Bittensor",
121
- ws_shutdown_timer=None,
116
+ self.substrate: AsyncSubstrateInterface | DiskCachedAsyncSubstrateInterface = (
117
+ substrate_class(
118
+ url=self.chain_endpoint,
119
+ ss58_format=SS58_FORMAT,
120
+ type_registry=TYPE_REGISTRY,
121
+ chain_name="Bittensor",
122
+ ws_shutdown_timer=None,
123
+ )
122
124
  )
123
125
 
124
126
  def __str__(self):
@@ -1653,6 +1655,29 @@ class SubtensorInterface:
1653
1655
 
1654
1656
  return SubnetHyperparameters.from_any(result)
1655
1657
 
1658
+ async def get_next_epoch_start_block(
1659
+ self, netuid: int, block_hash: Optional[str] = None
1660
+ ) -> Optional[int]:
1661
+ """
1662
+ Returns the block number at which the subnet's next epoch is expected to fire,
1663
+ considering both the automatic schedule and any pending owner-triggered epoch.
1664
+ It does not account for per-block epoch-cap deferrals, which can push the
1665
+ actual firing block slightly later.
1666
+
1667
+ Returns `None` if the subnet does not run epochs (tempo == 0) or if the chain
1668
+ does not expose the runtime API (runtimes without dynamic tempo).
1669
+ """
1670
+ try:
1671
+ result = await self.query_runtime_api(
1672
+ runtime_api="SubnetInfoRuntimeApi",
1673
+ method="get_next_epoch_start_block",
1674
+ params=[netuid],
1675
+ block_hash=block_hash,
1676
+ )
1677
+ except (ValueError, SubstrateRequestException):
1678
+ return None
1679
+ return None if result is None else int(result)
1680
+
1656
1681
  async def get_subnet_mechanisms(
1657
1682
  self, netuid: int, block_hash: Optional[str] = None
1658
1683
  ) -> int:
@@ -2680,7 +2705,7 @@ class SubtensorInterface:
2680
2705
  for idx, (_, result) in enumerate(batch_claimable):
2681
2706
  hotkey = unique_hotkeys[idx]
2682
2707
  if result:
2683
- for netuid, rate in result:
2708
+ for netuid, rate in result.items():
2684
2709
  if hotkey not in claimable_rates:
2685
2710
  claimable_rates[hotkey] = {}
2686
2711
  claimable_rates[hotkey][netuid] = fixed_to_float(rate, frac_bits=32)
@@ -855,6 +855,8 @@ def normalize_hyperparameters(
855
855
  "min_childkey_take": u16_normalized_float,
856
856
  "min_burn": Balance.from_rao,
857
857
  "max_burn": Balance.from_rao,
858
+ # Per-mille of tempo; normalized to the factor itself (1.0 = one full tempo).
859
+ "activity_cutoff_factor": lambda value: value / 1000,
858
860
  }
859
861
 
860
862
  normalized_values: list[tuple[str, str, str]] = []
@@ -42,15 +42,25 @@ async def get_childkey_completion_block(
42
42
  tempo_query = subtensor.get_hyperparameter(
43
43
  param_name="Tempo", netuid=netuid, block_hash=bh
44
44
  )
45
- block_number, blocks_since_last_step, tempo = await asyncio.gather(
45
+ block_number, blocks_since_last_step, tempo, next_epoch = await asyncio.gather(
46
46
  subtensor.substrate.get_block_number(block_hash=bh),
47
47
  blocks_since_last_step_query,
48
48
  tempo_query,
49
+ subtensor.get_next_epoch_start_block(netuid, block_hash=bh),
49
50
  )
50
51
  cooldown = block_number + 7200
51
- blocks_left_in_tempo = tempo - blocks_since_last_step
52
- next_tempo = block_number + blocks_left_in_tempo
53
- next_epoch_after_cooldown = (cooldown - next_tempo) % (tempo + 1) + cooldown
52
+ if next_epoch is not None and tempo:
53
+ # Dynamic-tempo scheduler: epochs fire at next_epoch, then every tempo blocks.
54
+ if next_epoch >= cooldown:
55
+ next_epoch_after_cooldown = next_epoch
56
+ else:
57
+ epochs_until_cooldown = (cooldown - next_epoch + tempo - 1) // tempo
58
+ next_epoch_after_cooldown = next_epoch + epochs_until_cooldown * tempo
59
+ else:
60
+ # Legacy modulo scheduler (chains without the dynamic-tempo runtime).
61
+ blocks_left_in_tempo = tempo - blocks_since_last_step
62
+ next_tempo = block_number + blocks_left_in_tempo
63
+ next_epoch_after_cooldown = (cooldown - next_tempo) % (tempo + 1) + cooldown
54
64
  return block_number, next_epoch_after_cooldown
55
65
 
56
66
 
@@ -932,9 +932,10 @@ async def subnets_list(
932
932
  subnet.blocks_since_last_step - prev_blocks_since_last_step
933
933
  )
934
934
  else:
935
- # Tempo restarted
935
+ # Tempo restarted; the epoch period is exactly tempo blocks
936
+ # under the dynamic-tempo scheduler.
936
937
  block_change = (
937
- subnet.blocks_since_last_step + subnet.tempo + 1
938
+ subnet.blocks_since_last_step + subnet.tempo
938
939
  ) - prev_blocks_since_last_step
939
940
  if block_change > 0:
940
941
  block_change_text = f" [pale_green3](+{block_change})[/pale_green3]"
@@ -14,6 +14,7 @@ from bittensor_cli.src import (
14
14
  HYPERPARAMS,
15
15
  HYPERPARAMS_MODULE,
16
16
  HYPERPARAMS_METADATA,
17
+ HYPERPARAMS_ROOT_EXTRINSIC,
17
18
  RootSudoOnly,
18
19
  COLOR_PALETTE,
19
20
  )
@@ -67,6 +68,15 @@ def allowed_value(
67
68
  """
68
69
  try:
69
70
  if not isinstance(value, bool):
71
+ if param == "activity_cutoff_factor":
72
+ # Bounds mirror MIN/MAX_ACTIVITY_CUTOFF_FACTOR_MILLI in subtensor.
73
+ factor = int(value)
74
+ if not 1_000 <= factor <= 50_000:
75
+ return (
76
+ False,
77
+ "between 1000 and 50000 (per-mille units; 1000 = one full tempo)",
78
+ )
79
+ return True, factor
70
80
  if param == "alpha_values":
71
81
  # Split the string into individual values
72
82
  alpha_low_str, alpha_high_str = value.split(",")
@@ -380,6 +390,7 @@ def search_metadata(
380
390
  "immunity_period": int,
381
391
  "commit_reveal_period": int,
382
392
  "adjustment_interval": int,
393
+ "factor_milli": int,
383
394
  "max_validators": int,
384
395
  "min_allowed_weights": int,
385
396
  "rho": int,
@@ -395,6 +406,7 @@ def search_metadata(
395
406
  "bool": string_to_bool,
396
407
  "u16": string_to_u16,
397
408
  "i16": string_to_i16,
409
+ "u32": int,
398
410
  "u64": string_to_u64,
399
411
  "MechId": int,
400
412
  "U64F64": string_to_u64f64,
@@ -404,6 +416,7 @@ def search_metadata(
404
416
  "bool": "bool",
405
417
  "u16": "float",
406
418
  "i16": "float (signed)",
419
+ "u32": "int",
407
420
  "u64": "float",
408
421
  "U64F64": "decimal",
409
422
  "TaoBalance": "Tao (float)",
@@ -598,13 +611,17 @@ async def set_hyperparameter_extrinsic(
598
611
  arbitrary_extrinsic = False
599
612
 
600
613
  extrinsic, sudo_ = HYPERPARAMS.get(parameter, ("", RootSudoOnly.FALSE))
614
+ # Resolve pallet and root-path override before `parameter` is potentially
615
+ # reassigned to the extrinsic name in normalize mode.
616
+ pallet = HYPERPARAMS_MODULE.get(parameter) or DEFAULT_PALLET
617
+ root_override = HYPERPARAMS_ROOT_EXTRINSIC.get(parameter)
601
618
  call_params = {"netuid": netuid}
602
619
  if normalize and parameter != "alpha_values":
603
620
  parameter = extrinsic
604
621
  extrinsic = None
605
622
  if not extrinsic:
606
623
  arbitrary_extrinsic, call_params = search_metadata(
607
- parameter, value, netuid, subtensor.substrate.metadata
624
+ parameter, value, netuid, subtensor.substrate.metadata, pallet_name=pallet
608
625
  )
609
626
  extrinsic = parameter
610
627
  if not arbitrary_extrinsic:
@@ -624,7 +641,6 @@ async def set_hyperparameter_extrinsic(
624
641
 
625
642
  substrate = subtensor.substrate
626
643
  msg_value = value if not arbitrary_extrinsic else call_params
627
- pallet = HYPERPARAMS_MODULE.get(parameter) or DEFAULT_PALLET
628
644
 
629
645
  if not arbitrary_extrinsic:
630
646
  extrinsic_params = await substrate.get_metadata_call_function(
@@ -665,13 +681,28 @@ async def set_hyperparameter_extrinsic(
665
681
  call_params=call_params,
666
682
  block_hash=block_hash,
667
683
  )
668
- if sudo_ is RootSudoOnly.TRUE:
669
- call = await substrate.compose_call(
684
+
685
+ async def sudo_wrapped() -> GenericCall:
686
+ # Root-sudo path: some hyperparams use a dedicated root extrinsic (e.g.
687
+ # AdminUtils.sudo_set_tempo) instead of Sudo-wrapping the owner call.
688
+ inner_call = call_
689
+ if root_override:
690
+ root_pallet, root_extrinsic = root_override
691
+ inner_call = await substrate.compose_call(
692
+ call_module=root_pallet,
693
+ call_function=root_extrinsic,
694
+ call_params=call_params,
695
+ block_hash=block_hash,
696
+ )
697
+ return await substrate.compose_call(
670
698
  call_module="Sudo",
671
699
  call_function="sudo",
672
- call_params={"call": call_},
700
+ call_params={"call": inner_call},
673
701
  block_hash=block_hash,
674
702
  )
703
+
704
+ if sudo_ is RootSudoOnly.TRUE:
705
+ call = await sudo_wrapped()
675
706
  elif sudo_ is RootSudoOnly.COMPLICATED:
676
707
  if not prompt:
677
708
  # In no-prompt mode, owners should take the owner path; non-owners
@@ -684,12 +715,7 @@ async def set_hyperparameter_extrinsic(
684
715
  quiet=quiet,
685
716
  )
686
717
  if to_sudo_or_not_to_sudo:
687
- call = await substrate.compose_call(
688
- call_module="Sudo",
689
- call_function="sudo",
690
- call_params={"call": call_},
691
- block_hash=block_hash,
692
- )
718
+ call = await sudo_wrapped()
693
719
  else:
694
720
  if subnet_owner != coldkey_ss58:
695
721
  err_msg = "This wallet doesn't own the specified subnet."
@@ -1600,3 +1626,102 @@ async def trim(
1600
1626
  await print_extrinsic_id(ext_receipt)
1601
1627
  print_success(f"[dark_sea_green3]{msg}[/dark_sea_green3]")
1602
1628
  return True
1629
+
1630
+
1631
+ async def trigger_epoch(
1632
+ wallet: Wallet,
1633
+ subtensor: "SubtensorInterface",
1634
+ netuid: int,
1635
+ proxy: Optional[str],
1636
+ period: int,
1637
+ prompt: bool,
1638
+ decline: bool,
1639
+ quiet: bool,
1640
+ json_output: bool,
1641
+ ) -> bool:
1642
+ """
1643
+ Manually triggers an epoch for a subnet owned by the wallet.
1644
+
1645
+ The epoch is scheduled to fire after the chain's admin freeze window, during
1646
+ which admin operations on the subnet are locked. Rate-limited on-chain, and
1647
+ rejected if a trigger is already pending, the next automatic epoch is
1648
+ imminent, or commit-reveal is enabled on the subnet.
1649
+ """
1650
+ coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address
1651
+ print_verbose("Confirming subnet owner")
1652
+ block_hash = await subtensor.substrate.get_chain_head()
1653
+ subnet_owner = await subtensor.query(
1654
+ module="SubtensorModule",
1655
+ storage_function="SubnetOwner",
1656
+ params=[netuid],
1657
+ block_hash=block_hash,
1658
+ )
1659
+ if subnet_owner != coldkey_ss58:
1660
+ err_msg = "This wallet doesn't own the specified subnet."
1661
+ if json_output:
1662
+ json_console.print_json(data={"success": False, "message": err_msg})
1663
+ else:
1664
+ print_error(err_msg)
1665
+ return False
1666
+ if prompt and not json_output:
1667
+ if not confirm_action(
1668
+ f"You are about to manually trigger an epoch on SN{netuid}. "
1669
+ f"This locks admin operations on the subnet until the epoch fires.",
1670
+ default=False,
1671
+ decline=decline,
1672
+ quiet=quiet,
1673
+ ):
1674
+ print_error("User aborted.")
1675
+ return False
1676
+ call = await subtensor.substrate.compose_call(
1677
+ call_module="SubtensorModule",
1678
+ call_function="trigger_epoch",
1679
+ call_params={"netuid": netuid},
1680
+ block_hash=block_hash,
1681
+ )
1682
+ with console.status(
1683
+ f":satellite: Triggering epoch on subnet "
1684
+ f"[{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}] ...",
1685
+ spinner="earth",
1686
+ ):
1687
+ success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic(
1688
+ call=call, wallet=wallet, era={"period": period}, proxy=proxy
1689
+ )
1690
+ if not success:
1691
+ if json_output:
1692
+ json_console.print_json(
1693
+ data={
1694
+ "success": False,
1695
+ "message": err_msg,
1696
+ "extrinsic_identifier": None,
1697
+ }
1698
+ )
1699
+ else:
1700
+ print_error(err_msg)
1701
+ return False
1702
+ else:
1703
+ ext_id = await ext_receipt.get_extrinsic_identifier()
1704
+ fires_at = None
1705
+ try:
1706
+ for event in await ext_receipt.triggered_events:
1707
+ if event["event_id"] == "EpochTriggered":
1708
+ fires_at = event["attributes"]["fires_at"]
1709
+ except KeyError:
1710
+ # The trigger still succeeded; the fires_at block is just a nice-to-have.
1711
+ pass
1712
+ msg = f"Epoch triggered on SN{netuid}"
1713
+ if fires_at is not None:
1714
+ msg += f"; it will fire at block {fires_at} at the earliest"
1715
+ if json_output:
1716
+ json_console.print_json(
1717
+ data={
1718
+ "success": True,
1719
+ "message": msg,
1720
+ "fires_at": fires_at,
1721
+ "extrinsic_identifier": ext_id,
1722
+ }
1723
+ )
1724
+ else:
1725
+ await print_extrinsic_id(ext_receipt)
1726
+ print_success(f"[dark_sea_green3]{msg}[/dark_sea_green3]")
1727
+ return True
@@ -1239,7 +1239,9 @@ async def overview(
1239
1239
  validator_trust = nn.validator_trust
1240
1240
  incentive = nn.incentive
1241
1241
  dividends = nn.dividends
1242
- emission = int(nn.emission / (subnet_tempo + 1) * 1e9) # Per block
1242
+ # Per block: the epoch period is exactly tempo blocks under the
1243
+ # dynamic-tempo scheduler (guard against tempo == 0).
1244
+ emission = int(nn.emission / max(subnet_tempo, 1) * 1e9)
1243
1245
  last_update = int(block - nn.last_update)
1244
1246
  validator_permit = nn.validator_permit
1245
1247
  row = [
@@ -152,10 +152,38 @@ class SetWeightsExtrinsic:
152
152
 
153
153
  return success, message, ext_id
154
154
 
155
+ async def _seconds_until_reveal(self, reveal_period: int) -> int:
156
+ """
157
+ Estimates the seconds until the reveal window opens. Commits made in epoch E
158
+ become revealable in epoch E + reveal_period, i.e. after the next epoch
159
+ boundary plus (reveal_period - 1) full tempos. Best-effort: owner tempo
160
+ changes, manually triggered epochs, or per-block epoch deferrals can shift
161
+ the actual boundary.
162
+ """
163
+ bh = await self.subtensor.substrate.get_chain_head()
164
+ current_block, tempo, next_epoch = await asyncio.gather(
165
+ self.subtensor.substrate.get_block_number(block_hash=bh),
166
+ self.subtensor.get_hyperparameter(
167
+ param_name="Tempo", netuid=self.netuid, block_hash=bh
168
+ ),
169
+ self.subtensor.get_next_epoch_start_block(self.netuid, block_hash=bh),
170
+ )
171
+ tempo = int(tempo)
172
+ if next_epoch is not None:
173
+ blocks_until_next_epoch = max(next_epoch - current_block, 0)
174
+ else:
175
+ # Legacy modulo scheduler (chains without the dynamic-tempo runtime).
176
+ remainder = (current_block + self.netuid + 1) % (tempo + 1)
177
+ blocks_until_next_epoch = (tempo + 1 - remainder) % (tempo + 1)
178
+ blocks_until_reveal = (
179
+ blocks_until_next_epoch + max(reveal_period - 1, 0) * tempo
180
+ )
181
+ return blocks_until_reveal * 12
182
+
155
183
  async def _commit_reveal(
156
184
  self, weight_uids: list[int], weight_vals: list[int]
157
185
  ) -> tuple[bool, str, Optional[str]]:
158
- interval = int(
186
+ reveal_period = int(
159
187
  await self.subtensor.get_hyperparameter(
160
188
  param_name="get_commit_reveal_period",
161
189
  netuid=self.netuid,
@@ -174,6 +202,7 @@ class SetWeightsExtrinsic:
174
202
  )
175
203
 
176
204
  if commit_success:
205
+ interval = await self._seconds_until_reveal(reveal_period)
177
206
  current_time = datetime.now().astimezone().replace(microsecond=0)
178
207
  reveal_time = (current_time + timedelta(seconds=interval)).isoformat()
179
208
  cli_retry_cmd = f"--netuid {self.netuid} --uids {weight_uids} --weights {self.weights} --reveal-using-salt {self.salt}"
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
4
4
 
5
5
  [project]
6
6
  name = "bittensor-cli"
7
- version = "9.23.0rc1"
7
+ version = "9.23.0rc3"
8
8
  description = "Bittensor CLI"
9
9
  readme = "README.md"
10
10
  authors = [