randomayzer/agents/antigravity/done/TASK-2026-08-20-seed-precommit.md

100 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 2.4 — Seed Pre-Commit Gate Report (Eliminating Seed Grinding)
**Date:** 2026-08-20
**Base Commit SHA:** `9927e74421223135a170de640255803ab513fd48`
**Result Commit SHA:** `78151572bd2ae01645d70a0768c6ece517e2cab0`
**Status:** IMPLEMENTED / READY FOR INDEPENDENT RE-REVIEW
**Assigned Agent:** Antigravity (Implementation Orchestrator)
---
## 1. Executive Summary
Закрыта критическая уязвимость манипуляции результатами розыгрышей (**Seed Grinding / Pre-computation attack**), при которой организатор мог локально перебрать seed'ы на открытом списке участников и передать в `POST /api/giveaways/[id]/draw` подобранный seed, гарантирующий победу нужного участника при успешном статусе верификации `verified: true`.
Реализована схема **Cryptographic Seed Pre-Commitment**:
1. Клиентский `seed` полностью исключён из входных схем (`createGiveawaySchema`, `executeDrawSchema`). Попытка передать `seed` в теле запроса `POST /draw` строго отклоняется со статусом `400 VALIDATION_ERROR`.
2. Seed генерируется на сервере исключительно через CSPRNG (`generateCryptoSecureSeed()`) в момент создания и блокировки неизменяемого слепка участников (`createAndLockSnapshot`) и сохраняется в БД (`Giveaway.seed`) в единой атомарной операции.
3. До момента проведения жеребьёвки (`DRAWN`) открытый `seed` скрыт от клиента во всех эндпоинтах (`POST /api/giveaways/[id]/snapshot`, `GET /api/giveaways/[id]`, `GET /api/giveaways`, `GET /api/giveaways/[id]/participants`). Клиенту отдаётся только криптографическое обязательство `seedCommitment = sha256(seed)`.
4. Роут жеребьёвки `POST /api/giveaways/[id]/draw` читает seed строго из базы данных (`giveaway.seed`). Любой fallback на генерацию seed в роуте жеребьёвки удалён. Если seed отсутствует — возвращается `409 CONFLICT`.
5. После завершения жеребьёвки `seed` раскрывается публично (`giveaway.seed` и `drawResult.seedUsed`), позволяя любому участнику подтвердить равенство `sha256(drawResult.seedUsed) === seedCommitment` и математическую честность через независимый `GET /api/giveaways/[id]/verify`.
---
## 2. Modified Files
| File | Type | Description |
|------|------|-------------|
| `src/core/randomizer/hasher.ts` | Core | Добавлена функция `computeSeedCommitment(seed: string): string` (SHA-256 hex digest). |
| `src/core/validation/giveaway-schemas.ts` | Validation | Удалено поле `seed` из `createGiveawaySchema` и `executeDrawSchema` (строгая `.strict()` валидация на draw). |
| `src/lib/repository/giveaway-repository.ts` | Repository | Добавлено поле `seedCommitment?: string \| null` в `GiveawayWithRelations`, удален `seed` из `CreateGiveawayInput`. |
| `src/lib/repository/memory-repository.ts` | Storage Driver | Инициализация `seed: null`, генерация и фиксация `seed` + `seedCommitment` в `createAndLockSnapshot`. |
| `src/lib/repository/prisma-repository.ts` | Storage Driver | Фиксация `seed` в БД внутри `$transaction` при `createAndLockSnapshot`, маппинг `seedCommitment`. |
| `src/app/api/giveaways/route.ts` | API Route | Удалена передача клиентского seed при создании розыгрыша. |
| `src/app/api/giveaways/[id]/snapshot/route.ts` | API Route | Возврат `seedCommitment` вместо раскрытия plaintext seed. |
| `src/app/api/giveaways/[id]/draw/route.ts` | API Route | Строгое чтение pre-committed seed из БД; `409 CONFLICT` при отсутствии; удалён fallback. |
| `src/app/api/giveaways/[id]/route.ts` | API Route | Маскирование `seed: null` до статуса `DRAWN`, отдача `seedCommitment`. |
| `src/app/giveaways/new/page.tsx` | Frontend UI | Удалено поле ручного ввода seed из шага 4; добавлен индикатор защиты от подбора (Seed Pre-Commitment) со значением SHA-256 commitment. |
| `tests/api-validation.test.ts` | Tests | Обновлены тесты валидации на строгое отклонение `seed`. |
| `tests/seed-precommit-gate.test.ts` | Tests (NEW) | Комплексный adversarial & regression test suite (7 тестов). |
| `tests/storage-driver.test.ts` | Tests | Исправлен мок `IGiveawayRepository` (добавлены `listGiveawaysSummary` и `getParticipantsPaginated`). |
---
## 3. Core Cryptographic Invariants Preserved
Ни один из базовых криптографических алгоритмов НЕ изменялся:
- `HMAC_SHA256_FY_V1`
- `DeterministicHmacStream`
- `executeDeterministicDrawV1`
- `computeParticipantsSnapshotHash`
- `computeConditionsHash`
- `computeDeterministicProofHash`
- `computeAuditEventHash`
- `verifyDrawResult`
---
## 4. API Contract & Database Migration
- **Database Migration Required:** `NO` (Поле `Giveaway.seed` уже существует в `prisma/schema.prisma` как nullable `String?` и готово к сохранению CSPRNG seed).
- **API Contract Changes:**
- `POST /api/giveaways`: поле `seed` удалено из входящего тела (автоматически отбрасывается `.strip()`).
- `POST /api/giveaways/[id]/draw`: поле `seed` строго запрещено в теле запроса (`.strict()`), возвращает `400 VALIDATION_ERROR` при попытке передачи.
- `POST /api/giveaways/[id]/snapshot`: в ответ добавлено поле `seedCommitment: string` (SHA-256 hex от сгенерированного seed).
- `GET /api/giveaways/[id]`: в объекте `giveaway` возвращается `seedCommitment: string | null`. До статуса `DRAWN` поле `giveaway.seed` маскируется (`null`), после проведения розыгрыша раскрывается исходный `seed`.
- `GET /api/giveaways`: поле `seed` отсутствует в `GiveawaySummary` и не утекает в списках.
---
## 5. Verification Evidence & Test Gate
Фактически выполненные команды:
```text
npx prisma generate -> Exit code 0 (Prisma Client v5.22.0 generated)
npm test -> Exit code 0 (49 test files, 284 tests passed, 0 failed)
npm run lint -> Exit code 0 (Next.js ESLint passed clean)
npm run build -> Exit code 0 (Next.js production build compiled successfully)
npx tsc --noEmit -> Exit code 0 (Clean TypeScript check)
```
### Regression Tests Summary (`tests/seed-precommit-gate.test.ts`):
- `adversarial attempt to pass custom seed in draw body fails with 400 and keeps status SNAPSHOT_LOCKED`**PASS**
- `grinding regression: local brute-force of 100 seeds cannot alter the pre-committed API winner`**PASS**
- `draw attempt on giveaway without locked snapshot and seed returns 409 Conflict`**PASS**
- `GET /api/giveaways/[id] masks seed before DRAWN and exposes seedCommitment`**PASS**
- `after DRAWN, sha256(seedUsed) strictly equals seedCommitment and verify endpoint succeeds`**PASS**
- `MemoryGiveawayRepository generates and locks seed during createAndLockSnapshot`**PASS**
- `PrismaGiveawayRepository maps seedCommitment correctly`**PASS**
---
## 6. UNVERIFIED Assertions & Tech Debt
1. **UNVERIFIED: Prisma integration harness with live DB:**
- В текущем тестовом сьюте все функциональные тесты выполняются с драйвером `STORAGE_DRIVER=memory`. Хотя `PrismaGiveawayRepository` полностью реализован, компилируется (`tsc --noEmit`), собирается (`npm run build`) и покрыт маппинг-тестами, его сквозное выполнение в интеграционном тесте с реальной БД PostgreSQL не автоматизировано в Vitest.
- **Рекомендация / Proposed Next Task:** Добавить тестовый сьют `tests/prisma-integration.test.ts` для запуска прогона репозитория против тестового экземпляра PostgreSQL.
2. **CRITICAL Finding Status:**
- Исполнитель не объявляет CRITICAL finding автоматически закрытым самостоятельно. Требуется независимое re-review ревизии.