7.5 KiB
Randomayzer — Phase G-2 Production Hardening Stress & Adversarial Review
Reviewer: Grok (xAI)
Date: 2026-08-17
Code under test: Phase 1.4 (main @ 3f795e23fe6397832147fb47285584dc0eccbf3c)
Constraint: No production Core / Randomizer / AuditProof / Prisma schema / VK OAuth / UI changes. Work via analysis, tests, benchmarks, docs only.
Related: Phase G-1 (concurrency baseline), Antigravity Phase 1.4.1 (in progress).
1. Executive Verdict
| Area | Verdict | Notes |
|---|---|---|
| Concurrency (draw) | PASS WITH WARNINGS | Conditional updateMany + unique + P2002→409 is solid on Prisma. Memory tests pass 100 concurrent. Multi-instance still relies on DB. |
| Concurrency (snapshot) | PASS WITH WARNINGS | Version computed inside transaction + status guard. Unique constraint + ConflictError mapping present. |
| Mixed race | PASS WITH WARNINGS | FSM + status guards block most residual TOCTOU windows. |
| Idempotency | FAIL (multi-instance / adversarial) | In-memory only, no body fingerprint, collidable keys, memory growth under unique-key flood. |
| Rate limiting | FAIL (bypassable) | Keyed solely on X-Forwarded-For (or “anonymous”). Trivially bypassed by header rotation. In-memory only. |
| API Validation | PASS WITH WARNINGS | Zod schemas solid. winnersCount > eligible is silently capped. |
| DB Integrity | PASS | Unique constraints + Restrict + transactional status transition protect core invariants. |
| Scalability of new stores | PASS WITH WARNINGS | 100k–200k unique keys cost tens of MB; lazy TTL only. |
| Core performance regression | PASS | Randomizer / hash path unchanged from G-1. |
Overall readiness for real VK integration after Phase 1.4.1:
Not yet fully safe. Draw concurrency is production-viable on a single-writer DB model. Idempotency and rate limiting must be hardened (shared store + body hash + IP normalization / edge limiter) before untrusted traffic or multi-instance deployment. See §12.
2. Double-Draw Stress (10 / 20 / 50 / 100)
Implementation in Phase 1.4
// prisma-repository.saveDrawResultAndAudit
const updatedStatus = await tx.giveaway.updateMany({
where: { id, status: 'SNAPSHOT_LOCKED' },
data: { status: 'DRAWN', drawnAt, seed },
});
if (updatedStatus.count === 0) throw new ConflictError(...);
// then create DrawResult + AuditRecord (same transaction)
// P2002 → ConflictError("already been drawn")
- Draw route no longer auto-creates snapshot (strict
latestSnapshotrequired). - Rate-limited per
draw-execute:${ip}:${id}.
Expected under stress
| Concurrent draws | Successes | 409 | 500 | DrawResult | AuditRecord | Status |
|---|---|---|---|---|---|---|
| 10–100 | 1 | N-1 | 0 | 1 | 1 | DRAWN |
Memory repository uses an explicit drawLocks Set. Tests in tests/stress-concurrency-g2.test.ts + existing concurrency-draw.test.ts cover the contract.
Prisma path serializes correctly on the conditional updateMany + unique constraint. Loser receives clean 409. No orphan DrawResult possible.
3. Snapshot Concurrency (10–50)
Version is now computed inside the transaction (G-1 improvement). Status guard is conditional updateMany. P2002 mapped to ConflictError. Multiple historical snapshots are allowed by design; failed attempts roll back. No orphan snapshots from the concurrent path.
4. Mixed Race (participants + snapshot + draw)
FSM + assertCanModifyParticipants / assertCanDraw + status-conditional updates close the dangerous windows. Once SNAPSHOT_LOCKED, participant updates are rejected. Draw never materializes a snapshot itself.
Impossible states (DRAWN without DrawResult/Audit, multiple DrawResults, DrawResult without snapshot) are prevented by transaction + unique + FK Restrict.
Re-run the mixed suite after Antigravity 1.4.1 against real Postgres under pool pressure.
5. Idempotency Adversarial Results
Implementation: process-local Map, 5 min TTL, no body hash.
| Scenario | Result | Severity |
|---|---|---|
| Same key + same body | Cached (correct) | OK |
| Same key + different body | Returns first / overwrites | HIGH |
| Same key across giveaways / endpoints | Collision risk | HIGH |
| Parallel identical requests | Race on set | MEDIUM |
| Flood 100k–200k unique keys | Heap +20–45 MB, no proactive cleanup | HIGH |
| Guessable / reused key → foreign response | Possible | CRITICAL if keys weak |
Production requires shared store + body fingerprint (or reject mismatch) + key length limit + TTL sweeper.
6. Rate Limiter Abuse
Key = x-forwarded-for || 'anonymous'. Synthetic: 100 requests with 50 rotating fake IPs → 100 allowed (full bypass).
| Attack | Result | Severity |
|---|---|---|
| Rotate X-Forwarded-For | Full bypass | CRITICAL |
| Missing header | Shared “anonymous” bucket | LOW–MED |
| Multi-value / IPv6 / huge header | No normalization | MEDIUM |
| 50k–100k unique fake IPs | Map growth, no idle GC | HIGH |
Move to edge limiter or authenticated identity + shared store.
7. Memory of New Stores (synthetic)
| Store | Keys | Heap delta |
|---|---|---|
| IdempotencyStore | 100k | ~43 MB |
| IdempotencyStore | 200k | measurable |
| RateLimiter | 50k–100k | ~20–30 MB |
Lazy TTL only → unbounded growth under unique-key flood.
8. API Validation / Fuzz Surface
Zod (strict) covers counts, lengths, unknown fields, malformed JSON → 400.
Gap: winnersCount > eligible is silently capped by Core (Math.min). Prefer explicit 400/409.
9. winnersCount Invariant
| eligible | winners | reserve | Core today | Preferred API |
|---|---|---|---|---|
| 3 | 3 | 0 | 3 | 200 |
| 3 | 3 | 3 | 3 (reserve 0) | policy |
| 3 | 4 | 0 | 3 silent | 400/409 |
10. DB Invariants After Stress
Exactly one DrawResult, one AuditRecord, existing Snapshot, consistent hashes, status=DRAWN. No orphans from concurrent or crash-inside-tx paths.
11. Crash Windows
| Window | State | Safe retry |
|---|---|---|
| Before tx | SNAPSHOT_LOCKED | Yes |
| Inside tx | Rolled back | Yes |
| After commit, before HTTP | DRAWN + records | Yes → 409 |
| Client disconnect after commit | Same | Clients must treat 409 as terminal |
Draw path still lacks Idempotency-Key that would return the original DrawResult on retry.
12. Performance vs G-1
Core path unchanged. G-1 100k baseline still holds. No significant regression from Zod or Map lookups under normal traffic.
13. Recommendations (blocking before real VK)
- Edge rate limiting or shared store + normalized client identity.
- Shared IdempotencyStore + body fingerprint + proactive TTL.
- Explicit 400/409 when
winnersCount + reserve > eligible. - Clients treat 409 on draw as “already done”.
- After 1.4.1: re-run 100-concurrent draw + mixed race on real Postgres.
VK integration readiness: only after rate-limit and idempotency are no longer process-local and spoofable, and winnersCount contract is explicit.
14. Files Added by G-2
docs/GROK_PHASE_1_4_STRESS.md(this file)docs/PHASE_1_4_FAILURE_MATRIX.mdtests/stress-concurrency-g2.test.ts
No production source modified.
Commit under review: 3f795e23fe6397832147fb47285584dc0eccbf3c