Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.
This repository was archived by the owner on Jul 10, 2026. It is now read-only.

CachedFetcher never caches falsy values — re-fetches forever #336

Description

@carladams1299-lab

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_numberasync_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_forasync_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:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions