Skip to content

Commit 410a199

Browse files
committed
Merge branch 'staging' into release/8.4.0
2 parents 582f4f5 + 875c7f1 commit 410a199

File tree

9 files changed

+218
-80
lines changed

9 files changed

+218
-80
lines changed

bittensor/core/async_subtensor.py

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -831,29 +831,18 @@ async def neurons(
831831
832832
Understanding the distribution and status of neurons within a subnet is key to comprehending the network's decentralized structure and the dynamics of its consensus and governance processes.
833833
"""
834-
if not block_hash:
835-
if reuse_block:
836-
block_hash = self.substrate.last_block_hash
837-
else:
838-
block_hash = await self.substrate.get_chain_head()
839-
840-
neurons_lite, weights, bonds = await asyncio.gather(
841-
self.neurons_lite(netuid=netuid, block_hash=block_hash),
842-
self.weights(netuid=netuid, block_hash=block_hash),
843-
self.bonds(netuid=netuid, block_hash=block_hash),
834+
hex_bytes_result = await self.query_runtime_api(
835+
runtime_api="NeuronInfoRuntimeApi",
836+
method="get_neurons",
837+
params=[netuid],
838+
block_hash=block_hash,
839+
reuse_block=reuse_block,
844840
)
845841

846-
weights_as_dict = {uid: w for uid, w in weights}
847-
bonds_as_dict = {uid: b for uid, b in bonds}
848-
849-
neurons = [
850-
NeuronInfo.from_weights_bonds_and_neuron_lite(
851-
neuron_lite, weights_as_dict, bonds_as_dict
852-
)
853-
for neuron_lite in neurons_lite
854-
]
842+
if hex_bytes_result is None:
843+
return []
855844

856-
return neurons
845+
return NeuronInfo.list_from_vec_u8(hex_to_bytes(hex_bytes_result))
857846

858847
async def neurons_lite(
859848
self, netuid: int, block_hash: Optional[str] = None, reuse_block: bool = False

bittensor/core/chain_data/neuron_info.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,57 @@ def from_vec_u8(cls, vec_u8: bytes) -> "NeuronInfo":
174174
),
175175
is_null=False,
176176
)
177+
178+
@classmethod
179+
def list_from_vec_u8(cls, vec_u8: bytes) -> list["NeuronInfo"]:
180+
nn = bt_decode.NeuronInfo.decode_vec(bytes(vec_u8))
181+
182+
def fix(n):
183+
stake_dict = process_stake_data(n.stake)
184+
total_stake = sum(stake_dict.values()) if stake_dict else Balance(0)
185+
axon_info = n.axon_info
186+
coldkey = decode_account_id(n.coldkey)
187+
hotkey = decode_account_id(n.hotkey)
188+
return NeuronInfo(
189+
hotkey=hotkey,
190+
coldkey=coldkey,
191+
uid=n.uid,
192+
netuid=n.netuid,
193+
active=n.active,
194+
stake=total_stake,
195+
stake_dict=stake_dict,
196+
total_stake=total_stake,
197+
rank=u16_normalized_float(n.rank),
198+
emission=n.emission / 1e9,
199+
incentive=u16_normalized_float(n.incentive),
200+
consensus=u16_normalized_float(n.consensus),
201+
trust=u16_normalized_float(n.trust),
202+
validator_trust=u16_normalized_float(n.validator_trust),
203+
dividends=u16_normalized_float(n.dividends),
204+
last_update=n.last_update,
205+
validator_permit=n.validator_permit,
206+
weights=[[e[0], e[1]] for e in n.weights],
207+
bonds=[[e[0], e[1]] for e in n.bonds],
208+
pruning_score=n.pruning_score,
209+
prometheus_info=PrometheusInfo(
210+
block=n.prometheus_info.block,
211+
version=n.prometheus_info.version,
212+
ip=str(netaddr.IPAddress(n.prometheus_info.ip)),
213+
port=n.prometheus_info.port,
214+
ip_type=n.prometheus_info.ip_type,
215+
),
216+
axon_info=AxonInfo(
217+
version=axon_info.version,
218+
ip=str(netaddr.IPAddress(axon_info.ip)),
219+
port=axon_info.port,
220+
ip_type=axon_info.ip_type,
221+
placeholder1=axon_info.placeholder1,
222+
placeholder2=axon_info.placeholder2,
223+
protocol=axon_info.protocol,
224+
hotkey=hotkey,
225+
coldkey=coldkey,
226+
),
227+
is_null=False,
228+
)
229+
230+
return [fix(n) for n in nn]

bittensor/core/subtensor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def _get_substrate(self):
216216
"""Establishes a connection to the Substrate node using configured parameters."""
217217
try:
218218
# Set up params.
219-
if not self.websocket:
219+
if self.websocket is None or self.websocket.close_code is not None:
220220
self.websocket = ws_client.connect(
221221
self.chain_endpoint,
222222
open_timeout=self._connection_timeout,

bittensor/utils/async_substrate_interface.py

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import asyncio
8+
import inspect
89
import json
910
import random
1011
from collections import defaultdict
@@ -1171,14 +1172,14 @@ async def _get_block_handler(
11711172
include_author: bool = False,
11721173
header_only: bool = False,
11731174
finalized_only: bool = False,
1174-
subscription_handler: Optional[Callable] = None,
1175+
subscription_handler: Optional[Callable[[dict], Awaitable[Any]]] = None,
11751176
):
11761177
try:
11771178
await self.init_runtime(block_hash=block_hash)
11781179
except BlockNotFound:
11791180
return None
11801181

1181-
async def decode_block(block_data, block_data_hash=None):
1182+
async def decode_block(block_data, block_data_hash=None) -> dict[str, Any]:
11821183
if block_data:
11831184
if block_data_hash:
11841185
block_data["header"]["hash"] = block_data_hash
@@ -1193,12 +1194,12 @@ async def decode_block(block_data, block_data_hash=None):
11931194

11941195
if "extrinsics" in block_data:
11951196
for idx, extrinsic_data in enumerate(block_data["extrinsics"]):
1196-
extrinsic_decoder = extrinsic_cls(
1197-
data=ScaleBytes(extrinsic_data),
1198-
metadata=self.__metadata,
1199-
runtime_config=self.runtime_config,
1200-
)
12011197
try:
1198+
extrinsic_decoder = extrinsic_cls(
1199+
data=ScaleBytes(extrinsic_data),
1200+
metadata=self.__metadata,
1201+
runtime_config=self.runtime_config,
1202+
)
12021203
extrinsic_decoder.decode(check_remaining=True)
12031204
block_data["extrinsics"][idx] = extrinsic_decoder
12041205

@@ -1314,23 +1315,29 @@ async def decode_block(block_data, block_data_hash=None):
13141315
if callable(subscription_handler):
13151316
rpc_method_prefix = "Finalized" if finalized_only else "New"
13161317

1317-
async def result_handler(message, update_nr, subscription_id):
1318-
new_block = await decode_block({"header": message["params"]["result"]})
1318+
async def result_handler(
1319+
message: dict, subscription_id: str
1320+
) -> tuple[Any, bool]:
1321+
reached = False
1322+
subscription_result = None
1323+
if "params" in message:
1324+
new_block = await decode_block(
1325+
{"header": message["params"]["result"]}
1326+
)
13191327

1320-
subscription_result = subscription_handler(
1321-
new_block, update_nr, subscription_id
1322-
)
1328+
subscription_result = await subscription_handler(new_block)
13231329

1324-
if subscription_result is not None:
1325-
# Handler returned end result: unsubscribe from further updates
1326-
self._forgettable_task = asyncio.create_task(
1327-
self.rpc_request(
1328-
f"chain_unsubscribe{rpc_method_prefix}Heads",
1329-
[subscription_id],
1330+
if subscription_result is not None:
1331+
reached = True
1332+
# Handler returned end result: unsubscribe from further updates
1333+
self._forgettable_task = asyncio.create_task(
1334+
self.rpc_request(
1335+
f"chain_unsubscribe{rpc_method_prefix}Heads",
1336+
[subscription_id],
1337+
)
13301338
)
1331-
)
13321339

1333-
return subscription_result
1340+
return subscription_result, reached
13341341

13351342
result = await self._make_rpc_request(
13361343
[
@@ -1343,7 +1350,7 @@ async def result_handler(message, update_nr, subscription_id):
13431350
result_handler=result_handler,
13441351
)
13451352

1346-
return result
1353+
return result["_get_block_handler"][-1]
13471354

13481355
else:
13491356
if header_only:
@@ -2770,3 +2777,49 @@ async def close(self):
27702777
await self.ws.shutdown()
27712778
except AttributeError:
27722779
pass
2780+
2781+
async def wait_for_block(
2782+
self,
2783+
block: int,
2784+
result_handler: Callable[[dict], Awaitable[Any]],
2785+
task_return: bool = True,
2786+
) -> Union[asyncio.Task, Union[bool, Any]]:
2787+
"""
2788+
Executes the result_handler when the chain has reached the block specified.
2789+
2790+
Args:
2791+
block: block number
2792+
result_handler: coroutine executed upon reaching the block number. This can be basically anything, but
2793+
must accept one single arg, a dict with the block data; whether you use this data or not is entirely
2794+
up to you.
2795+
task_return: True to immediately return the result of wait_for_block as an asyncio Task, False to wait
2796+
for the block to be reached, and return the result of the result handler.
2797+
2798+
Returns:
2799+
Either an asyncio.Task (which contains the running subscription, and whose `result()` will contain the
2800+
return of the result_handler), or the result itself, depending on `task_return` flag.
2801+
Note that if your result_handler returns `None`, this method will return `True`, otherwise
2802+
the return will be the result of your result_handler.
2803+
"""
2804+
2805+
async def _handler(block_data: dict[str, Any]):
2806+
required_number = block
2807+
number = block_data["header"]["number"]
2808+
if number >= required_number:
2809+
return (
2810+
r if (r := await result_handler(block_data)) is not None else True
2811+
)
2812+
2813+
args = inspect.getfullargspec(result_handler).args
2814+
if len(args) != 1:
2815+
raise ValueError(
2816+
"result_handler must take exactly one arg: the dict block data."
2817+
)
2818+
2819+
co = self._get_block_handler(
2820+
self.last_block_hash, subscription_handler=_handler
2821+
)
2822+
if task_return is True:
2823+
return asyncio.create_task(co)
2824+
else:
2825+
return await co

bittensor/utils/mock/subtensor_mock.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
from hashlib import sha256
2121
from types import SimpleNamespace
2222
from typing import Any, Optional, Union, TypedDict
23-
from unittest.mock import MagicMock
23+
from unittest.mock import MagicMock, patch
2424

2525
from bittensor_wallet import Wallet
26+
from substrateinterface.base import SubstrateInterface
27+
from websockets.sync.client import ClientConnection
2628

2729
from bittensor.core.chain_data import (
2830
NeuronInfo,
@@ -33,6 +35,7 @@
3335
from bittensor.core.types import AxonServeCallParams, PrometheusServeCallParams
3436
from bittensor.core.errors import ChainQueryError
3537
from bittensor.core.subtensor import Subtensor
38+
import bittensor.core.subtensor as subtensor_module
3639
from bittensor.utils import RAOPERTAO, u16_normalized_float
3740
from bittensor.utils.balance import Balance
3841

@@ -248,14 +251,22 @@ def setup(self) -> None:
248251

249252
self.network = "mock"
250253
self.chain_endpoint = "ws://mock_endpoint.bt"
251-
self.substrate = MagicMock()
254+
self.substrate = MagicMock(autospec=SubstrateInterface)
252255

253256
def __init__(self, *args, **kwargs) -> None:
254-
super().__init__()
255-
self.__dict__ = __GLOBAL_MOCK_STATE__
256-
257-
if not hasattr(self, "chain_state") or getattr(self, "chain_state") is None:
258-
self.setup()
257+
mock_substrate_interface = MagicMock(autospec=SubstrateInterface)
258+
mock_websocket = MagicMock(autospec=ClientConnection)
259+
mock_websocket.close_code = None
260+
with patch.object(
261+
subtensor_module,
262+
"SubstrateInterface",
263+
return_value=mock_substrate_interface,
264+
):
265+
super().__init__(websocket=mock_websocket)
266+
self.__dict__ = __GLOBAL_MOCK_STATE__
267+
268+
if not hasattr(self, "chain_state") or getattr(self, "chain_state") is None:
269+
self.setup()
259270

260271
def get_block_hash(self, block_id: int) -> str:
261272
return "0x" + sha256(str(block_id).encode()).hexdigest()[:64]

bittensor/utils/networking.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import netaddr
1111
import requests
1212
from retry import retry
13-
from websocket import WebSocketConnectionClosedException
13+
from websockets.exceptions import ConnectionClosed
1414

1515
from bittensor.utils.btlogging import logging
1616

@@ -193,7 +193,7 @@ def wrapper(self, *args, **kwargs):
193193

194194
try:
195195
return func(self, *args, **kwargs)
196-
except WebSocketConnectionClosedException:
196+
except ConnectionClosed:
197197
logging.console.warning(
198198
"WebSocket connection closed. Attempting to reconnect 5 times..."
199199
)

tests/helpers/helpers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ def get_mock_neuron_by_uid(uid: int, **kwargs) -> NeuronInfo:
125125

126126

127127
class FakeWebsocket(ClientConnection):
128+
close_code = None
129+
128130
def __init__(self, *args, seed, **kwargs):
129131
protocol = ClientProtocol(parse_uri("ws://127.0.0.1:9945"))
130132
super().__init__(socket=None, protocol=protocol, **kwargs)

tests/unit_tests/test_async_subtensor.py

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -992,41 +992,32 @@ async def test_neurons(subtensor, mocker):
992992
# Preps
993993
fake_netuid = 1
994994
fake_block_hash = "block_hash"
995-
fake_neurons = [mocker.Mock(), mocker.Mock()]
996-
fake_weights = [(1, [(10, 20), (30, 40)]), (2, [(50, 60), (70, 80)])]
997-
fake_bonds = [(1, [(10, 20), (30, 40)]), (2, [(50, 60), (70, 80)])]
998-
999-
mocked_neurons_lite = mocker.AsyncMock(return_value=fake_neurons)
1000-
subtensor.neurons_lite = mocked_neurons_lite
1001-
1002-
mocked_weights = mocker.AsyncMock(return_value=fake_weights)
1003-
subtensor.weights = mocked_weights
1004-
1005-
mocked_bonds = mocker.AsyncMock(return_value=fake_bonds)
1006-
subtensor.bonds = mocked_bonds
995+
fake_reuse_block_hash = True
1007996

1008-
mocked_neuron_info_method = mocker.Mock()
1009-
async_subtensor.NeuronInfo.from_weights_bonds_and_neuron_lite = (
1010-
mocked_neuron_info_method
997+
mocked_query_runtime_api = mocker.patch.object(
998+
subtensor, "query_runtime_api", return_value="NOT NONE"
999+
)
1000+
mocked_hex_to_bytes = mocker.patch.object(async_subtensor, "hex_to_bytes")
1001+
mocked_neuron_info_list_from_vec_u8 = mocker.patch.object(
1002+
async_subtensor.NeuronInfo, "list_from_vec_u8"
10111003
)
1012-
10131004
# Call
1014-
result = await subtensor.neurons(netuid=fake_netuid, block_hash=fake_block_hash)
1005+
result = await subtensor.neurons(
1006+
netuid=fake_netuid,
1007+
block_hash=fake_block_hash,
1008+
reuse_block=fake_reuse_block_hash,
1009+
)
10151010

10161011
# Asserts
1017-
mocked_neurons_lite.assert_awaited_once()
1018-
mocked_neurons_lite.assert_called_once_with(
1019-
netuid=fake_netuid, block_hash=fake_block_hash
1020-
)
1021-
mocked_weights.assert_awaited_once()
1022-
mocked_weights.assert_called_once_with(
1023-
netuid=fake_netuid, block_hash=fake_block_hash
1012+
mocked_query_runtime_api.assert_called_once_with(
1013+
runtime_api="NeuronInfoRuntimeApi",
1014+
method="get_neurons",
1015+
params=[fake_netuid],
1016+
block_hash=fake_block_hash,
1017+
reuse_block=fake_reuse_block_hash,
10241018
)
1025-
mocked_bonds.assert_awaited_once()
1026-
mocked_bonds.assert_called_once_with(netuid=fake_netuid, block_hash=fake_block_hash)
1027-
assert result == [
1028-
mocked_neuron_info_method.return_value for _ in range(len(fake_neurons))
1029-
]
1019+
mocked_hex_to_bytes.assert_called_once_with(mocked_query_runtime_api.return_value)
1020+
assert result == mocked_neuron_info_list_from_vec_u8.return_value
10301021

10311022

10321023
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)