Summary
CachedFetcher.__call__ uses a truthiness check on the cache lookup, so any cached value that is falsy (0, None, "", False, [], {}) is indistinguishable from a cache miss. The value is stored on every call and ignored on every read, so the underlying RPC is re-issued indefinitely.
Location
async_substrate_interface/utils/cache.py:562
async def __call__(self, *args: Any, **kwargs: Any) -> Any:
key = self.make_cache_key(args, kwargs)
if item := self._cache.get(key): # <-- truthiness, not `is not None`
return item
...
LRUCache.get (cache.py:489) also has no sentinel — it returns None on miss, so a legitimately-cached None cannot be distinguished from a miss even if the walrus check is fixed in isolation.
Impact
Two @cached_fetcher-decorated methods are affected in practice:
1. _cached_get_block_number — async_substrate.py:4231
Returns int. The genesis block has number 0, which is falsy. Every lookup of the genesis block hash re-issues chain_getHeader over the network. The runtime_cache.blocks_reverse short-circuit in get_block_number (line 4220) hides this for warm callers, but the LRU is supposed to absorb repeats.
2. get_block_runtime_version_for — async_substrate.py:2411
Explicitly returns None when runtime_info is None (line 2421). Every error case re-fetches forever — exactly when you do not want to hammer the RPC. This is the more painful instance: it is part of runtime resolution and unbounded retries land on whatever is already failing.
The other six @cached_fetcher call sites (get_runtime_for_version, get_parent_block_hash, get_block_runtime_info, _cached_get_block_hash, etc.) return non-empty strings, dicts, or runtime objects and are not affected in practice.
Reproduction
import asyncio
from async_substrate_interface import AsyncSubstrateInterface
async def main():
async with AsyncSubstrateInterface("wss://...") as substrate:
genesis_hash = await substrate.get_block_hash(0)
# Both calls hit the network; the second should be a cache hit.
n1 = await substrate._cached_get_block_number(genesis_hash)
n2 = await substrate._cached_get_block_number(genesis_hash)
assert n1 == n2 == 0
# Observe two `chain_getHeader` requests on the wire instead of one.
asyncio.run(main())
Severity
Real, low-to-medium. Not data-corrupting; the cache silently degrades to "no caching" for narrow inputs. The strongest reason to fix is the error-path behaviour in get_block_runtime_version_for — repeated network load when something is already going wrong is the worst time for it.
Environment
async-substrate-interface version:
- Python:
- OS:
Summary
CachedFetcher.__call__uses a truthiness check on the cache lookup, so any cached value that is falsy (0,None,"",False,[],{}) is indistinguishable from a cache miss. The value is stored on every call and ignored on every read, so the underlying RPC is re-issued indefinitely.Location
async_substrate_interface/utils/cache.py:562LRUCache.get(cache.py:489) also has no sentinel — it returnsNoneon miss, so a legitimately-cachedNonecannot be distinguished from a miss even if the walrus check is fixed in isolation.Impact
Two
@cached_fetcher-decorated methods are affected in practice:1.
_cached_get_block_number—async_substrate.py:4231Returns
int. The genesis block has number0, which is falsy. Every lookup of the genesis block hash re-issueschain_getHeaderover the network. Theruntime_cache.blocks_reverseshort-circuit inget_block_number(line 4220) hides this for warm callers, but the LRU is supposed to absorb repeats.2.
get_block_runtime_version_for—async_substrate.py:2411Explicitly returns
Nonewhenruntime_info is None(line 2421). Every error case re-fetches forever — exactly when you do not want to hammer the RPC. This is the more painful instance: it is part of runtime resolution and unbounded retries land on whatever is already failing.The other six
@cached_fetchercall sites (get_runtime_for_version,get_parent_block_hash,get_block_runtime_info,_cached_get_block_hash, etc.) return non-empty strings, dicts, or runtime objects and are not affected in practice.Reproduction
Severity
Real, low-to-medium. Not data-corrupting; the cache silently degrades to "no caching" for narrow inputs. The strongest reason to fix is the error-path behaviour in
get_block_runtime_version_for— repeated network load when something is already going wrong is the worst time for it.Environment
async-substrate-interfaceversion: