Skip to content

perf(proxy): cache proxy implementation lookups in memory - #460

Merged
spalen0 merged 2 commits into
mainfrom
proxy-impl-cache
Nov 5, 2026
Merged

spalen0 merged 2 commits into
mainfrom
proxy-impl-cache

Conversation

@spalen0

@spalen0 spalen0 commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

get_current_implementation is called from about 10 places, including source context, on-chain state, address labels and role-name resolution. Each call costs 2–4 RPC reads (EIP-1967/zeppelinos slots, then the beacon or getter fallback), and nothing cached the result. Explaining one transaction often looked up the same proxy several times.

Successful lookups are now cached in memory per (chain_id, address) for a short TTL.

  • PROXY_IMPL_CACHE_TTL_SECONDS sets the TTL. The default is 60, and 0 disables the cache.
  • Only successful reads are cached, including "not a proxy". When the RPC call raises, nothing is stored and the next call retries.
  • Nothing is written to disk, so no state carries across runs. Once the cache passes 1024 proxies, expired entries are pruned so it can't grow without bound in the long-running API server.
  • The two places that use the current implementation as an upgrade's "before" side read it with use_cache=False: format_upgrade_lines (Telegram alert lines) and ai_explainer._proxy_upgrade_block (the LLM's diff baseline). Upgrade alerts and explanations never compare against a stale address, and the fresh read also refreshes the cache.
  • The RPC logic is unchanged and now lives in _read_implementation.

Trade-off: the getter fallback (implementation() and similar) ignores errors from each call. If only eth_call fails, the lookup returns "no implementation" and that result stays cached for up to the TTL. Failures reading the storage slots still skip the cache.

Tests

  • tests/conftest.py clears the cache between tests, because many tests reuse a proxy address with different mocks.
  • tests/test_proxy_upgrade.py adds tests for cache hits (including mixed-case addresses), chain scoping, caching of non-proxy results, failures not being cached, TTL expiry, use_cache=False, the LLM upgrade baseline bypassing the cache, a TTL of 0, pruning, and upgrade alerts reading fresh.
  • uv run pytest -q tests/: 2081 passed, 4 skipped. ruff check, ruff format --check and ty check all pass.

🤖 Generated with Claude Code

spalen0 and others added 2 commits November 5, 2026 11:28
get_current_implementation is called from several enrichment steps
(source context, on-chain state, address labels, role names) and each
call costs 2-4 RPC reads. Cache successful lookups per (chain_id,
address) in memory for PROXY_IMPL_CACHE_TTL_SECONDS (default 60, 0
disables). Failed lookups are not cached, nothing is written to disk,
and expired entries are pruned once the cache passes 1024 proxies.

Upgrade alerts read the old implementation with use_cache=False so they
never show a stale address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_proxy_upgrade_block gives the LLM the current implementation to diff an
upgrade against. It still accepted cached lookups, so an implementation
changed within the cache TTL could make the LLM compare against outdated
source. Bypass the cache there, matching format_upgrade_lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@spalen0
spalen0 merged commit 0e9036f into main Nov 5, 2026
3 checks passed
@spalen0
spalen0 deleted the proxy-impl-cache branch November 5, 2026 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant