From fb400e7055c5114a94179ad84a1584cf91ad17ff Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 16:18:21 +0200 Subject: [PATCH 1/6] Adds a command to compare latencies of given connections to help the user pick the fastest for their region. --- bittensor_cli/cli.py | 59 ++++++++++++++++++- bittensor_cli/src/__init__.py | 1 + .../src/bittensor/subtensor_interface.py | 34 +++++++++-- 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 2044bae7..938fcbf0 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -42,7 +42,10 @@ from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters -from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.bittensor.subtensor_interface import ( + SubtensorInterface, + best_connection, +) from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -1048,6 +1051,9 @@ def __init__(self): "remove", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] )(self.liquidity_remove) + # utils app + self.utils_app.command("latency")(self.best_connection) + def generate_command_tree(self) -> Tree: """ Generates a rich.Tree of the commands, subcommands, and groups of this app @@ -6326,6 +6332,57 @@ def convert( f"{Balance.from_tao(tao).rao}{Balance.rao_unit}", ) + def best_connection( + self, + additional_networks: Optional[list[str]] = typer.Option( + None, + "--network", + help="Network(s) to test for the best connection", + ), + ): + f""" + This command will give you the latency of all finney-like network in additional to any additional networks you specify via the {arg__("--network")} flag + + The results are two-fold. One column is the overall time to initialise a connection, send a request, and wait for the result. The second column measures single ping-pong speed once connected. + + EXAMPLE + + [green]$[/green] btcli utils latency --network ws://189.234.12.45 --network wss://mysubtensor.duckdns.org + + """ + additional_networks = additional_networks or [] + if any(not x.startswith("ws") for x in additional_networks): + err_console.print( + "Invalid network endpoint. Ensure you are specifying a valid websocket endpoint.", + ) + return False + results: dict[str, list[float]] = self._run_command( + best_connection(Constants.finney_nodes + (additional_networks or [])) + ) + sorted_results = { + k: v for k, v in sorted(results.items(), key=lambda item: item[1][0]) + } + table = Table( + Column("Network"), + Column("End to End Latency", style="cyan"), + Column("Single Request Ping", style="cyan"), + title="Connection Latencies (seconds)", + caption="lower value is faster", + ) + for n_name, (overall_latency, single_request) in sorted_results.items(): + table.add_row(n_name, str(overall_latency), str(single_request)) + console.print(table) + fastest = next(iter(sorted_results.keys())) + if conf_net := self.config.get("network", ""): + if not conf_net.startswith("ws") and conf_net in Constants.networks: + conf_net = Constants.network_map[conf_net] + if conf_net != fastest: + console.print( + f"The fastest network is {fastest}. You currently have {conf_net} selected as your default network." + f"\nYou can update this with {arg__(f'btcli config set --network {fastest}')}" + ) + return True + def run(self): self.app() diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index afabc424..eb8f2acb 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -23,6 +23,7 @@ class Constants: dev_entrypoint = "wss://dev.chain.opentensor.ai:443" local_entrypoint = "ws://127.0.0.1:9944" latent_lite_entrypoint = "wss://lite.sub.latent.to:443" + finney_nodes = [finney_entrypoint, subvortex_entrypoint, latent_lite_entrypoint] network_map = { "finney": finney_entrypoint, "test": finney_test_entrypoint, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index ffe8d78e..05a09dfb 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1,21 +1,22 @@ import asyncio import os +import time from typing import Optional, Any, Union, TypedDict, Iterable import aiohttp +from async_substrate_interface.async_substrate import ( + DiskCachedAsyncSubstrateInterface, + AsyncSubstrateInterface, +) +from async_substrate_interface.errors import SubstrateRequestException from async_substrate_interface.utils.storage import StorageKey from bittensor_wallet import Wallet from bittensor_wallet.bittensor_wallet import Keypair from bittensor_wallet.utils import SS58_FORMAT from scalecodec import GenericCall -from async_substrate_interface.errors import SubstrateRequestException import typer +import websockets - -from async_substrate_interface.async_substrate import ( - DiskCachedAsyncSubstrateInterface, - AsyncSubstrateInterface, -) from bittensor_cli.src.bittensor.chain_data import ( DelegateInfo, StakeInfo, @@ -1654,3 +1655,24 @@ async def get_subnet_prices( map_[netuid_] = Balance.from_rao(int(current_price * 1e9)) return map_ + + +async def best_connection(networks: list[str]): + """ + Basic function to compare the latency of a given list of websocket endpoints + Args: + networks: list of network URIs + + Returns: + {network_name: [end_to_end_latency, single_request_latency]} + + """ + results = {n: [0.0, 0.0] for n in networks} + for network in networks: + t1 = time.monotonic() + async with websockets.connect(network) as websocket: + pong = await websocket.ping() + latency = await pong + t2 = time.monotonic() + results[network] = [t2 - t1, latency] + return results From 96539e9f4b79402e5ce0cdf0bfef9e6f8fdc59c5 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 16:37:38 +0200 Subject: [PATCH 2/6] Also send a real request --- bittensor_cli/cli.py | 13 ++++++++++--- bittensor_cli/src/bittensor/subtensor_interface.py | 11 ++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 938fcbf0..e2140c1f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6343,7 +6343,7 @@ def best_connection( f""" This command will give you the latency of all finney-like network in additional to any additional networks you specify via the {arg__("--network")} flag - The results are two-fold. One column is the overall time to initialise a connection, send a request, and wait for the result. The second column measures single ping-pong speed once connected. + The results are three-fold. One column is the overall time to initialise a connection, send the requests, and wait for the results. The second column measures single ping-pong speed once connected. The third makes a real world call to fetch the chain head. EXAMPLE @@ -6366,11 +6366,18 @@ def best_connection( Column("Network"), Column("End to End Latency", style="cyan"), Column("Single Request Ping", style="cyan"), + Column("Chain Head Request Latency", style="cyan"), title="Connection Latencies (seconds)", caption="lower value is faster", ) - for n_name, (overall_latency, single_request) in sorted_results.items(): - table.add_row(n_name, str(overall_latency), str(single_request)) + for n_name, ( + overall_latency, + single_request, + chain_head, + ) in sorted_results.items(): + table.add_row( + n_name, str(overall_latency), str(single_request), str(chain_head) + ) console.print(table) fastest = next(iter(sorted_results.keys())) if conf_net := self.config.get("network", ""): diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 05a09dfb..f4b765e1 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1664,15 +1664,20 @@ async def best_connection(networks: list[str]): networks: list of network URIs Returns: - {network_name: [end_to_end_latency, single_request_latency]} + {network_name: [end_to_end_latency, single_request_latency, chain_head_request_latency]} """ - results = {n: [0.0, 0.0] for n in networks} + results = {} for network in networks: t1 = time.monotonic() async with websockets.connect(network) as websocket: pong = await websocket.ping() latency = await pong + pt1 = time.monotonic() + await websocket.send( + "{'jsonrpc': '2.0', 'method': 'chain_getHead', 'params': [], 'id': '82'}" + ) + await websocket.recv() t2 = time.monotonic() - results[network] = [t2 - t1, latency] + results[network] = [t2 - t1, latency, t2 - pt1] return results From 293e349d23cd584d4881149229578e8a9a96176b Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 16:44:43 +0200 Subject: [PATCH 3/6] Fix help --- bittensor_cli/cli.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e2140c1f..f0e83fbd 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -622,7 +622,7 @@ class CLIManager: wallet_app: typer.Typer subnets_app: typer.Typer weights_app: typer.Typer - utils_app = typer.Typer(epilog=_epilog) + utils_app: typer.Typer view_app: typer.Typer asyncio_runner = asyncio @@ -695,6 +695,7 @@ def __init__(self): self.weights_app = typer.Typer(epilog=_epilog) self.view_app = typer.Typer(epilog=_epilog) self.liquidity_app = typer.Typer(epilog=_epilog) + self.utils_app = typer.Typer(epilog=_epilog) # config alias self.app.add_typer( @@ -1052,6 +1053,7 @@ def __init__(self): )(self.liquidity_remove) # utils app + self.utils_app.command("convert")(self.convert) self.utils_app.command("latency")(self.best_connection) def generate_command_tree(self) -> Tree: @@ -6302,7 +6304,6 @@ def liquidity_modify( ) @staticmethod - @utils_app.command("convert") def convert( from_rao: Optional[str] = typer.Option( None, "--rao", help="Convert amount from Rao" @@ -6340,8 +6341,8 @@ def best_connection( help="Network(s) to test for the best connection", ), ): - f""" - This command will give you the latency of all finney-like network in additional to any additional networks you specify via the {arg__("--network")} flag + """ + This command will give you the latency of all finney-like network in additional to any additional networks you specify via the '--network' flag The results are three-fold. One column is the overall time to initialise a connection, send the requests, and wait for the results. The second column measures single ping-pong speed once connected. The third makes a real world call to fetch the chain head. From 3398925eb332003e6b0f6f9158e808272cdfd516 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 17:01:49 +0200 Subject: [PATCH 4/6] No longer hide utils --- bittensor_cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f0e83fbd..6c3ab4b5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -770,7 +770,7 @@ def __init__(self): # utils app self.app.add_typer( - self.utils_app, name="utils", no_args_is_help=True, hidden=True + self.utils_app, name="utils", no_args_is_help=True, hidden=False ) # view app From ada539c741610de226ce1b6058d9739997b68daf Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 17:01:56 +0200 Subject: [PATCH 5/6] Ruff --- bittensor_cli/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 6c3ab4b5..7ccff4cd 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6343,11 +6343,11 @@ def best_connection( ): """ This command will give you the latency of all finney-like network in additional to any additional networks you specify via the '--network' flag - + The results are three-fold. One column is the overall time to initialise a connection, send the requests, and wait for the results. The second column measures single ping-pong speed once connected. The third makes a real world call to fetch the chain head. - + EXAMPLE - + [green]$[/green] btcli utils latency --network ws://189.234.12.45 --network wss://mysubtensor.duckdns.org """ From fcdc1c998a67454242fc0cf11abf59c5c151c113 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 1 Sep 2025 14:04:38 +0200 Subject: [PATCH 6/6] PR suggestions --- bittensor_cli/cli.py | 6 +++-- bittensor_cli/src/__init__.py | 2 +- .../src/bittensor/subtensor_interface.py | 25 +++++++++++-------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7ccff4cd..30b0c665 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6354,11 +6354,13 @@ def best_connection( additional_networks = additional_networks or [] if any(not x.startswith("ws") for x in additional_networks): err_console.print( - "Invalid network endpoint. Ensure you are specifying a valid websocket endpoint.", + "Invalid network endpoint. Ensure you are specifying a valid websocket endpoint" + f" (starting with [{COLORS.G.LINKS}]ws://[/{COLORS.G.LINKS}] or " + f"[{COLORS.G.LINKS}]wss://[/{COLORS.G.LINKS}]).", ) return False results: dict[str, list[float]] = self._run_command( - best_connection(Constants.finney_nodes + (additional_networks or [])) + best_connection(Constants.lite_nodes + additional_networks) ) sorted_results = { k: v for k, v in sorted(results.items(), key=lambda item: item[1][0]) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index eb8f2acb..67d3b0ec 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -23,7 +23,7 @@ class Constants: dev_entrypoint = "wss://dev.chain.opentensor.ai:443" local_entrypoint = "ws://127.0.0.1:9944" latent_lite_entrypoint = "wss://lite.sub.latent.to:443" - finney_nodes = [finney_entrypoint, subvortex_entrypoint, latent_lite_entrypoint] + lite_nodes = [finney_entrypoint, subvortex_entrypoint, latent_lite_entrypoint] network_map = { "finney": finney_entrypoint, "test": finney_test_entrypoint, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index f4b765e1..cb3b295f 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1669,15 +1669,18 @@ async def best_connection(networks: list[str]): """ results = {} for network in networks: - t1 = time.monotonic() - async with websockets.connect(network) as websocket: - pong = await websocket.ping() - latency = await pong - pt1 = time.monotonic() - await websocket.send( - "{'jsonrpc': '2.0', 'method': 'chain_getHead', 'params': [], 'id': '82'}" - ) - await websocket.recv() - t2 = time.monotonic() - results[network] = [t2 - t1, latency, t2 - pt1] + try: + t1 = time.monotonic() + async with websockets.connect(network) as websocket: + pong = await websocket.ping() + latency = await pong + pt1 = time.monotonic() + await websocket.send( + "{'jsonrpc': '2.0', 'method': 'chain_getHead', 'params': [], 'id': '82'}" + ) + await websocket.recv() + t2 = time.monotonic() + results[network] = [t2 - t1, latency, t2 - pt1] + except Exception as e: + err_console.print(f"Error attempting network {network}: {e}") return results