Skip to content

replaced in memory nonce in redis - #361

Open
KAMALDEEN333 wants to merge 3 commits into
Quantarq:mainfrom
KAMALDEEN333:Redis
Open

replaced in memory nonce in redis#361
KAMALDEEN333 wants to merge 3 commits into
Quantarq:mainfrom
KAMALDEEN333:Redis

Conversation

@KAMALDEEN333

Copy link
Copy Markdown

Replace in-memory nonce store with Redis
closes #192
Contributor
Problem Statement
The signed-nonce authentication in wallet_auth.py keeps state in a
module-level dictionary:

_nonce_store: Dict[str, Tuple[str, float]] = {}
def clean_expired_nonces():
cutoff = time.monotonic() - NONCE_TTL
expired = [n for n, (
, ts) in _nonce_store.items() if ts < cutoff]
for n in expired: _nonce_store.pop(n, None)
This dict is a per-process singleton. Two uvicorn workers do not share
state, so a nonce can be issued in worker A and consumed successfully in
worker B but rejected if nonce was issued in B and verified in A. Worse:
after a uvicorn reload, the dict is blown away, so issued-but-unused
nonces vanish (denial-of-service-by-deploy) and issued-and-consumed
nonce metadata is gone (ban replay-protection).

Why It Matters
This is the foundational replay-protection for the protocol. Without
shared state, the auth model collapses the moment the deployment hits >1
uvicorn worker, which is the basic uvicorn packaging.

Technical Context
Redis is already used by quantara/web_app/contract_tools/cache.py (see
get_redis_pool()). Pool is created lazily with redis.asyncio. Add a
thin NonceStore class to wallet_auth.py (or api/session.py) that
implements the same three methods, but read/write through Redis. Use
SET nonce EX 300 NX to issue and DEL to consume, where
NX guarantees no two workers can return the same nonce.

Expected Outcome
Nonce issuance/consumption is consistent across worker boundaries and
survives a deploy.

Acceptance Criteria

Restarting uvicorn mid-flight: outstanding nonces return 401 to any caller (auto-expired / non-existent).

Stress: 100 concurrent /nonce for the same wallet → 100 unique nonces (no duplicates).

Race: 2 concurrent signed POSTs with same nonce → exactly one 200, one 401.

tests/test_wallet_auth.py updated to use fakeredis.aioredis instead of the module-level dict.
Implementation Notes
Use redis.asyncio (get_redis_pool() already exists). Key shape:
quantara:nonce:{nonce} = wallet_id with EX=300. Never log the
nonce itself — they are seeds for replay attacks if leaked.

Files / Modules Affected

quantara/web_app/api/wallet_auth.py
quantara/web_app/contract_tools/cache.py

nonce = _generate_nonce(wallet_id)
assert _consume_nonce(nonce, wallet_id) is True
nonce = await _generate_nonce(wallet_id)
assert await _consume_nonce(nonce, wallet_id) is True
nonce = await _generate_nonce(wallet_id)
await _consume_nonce(nonce, wallet_id)
stored_wallet = await _mock_redis.get(_nonce_key(nonce))
assert stored_wallet is None
assert _consume_nonce(nonce, wallet_id) is True
assert _consume_nonce(nonce, wallet_id) is False
nonce = await _generate_nonce(wallet_id)
assert await _consume_nonce(nonce, wallet_id) is True
assert _consume_nonce(nonce, wallet_id) is False
nonce = await _generate_nonce(wallet_id)
assert await _consume_nonce(nonce, wallet_id) is True
assert await _consume_nonce(nonce, wallet_id) is False
assert _consume_nonce(nonce, "GATTACKER") is False
async def test_wrong_wallet_id_returns_false(self):
nonce = await _generate_nonce("GOWNER")
assert await _consume_nonce(nonce, "GATTACKER") is False
_generate_nonce("GNEW")
assert stale_nonce not in _nonce_store
async def test_unknown_nonce_returns_false(self):
assert await _consume_nonce("deadbeef" * 8, "GABCDEF") is False
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.

Replace in-memory nonce store with Redis

2 participants