Skip to content

WalletCache keys only the public key while WalletRepository stores user_id and public_key: two identities, one cache namespace #385

Description

@usmanimamu17-create

Problem

WalletCache._key (app/services/wallet_cache.py):

def _key(self, address: str) -> str:
    return f"wallet:{address}"

WalletRepository (app/repositories/wallet_repository.py) provides both get_by_user_id and get_by_public_key, and the wallet model has two stable identities (user_id unique, public_key unique). The cache only indexes by address, so:

  • A user-id lookup can never hit the cache (the gap tracked separately), and
  • The address-keyed entry is written from user-id reads (_fetch_wallet does cache.set(wallet.public_key, ...)), meaning the cache stores the same wallet object under the address key even though that entry is only consulted by address lookups.

Consequences:

  • Cache entries and DB identity can diverge: if a wallet is re-linked (same user, new address — link_wallet allows this), the old address key retains the stale wallet indefinitely until TTL, while the DB has the new mapping; an address-based read (get_balance) can return the pre-relink wallet.
  • The namespace conflates two lookup directions: there is no way to invalidate "all keys for user X" (no invalidate_prefix usage from WalletRegistry), so relink/update paths cannot clear the affected entries.
  • TTLCache.invalidate_prefix exists in app/utils/cache.py as the house pattern for this, but WalletCache has no prefix-invalidation at all.

Root cause

The cache was designed around the address lookup only, and the write/read paths never reconciled the two-key identity model.

Why this is architecturally hard

  1. The durable fix is either two namespaces (wallet:{address}, wallet:user:{user_id}) with invalidation on every write path in WalletRepository/WalletRegistry, or a single canonical key with lookup-by-index — a data-model decision with cache-coherence implications.
  2. Relink semantics (link_wallet changing the address for a user) require invalidating both the old and new address keys plus the user key atomically; Redis multi/pipe is the mechanism, and the ordering with the DB commit matters.
  3. Tests must cover the relink scenario (old address stale, new address fresh, user lookup fresh) — none exist today.

Proposed design

Add a user-id key namespace, invalidate both namespaces on every create/link/update, implement WalletCache.invalidate_prefix mirroring TTLCache, and add relink tests asserting no stale reads.

Acceptance criteria

Service

  • User-id and address lookups are cache-coherent after relink/update.
  • All wallet writes invalidate the affected cache keys.

Tests

  • A relink test asserts old-address staleness is bounded and new lookups are fresh.
  • Existing wallet tests pass.

Out of scope

The user-id cache gap (tracked separately) and simulated balances (tracked separately).

Getting started

pytest tests/test_wallet_persistence.py -q
make typecheck

Good first files to read: app/services/wallet_cache.py, app/services/wallet_registry.py, app/repositories/wallet_repository.py.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardStellar WaveIssues in the Stellar wave programThird CampaignCampaign: Third Campaignarea/walletsImported campaign issue labelpriority/mediumStandard backlog item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions