Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1d5b075
Allow for use of the new archive backup endpoints specification.
thewhaleking Jun 6, 2025
c368f35
Ruff
thewhaleking Jun 6, 2025
fbacb1e
Merge remote-tracking branch 'origin/staging' into feat/thewhaleking/…
thewhaleking Jun 10, 2025
d9d57ea
Bump async-substrate-interface req
thewhaleking Jun 10, 2025
c8e13c6
Merge branch 'feat/thewhaleking/cleanup-websocket-integration-data' i…
thewhaleking Jun 10, 2025
4047dd9
Adds integration test for RetrySubtensor with Archive Node
thewhaleking Jun 10, 2025
af184ff
Merge branch 'staging' into feat/thewhaleking/retry-archive-node-support
thewhaleking Jun 10, 2025
a4410d4
Adds archive endpoints to SubtensorAPI
thewhaleking Jun 11, 2025
d5669e3
Merge branch 'staging' into feat/thewhaleking/retry-archive-node-support
thewhaleking Jun 11, 2025
5cfbb74
Adds ws_shutdown_timer arg to AsyncSubtensor and SubtensorAPI
thewhaleking Jun 11, 2025
e61c9af
Docstring.
thewhaleking Jun 11, 2025
e592660
Merge branch 'feat/thewhaleking/retry-archive-node-support' into feat…
thewhaleking Jun 11, 2025
952d956
Bumps async-substrate-interface requirement
thewhaleking Jun 13, 2025
1ebb466
Docstring update
thewhaleking Jun 13, 2025
bbaf299
Merge branch 'feat/thewhaleking/retry-archive-node-support' into feat…
thewhaleking Jun 13, 2025
12297af
Ruff
thewhaleking Jun 13, 2025
8fbf49b
Merge pull request #2917 from opentensor/feat/thewhaleking/new-websoc…
thewhaleking Jun 18, 2025
b6dfa80
Merge branch 'staging' into feat/thewhaleking/retry-archive-node-support
basfroman Jun 18, 2025
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
12 changes: 11 additions & 1 deletion bittensor/core/async_subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def __init__(
fallback_endpoints: Optional[list[str]] = None,
retry_forever: bool = False,
_mock: bool = False,
archive_endpoints: Optional[list[str]] = None,
):
"""
Initializes an instance of the AsyncSubtensor class.
Expand All @@ -130,6 +131,9 @@ def __init__(
Defaults to `None`.
retry_forever: Whether to retry forever on connection errors. Defaults to `False`.
_mock: Whether this is a mock instance. Mainly just for use in testing.
archive_endpoints: Similar to fallback_endpoints, but specifically only archive nodes. Will be used in cases
where you are requesting a block that is too old for your current (presumably lite) node. Defaults to
`None`

Raises:
Any exceptions raised during the setup, configuration, or connection process.
Expand All @@ -152,6 +156,7 @@ def __init__(
fallback_endpoints=fallback_endpoints,
retry_forever=retry_forever,
_mock=_mock,
archive_endpoints=archive_endpoints,
)
if self.log_verbose:
logging.info(
Expand Down Expand Up @@ -290,6 +295,7 @@ def _get_substrate(
fallback_endpoints: Optional[list[str]] = None,
retry_forever: bool = False,
_mock: bool = False,
archive_endpoints: Optional[list[str]] = None,
) -> Union[AsyncSubstrateInterface, RetryAsyncSubstrate]:
"""Creates the Substrate instance based on provided arguments.

Expand All @@ -298,11 +304,14 @@ def _get_substrate(
Defaults to `None`.
retry_forever: Whether to retry forever on connection errors. Defaults to `False`.
_mock: Whether this is a mock instance. Mainly just for use in testing.
archive_endpoints: Similar to fallback_endpoints, but specifically only archive nodes. Will be used in cases
where you are requesting a block that is too old for your current (presumably lite) node. Defaults to
`None`

Returns:
the instance of the SubstrateInterface or RetrySyncSubstrate class.
"""
if fallback_endpoints or retry_forever:
if fallback_endpoints or retry_forever or archive_endpoints:
return RetryAsyncSubstrate(
url=self.chain_endpoint,
fallback_chains=fallback_endpoints,
Expand All @@ -312,6 +321,7 @@ def _get_substrate(
use_remote_preset=True,
chain_name="Bittensor",
_mock=_mock,
archive_nodes=archive_endpoints,
)
return AsyncSubstrateInterface(
url=self.chain_endpoint,
Expand Down
18 changes: 14 additions & 4 deletions bittensor/core/subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,12 @@ class Subtensor(SubtensorMixin):
def __init__(
self,
network: Optional[str] = None,
config: Optional["Config"] = None,
config: Optional[Config] = None,
log_verbose: bool = False,
fallback_endpoints: Optional[list[str]] = None,
retry_forever: bool = False,
_mock: bool = False,
archive_endpoints: Optional[list[str]] = None,
):
"""
Initializes an instance of the Subtensor class.
Expand All @@ -132,6 +133,9 @@ def __init__(
Defaults to `None`.
retry_forever: Whether to retry forever on connection errors. Defaults to `False`.
_mock: Whether this is a mock instance. Mainly just for use in testing.
archive_endpoints: Similar to fallback_endpoints, but specifically only archive nodes. Will be used in cases
where you are requesting a block that is too old for your current (presumably lite) node. Defaults to
`None`

Raises:
Any exceptions raised during the setup, configuration, or connection process.
Expand All @@ -152,6 +156,7 @@ def __init__(
fallback_endpoints=fallback_endpoints,
retry_forever=retry_forever,
_mock=_mock,
archive_endpoints=archive_endpoints,
)
if self.log_verbose:
logging.info(
Expand All @@ -173,19 +178,23 @@ def _get_substrate(
fallback_endpoints: Optional[list[str]] = None,
retry_forever: bool = False,
_mock: bool = False,
archive_endpoints: Optional[list[str]] = None,
) -> Union[SubstrateInterface, RetrySyncSubstrate]:
"""Creates the Substrate instance based on provided arguments.

Arguments:
fallback_endpoints: List of fallback chains endpoints to use if main network isn't available.
Defaults to `None`.
fallback_endpoints: List of fallback chains endpoints to use if main network isn't available. Defaults to
`None`.
retry_forever: Whether to retry forever on connection errors. Defaults to `False`.
_mock: Whether this is a mock instance. Mainly just for use in testing.
archive_endpoints: Similar to fallback_endpoints, but specifically only archive nodes. Will be used in cases
where you are requesting a block that is too old for your current (presumably lite) node. Defaults to
`None`

Returns:
the instance of the SubstrateInterface or RetrySyncSubstrate class.
"""
if fallback_endpoints or retry_forever:
if fallback_endpoints or retry_forever or archive_endpoints:
return RetrySyncSubstrate(
url=self.chain_endpoint,
ss58_format=SS58_FORMAT,
Expand All @@ -195,6 +204,7 @@ def _get_substrate(
fallback_chains=fallback_endpoints,
retry_forever=retry_forever,
_mock=_mock,
archive_nodes=archive_endpoints,
)
return SubstrateInterface(
url=self.chain_endpoint,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ dependencies = [
"uvicorn",
"bittensor-drand>=0.5.0",
"bittensor-wallet>=3.0.8",
"async-substrate-interface>=1.2.0"
"async-substrate-interface>=1.3.0"
]

[project.optional-dependencies]
Expand Down
4 changes: 4 additions & 0 deletions tests/helpers/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import itertools
import json
import time
from collections import deque
Expand Down Expand Up @@ -204,6 +205,9 @@ def recv(self, *args, **kwargs):
response = WEBSOCKET_RESPONSES[self.seed][item["method"]][
json.dumps(item["params"])
]
if isinstance(response, itertools.cycle):
# Allows us to cycle through different responses for the same method/params combo
response = next(response)
response["id"] = _id
return json.dumps(response)
except (KeyError, TypeError):
Expand Down
203 changes: 203 additions & 0 deletions tests/helpers/integration_websocket_data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# This is dictionary of raw websocket send vs recv data for given methods used for integration testing, essentially
# stubbing the network I/O only
# This is basically a JSON file, but is not a JSON file because of the discrepancies between JSON and Python types

from itertools import cycle


WEBSOCKET_RESPONSES = {
"blocks_since_last_update": {
"chain_getHead": {
Expand Down Expand Up @@ -9049,4 +9053,203 @@
},
"system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}},
},
"retry_archive": {
"system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}},
"chain_getHead": {
"[]": {
"jsonrpc": "2.0",
"result": "0xae6bd3bf8cc2a7660a315510f72a7e8c8dc31ad58c8d847e86682ee07c6e8938",
}
},
"chain_getHeader": {
'["0x0f28c25f01cc4b11bb1454c323bd3d04658e4e64b69fcd21ffa01bac0ae47b19"]': {
"jsonrpc": "2.0",
"result": {
"parentHash": "0x3c1304811d6b0f5741c0ce9fcd46416c3fe151561463134b0fa9b68f1bd0188d",
"number": "0x57c8bb",
"stateRoot": "0x48b804f020e871af4eae34fb9d1fae077ff988016e65fafc9e3acbcdc8cfc8d9",
"extrinsicsRoot": "0x9b28450ef2ca7730b29e104d85e65e23cf021d405fb0fb29226dd04b89d36182",
"digest": {
"logs": [
"0x06617572612023b8b00800000000",
"0x0466726f6e8801446b1f0502f0e66079329d4718fb2f5cd5660a74a1bc0c7556c9ecbd92b54b3700",
"0x05617572610101a8461545e8f4947dd540f2f2131fdf29117e471a9c46970a408ef9684dbf495cdaebeaf8a42efcf0a57fdf75ddf807fb1f4c21fb1fb74b6715cf59a23f24d98e",
]
},
},
},
"[null]": {
"jsonrpc": "2.0",
"result": {
"parentHash": "0x3c1304811d6b0f5741c0ce9fcd46416c3fe151561463134b0fa9b68f1bd0188d",
"number": "0x57c8bb",
"stateRoot": "0x48b804f020e871af4eae34fb9d1fae077ff988016e65fafc9e3acbcdc8cfc8d9",
"extrinsicsRoot": "0x9b28450ef2ca7730b29e104d85e65e23cf021d405fb0fb29226dd04b89d36182",
"digest": {
"logs": [
"0x06617572612023b8b00800000000",
"0x0466726f6e8801446b1f0502f0e66079329d4718fb2f5cd5660a74a1bc0c7556c9ecbd92b54b3700",
"0x05617572610101a8461545e8f4947dd540f2f2131fdf29117e471a9c46970a408ef9684dbf495cdaebeaf8a42efcf0a57fdf75ddf807fb1f4c21fb1fb74b6715cf59a23f24d98e",
]
},
},
},
'["0x0ba73c8d5a0b78f7336a3e394a2a1fa7b7dec0924937b1dfb05a50ac3dd5cfc1"]': {
"jsonrpc": "2.0",
"result": {
"parentHash": "0x6c0da8a9dd5994d374d43210dc401f000b2fe02541bc2e7cd54719655d751a18",
"number": "0x57c4d3",
"stateRoot": "0xc80611aa0e605bc2d0446479dcbce6bee0e1524d07b0a9c6293a190d6a500fef",
"extrinsicsRoot": "0x9ff58560a13b06d4a61af01ca9f0dd2a96cf5bfa6bbc2208fc7e6f763bad5590",
"digest": {
"logs": [
"0x0661757261203bb4b00800000000",
"0x0466726f6e880102e3f23c7b0347be401daa27b4bd35d09948dc1450bec025807957f499347c5800",
"0x05617572610101bcc856c718c722f327d3fbc3f94c34d97e0bfc794531e94f5590b44b3516c7598ca0ec488b81f9fbf5a9bbb5d18663bd69ca032d1c7134681d9c112387612a87",
]
},
},
},
'["0xae6bd3bf8cc2a7660a315510f72a7e8c8dc31ad58c8d847e86682ee07c6e8938"]': {
"jsonrpc": "2.0",
"result": {
"parentHash": "0x0f28c25f01cc4b11bb1454c323bd3d04658e4e64b69fcd21ffa01bac0ae47b19",
"number": "0x57c8bc",
"stateRoot": "0x87f0259d14e63525a311f4afabd7273519ac2e98e2dc70cf17292b615889b714",
"extrinsicsRoot": "0x24c53c5248e9e822fb29c381cee462e5f830bba62097d58607dead8da7f3b64b",
"digest": {
"logs": [
"0x06617572612024b8b00800000000",
"0x0466726f6e88016e91817304fa73ea8595ba3bd2e26fb838c7495ae83501bb266013b2307170f200",
"0x0561757261010152b29d8299665fd87748987347b6b91d206c04a5939ddb543e4776adcfb0ff47196fdaec1a0e2a2225e0485f1fa91b5c4421ff54944df488dcf35ecbc7a57087",
]
},
},
},
},
"state_getRuntimeVersion": {
'["0x3c1304811d6b0f5741c0ce9fcd46416c3fe151561463134b0fa9b68f1bd0188d"]': {
"jsonrpc": "2.0",
"result": {
"specName": "node-subtensor",
"implName": "node-subtensor",
"authoringVersion": 1,
"specVersion": 274,
"implVersion": 1,
"apis": [
["0xdf6acb689907609b", 5],
["0x37e397fc7c91f5e4", 2],
["0x40fe3ad401f8959a", 6],
["0xfbc577b9d747efd6", 1],
["0xd2bc9897eed08f15", 3],
["0xf78b278be53f454c", 2],
["0xdd718d5cc53262d4", 1],
["0xab3c0572291feb8b", 1],
["0xed99c5acb25eedf5", 3],
["0xbc9d89904f5b923f", 1],
["0x37c8bb1350a9a2a8", 4],
["0xf3ff14d5ab527059", 3],
["0x582211f65bb14b89", 5],
["0xe65b00e46cedd0aa", 2],
["0x42e62be4a39e5b60", 1],
["0x806df4ccaa9ed485", 1],
["0x8375104b299b74c5", 1],
["0x5d1fbfbe852f2807", 1],
["0xc6886e2f8e598b0a", 1],
],
"transactionVersion": 1,
"stateVersion": 1,
},
},
'["0x6c0da8a9dd5994d374d43210dc401f000b2fe02541bc2e7cd54719655d751a18"]': cycle(
(
{
"jsonrpc": "2.0",
"id": "oj19",
"error": {
"code": 4003,
"message": "Client error: Api called for an unknown Block: State already discarded for 0x6c0da8a9dd5994d374d43210dc401f000b2fe02541bc2e7cd54719655d751a18",
},
},
{
"jsonrpc": "2.0",
"result": {
"specName": "node-subtensor",
"implName": "node-subtensor",
"authoringVersion": 1,
"specVersion": 273,
"implVersion": 1,
"apis": [
["0xdf6acb689907609b", 5],
["0x37e397fc7c91f5e4", 2],
["0x40fe3ad401f8959a", 6],
["0xfbc577b9d747efd6", 1],
["0xd2bc9897eed08f15", 3],
["0xf78b278be53f454c", 2],
["0xdd718d5cc53262d4", 1],
["0xab3c0572291feb8b", 1],
["0xed99c5acb25eedf5", 3],
["0xbc9d89904f5b923f", 1],
["0x37c8bb1350a9a2a8", 4],
["0xf3ff14d5ab527059", 3],
["0x582211f65bb14b89", 5],
["0xe65b00e46cedd0aa", 2],
["0x42e62be4a39e5b60", 1],
["0x806df4ccaa9ed485", 1],
["0x8375104b299b74c5", 1],
["0x5d1fbfbe852f2807", 1],
["0xc6886e2f8e598b0a", 1],
],
"transactionVersion": 1,
"stateVersion": 1,
},
},
)
),
'["0x0f28c25f01cc4b11bb1454c323bd3d04658e4e64b69fcd21ffa01bac0ae47b19"]': {
"jsonrpc": "2.0",
"result": {
"specName": "node-subtensor",
"implName": "node-subtensor",
"authoringVersion": 1,
"specVersion": 274,
"implVersion": 1,
"apis": [
["0xdf6acb689907609b", 5],
["0x37e397fc7c91f5e4", 2],
["0x40fe3ad401f8959a", 6],
["0xfbc577b9d747efd6", 1],
["0xd2bc9897eed08f15", 3],
["0xf78b278be53f454c", 2],
["0xdd718d5cc53262d4", 1],
["0xab3c0572291feb8b", 1],
["0xed99c5acb25eedf5", 3],
["0xbc9d89904f5b923f", 1],
["0x37c8bb1350a9a2a8", 4],
["0xf3ff14d5ab527059", 3],
["0x582211f65bb14b89", 5],
["0xe65b00e46cedd0aa", 2],
["0x42e62be4a39e5b60", 1],
["0x806df4ccaa9ed485", 1],
["0x8375104b299b74c5", 1],
["0x5d1fbfbe852f2807", 1],
["0xc6886e2f8e598b0a", 1],
],
"transactionVersion": 1,
"stateVersion": 1,
},
},
},
"chain_getBlockHash": {
"[5752019]": {
"jsonrpc": "2.0",
"result": "0x0ba73c8d5a0b78f7336a3e394a2a1fa7b7dec0924937b1dfb05a50ac3dd5cfc1",
}
},
"chain_getBlock": {
'["0x0ba73c8d5a0b78f7336a3e394a2a1fa7b7dec0924937b1dfb05a50ac3dd5cfc1"]': {
"result": {"block": {}}
}
},
},
}
14 changes: 12 additions & 2 deletions tests/integration_tests/test_subtensor_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ def netuid():
yield 23


async def prepare_test(mocker, seed):
async def prepare_test(mocker, seed, **subtensor_args):
"""
Helper function: sets up the test environment.
"""
mocker.patch(
"async_substrate_interface.sync_substrate.connect",
mocker.Mock(return_value=FakeWebsocket(seed=seed)),
)
subtensor = Subtensor("unknown", _mock=True)
subtensor = Subtensor("unknown", _mock=True, **subtensor_args)
return subtensor


Expand Down Expand Up @@ -151,3 +151,13 @@ def test_mock_subtensor_force_register_neuron():
assert neuron1.coldkey == "cc1"
assert neuron2.hotkey == "hk2"
assert neuron2.coldkey == "cc2"


@pytest.mark.asyncio
async def test_archive_node_retry(mocker):
subtensor = await prepare_test(
mocker, "retry_archive", archive_endpoints=["ws://fake-endpoi.nt"]
)
current_block = subtensor.substrate.get_block_number()
old_block = current_block - 1000
assert isinstance((subtensor.substrate.get_block(block_number=old_block)), dict)
Loading