1+ import bisect
12import logging
3+ import os
24from abc import ABC
35from collections import defaultdict , deque
46from collections .abc import Iterable
57from contextlib import suppress
68from dataclasses import dataclass
79from datetime import datetime
8- from typing import Optional , Union , Any
10+ from typing import Optional , Union , Any , Sequence
911
1012import scalecodec .types
1113from bt_decode import PortableRegistry , encode as encode_by_type_string
1719
1820from .const import SS58_FORMAT
1921from .utils import json
20- from .utils .cache import AsyncSqliteDB
22+ from .utils .cache import AsyncSqliteDB , LRUCache
2123
2224logger = logging .getLogger ("async_substrate_interface" )
25+ SUBSTRATE_RUNTIME_CACHE_SIZE = int (os .getenv ("SUBSTRATE_RUNTIME_CACHE_SIZE" , "16" ))
26+ SUBSTRATE_CACHE_METHOD_SIZE = int (os .getenv ("SUBSTRATE_CACHE_METHOD_SIZE" , "512" ))
2327
2428
2529class RuntimeCache :
@@ -41,11 +45,45 @@ class RuntimeCache:
4145 versions : dict [int , "Runtime" ]
4246 last_used : Optional ["Runtime" ]
4347
44- def __init__ (self ):
45- self .blocks = {}
46- self .block_hashes = {}
47- self .versions = {}
48- self .last_used = None
48+ def __init__ (self , known_versions : Optional [Sequence [tuple [int , int ]]] = None ):
49+ # {block: block_hash, ...}
50+ self .blocks : LRUCache = LRUCache (max_size = SUBSTRATE_CACHE_METHOD_SIZE )
51+ # {block_hash: specVersion, ...}
52+ self .block_hashes : LRUCache = LRUCache (max_size = SUBSTRATE_CACHE_METHOD_SIZE )
53+ # {specVersion: Runtime, ...}
54+ self .versions : LRUCache = LRUCache (max_size = SUBSTRATE_RUNTIME_CACHE_SIZE )
55+ # [(block, specVersion), ...]
56+ self .known_versions : list [tuple [int , int ]] = []
57+ # [block, ...] for binary search (excludes last item)
58+ self ._known_version_blocks : list [int ] = []
59+ if known_versions :
60+ self .add_known_versions (known_versions )
61+ self .last_used : Optional ["Runtime" ] = None
62+
63+ def add_known_versions (self , known_versions : Sequence [tuple [int , int ]]):
64+ """
65+ Known versions are a map of {block: specVersion} for when runtimes change.
66+
67+ E.g.
68+ [
69+ (561, 102),
70+ (1075, 103),
71+ ...,
72+ (7257645, 367)
73+ ]
74+
75+ This mapping is generally user-created or pulled from an external API, such as
76+ https://api.tao.app/docs#/chain/get_runtime_versions_api_beta_chain_runtime_version_get
77+
78+ By preloading the known versions, there can be significantly fewer chain calls to determine version.
79+
80+ Note that because the last runtime in the supplied known versions will be ignored, as otherwise we would
81+ have to assume that the final known version never changes.
82+ """
83+ known_versions = list (sorted (known_versions , key = lambda v : v [0 ]))
84+ self .known_versions = known_versions
85+ # Cache block numbers (excluding last) for O(log n) binary search lookups
86+ self ._known_version_blocks = [v [0 ] for v in known_versions [:- 1 ]]
4987
5088 def add_item (
5189 self ,
@@ -59,11 +97,11 @@ def add_item(
5997 """
6098 self .last_used = runtime
6199 if block is not None and block_hash is not None :
62- self .blocks [ block ] = block_hash
100+ self .blocks . set ( block , block_hash )
63101 if block_hash is not None and runtime_version is not None :
64- self .block_hashes [ block_hash ] = runtime_version
102+ self .block_hashes . set ( block_hash , runtime_version )
65103 if runtime_version is not None :
66- self .versions [ runtime_version ] = runtime
104+ self .versions . set ( runtime_version , runtime )
67105
68106 def retrieve (
69107 self ,
@@ -75,26 +113,35 @@ def retrieve(
75113 Retrieves a Runtime object from the cache, using the key of its block number, block hash, or runtime version.
76114 Retrieval happens in this order. If no Runtime is found mapped to any of your supplied keys, returns `None`.
77115 """
116+ # No reason to do this lookup if the runtime version is already supplied in this call
117+ if block is not None and runtime_version is None and self ._known_version_blocks :
118+ # _known_version_blocks excludes the last item (see note in `add_known_versions`)
119+ idx = bisect .bisect_right (self ._known_version_blocks , block ) - 1
120+ if idx >= 0 :
121+ runtime_version = self .known_versions [idx ][1 ]
122+
78123 runtime = None
79124 if block is not None :
80125 if block_hash is not None :
81- self .blocks [ block ] = block_hash
126+ self .blocks . set ( block , block_hash )
82127 if runtime_version is not None :
83- self .block_hashes [block_hash ] = runtime_version
84- with suppress (KeyError ):
85- runtime = self .versions [self .block_hashes [self .blocks [block ]]]
128+ self .block_hashes .set (block_hash , runtime_version )
129+ with suppress (AttributeError ):
130+ runtime = self .versions .get (
131+ self .block_hashes .get (self .blocks .get (block ))
132+ )
86133 self .last_used = runtime
87134 return runtime
88135 if block_hash is not None :
89136 if runtime_version is not None :
90- self .block_hashes [ block_hash ] = runtime_version
91- with suppress (KeyError ):
92- runtime = self .versions [ self .block_hashes [ block_hash ]]
137+ self .block_hashes . set ( block_hash , runtime_version )
138+ with suppress (AttributeError ):
139+ runtime = self .versions . get ( self .block_hashes . get ( block_hash ))
93140 self .last_used = runtime
94141 return runtime
95142 if runtime_version is not None :
96- with suppress ( KeyError ):
97- runtime = self . versions [ runtime_version ]
143+ runtime = self . versions . get ( runtime_version )
144+ if runtime is not None :
98145 self .last_used = runtime
99146 return runtime
100147 return runtime
@@ -110,16 +157,21 @@ async def load_from_disk(self, chain_endpoint: str):
110157 logger .debug ("No runtime mappings in disk cache" )
111158 else :
112159 logger .debug ("Found runtime mappings in disk cache" )
113- self .blocks = block_mapping
114- self .block_hashes = block_hash_mapping
115- self .versions = {
116- x : Runtime .deserialize (y ) for x , y in runtime_version_mapping .items ()
117- }
160+ self .blocks .cache = block_mapping
161+ self .block_hashes .cache = block_hash_mapping
162+ for x , y in runtime_version_mapping .items ():
163+ self .versions .cache [x ] = Runtime .deserialize (y )
118164
119165 async def dump_to_disk (self , chain_endpoint : str ):
120166 db = AsyncSqliteDB (chain_endpoint = chain_endpoint )
167+ blocks = self .blocks .cache
168+ block_hashes = self .block_hashes .cache
169+ versions = self .versions .cache
121170 await db .dump_runtime_cache (
122- chain_endpoint , self .blocks , self .block_hashes , self .versions
171+ chain = chain_endpoint ,
172+ block_mapping = blocks ,
173+ block_hash_mapping = block_hashes ,
174+ version_mapping = versions ,
123175 )
124176
125177
0 commit comments