diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 16a40a6608..9e9f45ef9b 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -120,6 +120,8 @@ def __init__( fallback_endpoints: Optional[list[str]] = None, retry_forever: bool = False, _mock: bool = False, + archive_endpoints: Optional[list[str]] = None, + websocket_shutdown_timer: float = 5.0, ): """ Initializes an instance of the AsyncSubtensor class. @@ -132,6 +134,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. @@ -154,6 +159,8 @@ def __init__( fallback_endpoints=fallback_endpoints, retry_forever=retry_forever, _mock=_mock, + archive_endpoints=archive_endpoints, + ws_shutdown_timer=websocket_shutdown_timer, ) if self.log_verbose: logging.info( @@ -292,6 +299,8 @@ def _get_substrate( fallback_endpoints: Optional[list[str]] = None, retry_forever: bool = False, _mock: bool = False, + archive_endpoints: Optional[list[str]] = None, + ws_shutdown_timer: float = 5.0, ) -> Union[AsyncSubstrateInterface, RetryAsyncSubstrate]: """Creates the Substrate instance based on provided arguments. @@ -300,11 +309,16 @@ 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` + ws_shutdown_timer: Amount of time, in seconds, to wait after the last response from the chain to close the + connection. 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, @@ -314,6 +328,8 @@ def _get_substrate( use_remote_preset=True, chain_name="Bittensor", _mock=_mock, + archive_nodes=archive_endpoints, + ws_shutdown_timer=ws_shutdown_timer, ) return AsyncSubstrateInterface( url=self.chain_endpoint, @@ -322,6 +338,7 @@ def _get_substrate( use_remote_preset=True, chain_name="Bittensor", _mock=_mock, + ws_shutdown_timer=ws_shutdown_timer, ) # Subtensor queries =========================================================================================== diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 788130f4b1..4f5aab1e83 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -117,11 +117,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. @@ -134,6 +135,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. @@ -154,6 +158,7 @@ def __init__( fallback_endpoints=fallback_endpoints, retry_forever=retry_forever, _mock=_mock, + archive_endpoints=archive_endpoints, ) if self.log_verbose: logging.info( @@ -175,19 +180,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, @@ -197,6 +206,7 @@ def _get_substrate( fallback_chains=fallback_endpoints, retry_forever=retry_forever, _mock=_mock, + archive_nodes=archive_endpoints, ) return SubstrateInterface( url=self.chain_endpoint, diff --git a/bittensor/core/subtensor_api/__init__.py b/bittensor/core/subtensor_api/__init__.py index 2d021cd1ce..5ae0bf7134 100644 --- a/bittensor/core/subtensor_api/__init__.py +++ b/bittensor/core/subtensor_api/__init__.py @@ -29,6 +29,10 @@ class SubtensorApi: retry_forever: Whether to retry forever on connection errors. Defaults to `False`. log_verbose: Enables or disables verbose logging. 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` + websocket_shutdown_timer: Amount of time, in seconds, to wait after the last response from the chain to close + the connection. Only applicable to AsyncSubtensor. Example: # sync version @@ -75,10 +79,14 @@ def __init__( retry_forever: bool = False, log_verbose: bool = False, mock: bool = False, + archive_endpoints: Optional[list[str]] = None, + websocket_shutdown_timer: float = 5.0, ): self.network = network self._fallback_endpoints = fallback_endpoints + self._archive_endpoints = archive_endpoints self._retry_forever = retry_forever + self._ws_shutdown_timer = websocket_shutdown_timer self._mock = mock self.log_verbose = log_verbose self.is_async = async_subtensor @@ -119,6 +127,8 @@ def _get_subtensor(self) -> Union["_Subtensor", "_AsyncSubtensor"]: fallback_endpoints=self._fallback_endpoints, retry_forever=self._retry_forever, _mock=self._mock, + archive_endpoints=self._archive_endpoints, + websocket_shutdown_timer=self._ws_shutdown_timer, ) self.initialize = _subtensor.initialize return _subtensor @@ -130,6 +140,7 @@ def _get_subtensor(self) -> Union["_Subtensor", "_AsyncSubtensor"]: fallback_endpoints=self._fallback_endpoints, retry_forever=self._retry_forever, _mock=self._mock, + archive_endpoints=self._archive_endpoints, ) def _determine_chain_endpoint(self) -> str: diff --git a/pyproject.toml b/pyproject.toml index 51f6c28bff..91869e8822 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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.1" ] [project.optional-dependencies] diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py index df9cd0a388..fed6638c52 100644 --- a/tests/helpers/helpers.py +++ b/tests/helpers/helpers.py @@ -1,4 +1,5 @@ import asyncio +import itertools import json import time from collections import deque @@ -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): diff --git a/tests/helpers/integration_websocket_data.py b/tests/helpers/integration_websocket_data.py index bede3b684e..41b4f64d1f 100644 --- a/tests/helpers/integration_websocket_data.py +++ b/tests/helpers/integration_websocket_data.py @@ -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": { @@ -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": {}} + } + }, + }, } diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index 0e7c62151a..6c23ae82af 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -17,7 +17,7 @@ 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. """ @@ -25,7 +25,7 @@ async def prepare_test(mocker, seed): "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 @@ -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)