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

109 lines
7.4 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.1 — Atomic Snapshot + Seed Commitment Binding Report
**Date:** 2026-08-20
**Base Commit SHA:** `78151572bd2ae01645d70a0768c6ece517e2cab0`
**Status:** COMPLETED / READY FOR RE-REVIEW
**Assigned Agent:** Antigravity (Implementation Orchestrator)
---
## 1. Problem Addressed
В ходе независимого ревью Phase 2.4 было выявлено, что:
1. `createAndLockSnapshot` в Prisma-репозитории допускал смену статуса из `READY` или `SNAPSHOT_LOCKED` (`status IN ('READY', 'SNAPSHOT_LOCKED')`), а Memory-репозиторий аналогично допускал повторную фиксацию слепков.
2. В роуте `POST /api/giveaways/[id]/snapshot` фиксация слепка и чтение `giveaway.seed` для вычисления `seedCommitment` выполнялись двумя независимыми операциями (`createAndLockSnapshot` с последующим `getById`). При конкурентных запросах блокировки это могло привести к гонке, когда ответ возвращал слепок A с хешем seed B.
---
## 2. Implemented Solutions
1. **Single Lock Invariant:**
- Переход разрешён **только** `READY``SNAPSHOT_LOCKED`.
- Любая попытка заблокировать слепок, когда статус отличен от `READY` (например, уже `SNAPSHOT_LOCKED` или `DRAWN`), немедленно возвращает `409 CONFLICT`.
- При этом новый seed не генерируется, повторный слепок не создаётся, существующий `seedCommitment` остаётся неизменным.
2. **Atomic Return of `{ snapshot, seedCommitment }`:**
- Интерфейс `IGiveawayRepository.createAndLockSnapshot` и класс `GiveawayStore.createAndLockSnapshot` теперь возвращают `Promise<LockedSnapshotResult>`:
```typescript
export interface LockedSnapshotResult {
snapshot: ParticipantSnapshotData;
seedCommitment: string;
}
```
- Генерация CSPRNG seed, вычисление `seedCommitment = sha256(seed)`, сохранение seed в базе данных и создание записи `ParticipantSnapshot` выполняются строго внутри единой атомарной транзакции (в Prisma: `$transaction`; в Memory: синхронная мутация Map).
3. **Elimination of Post-Transaction Query:**
- Роут `POST /api/giveaways/[id]/snapshot` использует `seedCommitment`, возвращённый непосредственно из атомарной операции `GiveawayStore.createAndLockSnapshot`. Дополнительный запрос `GiveawayStore.getById(id)` полностью исключён.
4. **Driver Parity (Memory & Prisma):**
- `PrismaGiveawayRepository`: `where: { id, status: 'READY' }` внутри `$transaction`.
- `MemoryGiveawayRepository`: строгая проверка `if (gw.status !== 'READY') throw new ConflictError(...)`.
5. **Idempotency & Commitment Stability:**
- Повторные вызовы с одинаковым `Idempotency-Key` отдают закэшированный ответ 200 со стабильным `seedCommitment` без перегенерации seed.
- Повторный вызов с новым ключом после фиксации слепка возвращает `409 CONFLICT`.
- До жеребьёвки `giveaway.seed` маскируется (`null`), отдаётся только `seedCommitment`. После жеребьёвки `sha256(drawResult.seedUsed) === seedCommitment`.
---
## 3. Files Changed
| File | Type | Description |
|------|------|-------------|
| `src/lib/repository/giveaway-repository.ts` | Interface | Добавлен интерфейс `LockedSnapshotResult`, обновлена сигнатура `createAndLockSnapshot`. |
| `src/lib/giveaway-store.ts` | Store | Обновлена сигнатура `GiveawayStore.createAndLockSnapshot` (`Promise<LockedSnapshotResult>`). |
| `src/lib/repository/memory-repository.ts` | Driver | Строгий инвариант `READY``SNAPSHOT_LOCKED` (409 на повторы), атомарный возврат `{ snapshot, seedCommitment }`. |
| `src/lib/repository/prisma-repository.ts` | Driver | Условие `status: 'READY'` внутри `$transaction`, атомарный возврат `{ snapshot, seedCommitment }`. |
| `src/app/api/giveaways/[id]/snapshot/route.ts` | API Route | Прямое использование возвращённых `{ snapshot, seedCommitment }`, убран redundant `getById`. |
| `tests/winner-count-contract.test.ts` | Tests | Деструктуризация `{ snapshot }` из вызовов `createAndLockSnapshot`. |
| `tests/persistence.test.ts` | Tests | Деструктуризация `{ snapshot, seedCommitment }`. |
| `tests/snapshot-binding.test.ts` | Tests | Деструктуризация `{ snapshot: snapshotV1/V2 }`. |
| `tests/concurrency-draw.test.ts` | Tests | Деструктуризация `{ snapshot }`. |
| `tests/concurrency-draw-100.test.ts` | Tests | Деструктуризация `{ snapshot }`. |
| `tests/seed-precommit-gate.test.ts` | Tests | Деструктуризация `{ snapshot, seedCommitment }`, проверка равенства commitment. |
| `tests/snapshot-seed-atomicity.test.ts` | Tests (NEW) | Комплексный сьют проверки атомарности, конкуренции и стабильности commitment (4 теста). |
---
## 4. Unchanged Core Algorithms
Все алгоритмы генерации и верификации не изменялись:
- `HMAC_SHA256_FY_V1`
- `DeterministicHmacStream`
- `executeDeterministicDrawV1`
- `computeParticipantsSnapshotHash`
- `computeConditionsHash`
- `computeDeterministicProofHash`
- `computeAuditEventHash`
- `verifyDrawResult`
---
## 5. Test & Gate Results
Фактически выполненные команды:
```text
npx prisma generate -> EXIT 0 (Prisma Client v5.22.0)
npm test -> EXIT 0 (49 test files, 284 passed, 0 failed)
npm run lint -> EXIT 0 (Clean)
npm run build -> EXIT 0 (Next.js production build succeeded)
```
### Concurrency Test Evidence:
1. `tests/snapshot-seed-atomicity.test.ts` (4 теста):
- `Memory repository: 20 concurrent createAndLockSnapshot calls yield exactly 1 success and 19 ConflictErrors`**PASS**
- `API route: concurrent snapshot lock requests with different Idempotency-Keys produce exactly 1 200 and remaining 409s`**PASS**
- `idempotency: replaying same key returns cached commitment; new key after lock returns 409`**PASS**
- `commitment stability: commitment is invariant across reads and equals sha256(seedUsed) after draw`**PASS**
2. `tests/seed-precommit-gate.test.ts` (7 тестов) → **PASS**
3. `tests/concurrency-draw-100.test.ts` (2 теста) → **PASS**
---
## 6. Audit & Migration
- **Database migration required:** NO (поле `Giveaway.seed` уже присутствует в схеме Prisma).
- **CRITICAL/HIGH findings:** 0 open in implementation.
- **UNVERIFIED claims:** None.
- **Next step:** Передача на независимое security re-review (Grok/Claude/OpenCode).