Skip to content

Commit 563a8e1

Browse files
authored
[v10] Adds block backwards compatibility for fetching metagraph. (#3139)
* Adds block backwards compatibility for fetching metagraph. * Test fixes * Test fixes * Integration data updated * Tests * Integration * More tests fixed * Reusable * Ruff * Apply logic to sync subtensor * Reusable runtime logic async * Reusable runtime logic sync * Merge
1 parent 9984cff commit 563a8e1

File tree

9 files changed

+689
-79
lines changed

9 files changed

+689
-79
lines changed

bittensor/core/async_subtensor.py

Lines changed: 180 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,148 @@ async def determine_block_hash(
460460
return await self.get_block_hash(block)
461461
return None
462462

463+
async def _runtime_method_exists(
464+
self, api: str, method: str, block_hash: str
465+
) -> bool:
466+
"""
467+
Check if a runtime call method exists at the given block.
468+
469+
The complicated logic here comes from the fact that there are two ways in which runtime calls
470+
are stored: the new and primary method is through the Metadata V15, but the V14 is a good backup (implemented
471+
around mid 2024)
472+
473+
Returns:
474+
True if the runtime call method exists, False otherwise.
475+
"""
476+
runtime = await self.substrate.init_runtime(block_hash=block_hash)
477+
if runtime.metadata_v15 is not None:
478+
metadata_v15_value = runtime.metadata_v15.value()
479+
apis = {entry["name"]: entry for entry in metadata_v15_value["apis"]}
480+
try:
481+
api_entry = apis[api]
482+
methods = {entry["name"]: entry for entry in api_entry["methods"]}
483+
_ = methods[method]
484+
return True
485+
except KeyError:
486+
return False
487+
else:
488+
try:
489+
await self.substrate.get_metadata_runtime_call_function(
490+
api=api,
491+
method=method,
492+
block_hash=block_hash,
493+
)
494+
return True
495+
except ValueError:
496+
return False
497+
498+
async def _query_with_fallback(
499+
self,
500+
*args: tuple[str, str, Optional[list[Any]]],
501+
block_hash: Optional[str] = None,
502+
default_value: Any = ValueError,
503+
):
504+
"""
505+
Queries the subtensor node with a given set of args, falling back to the next group if the method
506+
does not exist at the given block. This method exists to support backwards compatibility for blocks.
507+
508+
Parameters:
509+
*args: Tuples containing (module, storage_function, params) in the order they should be attempted.
510+
block_hash: The hash of the block being queried. If not provided, the chain tip will be used.
511+
default_value: The default value to return if none of the methods exist at the given block.
512+
513+
Returns:
514+
The value returned by the subtensor node, or the default value if none of the methods exist at the given
515+
block.
516+
517+
Raises:
518+
ValueError: If no default value is provided, and none of the methods exist at the given block, a
519+
ValueError will be raised.
520+
521+
Example:
522+
value = await self._query_with_fallback(
523+
# the first attempt will be made to SubtensorModule.MechanismEmissionSplit with params `[1]`
524+
("SubtensorModule", "MechanismEmissionSplit", [1]),
525+
# if it does not exist at the given block, the next attempt will be made to
526+
# SubtensorModule.MechanismEmission with params `None`
527+
("SubtensorModule", "MechanismEmission", None),
528+
block_hash="0x1234",
529+
# if none of the methods exist at the given block, the default value of `None` will be returned
530+
default_value=None,
531+
)
532+
"""
533+
if block_hash is None:
534+
block_hash = await self.substrate.get_chain_head()
535+
for module, storage_function, params in args:
536+
if await self.substrate.get_metadata_storage_function(
537+
module_name=module,
538+
storage_name=storage_function,
539+
block_hash=block_hash,
540+
):
541+
return await self.substrate.query(
542+
module=module,
543+
storage_function=storage_function,
544+
block_hash=block_hash,
545+
params=params,
546+
)
547+
if not isinstance(default_value, ValueError):
548+
return default_value
549+
else:
550+
raise default_value
551+
552+
async def _runtime_call_with_fallback(
553+
self,
554+
*args: tuple[str, str, Optional[list[Any]] | dict[str, Any]],
555+
block_hash: Optional[str] = None,
556+
default_value: Any = ValueError,
557+
):
558+
"""
559+
Makes a runtime call to the subtensor node with a given set of args, falling back to the next group if the
560+
api.method does not exist at the given block. This method exists to support backwards compatibility for blocks.
561+
562+
Parameters:
563+
*args: Tuples containing (api, method, params) in the order they should be attempted.
564+
block_hash: The hash of the block being queried. If not provided, the chain tip will be used.
565+
default_value: The default value to return if none of the methods exist at the given block.
566+
567+
Raises:
568+
ValueError: If no default value is provided, and none of the methods exist at the given block, a
569+
ValueError will be raised.
570+
571+
Example:
572+
query = await self._runtime_call_with_fallback(
573+
# the first attempt will be made to SubnetInfoRuntimeApi.get_selective_mechagraph with the
574+
# given params
575+
(
576+
"SubnetInfoRuntimeApi",
577+
"get_selective_mechagraph",
578+
[netuid, mechid, [f for f in range(len(SelectiveMetagraphIndex))]],
579+
),
580+
# if it does not exist at the given block, the next attempt will be made as such:
581+
("SubnetInfoRuntimeApi", "get_metagraph", [[netuid]]),
582+
block_hash=block_hash,
583+
# if none of the methods exist at the given block, the default value will be returned
584+
default_value=None,
585+
)
586+
587+
"""
588+
if block_hash is None:
589+
block_hash = await self.substrate.get_chain_head()
590+
for api, method, params in args:
591+
if await self._runtime_method_exists(
592+
api=api, method=method, block_hash=block_hash
593+
):
594+
return await self.substrate.runtime_call(
595+
api=api,
596+
method=method,
597+
block_hash=block_hash,
598+
params=params,
599+
)
600+
if not isinstance(default_value, ValueError):
601+
return default_value
602+
else:
603+
raise default_value
604+
463605
async def get_hyperparameter(
464606
self,
465607
param_name: str,
@@ -2667,11 +2809,10 @@ async def get_mechanism_emission_split(
26672809
whole numbers). Returns None if emission is evenly split or if the data is unavailable.
26682810
"""
26692811
block_hash = await self.determine_block_hash(block, block_hash, reuse_block)
2670-
result = await self.substrate.query(
2671-
module="SubtensorModule",
2672-
storage_function="MechanismEmissionSplit",
2673-
params=[netuid],
2812+
result = await self._query_with_fallback(
2813+
("SubtensorModule", "MechanismEmissionSplit", [netuid]),
26742814
block_hash=block_hash,
2815+
default_value=None,
26752816
)
26762817
if result is None or not hasattr(result, "value"):
26772818
return None
@@ -2697,11 +2838,10 @@ async def get_mechanism_count(
26972838
The number of mechanisms for the given subnet.
26982839
"""
26992840
block_hash = await self.determine_block_hash(block, block_hash, reuse_block)
2700-
query = await self.substrate.query(
2701-
module="SubtensorModule",
2702-
storage_function="MechanismCountCurrent",
2703-
params=[netuid],
2841+
query = await self._query_with_fallback(
2842+
("SubtensorModule", "MechanismCountCurrent", [netuid]),
27042843
block_hash=block_hash,
2844+
default_value=None,
27052845
)
27062846
return getattr(query, "value", 1)
27072847

@@ -2763,22 +2903,43 @@ async def get_metagraph_info(
27632903
block_hash = await self.determine_block_hash(block, block_hash, reuse_block)
27642904
if not block_hash and reuse_block:
27652905
block_hash = self.substrate.last_block_hash
2906+
if not block_hash:
2907+
block_hash = await self.substrate.get_chain_head()
27662908

2767-
indexes = (
2768-
[
2909+
# Normalize selected_indices to a list of integers
2910+
if selected_indices is not None:
2911+
indexes = [
27692912
f.value if isinstance(f, SelectiveMetagraphIndex) else f
27702913
for f in selected_indices
27712914
]
2772-
if selected_indices is not None
2773-
else [f for f in range(len(SelectiveMetagraphIndex))]
2774-
)
2915+
if 0 not in indexes:
2916+
indexes = [0] + indexes
2917+
query = await self._runtime_call_with_fallback(
2918+
(
2919+
"SubnetInfoRuntimeApi",
2920+
"get_selective_mechagraph",
2921+
[netuid, mechid, indexes],
2922+
),
2923+
("SubnetInfoRuntimeApi", "get_selective_metagraph", [netuid, indexes]),
2924+
block_hash=block_hash,
2925+
default_value=ValueError(
2926+
"You have specified `selected_indices` to retrieve metagraph info selectively, but the "
2927+
"selective runtime calls are not available at this block (probably too old). Do not specify "
2928+
"`selected_indices` to retrieve metagraph info selectively."
2929+
),
2930+
)
2931+
else:
2932+
query = await self._runtime_call_with_fallback(
2933+
(
2934+
"SubnetInfoRuntimeApi",
2935+
"get_selective_mechagraph",
2936+
[netuid, mechid, [f for f in range(len(SelectiveMetagraphIndex))]],
2937+
),
2938+
("SubnetInfoRuntimeApi", "get_metagraph", [[netuid]]),
2939+
block_hash=block_hash,
2940+
default_value=None,
2941+
)
27752942

2776-
query = await self.substrate.runtime_call(
2777-
api="SubnetInfoRuntimeApi",
2778-
method="get_selective_mechagraph",
2779-
params=[netuid, mechid, indexes if 0 in indexes else [0] + indexes],
2780-
block_hash=block_hash,
2781-
)
27822943
if getattr(query, "value", None) is None:
27832944
logging.error(
27842945
f"Subnet mechanism {netuid}.{mechid if mechid else 0} does not exist."

bittensor/core/metagraph.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,7 +1413,7 @@ async def sync(
14131413
lite = self.lite
14141414

14151415
subtensor = await self._initialize_subtensor(subtensor)
1416-
1416+
cur_block = None
14171417
if (
14181418
subtensor.chain_endpoint != settings.ARCHIVE_ENTRYPOINT
14191419
or subtensor.network != "archive"
@@ -1425,7 +1425,10 @@ async def sync(
14251425
"network for subtensor and retry."
14261426
)
14271427
if block is None:
1428-
block = await subtensor.get_current_block()
1428+
if cur_block is not None:
1429+
block = cur_block
1430+
else:
1431+
block = await subtensor.get_current_block()
14291432

14301433
# Assign neurons based on 'lite' flag
14311434
await self._assign_neurons(block, lite, subtensor)
@@ -1654,11 +1657,11 @@ async def _apply_extra_info(self, block: int):
16541657
)
16551658
if metagraph_info:
16561659
self._apply_metagraph_info_mixin(metagraph_info=metagraph_info)
1657-
self.mechanism_count = await self.subtensor.get_mechanism_count(
1658-
netuid=self.netuid, block=block
1659-
)
1660-
self.emissions_split = await self.subtensor.get_mechanism_emission_split(
1661-
netuid=self.netuid, block=block
1660+
self.mechanism_count, self.emissions_split = await asyncio.gather(
1661+
self.subtensor.get_mechanism_count(netuid=self.netuid, block=block),
1662+
self.subtensor.get_mechanism_emission_split(
1663+
netuid=self.netuid, block=block
1664+
),
16621665
)
16631666

16641667

0 commit comments

Comments
 (0)