Engineering / August 9, 2026 · Updated August 11, 2026
When replay protection is unavailable, login stops
Why ZKAuth rejects a proof when its single-use registry cannot make an atomic decision.

Mohith · 5 min read

Every accepted login proof must be used once. That property depends on more than cryptographic verification: the engine also needs an atomic record of whether another request already consumed the proof.
The clearest example in ZKAuth is replay protection. Every login proof is single-use: once the engine accepts a proof, its hash goes into a registry, and any attempt to use it again is rejected as a replay attack. That registry is the last line of the verification pipeline, and it has to be atomic: two concurrent requests carrying the same proof must not both win.
INSERT INTO zkp_proof_registry (proof_hash, client_id, user_id, expires_at, created_at)VALUES ($1, $2, $3, $4, NOW())ON CONFLICT (proof_hash) DO NOTHING;-- rowCount === 0 -> someone already used this proof. Reject.The database enforces uniqueness, not application code. There is no check-then-set window, no lock to forget, no race to lose. If the insert affects zero rows, the proof was already spent and the login is refused.
Concurrent requests
Atomic registry
Outcomes
The outage branch is a security decision
If the registry is unreachable, the engine cannot establish that a request is fresh. Allowing the login would silently remove the single-use guarantee precisely when the system has the least evidence.
In production, the ZKAuth engine refuses instead:
if (process.env.NODE_ENV === "production" && this.redisReplayProtection && !this.redisConnected) { throw new ZKPError("Replay protection backend unavailable");}
// ...and the same rule for the database registry:if (process.env.NODE_ENV === "production") { throw new ZKPError("Replay protection registry unavailable");}Failing closed means a bad enough outage turns into rejected logins instead of silently weakened security. That is a real availability cost, chosen on purpose: an auth outage is recoverable and visible, while accepted replays are neither.
The tests target the failure property
The unit suite for the proof service is written from the adversary’s side of the table. Among the cases that must pass on every commit: a replayed proof with a fresh nonce is still detected, only one of N concurrent identical proofs wins, stale and future-dated timestamps are rejected, tampered public signals fail on the tenant and email bindings, and the fail-closed branches actually throw when their backend is simulated away.
The broader assumptions and executed checks are documented in the threat model and evidence record.

Mohith
Founder, ZKAuth
August 9, 2026
Next post
Why project API keys stay on the server