Skip to content

Commit c95b9b6

Browse files
authored
Merge pull request #30 from opentensor/feat/metadata-v15-cache
Feat/metadata v15 cache
2 parents 0df24d0 + f0f25df commit c95b9b6

File tree

5 files changed

+441
-13
lines changed

5 files changed

+441
-13
lines changed

async_substrate_interface/async_substrate.py

+119-6
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@
4949
Preprocessed,
5050
)
5151
from async_substrate_interface.utils import hex_to_bytes, json
52+
from async_substrate_interface.utils.decoding import (
53+
_determine_if_old_runtime_call,
54+
_bt_decode_to_dict_or_list,
55+
)
5256
from async_substrate_interface.utils.storage import StorageKey
57+
from async_substrate_interface.type_registry import _TYPE_REGISTRY
5358

5459
if TYPE_CHECKING:
5560
from websockets.asyncio.client import ClientConnection
@@ -706,6 +711,8 @@ def __init__(
706711
ss58_format=self.ss58_format, implements_scale_info=True
707712
)
708713
self._metadata_cache = {}
714+
self._metadata_v15_cache = {}
715+
self._old_metadata_v15 = None
709716
self._nonces = {}
710717
self.metadata_version_hex = "0x0f000000" # v15
711718
self.reload_type_registry()
@@ -800,6 +807,20 @@ async def load_registry(self):
800807
)
801808
self.registry = PortableRegistry.from_metadata_v15(self.metadata_v15)
802809

810+
async def _load_registry_at_block(self, block_hash: str) -> MetadataV15:
811+
# Should be called for any block that fails decoding.
812+
# Possibly the metadata was different.
813+
metadata_rpc_result = await self.rpc_request(
814+
"state_call",
815+
["Metadata_metadata_at_version", self.metadata_version_hex],
816+
block_hash=block_hash,
817+
)
818+
metadata_option_hex_str = metadata_rpc_result["result"]
819+
metadata_option_bytes = bytes.fromhex(metadata_option_hex_str[2:])
820+
old_metadata = MetadataV15.decode_from_metadata_option(metadata_option_bytes)
821+
822+
return old_metadata
823+
803824
async def _wait_for_registry(self, _attempt: int = 1, _retries: int = 3) -> None:
804825
async def _waiter():
805826
while self.registry is None:
@@ -930,7 +951,10 @@ async def get_runtime(block_hash, block_id) -> Runtime:
930951
if (
931952
(block_hash and block_hash == self.last_block_hash)
932953
or (block_id and block_id == self.block_id)
933-
) and self._metadata is not None:
954+
) and all(
955+
x is not None
956+
for x in [self._metadata, self._old_metadata_v15, self.metadata_v15]
957+
):
934958
return Runtime(
935959
self.chain,
936960
self.runtime_config,
@@ -976,9 +1000,9 @@ async def get_runtime(block_hash, block_id) -> Runtime:
9761000
f"No runtime information for block '{block_hash}'"
9771001
)
9781002
# Check if runtime state already set to current block
979-
if (
980-
runtime_info.get("specVersion") == self.runtime_version
981-
and self._metadata is not None
1003+
if runtime_info.get("specVersion") == self.runtime_version and all(
1004+
x is not None
1005+
for x in [self._metadata, self._old_metadata_v15, self.metadata_v15]
9821006
):
9831007
return Runtime(
9841008
self.chain,
@@ -1002,6 +1026,8 @@ async def get_runtime(block_hash, block_id) -> Runtime:
10021026
self.runtime_version
10031027
]
10041028
else:
1029+
# TODO when I get time, I'd like to add this and the metadata v15 as tasks with callbacks
1030+
# TODO to update the caches, but I don't have time now.
10051031
metadata = self._metadata = await self.get_block_metadata(
10061032
block_hash=runtime_block_hash, decode=True
10071033
)
@@ -1015,6 +1041,30 @@ async def get_runtime(block_hash, block_id) -> Runtime:
10151041
self._metadata_cache[self.runtime_version] = self._metadata
10161042
else:
10171043
metadata = self._metadata
1044+
1045+
if self.runtime_version in self._metadata_v15_cache:
1046+
# Get metadata v15 from cache
1047+
logging.debug(
1048+
"Retrieved metadata v15 for {} from memory".format(
1049+
self.runtime_version
1050+
)
1051+
)
1052+
metadata_v15 = self._old_metadata_v15 = self._metadata_v15_cache[
1053+
self.runtime_version
1054+
]
1055+
else:
1056+
metadata_v15 = (
1057+
self._old_metadata_v15
1058+
) = await self._load_registry_at_block(block_hash=runtime_block_hash)
1059+
logging.debug(
1060+
"Retrieved metadata v15 for {} from Substrate node".format(
1061+
self.runtime_version
1062+
)
1063+
)
1064+
1065+
# Update metadata v15 cache
1066+
self._metadata_v15_cache[self.runtime_version] = metadata_v15
1067+
10181068
# Update type registry
10191069
self.reload_type_registry(use_remote_preset=False, auto_discover=True)
10201070

@@ -2487,6 +2537,56 @@ async def get_chain_finalised_head(self):
24872537

24882538
return response.get("result")
24892539

2540+
async def _do_runtime_call_old(
2541+
self,
2542+
api: str,
2543+
method: str,
2544+
params: Optional[Union[list, dict]] = None,
2545+
block_hash: Optional[str] = None,
2546+
) -> ScaleType:
2547+
logging.debug(
2548+
f"Decoding old runtime call: {api}.{method} with params: {params} at block hash: {block_hash}"
2549+
)
2550+
runtime_call_def = _TYPE_REGISTRY["runtime_api"][api]["methods"][method]
2551+
2552+
# Encode params
2553+
param_data = b""
2554+
2555+
if "encoder" in runtime_call_def:
2556+
param_data = runtime_call_def["encoder"](params)
2557+
else:
2558+
for idx, param in enumerate(runtime_call_def["params"]):
2559+
param_type_string = f"{param['type']}"
2560+
if isinstance(params, list):
2561+
param_data += await self.encode_scale(
2562+
param_type_string, params[idx]
2563+
)
2564+
else:
2565+
if param["name"] not in params:
2566+
raise ValueError(
2567+
f"Runtime Call param '{param['name']}' is missing"
2568+
)
2569+
2570+
param_data += await self.encode_scale(
2571+
param_type_string, params[param["name"]]
2572+
)
2573+
2574+
# RPC request
2575+
result_data = await self.rpc_request(
2576+
"state_call", [f"{api}_{method}", param_data.hex(), block_hash]
2577+
)
2578+
result_vec_u8_bytes = hex_to_bytes(result_data["result"])
2579+
result_bytes = await self.decode_scale("Vec<u8>", result_vec_u8_bytes)
2580+
2581+
# Decode result
2582+
# Get correct type
2583+
result_decoded = runtime_call_def["decoder"](bytes(result_bytes))
2584+
as_dict = _bt_decode_to_dict_or_list(result_decoded)
2585+
logging.debug("Decoded old runtime call result: ", as_dict)
2586+
result_obj = ScaleObj(as_dict)
2587+
2588+
return result_obj
2589+
24902590
async def runtime_call(
24912591
self,
24922592
api: str,
@@ -2513,14 +2613,27 @@ async def runtime_call(
25132613
params = {}
25142614

25152615
try:
2516-
metadata_v15 = self.metadata_v15.value()
2517-
apis = {entry["name"]: entry for entry in metadata_v15["apis"]}
2616+
if block_hash:
2617+
# Use old metadata v15 from init_runtime call
2618+
metadata_v15 = self._old_metadata_v15
2619+
else:
2620+
metadata_v15 = self.metadata_v15
2621+
2622+
self.registry = PortableRegistry.from_metadata_v15(metadata_v15)
2623+
metadata_v15_value = metadata_v15.value()
2624+
2625+
apis = {entry["name"]: entry for entry in metadata_v15_value["apis"]}
25182626
api_entry = apis[api]
25192627
methods = {entry["name"]: entry for entry in api_entry["methods"]}
25202628
runtime_call_def = methods[method]
25212629
except KeyError:
25222630
raise ValueError(f"Runtime API Call '{api}.{method}' not found in registry")
25232631

2632+
if _determine_if_old_runtime_call(runtime_call_def, metadata_v15_value):
2633+
result = await self._do_runtime_call_old(api, method, params, block_hash)
2634+
2635+
return result
2636+
25242637
if isinstance(params, list) and len(params) != len(runtime_call_def["inputs"]):
25252638
raise ValueError(
25262639
f"Number of parameter provided ({len(params)}) does not "

async_substrate_interface/sync_substrate.py

+114-6
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@
3131
ScaleObj,
3232
)
3333
from async_substrate_interface.utils import hex_to_bytes, json
34+
from async_substrate_interface.utils.decoding import (
35+
_determine_if_old_runtime_call,
36+
_bt_decode_to_dict_or_list,
37+
)
3438
from async_substrate_interface.utils.storage import StorageKey
39+
from async_substrate_interface.type_registry import _TYPE_REGISTRY
3540

3641

3742
ResultHandler = Callable[[dict, Any], tuple[dict, bool]]
@@ -506,6 +511,8 @@ def __init__(
506511
ss58_format=self.ss58_format, implements_scale_info=True
507512
)
508513
self._metadata_cache = {}
514+
self._metadata_v15_cache = {}
515+
self._old_metadata_v15 = None
509516
self.metadata_version_hex = "0x0f000000" # v15
510517
self.reload_type_registry()
511518
self.ws = self.connect(init=True)
@@ -604,6 +611,20 @@ def load_registry(self):
604611
)
605612
self.registry = PortableRegistry.from_metadata_v15(self.metadata_v15)
606613

614+
def _load_registry_at_block(self, block_hash: str) -> MetadataV15:
615+
# Should be called for any block that fails decoding.
616+
# Possibly the metadata was different.
617+
metadata_rpc_result = self.rpc_request(
618+
"state_call",
619+
["Metadata_metadata_at_version", self.metadata_version_hex],
620+
block_hash=block_hash,
621+
)
622+
metadata_option_hex_str = metadata_rpc_result["result"]
623+
metadata_option_bytes = bytes.fromhex(metadata_option_hex_str[2:])
624+
old_metadata = MetadataV15.decode_from_metadata_option(metadata_option_bytes)
625+
626+
return old_metadata
627+
607628
def decode_scale(
608629
self,
609630
type_string: str,
@@ -685,7 +706,10 @@ def get_runtime(block_hash, block_id) -> Runtime:
685706
if (
686707
(block_hash and block_hash == self.last_block_hash)
687708
or (block_id and block_id == self.block_id)
688-
) and self._metadata is not None:
709+
) and all(
710+
x is not None
711+
for x in [self._metadata, self._old_metadata_v15, self.metadata_v15]
712+
):
689713
return Runtime(
690714
self.chain,
691715
self.runtime_config,
@@ -727,9 +751,9 @@ def get_runtime(block_hash, block_id) -> Runtime:
727751
f"No runtime information for block '{block_hash}'"
728752
)
729753
# Check if runtime state already set to current block
730-
if (
731-
runtime_info.get("specVersion") == self.runtime_version
732-
and self._metadata is not None
754+
if runtime_info.get("specVersion") == self.runtime_version and all(
755+
x is not None
756+
for x in [self._metadata, self._old_metadata_v15, self.metadata_v15]
733757
):
734758
return Runtime(
735759
self.chain,
@@ -766,6 +790,29 @@ def get_runtime(block_hash, block_id) -> Runtime:
766790
self._metadata_cache[self.runtime_version] = self._metadata
767791
else:
768792
metadata = self._metadata
793+
794+
if self.runtime_version in self._metadata_v15_cache:
795+
# Get metadata v15 from cache
796+
logging.debug(
797+
"Retrieved metadata v15 for {} from memory".format(
798+
self.runtime_version
799+
)
800+
)
801+
metadata_v15 = self._old_metadata_v15 = self._metadata_v15_cache[
802+
self.runtime_version
803+
]
804+
else:
805+
metadata_v15 = self._old_metadata_v15 = self._load_registry_at_block(
806+
block_hash=runtime_block_hash
807+
)
808+
logging.debug(
809+
"Retrieved metadata v15 for {} from Substrate node".format(
810+
self.runtime_version
811+
)
812+
)
813+
# Update metadata v15 cache
814+
self._metadata_v15_cache[self.runtime_version] = metadata_v15
815+
769816
# Update type registry
770817
self.reload_type_registry(use_remote_preset=False, auto_discover=True)
771818

@@ -2211,6 +2258,54 @@ def get_chain_finalised_head(self):
22112258

22122259
return response.get("result")
22132260

2261+
def _do_runtime_call_old(
2262+
self,
2263+
api: str,
2264+
method: str,
2265+
params: Optional[Union[list, dict]] = None,
2266+
block_hash: Optional[str] = None,
2267+
) -> ScaleType:
2268+
logging.debug(
2269+
f"Decoding old runtime call: {api}.{method} with params: {params} at block hash: {block_hash}"
2270+
)
2271+
runtime_call_def = _TYPE_REGISTRY["runtime_api"][api]["methods"][method]
2272+
2273+
# Encode params
2274+
param_data = b""
2275+
2276+
if "encoder" in runtime_call_def:
2277+
param_data = runtime_call_def["encoder"](params)
2278+
else:
2279+
for idx, param in enumerate(runtime_call_def["params"]):
2280+
param_type_string = f"{param['type']}"
2281+
if isinstance(params, list):
2282+
param_data += self.encode_scale(param_type_string, params[idx])
2283+
else:
2284+
if param["name"] not in params:
2285+
raise ValueError(
2286+
f"Runtime Call param '{param['name']}' is missing"
2287+
)
2288+
2289+
param_data += self.encode_scale(
2290+
param_type_string, params[param["name"]]
2291+
)
2292+
2293+
# RPC request
2294+
result_data = self.rpc_request(
2295+
"state_call", [f"{api}_{method}", param_data.hex(), block_hash]
2296+
)
2297+
result_vec_u8_bytes = hex_to_bytes(result_data["result"])
2298+
result_bytes = self.decode_scale("Vec<u8>", result_vec_u8_bytes)
2299+
2300+
# Decode result
2301+
# Get correct type
2302+
result_decoded = runtime_call_def["decoder"](bytes(result_bytes))
2303+
as_dict = _bt_decode_to_dict_or_list(result_decoded)
2304+
logging.debug("Decoded old runtime call result: ", as_dict)
2305+
result_obj = ScaleObj(as_dict)
2306+
2307+
return result_obj
2308+
22142309
def runtime_call(
22152310
self,
22162311
api: str,
@@ -2237,14 +2332,27 @@ def runtime_call(
22372332
params = {}
22382333

22392334
try:
2240-
metadata_v15 = self.metadata_v15.value()
2241-
apis = {entry["name"]: entry for entry in metadata_v15["apis"]}
2335+
if block_hash:
2336+
# Use old metadata v15 from init_runtime call
2337+
metadata_v15 = self._old_metadata_v15
2338+
else:
2339+
metadata_v15 = self.metadata_v15
2340+
2341+
self.registry = PortableRegistry.from_metadata_v15(metadata_v15)
2342+
metadata_v15_value = metadata_v15.value()
2343+
2344+
apis = {entry["name"]: entry for entry in metadata_v15_value["apis"]}
22422345
api_entry = apis[api]
22432346
methods = {entry["name"]: entry for entry in api_entry["methods"]}
22442347
runtime_call_def = methods[method]
22452348
except KeyError:
22462349
raise ValueError(f"Runtime API Call '{api}.{method}' not found in registry")
22472350

2351+
if _determine_if_old_runtime_call(runtime_call_def, metadata_v15_value):
2352+
result = self._do_runtime_call_old(api, method, params, block_hash)
2353+
2354+
return result
2355+
22482356
if isinstance(params, list) and len(params) != len(runtime_call_def["inputs"]):
22492357
raise ValueError(
22502358
f"Number of parameter provided ({len(params)}) does not "

0 commit comments

Comments
 (0)